В практике разработки на платформе 1С:Предприятие часто встречаются задачи, связанные с получением данных из регистров накопления (оборотов) одновременно с получением цен или других характеристик из регистров сведений. На первый взгляд простой запрос может скрывать в себе "мины замедленного действия", которые приведут к катастрофическому падению производительности на реальных базах данных. Разберем детально представленный запрос, выявим его слабые места и определим, как сделать его рабочим и быстрым.
Рассмотрим блок выбора полей в начале запроса. Проанализируем конструкцию ТоварыНаСкладахОбороты.Регистратор.Номер. Поле Регистратор в регистрах накопления почти всегда имеет составной тип данных. Это означает, что в нем могут храниться ссылки на десятки различных видов документов (Поступление товаров, Реализация, Списание и т.д.).
Выясним причину, почему это плохо: когда платформа 1С видит обращение через точку к полю составного типа, она генерирует SQL-запрос, содержащий LEFT JOIN со всеми таблицами документов, которые входят в этот составной тип. Если в вашей конфигурации 50 документов могут делать движения по этому регистру, СУБД выполнит 50 лишних соединений. Это приводит к раздуванию плана запроса и замедлению выполнения в десятки раз.
Решение: Если вам действительно необходимы данные регистратора, следует использовать конструкцию ВЫРАЗИТЬ(Регистратор КАК Документ.ВашТип).Номер, либо, что еще лучше, получать данные регистратора из физической таблицы после помещения основных движений во временную таблицу.
Посмотрим на секцию ГДЕ в исходном коде, где выполняется фильтрация по артикулу номенклатуры. Проанализируем ситуацию: использование условий в секции ГДЕ для виртуальных таблиц (таких как Обороты) является грубой ошибкой. В этом случае СУБД сначала выберет все записи из регистра за период, рассчитает по ним итоги, и только потом отсечет лишние по условию Артикул.
Разберем правильный подход: фильтры необходимо передавать в параметры виртуальной таблицы. Однако, так как фильтр идет по реквизиту (Артикул), а не по самой ссылке, напрямую передать его в параметры нельзя. Правильный алгоритм действий будет следующим:
Номенклатура, чей артикул соответствует списку.Условие виртуальной таблицы Обороты.Это позволит системе прочитать из базы только нужные данные, значительно сократив нагрузку на дисковую подсистему.
Рассмотрим соединение с регистром ЦеныНоменклатуры. В запросе используется попытка получить максимальный период через подзапрос в условии ПО. Рассмотрим подробнее, к чему это приведет:
Во-первых, использование МАКСИМУМ внутри соединения заставляет СУБД выполнять вложенные циклы для каждой строки оборотов. Во-вторых, условие ЦеныПрошлогоМесяца.Период > НАЧАЛОПЕРИОДА(...) логически опасно. Если для одной номенклатуры в течение месяца было два изменения цены, левое соединение найдет обе записи (если они обе подходят под условия), что приведет к дублированию строк. В результате суммы КоличествоПриход и КоличествоРасход в итоговом отчете будут задвоены или затроены.
Проанализируем верный способ получения цен. Для получения актуальной цены на момент движения лучше всего использовать следующий алгоритм:
Выясним, как сделать запрос профессиональным. Разберем по шагам структуру оптимального решения. Вместо одного огромного запроса с подзапросами, мы должны использовать Менеджер временных таблиц.
Посмотрим на пример того, как должна выглядеть структура запроса (схематично):
// Шаг 1: Подготовка списка номенклатуры
ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура ГДЕ Артикул В (&МассивАртикулов) ПОМЕСТИТЬ ВТ_СписокТоваров;
// Шаг 2: Получение оборотов с фильтром по ВТ
ВЫБРАТЬ ... ПОМЕСТИТЬ ВТ_Обороты ИЗ РегистрНакопления.ТоварыНаСкладах.Обороты(&Начало, &Конец, Регистратор, Номенклатура В (ВЫБРАТЬ Ссылка ИЗ ВТ_СписокТоваров)) КАК Т;
// Шаг 3: Соединение с ценами (через СрезПоследних или подготовленную ВТ цен)
ВЫБРАТЬ ... ИЗ ВТ_Обороты КАК Обороты ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&КонецПериода, Номенклатура В (ВЫБРАТЬ Ссылка ИЗ ВТ_СписокТоваров)) КАК Цены;
Важный момент: обратите внимание на функцию ВЫРАЗИТЬ. В исходном запросе она применена к СерияНоменклатуры. Это имеет смысл только если поле имеет составной тип. Если поле типизировано жестко, ВЫРАЗИТЬ не требуется. Однако, как мы разбирали выше, ВЫРАЗИТЬ критически необходим при обращении к полям Регистратора, если вы не вынесли их получение в отдельный шаг.
Проанализировав ситуацию, можно сделать вывод, что исходный запрос в сообщении 0 является нерабочим для высоконагруженных систем. Он вернет неверные количественные данные при наличии нескольких цен и будет работать крайне медленно из-за разыменования ссылок составного типа Регистратор.
Основные рекомендации по исправлению:
Регистратора в основном теле запроса без использования ВЫРАЗИТЬ.Период, Условие).ПО при соединении с большими таблицами, это "убивает" индексную оптимизацию SQL-сервера.ЕСТЬNULL при получении данных из правых таблиц в ЛЕВОМ СОЕДИНЕНИИ, чтобы избежать пустых значений в арифметических операциях.