Почему план выполнения SQL отличается от текста запроса и как оптимизировать избыточные соединения

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В практике разработчика 1С часто встречается ситуация, когда при анализе работы системы через SQL Profiler обнаруживается странное поведение: текст запроса, сформированный платформой, выглядит громоздким и содержит десятки LEFT OUTER JOIN, в то время как графический план выполнения в SQL Server Management Studio (SSMS) показывает работу всего с парой таблиц. Разберем подробно, почему возникает этот диссонанс, кому стоит верить и как сделать код более эффективным.

Анализ механизма трансляции запросов 1С в SQL

Рассмотрим ситуацию на конкретном примере. Допустим, мы пишем простой запрос к регистру накопления, чтобы получить дату регистратора:


ВЫБРАТЬ ПЕРВЫЕ 10
    ЗаказыКлиентов.Регистратор.Дата КАК РегистраторДата
ИЗ
    РегистрНакопления.ЗаказыКлиентов КАК ЗаказыКлиентов
ГДЕ
    ЗаказыКлиентов.Регистратор ССЫЛКА Документ.ЗаказКлиента

Проанализируем, что происходит "под капотом". Поле Регистратор в любом регистре является составным типом данных. На уровне СУБД это означает, что ссылка хранится в нескольких колонках (тип, идентификатор). Когда мы обращаемся "через точку" к реквизиту Дата, платформа 1С не знает заранее, какой именно тип документа там окажется в момент выполнения, поэтому она генерирует SQL-код, включающий соединения со всеми возможными таблицами документов, которые могут быть регистраторами для этого регистра.

В тексте запроса (SQL Profiler) мы увидим огромный блок CASE и длинный список LEFT OUTER JOIN ко всем таблицам _DocumentX. Однако в секции WHERE платформа добавит условие фильтрации по типу, например: WHERE T1._RecorderTRef = 0x000000D1.

Почему план выполнения выглядит иначе

Выясним причину, по которой СУБД игнорирует лишние соединения в плане выполнения. Современные версии MS SQL Server обладают продвинутым оптимизатором, который использует механизм Join Elimination (исключение соединений) и Contradiction Detection (обнаружение противоречий).

Проанализируем логику оптимизатора:

  1. Оптимизатор видит условие WHERE, которое жестко ограничивает тип регистратора одним конкретным значением.
  2. Он сопоставляет это условие с блоками ON в секциях LEFT JOIN.
  3. Для всех таблиц, кроме одной нужной, условие соединения становится заведомо ложным (противоречивым).
  4. Поскольку используется именно левое соединение, и данные из этих таблиц (согласно блоку CASE) при невыполнении условия все равно превратятся в NULL, СУБД принимает решение вообще не обращаться к этим таблицам физически.

Таким образом, отвечая на вопрос "кому верить", мы можем утверждать: верить нужно плану выполнения. Именно он показывает реальные физические операции (чтения, сканирования индексов), которые выполняет сервер. Текст запроса — это лишь декларация намерений, которую СУБД перерабатывает в оптимальный алгоритм.

Использование конструкции ВЫРАЗИТЬ для оптимизации

Несмотря на "умное" поведение SQL-сервера, оставлять запрос в исходном виде — плохая практика. Рассмотрим, почему стоит использовать ВЫРАЗИТЬ(). Когда мы пишем:


ВЫБРАТЬ ПЕРВЫЕ 10
    (ВЫРАЗИТЬ(ЗаказыКлиентов.Регистратор КАК Документ.ЗаказКлиента)).Дата КАК РегистраторДата
ИЗ
    РегистрНакопления.ЗаказыКлиентов КАК ЗаказыКлиентов

Мы даем платформе 1С прямую команду: "Я точно знаю, что здесь будет этот тип документа". В этом случае 1С при трансляции в SQL выбросит все лишние LEFT JOIN еще на этапе формирования текста. Это дает несколько преимуществ:

Альтернативный подход: Явное внутреннее соединение

Рассмотрим еще один способ, который часто оказывается эффективнее обращения через точку. Мы можем использовать ВНУТРЕННЕЕ СОЕДИНЕНИЕ (INNER JOIN) с конкретной таблицей документа:


ВЫБРАТЬ ПЕРВЫЕ 10
    ДокЗаказ.Дата КАК РегистраторДата
ИЗ
    РегистрНакопления.ЗаказыКлиентов КАК ЗаказыКлиентов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ 
    Документ.ЗаказКлиента КАК ДокЗаказ 
ПО 
    ЗаказыКлиентов.Регистратор = ДокЗаказ.Ссылка

В этом случае мы не только избавляемся от лишних соединений, но и помогаем оптимизатору использовать более быстрые алгоритмы соединения, такие как Hash Join или Merge Join, если объем данных велик. Кроме того, INNER JOIN автоматически выполняет роль фильтра, заменяя конструкцию ССЫЛКА в условии ГДЕ.

Использование системных регистров (Данные первичных документов)

В современных типовых конфигурациях (ERP 2.5, КА 2.5, БП 3.0) реализован еще более продвинутый механизм получения базовых реквизитов (Дата, Номер). Проанализируем работу регистра сведения ДанныеПервичныхДокументов. В этот регистр при проведении любого документа записываются его основные данные. Вместо того чтобы соединяться с тяжелыми таблицами документов (где может быть сотни полей), мы можем выполнить одно соединение с легким регистром сведений. Это особенно актуально для отчетов, выводящих реестры документов разных типов.

Итоги и рекомендации

Подведем итог нашему разбору. Различие между текстом и планом — это нормальная работа оптимизатора СУБД, но это не повод писать неоптимальные запросы на языке 1С.

Основные правила для разработчика:

  1. Избегайте обращения через точку к полям составного типа (Регистратор, Владелец, любой другой составной реквизит).
  2. Всегда используйте ВЫРАЗИТЬ(), если вам нужно получить реквизит конкретного типа из составного поля.
  3. Для повышения производительности в сложных отчетах отдавайте предпочтение явному ВНУТРЕННЕМУ СОЕДИНЕНИЮ вместо разыменования ссылки.
  4. Помните, что ССЫЛКА в блоке ГДЕ только фильтрует результат, но не сокращает количество соединений в блоке ВЫБРАТЬ, в то время как ВЫРАЗИТЬ влияет непосредственно на структуру соединений.

Применяя эти методы, мы делаем систему более устойчивой к росту объема данных и снижаем вероятность внезапной деградации производительности при обновлении платформы или версии СУБД.

← На главную