Как оптимизировать запрос к реквизитам составного типа при получении даты документа?

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

В процессе разработки на платформе 1С:Предприятие одной из наиболее часто встречающихся задач является получение данных из связанных объектов. Однако, когда речь заходит о реквизитах составного типа, стандартный подход «через точку» может привести к катастрофическому падению производительности. Разберем детально, почему это происходит и как правильно оптимизировать такие запросы на примере получения даты документа продажи из регистра сведений.

Анализ проблемы: почему обращение через точку неэффективно

Рассмотрим ситуацию, когда у нас есть регистр сведений ДокументыРеализацииСерий, в котором измерение ДокументПродажи имеет составной тип (например, включает в себя десятки видов документов: «Реализация товаров и услуг», «Отчет розничных продаж», «Передача товаров» и так далее). Проанализируем, что происходит на уровне СУБД, когда мы пишем ДокументПродажи.Дата.

Платформа 1С при генерации SQL-запроса вынуждена обеспечить получение данных для любого возможного типа, входящего в составное поле. В результате в SQL-запросе создается конструкция LEFT OUTER JOIN для каждой физической таблицы документа, входящего в этот тип. Если в составной тип включено 50 документов, вы получите 50 соединений в одном запросе. Это приводит к следующим негативным последствиям:

  1. Раздувание плана запроса: Оптимизатору СУБД становится крайне сложно построить эффективный план выполнения.
  2. Избыточное чтение: Даже если в регистре фактически содержатся ссылки только на один тип документа, СУБД все равно проверит индексы всех 50 таблиц.
  3. Нагрузка на TempDB: При использовании виртуальных таблиц (например, СрезПоследних) в сочетании с обращениями через точку, объем временных данных в SQL значительно возрастает.

Как подтвердили технические тесты (сообщение 11), даже при наличии всего одной записи в регистре, план выполнения запроса включает в себя NESTED OUTER LOOP и RANGE SCAN по всем таблицам документов, которые теоретически могут быть записаны в это поле.

Решение 1: Денормализация данных (самый быстрый способ)

Проанализируем наиболее радикальный, но самый эффективный с точки зрения скорости чтения метод — денормализацию. Если бизнес-логика подразумевает, что дата документа продажи требуется вам постоянно и в больших объемах, лучшим решением будет хранить эту дату непосредственно в регистре сведений.

Алгоритм реализации:

  1. Добавим в регистр сведений ДокументыРеализацииСерий новый реквизит ДатаДокументаПродажи (тип Дата).
  2. В модуле набора записей регистра или в коде, отвечающем за запись в этот регистр, обеспечим заполнение этого поля данными из ДокументПродажи.Дата.
  3. Обновим существующие записи в базе данных (обработкой).

Теперь наш запрос станет максимально простым и быстрым, так как нам не нужно будет обращаться к таблицам документов вовсе:


ВЫБРАТЬ
    СрезПоследних.ДокументПродажи КАК ДокументПродажи,
    СрезПоследних.ДатаДокументаПродажи КАК ДатаДокументаПродажи,
    СрезПоследних.Номенклатура КАК Номенклатура
ИЗ
    РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(&Период, Серия = &Серия) КАК СрезПоследних

Решение 2: Использование специализированного Реестра документов

В современных типовых конфигурациях (таких как УТ 11, ERP 2, КА 2) часто используется механизм Реестра документов. Это специальный регистр сведений, в котором хранятся ссылки на все документы системы и их базовые реквизиты: дата, номер, сумма, пометка удаления и прочее.

Если в вашей конфигурации есть аналогичный регистр (например, РегистрСведений.РеестрДокументов), мы можем заменить обращение к составному типу соединением с этой «плоской» таблицей. Разберем этот пример:


