В процессе разработки на платформе 1С:Предприятие одной из наиболее часто встречающихся задач является получение данных из связанных объектов. Однако, когда речь заходит о реквизитах составного типа, стандартный подход «через точку» может привести к катастрофическому падению производительности. Разберем детально, почему это происходит и как правильно оптимизировать такие запросы на примере получения даты документа продажи из регистра сведений.
Рассмотрим ситуацию, когда у нас есть регистр сведений ДокументыРеализацииСерий, в котором измерение ДокументПродажи имеет составной тип (например, включает в себя десятки видов документов: «Реализация товаров и услуг», «Отчет розничных продаж», «Передача товаров» и так далее). Проанализируем, что происходит на уровне СУБД, когда мы пишем ДокументПродажи.Дата.
Платформа 1С при генерации SQL-запроса вынуждена обеспечить получение данных для любого возможного типа, входящего в составное поле. В результате в SQL-запросе создается конструкция LEFT OUTER JOIN для каждой физической таблицы документа, входящего в этот тип. Если в составной тип включено 50 документов, вы получите 50 соединений в одном запросе. Это приводит к следующим негативным последствиям:
СрезПоследних) в сочетании с обращениями через точку, объем временных данных в SQL значительно возрастает.Как подтвердили технические тесты (сообщение 11), даже при наличии всего одной записи в регистре, план выполнения запроса включает в себя NESTED OUTER LOOP и RANGE SCAN по всем таблицам документов, которые теоретически могут быть записаны в это поле.
Проанализируем наиболее радикальный, но самый эффективный с точки зрения скорости чтения метод — денормализацию. Если бизнес-логика подразумевает, что дата документа продажи требуется вам постоянно и в больших объемах, лучшим решением будет хранить эту дату непосредственно в регистре сведений.
Алгоритм реализации:
ДокументыРеализацииСерий новый реквизит ДатаДокументаПродажи (тип Дата).ДокументПродажи.Дата.Теперь наш запрос станет максимально простым и быстрым, так как нам не нужно будет обращаться к таблицам документов вовсе:
ВЫБРАТЬ
СрезПоследних.ДокументПродажи КАК ДокументПродажи,
СрезПоследних.ДатаДокументаПродажи КАК ДатаДокументаПродажи,
СрезПоследних.Номенклатура КАК Номенклатура
ИЗ
РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(&Период, Серия = &Серия) КАК СрезПоследних
В современных типовых конфигурациях (таких как УТ 11, ERP 2, КА 2) часто используется механизм Реестра документов. Это специальный регистр сведений, в котором хранятся ссылки на все документы системы и их базовые реквизиты: дата, номер, сумма, пометка удаления и прочее.
Если в вашей конфигурации есть аналогичный регистр (например, РегистрСведений.РеестрДокументов), мы можем заменить обращение к составному типу соединением с этой «плоской» таблицей. Разберем этот пример:
ВЫБРАТЬ
Срез.ДокументПродажи КАК ДокументПродажи,
ЕСТЬNULL(Реестр.ДатаДокумента, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаДокументаПродажи
ИЗ
РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(, Серия = &Серия) КАК Срез
ЛЕВОЕ СОЕДИНЕНИЕ
РегистрСведений.РеестрДокументов КАК Реестр
ПО
Срез.ДокументПродажи = Реестр.Ссылка
Важный момент: Соединение с одной физической таблицей регистра сведений всегда работает быстрее, чем соединение с десятками таблиц документов через точку.
Если изменить структуру метаданных нельзя, а реестра документов в системе нет, следует использовать конструкцию ВЫРАЗИТЬ. Это позволяет «подсказать» платформе, какие именно типы документов нас интересуют, тем самым ограничивая количество соединений в SQL.
Рассмотрим, как это выглядит на практике, если мы знаем, что в продаже могут участвовать только три типа документов:
ВЫБРАТЬ
Срез.ДокументПродажи КАК ДокументПродажи,
ВЫБОР
КОГДА Срез.ДокументПродажи ССЫЛКА Документ.РеализацияТоваровУслуг
ТОГДА ВЫРАЗИТЬ(Срез.ДокументПродажи КАК Документ.РеализацияТоваровУслуг).Дата
КОГДА Срез.ДокументПродажи ССЫЛКА Документ.ОтчетРозничныхПродаж
ТОГДА ВЫРАЗИТЬ(Срез.ДокументПродажи КАК Документ.ОтчетРозничныхПродаж).Дата
ИНАЧЕ ЕСТЬNULL(Срез.ДокументПродажи.Дата, ДАТАВРЕМЯ(1, 1, 1))
КОНЕЦ КАК ДатаДокументаПродажи
ИЗ
РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(, Серия = &Серия) КАК Срез
Хотя этот код кажется громоздким, для СУБД он гораздо «прозрачнее». Если же типов слишком много, можно применить метод двух этапов:
ОБЪЕДИНИТЬ ВСЕ) по конкретным таблицам документов.Проанализируем еще одну архитектурную рекомендацию: не выполнять соединения с виртуальными таблицами напрямую. Вместо этого лучше сначала поместить результат работы виртуальной таблицы (например, СрезПоследних) во временную таблицу, а уже затем соединять её с другими данными.
Выясним причину: виртуальная таблица — это вложенный подзапрос. Когда SQL-сервер видит соединение сложного подзапроса с десятком других таблиц (через точку или явно), он часто ошибается в выборе индексов. Разрыв запроса на две части с использованием ПОМЕСТИТЬ позволяет оптимизатору построить два простых и быстрых плана вместо одного «монструозного».
Посмотрим на итоговый рекомендуемый шаблон запроса:
// 1. Получаем срез последних во временную таблицу
ВЫБРАТЬ
Срез.ДокументПродажи КАК ДокументПродажи,
Срез.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ втСрез
ИЗ
РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(&Период, Серия = &Серия) КАК Срез
;
// 2. Получаем дату, соединяясь только с нужными таблицами или реестром
ВЫБРАТЬ
втСрез.ДокументПродажи,
втСрез.Номенклатура,
ЕСТЬNULL(Реестр.ДатаДокумента, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаДокументаПродажи
ИЗ
втСрез КАК втСрез
ЛЕВОЕ СОЕДИНЕНИЕ
РегистрСведений.РеестрДокументов КАК Реестр
ПО
втСрез.ДокументПродажи = Реестр.Ссылка
Подведем итоги: для достижения максимальной производительности в 1С при работе с составными типами всегда старайтесь либо денормализовать данные (хранить дату в самом регистре), либо использовать промежуточные плоские таблицы (реестры), либо явно ограничивать типы через ВЫРАЗИТЬ.