Казалось бы, добавление жесткого ограничивающего условия во вложенный запрос должно уменьшать объем обрабатываемых данных и ускорять выполнение программы. Однако на практике разработчики 1С часто сталкиваются с противоположным эффектом: стоит добавить фильтр по дате во вложенный запрос, как скорость его выполнения падает в разы, а иногда и в десятки раз. В рамках этой статьи мы подробно разберем, почему так происходит, какие скрытые механизмы запускаются на уровне СУБД и как правильно оптимизировать подобные запросы согласно стандартам разработки 1С.
Рассмотрим типичный пример запроса, с которым столкнулся автор темы. В нем происходит левое соединение документов ОтчетОРозничныхПродажах с вложенным запросом по документам ПриходныйКассовыйОрдер (ПКО), в котором рассчитывается сумма документов с группировкой по ссылке:
ВЫБРАТЬ
ОтчетОРозничныхПродажах.Ссылка КАК ОРП,
ПриходныйКассовыйОрдер.Ссылка КАК ПКО
ИЗ
Документ.ОтчетОРозничныхПродажах КАК ОтчетОРозничныхПродажах
ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
Ссылка,
СУММА(СуммаДокумента) КАК Сумма
ИЗ
Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
//ГДЕ Дата МЕЖДУ &НачалоПериода И &КонецПериода
СГРУППИРОВАТЬ ПО
Ссылка) КАК ПриходныйКассовыйОрдер
ПО КОНЕЦПЕРИОДА(ОтчетОРозничныхПродажах.Дата, ДЕНЬ) = КОНЕЦПЕРИОДА(ПриходныйКассовыйОрдер.Дата, ДЕНЬ)
И ОтчетОРозничныхПродажах.Организация = ПриходныйКассовыйОрдер.Организация
И ОтчетОРозничныхПродажах.Контрагент = ПриходныйКассовыйОрдер.Контрагент
ГДЕ
ОтчетОРозничныхПродажах.Дата МЕЖДУ &НачалоПериода И &КонецПериода
Автор заметил интересную особенность: если раскомментировать условие по дате внутри вложенного запроса, время выполнения существенно увеличивается. Давайте разберем по шагам, какие архитектурные и системные факторы влияют на это поведение.
Проанализируем секцию ПО в условии соединения нашего запроса. Мы видим сравнение полей:
КОНЕЦПЕРИОДА(ОтчетОРозничныхПродажах.Дата, ДЕНЬ) = КОНЕЦПЕРИОДА(ПриходныйКассовыйОрдер.Дата, ДЕНЬ)ОтчетОРозничныхПродажах.Организация = ПриходныйКассовыйОрдер.ОрганизацияОтчетОРозничныхПродажах.Контрагент = ПриходныйКассовыйОрдер.КонтрагентОбратите внимание на правую часть сравнений. Мы обращаемся к полям Дата, Организация и Контрагент виртуальной таблицы ПриходныйКассовыйОрдер. Но во вложенном запросе мы выбираем только два поля: Ссылка и Сумма! Физически во вложенной выборке нет ни даты, ни организации, ни контрагента.
Платформа 1С «прощает» разработчику такую небрежность и автоматически разыменовывает эти поля. Однако на уровне СУБД транслятор запросов 1С вынужден сгенерировать дополнительное неявное левое соединение результирующего набора подзапроса с физической таблицей документов ПКО по полю Ссылка (первичному ключу), чтобы извлечь недостающие данные для связи. В итоге вместо одного простого соединения СУБД строит сложную цепочку связей из трех таблиц, что сильно усложняет построение оптимального плана выполнения.
Главная причина замедления при добавлении условия кроется в алгоритмах оптимизатора СУБД (MS SQL Server, PostgreSQL и др.) и понятии «проталкивание предикатов» (Predicate Pushdown). Выясним, как меняется логика СУБД в двух сценариях:
ОтчетОРозничныхПродажах ограничена по датам (МЕЖДУ &НачалоПериода И &КонецПериода), а соединение выполняется по равенству дат. Используя правила логической транзитивности, оптимизатор сам «проталкивает» этот фильтр внутрь вложенного запроса. СГРУППИРОВАТЬ ПО), оптимизатор СУБД воспринимает этот подзапрос как изолированный логический блок, который необходимо вычислить предварительно.tempdb при нехватке ресурсов) и только после этого соединяет полученный массив с основной таблицей через тяжелые алгоритмы Hash Match (хеш-соединение) или Merge Join. Полное сканирование и группировка данных «в лоб» практически всегда работают медленнее, чем каскадный поиск по индексам.Во вложенном запросе используется группировка по полю Ссылка: СГРУППИРОВАТЬ ПО Ссылка. Поле Ссылка является уникальным идентификатором (первичным ключом таблицы СУБД). Каждая строка в таблице ПКО имеет уникальную ссылку.
Группировка по уникальному полю для подсчета агрегатной суммы реквизита шапки (который и так один для каждого документа) является избыточной операцией. Для СУБД наличие конструкции GROUP BY служит жестким барьером, мешающим оптимизатору упростить («схлопнуть») структуру запроса во внешний уровень. Без этой группировки СУБД могла бы применить оптимизацию упрощения соединений (View Merging), что полностью нивелировало бы падение производительности.
Соединения с вложенными запросами — это известное нарушение стандартов разработки 1С. Они делают запросы непредсказуемыми: при небольшом объеме данных запрос может работать быстро, но по мере роста базы данных производительность лавинообразно падает. Единственным надежным и стабильным решением является использование **Временных Таблиц (ВТ)**.
Рассмотрим по шагам, как переписать наш запрос с использованием временных таблиц:
В первом запросе пакета мы выбираем только нужные документы ПКО за указанный период. Сразу получаем из них все необходимые для связи реквизиты (дату, организацию, контрагента и сумму), избавляясь от последующих неявных соединений «через точку».
ВЫБРАТЬ
ПриходныйКассовыйОрдер.Ссылка КАК Ссылка,
КОНЕЦПЕРИОДА(ПриходныйКассовыйОрдер.Date, ДЕНЬ) КАК ПериодДень,
ПриходныйКассовыйОрдер.Организация КАК Организация,
ПриходныйКассовыйОрдер.Контрагент КАК Контрагент,
СУММА(ПриходныйКассовыйОрдер.СуммаДокумента) КАК Сумма
ПОМЕСТИТЬ ВТ_ПКО
ИЗ
Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
ГДЕ
ПриходныйКассовыйОрдер.Дата МЕЖДУ &НачалоПериода И &КонецПериода
СГРУППИРОВАТЬ ПО
ПриходныйКассовыйОрдер.Ссылка,
КОНЕЦПЕРИОДА(ПриходныйКассовыйОрдер.Date, ДЕНЬ),
ПриходныйКассовыйОрдер.Организация,
ПриходныйКассовыйОрдер.Контрагент
Для того чтобы последующее соединение отработало максимально быстро, мы должны проиндексировать временную таблицу по полям, которые будут участвовать в связи:
ИНДЕКСИРОВАТЬ ПО
ПериодДень,
Организация,
Контрагент
;
В следующем запросе пакета мы производим левое соединение таблицы ОРП с нашей подготовленной и проиндексированной временной таблицей ВТ_ПКО:
ВЫБРАТЬ
ОтчетОРозничныхПродажах.Ссылка КАК ОРП,
ВТ_ПКО.Ссылка КАК ПКО
ИЗ
Документ.ОтчетОРозничныхПродажах КАК ОтчетОРозничныхПродажах
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПКО КАК ВТ_ПКО
ПО КОНЕЦПЕРИОДА(ОтчетОРозничныхПродажах.Дата, ДЕНЬ) = ВТ_ПКО.ПериодДень
И ОтчетОРозничныхПродажах.Организация = ВТ_ПКО.Организация
И ОтчетОРозничныхПродажах.Контрагент = ВТ_ПКО.Контрагент
ГДЕ
ОтчетОРозничныхПродажах.Дата МЕЖДУ &НачалоПериода И &КонецПериода
Проанализируем преимущества перевода запроса на временные таблицы:
Таким образом, отказ от вложенных запросов в пользу временных таблиц не только решает текущую проблему с падением скорости, но и гарантирует стабильную и быструю работу вашей конфигурации при любом масштабе базы данных.