Среди разработчиков 1С нередко возникает спор о том, на каком этапе выполнения запроса накладываются отборы из секции ГДЕ. Одни утверждают, что условия фильтрации всегда накладываются строго на результирующую выборку уже после соединения таблиц. Другие демонстрируют планы выполнения в профайлере, где СУБД выполняет предварительную фильтрацию по индексам еще до операции соединения. Разберем детально, как именно платформа 1С и СУБД обрабатывают условия связи и фильтрации, почему логический порядок отличается от физического и как не допустить критических ошибок при проектировании запросов.
Для понимания механизма работы запросов важно четко разделять два понятия: стандартную логическую модель реляционной алгебры и физический план выполнения, который формирует оптимизатор СУБД.
С точки зрения реляционной логики языка SQL и платформы 1С порядок обработки следующий:
ИЗ по предикатам секции ПО.ЛЕВОЕ СОЕДИНЕНИЕ для строк левой таблицы, не нашедших соответствия в правой таблице, формируются поля со значениями NULL.ГДЕ.Однако реляционная СУБД (например, MS SQL Server или PostgreSQL) никогда не выполняет запросы буквально «в лоб», если это приводит к неэффективной работе с памятью и диском. Главная задача оптимизатора запросов — выдать точный результат, затратив минимум ресурсов. Если изменение последовательности операций не влияет на итоговый результат выборки, оптимизатор имеет полное право поменять порядок действий: сначала отфильтровать строки по индексу (Index Seek), а затем выполнить соединение (Nested Loops, Hash Match или Merge Join).
Рассмотрим запрос, вызвавший споры в сообществе разработчиков:
ВЫБРАТЬ
Т1.Рес1,
Т2.Рес1
ИЗ
РегистрСведений.хх КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.хх КАК Т2
ПО Т1.Изм1 = Т2.Изм1
И Т1.Изм2 = Т2.Изм2
ГДЕ
Т2.Изм2 = "блабла"
Проанализируем, почему профайлер показывает предварительную выборку строк с поиском по индексу для обеих таблиц, хотя разработчик указал левое соединение и отбор в секции ГДЕ.
В данном запросе срабатывают сразу два механизма оптимизации СУБД:
Т2.Изм2 = "блабла" находится в секции ГДЕ. Любая строка левой таблицы, для которой в правой таблице нет пары, вернет в поле Т2.Изм2 значение NULL. Так как сравнение NULL = "блабла" дает ложь, эти строки гарантированно будут отсечены. Оптимизатор СУБД понимает, что левое соединение в таком виде полностью эквивалентно ВНУТРЕННЕЕ СОЕДИНЕНИЕ, и неявно преобразует его на этапе компиляции плана.ПО присутствует строгое равенство Т1.Изм2 = Т2.Изм2, а в ГДЕ задано Т2.Изм2 = "блабла", оптимизатор автоматически генерирует дополнительное условие Т1.Изм2 = "блабла". В итоге СУБД производит индексный поиск (Index Seek) по обеим таблицам еще до их объединения.Таким образом, логически условие ГДЕ относится ко всей выборке, но физически СУБД применила его к обоим источникам до соединения ради многократного ускорения операции.
Поведение запроса кардинально меняется, если перенести фильтрацию правой таблицы в условие соединения:
ВЫБРАТЬ
Т1.Рес1,
Т2.Рес1
ИЗ
РегистрСведений.хх КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.хх КАК Т2
ПО Т1.Изм1 = Т2.Изм1
И Т1.Изм2 = Т2.Изм2
И Т2.Изм2 = "блабла"
Разберем различия в результатах выполнения:
ПО: условие на правую таблицу фильтрует только присоединяемые данные. Если строка правой таблицы не соответствует значению "блабла", строка левой таблицы не удаляется из результата — поля правой таблицы просто заполняются значениями NULL.ГДЕ: жесткое условие на равенство или неравенство поля правой таблицы гарантированно отсекает всю строку целиком, превращая левое соединение во внутреннее.ПО: если в левом соединении поместить условие на левую таблицу (например, ПО Т1.Изм2 = "блабла"), строки левой таблицы все равно останутся в выборке, но для неподошедших строк поля правой таблицы станут NULL. Фильтровать левую таблицу необходимо исключительно в секции ГДЕ.Посмотрим на сценарий, когда физический порядок выполнения строго следует логическому. Единственным корректным поводом ставить условие на правую таблицу левого соединения в секции ГДЕ является поиск отсутствующих записей:
ВЫБРАТЬ
Т1.Изм1,
Т1.Рес1
ИЗ
РегистрСведений.хх КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.хх КАК Т2
ПО Т1.Изм1 = Т2.Изм1
ГДЕ
Т2.Изм1 ЕСТЬ NULL
В этом случае проверка ЕСТЬ NULL не отвергает отсутствующие строки, а, напротив, оставляет только их. Оптимизатор СУБД не может заменить соединение на внутреннее и выполняет антисоединение (Anti Semi Join), отсекая строки именно на этапе проверки результата слияния.
При написании запросов в 1С необходимо учитывать технические ограничения платформы и правила разработки:
ГДЕ. Если в логике требуется обязательное наличие записей в обеих таблицах, всегда пишите ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Это предотвращает ошибки оптимизатора при расчете кардинальности и статистики в сложных запросах с множеством таблиц.ПО. Если для соединения требуется сложная фильтрация правой таблицы, предварительно выгружайте данные во временную таблицу.МенеджерВременныхТаблиц с явным указанием ИНДЕКСИРОВАТЬ ПО. Это исключает непредсказуемое поведение оптимизатора.ПО или ГДЕ, иначе СУБД сначала рассчитает итоги по всей базе данных, что приведет к резкому падению производительности.ГДЕ. Если для правой таблицы левого соединения действуют правила RLS без проверки на NULL, платформенный код может непреднамеренно преобразовать левое соединение во внутреннее, скрыв часть данных левой таблицы.