В сообществе разработчиков 1С не утихают споры о том, как правильно и максимально эффективно получать реквизиты связанных объектов в запросах. Существует популярное мнение, что обращение через точку (неявное соединение) работает медленнее, чем явное левое или внутреннее соединение (LEFT JOIN / INNER JOIN). В рамках данной статьи мы подробно разберем этот вопрос, проанализируем реальные планы SQL-запросов на миллионных объемах данных, выясним влияние статистики СУБД и определим, когда использование «точек» абсолютно безопасно, а когда может привести к фатальному падению производительности системы.
Рассмотрим классическую задачу: нам необходимо выбрать данные из табличной части документа и одновременно получить несколько реквизитов из шапки этого же документа (например, КФО, Контрагент, Договор, Комментарий). Автор темы предложил следующий вариант запроса:
ВЫБРАТЬ
ПередачаОбъектовОСОсновныеСредства.ОсновноеСредство КАК ОсновноеСредство,
ПередачаОбъектовОСОсновныеСредства.КПС КАК КПС,
ПередачаОбъектовОСОсновныеСредства.Сумма КАК Сумма,
ПередачаОбъектовОСОсновныеСредства.Ссылка.КФО КАК КФО,
ПередачаОбъектовОСОсновныеСредства.Ссылка.Контрагент КАК Контрагент,
ПередачаОбъектовОСОсновныеСредства.Ссылка.Договор КАК Договор,
ПередачаОбъектовОСОсновныеСредства.Ссылка.Комментарий КАК Комментарий
ИЗ
Документ.ПередачаОбъектовОС.ОсновныеСредства КАК ПередачаОбъектовОСОсновныеСредства
ГДЕ
ПередачаОбъектовОСОсновныеСредства.Ссылка.ВидОперации В(&ВидыПередачи)
И ПередачаОбъектовОСОсновныеСредства.Ссылка.Дата МЕЖДУ &Дата1 И &Дата2
В данном запросе получение реквизитов шапки (таких как КФО, Контрагент, Договор) происходит через обращение к предопределенному полю Ссылка табличной части. Возникает закономерный вопрос: насколько эффективен такой подход с точки зрения СУБД? Не будет ли лучше заменить эти обращения явным соединением табличной части с шапкой документа?
Давайте разберем по шагам, что происходит под капотом платформы 1С:Предприятие, когда она парсит запрос с обращениями через точку к реквизитам одного и того же объекта. Некоторые начинающие разработчики полагают, что каждое обращение вида Ссылка.Реквизит порождает отдельное соединение с таблицей СУБД. Это заблуждение.
Технологическая платформа 1С оптимизирует трансляцию запросов. Если в рамках одного запроса (в полях выборки или условиях фильтрации) происходит обращение к разным реквизитам одного и того же ссылочного объекта, платформа группирует эти обращения. На уровне СУБД будет сгенерировано ровно одно левое соединение (LEFT OUTER JOIN) с таблицей этого объекта.
Таким образом, исходный запрос автора темы будет транслирован в SQL-запрос, где табличная часть _DocumentXX_VT_YY соединяется с таблицей шапки _DocumentXX по полю _DescriptionRef (или аналогичному внутреннему идентификатору ссылки).
Для сравнения рассмотрим, как выглядел бы этот же запрос, если бы мы написали его с использованием явного левого соединения, как часто рекомендуют на различных курсах по оптимизации:
ВЫБРАТЬ
ПередачаОбъектовОСОсновныеСредства.ОсновноеСредство КАК ОсновноеСредство,
ПередачаОбъектовОСОсновныеСредства.КПС КАК КПС,
ПередачаОбъектовОСОсновныеСредства.Сумма КАК Сумма,
ДокументШапка.КФО КАК КФО,
ДокументШапка.Контрагент КАК Контрагент,
ДокументШапка.Договор КАК Договор,
ДокументШапка.Комментарий КАК Комментарий
ИЗ
Документ.ПередачаОбъектовОС.ОсновныеСредства КАК ПередачаОбъектовОСОсновныеСредства
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПередачаОбъектовОС КАК ДокументШапка
ПО ПередачаОбъектовОСОсновныеСредства.Ссылка = ДокументШапка.Ссылка
ГДЕ
ДокументШапка.ВидОперации В(&ВидыПередачи)
И ДокументШапка.Дата МЕЖДУ &Дата1 И &Дата2
На первый взгляд, этот запрос кажется более контролируемым. Мы явно указываем СУБД, какую таблицу и по какому условию необходимо присоединить. Но давайте обратимся к практике и посмотрим, как эти два варианта ведут себя на реальной базе данных под управлением MS SQL Server.
В ходе обсуждения на форуме был проведен наглядный эксперимент на крупной базе данных (выборка за год, объем возвращаемых данных — более 2.5 миллионов строк). Были замерены показатели производительности и проанализированы планы выполнения (Execution Plans) для обоих вариантов написания запроса.
Результаты первоначального замера (на тестовой копии базы):
Index Seek для поиска по дате.Clustered Index Scan (сканирование кластерного индекса), что обычно считается менее эффективным, но в данном конкретном случае за счет кэширования и особенностей распределения данных запрос отработал даже быстрее.Такая разница в планах выполнения при одинаковой логической сути запросов вызвала вопросы. Почему СУБД построила разные планы для фактически идентичных по структуре запросов?
Разгадка кроется в состоянии базы данных, на которой проводился первоначальный тест. На тестовой копии статистика распределения данных СУБД оказалась устаревшей. Как только на тестовой базе была выполнена команда обновления статистики (UPDATE STATISTICS), оптимизатор MS SQL Server перестроил планы выполнения.
Результаты после обновления статистики:
После актуализации статистических данных СУБД стала абсолютно одинаково оценивать стоимость выполнения обоих запросов. План выполнения для запроса через точку стал стопроцентно идентичен плану выполнения запроса через явное левое соединение. Время выполнения и количество логических чтений полностью сравнялись.
Это подтверждает важнейший теоретический аспект: современный оптимизатор СУБД (будь то MS SQL Server или PostgreSQL) при наличии актуальной статистики отлично понимает структуру связей «табличная часть — шапка документа». Физическая связь всегда идет по уникальному первичному ключу Ссылка (который гарантированно индексирован), поэтому СУБД оптимизирует неявный LEFT JOIN (порожденный точками) точно так же, как и явный.
Из результатов эксперимента может показаться, что разницы между точками и соединениями нет вообще, и можно всегда писать запросы через точку ради экономии времени и улучшения читаемости кода. Однако это справедливо только для простых (однотипных) полей.
Давайте выясним причину, почему разыменование через точку считается одной из главных угроз производительности в 1С. Это происходит, когда поле, к которому мы обращаемся через точку, имеет составной тип данных.
Представим, что мы пишем запрос к таблице регистра накопления и хотим получить дату регистратора. Поле Регистратор по умолчанию имеет составной тип, в который могут входить десятки различных документов. Если мы напишем:
ВЫБРАТЬ
Продажи.Регистратор.Дата КАК ДатаРегистратора
ИЗ
РегистрНакопления.Продажи КАК Продажи
Платформа 1С будет вынуждена построить в результирующем SQL-запросе левое соединение (LEFT JOIN) со всеми таблицами документов, которые могут выступать в роли регистратора для данного регистра! Если в состав типа входит 50 документов, мы получим гигантский SQL-запрос с 50 соединениями. Оптимизатор СУБД гарантированно запутается в таком запросе, построит неоптимальный план выполнения, что приведет к колоссальным блокировкам и зависанию системы.
Как решать проблему составных типов:
Если вам действительно необходимо получить реквизит составного поля через точку, вы обязаны предварительно ограничить тип этого поля с помощью конструкции ВЫРАЗИТЬ(). Рассмотрим правильный пример:
ВЫБРАТЬ
ВЫРАЗИТЬ(Продажи.Регистратор КАК Документ.РеализацияТоваровУслуг).Дата КАК ДатаРегистратора
ИЗ
РегистрНакопления.Продажи КАК Продажи
В этом случае платформа 1С сделает левое соединение только с одной таблицей документа РеализацияТоваровУслуг, и запрос отработает моментально.
Еще один важный фактор, который необходимо учитывать в реальных корпоративных системах — это механизм ограничения доступа на уровне записей (RLS). Если вы обращаетесь через точку к реквизиту объекта (например, Ссылка.Контрагент.Наименование), на который наложены правила RLS, платформа 1С автоматически добавит в итоговый SQL-текст все подзапросы, проверяющие права текущего пользователя на таблицу контрагентов.
Если в одном запросе присутствует множество таких переходов по точкам к разным справочникам с активным RLS, итоговый текст SQL-запроса раздуется до нечитаемых СУБД масштабов. В таких сложных сценариях явное левое соединение позволяет разработчику более жестко контролировать выборку и минимизировать негативное влияние RLS.
Обратимся к рекомендациям ИТС. Стандарты разработки регламентируют следующие правила:
ВЫРАЗИТЬ() является грубым нарушением стандартов и подлежит обязательному исправлению.Ссылка (например, ПередачаОбъектовОСОсновныеСредства.Ссылка.Ссылка). Это избыточная операция, которая создает абсолютно бесполезное дополнительное соединение СУБД ради получения значения, которое уже присутствует в исходной таблице табличной части.Проанализировав ситуацию, мы можем подвести итоги совместной работы по исследованию производительности запросов:
LEFT JOIN в простых запросах — это «оптимизация ради оптимизации», которая лишь раздувает текст запроса и ухудшает его читаемость для других разработчиков.Регистратор, Владелец, составные свойства) использование разыменования через точку без явного ограничения типа через ВЫРАЗИТЬ() **категорически запрещено**.