Как оптимизировать сложный запрос к регистрам 1С и исправить ошибки производительности?

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

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

Анализ проблем обращения к полям через точку (Разыменование ссылок)

Рассмотрим блок выбора полей в начале запроса. Проанализируем конструкцию ТоварыНаСкладахОбороты.Регистратор.Номер. Поле Регистратор в регистрах накопления почти всегда имеет составной тип данных. Это означает, что в нем могут храниться ссылки на десятки различных видов документов (Поступление товаров, Реализация, Списание и т.д.).

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

Решение: Если вам действительно необходимы данные регистратора, следует использовать конструкцию ВЫРАЗИТЬ(Регистратор КАК Документ.ВашТип).Номер, либо, что еще лучше, получать данные регистратора из физической таблицы после помещения основных движений во временную таблицу.

Оптимизация условий фильтрации: ГДЕ против параметров виртуальной таблицы

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

Разберем правильный подход: фильтры необходимо передавать в параметры виртуальной таблицы. Однако, так как фильтр идет по реквизиту (Артикул), а не по самой ссылке, напрямую передать его в параметры нельзя. Правильный алгоритм действий будет следующим:

  1. Создадим временную таблицу, в которую выберем все ссылки Номенклатура, чей артикул соответствует списку.
  2. Передадим эту временную таблицу (или массив ссылок) в параметр Условие виртуальной таблицы Обороты.

Это позволит системе прочитать из базы только нужные данные, значительно сократив нагрузку на дисковую подсистему.

Логические ошибки при получении цен и дублирование данных

Рассмотрим соединение с регистром ЦеныНоменклатуры. В запросе используется попытка получить максимальный период через подзапрос в условии ПО. Рассмотрим подробнее, к чему это приведет:

Во-первых, использование МАКСИМУМ внутри соединения заставляет СУБД выполнять вложенные циклы для каждой строки оборотов. Во-вторых, условие ЦеныПрошлогоМесяца.Период > НАЧАЛОПЕРИОДА(...) логически опасно. Если для одной номенклатуры в течение месяца было два изменения цены, левое соединение найдет обе записи (если они обе подходят под условия), что приведет к дублированию строк. В результате суммы КоличествоПриход и КоличествоРасход в итоговом отчете будут задвоены или затроены.

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

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

Использование временных таблиц и типизация

Выясним, как сделать запрос профессиональным. Разберем по шагам структуру оптимального решения. Вместо одного огромного запроса с подзапросами, мы должны использовать Менеджер временных таблиц.

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


// Шаг 1: Подготовка списка номенклатуры
ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура ГДЕ Артикул В (&МассивАртикулов) ПОМЕСТИТЬ ВТ_СписокТоваров;

// Шаг 2: Получение оборотов с фильтром по ВТ
ВЫБРАТЬ ... ПОМЕСТИТЬ ВТ_Обороты ИЗ РегистрНакопления.ТоварыНаСкладах.Обороты(&Начало, &Конец, Регистратор, Номенклатура В (ВЫБРАТЬ Ссылка ИЗ ВТ_СписокТоваров)) КАК Т;

// Шаг 3: Соединение с ценами (через СрезПоследних или подготовленную ВТ цен)
ВЫБРАТЬ ... ИЗ ВТ_Обороты КАК Обороты ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&КонецПериода, Номенклатура В (ВЫБРАТЬ Ссылка ИЗ ВТ_СписокТоваров)) КАК Цены;

Важный момент: обратите внимание на функцию ВЫРАЗИТЬ. В исходном запросе она применена к СерияНоменклатуры. Это имеет смысл только если поле имеет составной тип. Если поле типизировано жестко, ВЫРАЗИТЬ не требуется. Однако, как мы разбирали выше, ВЫРАЗИТЬ критически необходим при обращении к полям Регистратора, если вы не вынесли их получение в отдельный шаг.

Итоги разбора

Проанализировав ситуацию, можно сделать вывод, что исходный запрос в сообщении 0 является нерабочим для высоконагруженных систем. Он вернет неверные количественные данные при наличии нескольких цен и будет работать крайне медленно из-за разыменования ссылок составного типа Регистратор.

Основные рекомендации по исправлению:

← На главную