В практике разработки на платформе 1С:Предприятие часто встречается парадоксальная ситуация: запрос, в который добавили, казалось бы, избыточные или дублирующие условия, начинает работать значительно быстрее. На первый взгляд это противоречит логике — чем больше фильтров и соединений, тем больше работы у системы. Однако, если мы заглянем "под капот" взаимодействия 1С и СУБД (чаще всего MS SQL Server), мы увидим сложную работу оптимизатора запросов. В данной статье мы подробно разберем, почему комбинация ВНУТРЕННЕЕ СОЕДИНЕНИЕ и условия ГДЕ ... В (ВЫБРАТЬ ...) может ускорить выполнение кода в несколько раз.
Рассмотрим ситуацию на конкретном примере. У нас есть запрос к регистру сведений АТС_УпаковочныеЛисты. Автор столкнулся с тем, что по отдельности соединение с временной таблицей или условие В работают медленнее (около 0.11 сек), чем их совместное использование (0.03 сек).
Посмотрим на структуру запроса, который вызывает вопросы:
ВЫБРАТЬ
АТС_УпаковочныеЛисты.ДокументОснование КАК ЗК
ПОМЕСТИТЬ вт_ЗК
ИЗ
РегистрСведений.АТС_УпаковочныеЛисты КАК АТС_УпаковочныеЛисты
ГДЕ
АТС_УпаковочныеЛисты.РасходныйОрдер = &РасходныйОрдер;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ РАЗЛИЧНЫЕ
АТС_УпаковочныеЛисты.УпаковочныйЛист КАК УпаковочныйЛист
ИЗ
РегистрСведений.АТС_УпаковочныеЛисты КАК АТС_УпаковочныеЛисты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ вт_ЗК КАК вт_ЗК
ПО АТС_УпаковочныеЛисты.ДокументОснование = вт_ЗК.ЗК
ГДЕ
НЕ АТС_УпаковочныеЛисты.УпаковочныйЛист В (&МассивТолькоЧтоСобранныхУпаклистов)
И АТС_УпаковочныеЛисты.ДокументОснование В (ВЫБРАТЬ вт_ЗК.ЗК ИЗ вт_ЗК)
И НЕ АТС_УпаковочныеЛисты.УпаковочныйЛист.Подпаковка
Здесь мы видим избыточность: данные фильтруются по ДокументОснование дважды — через JOIN с временной таблицей вт_ЗК и через подзапрос В (ВЫБРАТЬ...) из той же таблицы. Выясним причины, по которым это ускоряет работу.
Проанализируем, как СУБД строит план выполнения. Оптимизатор SQL Server пытается предсказать, сколько строк вернется на каждом этапе. Это называется оценкой кардинальности. На основе этой оценки выбирается тип физического оператора для соединения таблиц:
Когда используется только INNER JOIN, оптимизатор может ошибиться в расчетах из-за устаревшей статистики или сложности связей и выбрать Hash Match там, где Nested Loops был бы быстрее. Добавляя условие IN (SELECT...), мы даем оптимизатору "подсказку". Совокупность двух фильтров снижает ожидаемое количество строк в глазах планировщика, что заставляет его выбрать более легкий алгоритм (например, поиск по индексу внутри цикла).
Разберем разницу в логике обработки. В языке SQL конструкция IN (Подзапрос) часто транслируется в оператор Left Semi Join. Его ключевое отличие от обычного INNER JOIN заключается в том, что он прекращает поиск в правой таблице (в нашем случае вт_ЗК), как только найдено первое совпадение.
В то время как ВНУТРЕННЕЕ СОЕДИНЕНИЕ заставляет базу данных найти все совпадения, что при наличии дублей в вт_ЗК увеличивает объем промежуточных данных, которые потом все равно отсекаются ключевым словом РАЗЛИЧНЫЕ. Добавление IN позволяет оптимизатору применить логику полусоединения на более раннем этапе, что существенно сокращает нагрузку на дисковую подсистему и память.
Выясним, как глубоко СУБД "проталкивает" фильтры. Иногда наличие избыточного условия помогает базе данных выполнить фильтрацию основной таблицы АТС_УпаковочныеЛисты еще до того, как начнется процесс соединения с чем-либо еще. Это уменьшает количество логических чтений. Вместо того чтобы сначала соединить две таблицы в памяти, а потом проверять условия, СУБД отсекает ненужные записи сразу при чтении индекса.
Обратим внимание на строку АТС_УпаковочныеЛисты.УпаковочныйЛист.Подпаковка. В терминах платформы 1С — это обращение через точку к реквизиту ссылочного типа. При трансляции в SQL это превращается в неявное LEFT JOIN с таблицей справочника (в данном случае, вероятно, справочник УпаковочныеЛисты).
Проанализируем ситуацию: если условий в секции ГДЕ мало, оптимизатор может решить, что сначала нужно выполнить это неявное соединение со справочником. Но если мы добавляем жесткие ограничения через JOIN и IN одновременно, база данных получает четкий сигнал: сначала нужно максимально "схлопнуть" выборку по основной таблице регистра, и только для оставшихся нескольких строк идти в таблицу справочника за полем Подпаковка. Это классический пример изменения порядка соединений, который дает кратный прирост скорости.
Важно помнить, как 1С работает с tempdb. Если при создании временной таблицы вт_ЗК не было использовано ключевое слово ИНДЕКСИРОВАТЬ ПО, то любое соединение с ней может привести к полному сканированию этой таблицы (Table Scan). Рассмотрим, как это исправить:
ВЫБРАТЬ
АТС_УпаковочныеЛисты.ДокументОснование КАК ЗК
ПОМЕСТИТЬ вт_ЗК
ИЗ
РегистрСведений.АТС_УпаковочныеЛисты КАК АТС_УпаковочныеЛисты
ГДЕ
АТС_УпаковочныеЛисты.РасходныйОрдер = &РасходныйОрдер
ИНДЕКСИРОВАТЬ ПО
ЗК;
Использование IN часто провоцирует СУБД на создание временного индекса в оперативной памяти (Bitmap Filter) или использование констант, что нивелирует отсутствие явного индекса во временной таблице.
Если вы столкнулись с подобным поведением, выполните следующие шаги для стабилизации производительности:
Scan (плохо) и заменяйте их на Seek (хорошо).ПО в соединениях или В в условиях.вт_ЗК содержит дубликаты, лучше использовать В (ВЫБРАТЬ РАЗЛИЧНЫЕ ...), чтобы не перегружать оптимизатор.Резюмируя: ускорение в вашем случае — это результат того, что дополнительные условия "помогли" планировщику СУБД обойти ограничения 1С-овской трансляции и выбрать более эффективный алгоритм доступа к данным. Однако полагаться на "избыточность" как на основной метод оптимизации не стоит — лучше стремиться к пониманию физического плана выполнения запроса.