Как ускорить работу отчета по статьям затрат в 1С: от неоптимальных выборок к профессиональным запросам

Программист 1С v8.3 (Обычные формы) 1C:Бухгалтерия Бухгалтерский учет IT и автоматизация бизнеса
← На главную

Каждый начинающий разработчик 1С рано или поздно сталкивается с ситуацией, когда написанный им отчет отлично работает на тестовой базе из трех документов, но «умирает» или безнадежно зависает на реальной базе клиента. Клиент нервничает, требует ускорить работу, а код отчета выглядит как длинная «портянка» с циклами, выборками и ручным выводом областей в табличный документ.

В этой статье мы подробно разберем классическую проблему производительности на примере реального отчета по статьям затрат. Мы выясним, почему исходный код работал крайне медленно, разберем ключевые ошибки проектирования и по шагам пройдем путь оптимизации — от процедурного обхода документов до составления быстрого, профессионального запроса и использования преимуществ Системы Компоновки Данных (СКД).

Анализ исходного алгоритма: почему отчет безбожно тормозит

Для начала давайте внимательно проанализируем исходный код, который был представлен в первом сообщении темы. Процедура ПоСтатьямЗатрат выполняет чтение данных и формирование отчета вручную. Выделим основные архитектурные ошибки, которые приводят к катастрофическому падению производительности.

  1. Выборка документов за период в цикле.

    Использование конструкции Документы.ПоступлениеТоваровУслуг.Выбрать(Дата1, КонецДня(Дата2)) — это наследие старой платформы 1С:Предприятие 7.7. На платформе 1С 8.х такой подход крайне неэффективен. При объектном обходе СУБД считывает из базы абсолютно все реквизиты каждого документа (включая шапку, табличные части, комментарии, служебные свойства), даже если из документа нам нужны всего два-три поля. Это создает огромный избыточный трафик между сервером СУБД и сервером приложений 1С.

  2. Проблема N+1 запросов при обходе табличных частей.

    Внутри основного цикла по документам выполняется вложенный цикл по табличной части: Для Каждого ТекСтр Из ВыборкаДокументов.Услуги Цикл. В платформе 1С объектные данные табличных частей считываются лениво (по требованию). Это означает, что при обращении к свойству Услуги для каждого отдельного документа платформа выполняет дополнительный одиночный запрос к СУБД для чтения строк его табличной части. Если в выборку попало 1000 документов, система сделает еще 1000 запросов к базе данных. Это классическая проблема N+1, которая гарантированно уничтожает производительность.

  3. Получение курса валюты в цикле.

    Посмотрим на строчку кода: ОбщегоНазначения.ПолучитьКурсВалюты(Дол, ВыборкаДокументов.Дата).Курс. Эта функция вызывается внутри цикла по строкам табличной части каждого документа, если валюта договора отличается от регламентированной. Получение курса валюты на определенную дату — это еще один запрос к регистру сведений КурсыВалют. Если у вас в базе 5 000 строк услуг, система сделает 5 000 отдельных запросов к регистру сведений в цикле! Это грубейшая ошибка оптимизации.

  4. Обращение через точку к реквизитам и родителям объектов.

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

  5. Сложные тройные циклы при выводе в табличный документ.

    После подготовки таблиц значений ТЗ, ТЗСтЗат и ТЗГр, разработчик пытается вручную построить кросс-таблицу (матрицу), используя вложенные циклы:

    
    Для Каждого ТекСтр1 Из ТЗГр Цикл
        ...
        Для Каждого ТекСтр2 Из ТЗСтЗат Цикл
            ...
            Для Каждого ТекСтр3 Из ТЗ Цикл
                Если (ТекСтр3.Группа=ТекСтр1.Группа) И (ТекСтр3.StrЗатрат=ТекСтр2.StrЗатрат) Тогда
                    ...
                КонецЕсли;
            КонецЦикла;
        КонецЦикла;
    КонецЦикла;
    

    Это классический алгоритм с вычислительной сложностью O(N * M * K). На больших объемах данных количество итераций этого цикла будет исчисляться миллионами, что приведет к зависанию интерфейса клиента.

Шаг 1: Отказываемся от документов в пользу регистров

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

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