ВЫБРАТЬ
    Срез.ДокументПродажи КАК ДокументПродажи,
    ЕСТЬNULL(Реестр.ДатаДокумента, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаДокументаПродажи
ИЗ
    РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(, Серия = &Серия) КАК Срез
ЛЕВОЕ СОЕДИНЕНИЕ
    РегистрСведений.РеестрДокументов КАК Реестр
ПО
    Срез.ДокументПродажи = Реестр.Ссылка

Важный момент: Соединение с одной физической таблицей регистра сведений всегда работает быстрее, чем соединение с десятками таблиц документов через точку.

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

Если изменить структуру метаданных нельзя, а реестра документов в системе нет, следует использовать конструкцию ВЫРАЗИТЬ. Это позволяет «подсказать» платформе, какие именно типы документов нас интересуют, тем самым ограничивая количество соединений в SQL.

Рассмотрим, как это выглядит на практике, если мы знаем, что в продаже могут участвовать только три типа документов:


ВЫБРАТЬ
    Срез.ДокументПродажи КАК ДокументПродажи,
    ВЫБОР
        КОГДА Срез.ДокументПродажи ССЫЛКА Документ.РеализацияТоваровУслуг
            ТОГДА ВЫРАЗИТЬ(Срез.ДокументПродажи КАК Документ.РеализацияТоваровУслуг).Дата
        КОГДА Срез.ДокументПродажи ССЫЛКА Документ.ОтчетРозничныхПродаж
            ТОГДА ВЫРАЗИТЬ(Срез.ДокументПродажи КАК Документ.ОтчетРозничныхПродаж).Дата
        ИНАЧЕ ЕСТЬNULL(Срез.ДокументПродажи.Дата, ДАТАВРЕМЯ(1, 1, 1))
    КОНЕЦ КАК ДатаДокументаПродажи
ИЗ
    РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(, Серия = &Серия) КАК Срез

Хотя этот код кажется громоздким, для СУБД он гораздо «прозрачнее». Если же типов слишком много, можно применить метод двух этапов:

  1. Выгрузим во временную таблицу только ссылки на документы из среза последних.
  2. Вторым запросом получим даты только для тех типов документов, которые реально присутствуют в этой временной таблице, используя объединение результатов (ОБЪЕДИНИТЬ ВСЕ) по конкретным таблицам документов.

Рекомендация по работе с виртуальными таблицами

Проанализируем еще одну архитектурную рекомендацию: не выполнять соединения с виртуальными таблицами напрямую. Вместо этого лучше сначала поместить результат работы виртуальной таблицы (например, СрезПоследних) во временную таблицу, а уже затем соединять её с другими данными.

Выясним причину: виртуальная таблица — это вложенный подзапрос. Когда SQL-сервер видит соединение сложного подзапроса с десятком других таблиц (через точку или явно), он часто ошибается в выборе индексов. Разрыв запроса на две части с использованием ПОМЕСТИТЬ позволяет оптимизатору построить два простых и быстрых плана вместо одного «монструозного».

Посмотрим на итоговый рекомендуемый шаблон запроса:


// 1. Получаем срез последних во временную таблицу
ВЫБРАТЬ
    Срез.ДокументПродажи КАК ДокументПродажи,
    Срез.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ втСрез
ИЗ
    РегистрСведений.ДокументыРеализацииСерий.СрезПоследних(&Период, Серия = &Серия) КАК Срез
;
// 2. Получаем дату, соединяясь только с нужными таблицами или реестром
ВЫБРАТЬ
    втСрез.ДокументПродажи,
    втСрез.Номенклатура,
    ЕСТЬNULL(Реестр.ДатаДокумента, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаДокументаПродажи
ИЗ
    втСрез КАК втСрез
ЛЕВОЕ СОЕДИНЕНИЕ
    РегистрСведений.РеестрДокументов КАК Реестр
ПО
    втСрез.ДокументПродажи = Реестр.Ссылка

Подведем итоги: для достижения максимальной производительности в 1С при работе с составными типами всегда старайтесь либо денормализовать данные (хранить дату в самом регистре), либо использовать промежуточные плоские таблицы (реестры), либо явно ограничивать типы через ВЫРАЗИТЬ.

← На главную