Шаг 2: Анализируем и исправляем ошибки в первом запросе

В процессе обсуждения на форуме (сообщение №8) автор попытался написать запрос к регистру бухгалтерии, но допустил несколько критических ошибок, которые могут сделать работу отчета еще медленнее. Рассмотрим этот запрос подробнее:


ВЫБРАТЬ
    ВЫБОР
        КОГДА ХозрасчетныйОбороты.Счет В ИЕРАРХИИ (&Счет94)
            ТОГДА ХозрасчетныйОбороты.Субконто2
        ИНАЧЕ ХозрасчетныйОбороты.Субконто3
    КОНЕЦ КАК СтатьяЗатрат,
    ХозрасчетныйОбороты.Регистратор.Группа КАК Группа,
    ХозрасчетныйОбороты.СуммаОборот КАК Сумма,
    ХозрасчетныйОбороты.Регистратор
ИЗ
    РегистрБухгалтерии.Хозрасчетный.Обороты(&Начало, &Конец, Авто, Счет В ИЕРАРХИИ (&Счет94, &Счет20), , , , ) КАК ХозрасчетныйОбороты
ГДЕ
    НЕ ХозрасчетныйОбороты.Регистратор.Группа ЕСТЬ NULL

Давайте выясним причины, по которым данный запрос является неоптимальным, и разберем, как его исправить.

Проблема разыменования составных полей (Регистратор.Группа)

Поле Регистратор в регистре бухгалтерии является типом ДокументСсылка (составной тип данных), который может содержать ссылки на сотни различных видов документов в системе. Когда мы пишем в запросе ХозрасчетныйОбороты.Регистратор.Группа, платформа 1С на уровне СУБД вынуждена выполнить неявное левое соединение (LEFT JOIN) со всеми физическими таблицами документов, которые могут выступать в роли регистратора для этого регистра!

Если в вашей конфигурации зарегистрировано, например, 80 видов документов, СУБД попытается объединить 80 таблиц. Это приводит к построению чудовищных планов запросов SQL, огромному потреблению оперативной памяти и падению производительности сервера. Для решения этой проблемы необходимо либо ограничить тип регистратора с помощью функции ВЫРАЗИТЬ(), либо, что еще лучше, вынести выборку данных во временную таблицу и соединить её только с конкретной таблицей документа ПоступлениеТоваровУслуг.

Проблема использования условия В ИЕРАРХИИ

Условие Счет В ИЕРАРХИИ (&Счет94, &Счет20) в параметрах виртуальной таблицы работает медленно, так как СУБД приходится анализировать структуру плана счетов для поиска всех подчиненных счетов. Намного эффективнее передавать в параметры запроса массив конкретных счетов (выбранных заранее) или использовать раздельные запросы, объединенные через оператор ОБЪЕДИНИТЬ ВСЕ.

Использование конструкции ВЫБОР для определения субконто

Автор пытается динамически определить, из какого субконто (второго или третьего) брать статью затрат в зависимости от счета. Использование логики ВЫБОР КОГДА... в полях запроса не позволяет СУБД использовать индексы и усложняет оптимизацию плана выполнения. Платформа 1С предлагает для этой задачи изящное и высокопроизводительное решение — использование параметра ВидыСубконто.

Шаг 3: Используем магию параметра «ВидыСубконто»

Рассмотрим подробнее, как работает параметр ВидыСубконто в виртуальной таблице Обороты регистра бухгалтерии. Этот параметр позволяет нам передать массив или фиксированное значение предопределенного вида субконто (в нашем случае — СтатьиЗатрат).

Если мы передаем этот параметр в виртуальную таблицу, платформа автоматически выполняет перепозиционирование субконто на уровне СУБД. Независимо от того, на каком месте (первом, втором или третьем) находится субконто «Статьи затрат» на конкретном счете (на 20-м счете это может быть субконто 3, а на 26-м — субконто 2), в результате запроса статья затрат всегда будет возвращаться в поле Субконто1!

Это революционное преимущество позволяет нам полностью избавиться от громоздких условий ВЫБОР КОГДА... и сделать запрос простым, линейным и очень быстрым.

Шаг 4: Строим правильный и быстрый запрос по шагам

Давайте спроектируем оптимальный пакетный запрос, который решит все проблемы производительности. Мы разделим работу на три логических шага с использованием временных таблиц.

Шаг 4.1: Выбираем обороты во временную таблицу. На этом шаге мы быстро получаем только агрегированные обороты по нужным счетам и сразу позиционируем аналитику по статьям затрат.

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

Шаг 4.3: Соединяем временную таблицу с документом. Мы выполним точечное соединение только с одной физической таблицей документа ПоступлениеТоваровУслуг, откуда заберем реквизит Группа.

Посмотрим на пример реализации такого пакета запросов в коде 1С:


// 1. Подготовим массив счетов, чтобы избежать медленного условия "В ИЕРАРХИИ"
МассивСчетов = Новый Массив;
МассивСчетов.Добавить(ПланыСчетов.Хозрасчетный.ОбщепроизводственныеРасходы);
МассивСчетов.Добавить(ПланыСчетов.Хозрасчетный.ОсновноеПроизводство);
// Также можно программно получить все подчиненные счета, если это необходимо

// 2. Создадим массив видов субконто для позиционирования статьи затрат в Субконто1
МассивСубконто = Новый Массив;
МассивСубконто.Добавить(ПланыВидовХарактеристик.ВидыСубконтоХозрасчетные.СтатьиЗатрат);

Запрос = Новый Запрос;
Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(Дата1));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(Дата2));
Запрос.УстановитьПараметр("СписокСчетов", МассивСчетов);
Запрос.УстановитьПараметр("ВидыСубконто", МассивСубконто);
Запрос.УстановитьПараметр("ИсключаемаяГруппа", "Администрация");

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

РезультатЗапроса = Запрос.Выполнить();

Проанализируем преимущества этого запроса:

Шаг 5: Переводим отчет на Систему Компоновки Данных (СКД)

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

Система Компоновки Данных (СКД) — это мощный встроенный инструмент платформы 1С, который берет на себя всю рутинную работу по расчету итогов, группировке, сортировке и выводу данных в табличный документ. Настройка такого отчета в СКД займет не более 5 минут:

  1. Создаем новый отчет и открываем схему компоновки данных.
  2. Добавляем набор данных - запрос и вставляем туда наш оптимизированный запрос (без использования временных таблиц, так как СКД сама умеет оптимизировать простые запросы, либо оставляем пакетный запрос для максимального контроля производительности).
  3. На вкладке «Настройки» создаем структуру отчета типа «Таблица» (кросс-таблица).
  4. В строки таблицы добавляем группировку по полю Группа.
  5. В колонки таблицы добавляем группировку по полю СтатьяЗатрат.
  6. В выбранные поля (ресурсы) добавляем поле Сумма и определяем его как ресурс с функцией Сумма(Сумма).

Использование СКД дает нам колоссальные преимущества:

Сравнительный анализ решений: Было / Стало

Давайте наглядно сопоставим характеристики исходного процедурного решения и нового оптимизированного подхода на базе запросов и СКД:

Критерий сравнения Исходный код (Объектный обход) Новый подход (Запрос + СКД)
Источник данных Документы и их табличные части (медленное чтение всех реквизитов) Виртуальные таблицы оборотов регистра бухгалтерии (только нужные агрегированные данные)
Получение курсов валют В цикле по каждой строке (тысячи запросов к СУБД) Автоматически из готовых рублевых сумм проводок в регистре (0 дополнительных запросов)
Разыменование через точку Постоянное (к родителям счетов, договорам, свойствам документов) Исключено (используется строгое внутреннее соединение с одной таблицей в запросе)
Формирование кросс-таблицы Тройной цикл по таблицам значений на встроенном языке 1С Автоматически средствами платформы СКД на уровне движка СУБД
Скорость работы на больших базах Крайне низкая (зависание системы при росте объема данных) Мгновенная (доли секунды даже на сотнях тысяч проводок)

Подводя итог, можно с уверенностью сказать: оптимизация отчетов в 1С — это не просто переписывание кода, а смена парадигмы мышления. Отказываясь от процедурного перебора объектов в пользу мощного механизма запросов и декларативного описания отчетов в СКД, мы получаем надежное, масштабируемое и молниеносное решение, которое порадует даже самого требовательного клиента.

← На главную