В процессе разработки часто возникают задачи, требующие получения сложных данных из системы. При этом написанный запрос может выполняться дольше ожидаемого, даже если на первый взгляд кажется, что время выполнения в доли секунды приемлемо. Такие "небольшие" задержки, будучи частью большого алгоритма, могут приводить к значительному замедлению работы всей системы. Рассмотрим на конкретном примере, как найти и устранить узкие места в пакетном запросе 1С, который получает данные о заказах и последнюю цену поступления номенклатуры.
Исходная ситуация: есть сложный пакетный запрос, который выполняется около 0,4 секунды. Автор отмечает, что это лишь часть большого процесса, который в целом стал работать значительно медленнее — с 4 секунд до 2-3 минут. Проанализируем ключевые части этого запроса и разберем, какие конструкции приводят к падению производительности.
Одним из самых узких мест в запросе является получение даты документа-регистратора через точку. Посмотрим на фрагмент кода из запроса автора:
ВЫБРАТЬ
ЗапасыОстаткиИОбороты.Номенклатура КАК Номенклатура,
МАКСИМУМ(ЗапасыОстаткиИОбороты.Регистратор.Дата) КАК РегистраторДата
ПОМЕСТИТЬ ВТ_Поступления
ИЗ
РегистрНакопления.Запасы.ОстаткиИОбороты(...) КАК ЗапасыОстаткиИОбороты
ГДЕ
...
И ЗапасыОстаткиИОбороты.Регистратор.Дата <= &ДатаЗаказа
...
На первый взгляд, это простой способ получить дату документа. Однако "под капотом" платформа 1С выполняет неявное соединение (implicit join). Поле Регистратор в регистре накопления имеет составной тип данных. Это означает, что регистратором может быть документ любого из нескольких определенных типов (ПриходнаяНакладная, РасходнаяНакладная, СборкаЗапасов и т.д.).
Когда мы обращаемся к Регистратор.Дата, система не знает заранее, из таблицы какого именно документа нужно взять дату. Поэтому она вынуждена соединять таблицу движений регистра со всеми возможными таблицами документов-регистраторов. Это крайне ресурсоемкая операция, особенно если в регистре много движений и много типов регистраторов.
Решение: Вместо Регистратор.Дата необходимо использовать поле Период. В 99.9% случаев дата документа-регистратора и период, которым он делает движения в регистре, совпадают. Поле Период находится непосредственно в таблице движений регистра, хорошо проиндексировано и не требует никаких дополнительных соединений.
Заменим обращение к дате регистратора на поле Период в двух проблемных запросах:
Было в запросе на поиск последней даты поступления:
...
МАКСИМУМ(ЗапасыОстаткиИОбороты.Регистратор.Дата) КАК РегистраторДата
...
И ЗапасыОстаткиИОбороты.Регистратор.Дата <= &ДатаЗаказа
...
Стало:
...
МАКСИМУМ(ЗапасыОстаткиИОбороты.Период) КАК ДатаПоступления
...
И ЗапасыОстаткиИОбороты.Период <= &ДатаЗаказа
...
Было в запросе на получение цены:
...
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПОСТУПЛЕНИЯ КАК ВТ_ПОСТУПЛЕНИЯ
ПО ЗапасыОстаткиИОбороты.Номенклатура = ВТ_ПОСТУПЛЕНИЯ.Номенклатура
И ЗапасыОстаткиИОбороты.Регистратор.Дата = ВТ_ПОСТУПЛЕНИЯ.РегистраторДата
...
Стало:
...
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Поступления КАК ВТ_Поступления
ПО ЗапасыОстаткиИОбороты.Номенклатура = ВТ_Поступления.Номенклатура
И ЗапасыОстаткиИОбороты.Период = ВТ_Поступления.ДатаПоступления
...
Эта простая замена кардинально ускорит выполнение запроса, так как из плана запроса СУБД исчезнут лишние и очень "тяжелые" соединения.
Виртуальные таблицы, такие как ОстаткиИОбороты, — мощный инструмент. Но их нужно использовать правильно. Проанализируем еще один фрагмент:
ИЗ
РегистрНакопления.Запасы.ОстаткиИОбороты(
,
,
Регистратор,
,
Номенклатура В
(ВЫБРАТЬ ... )) КАК ЗапасыОстаткиИОбороты
ГДЕ
ТИПЗНАЧЕНИЯ(ЗапасыОстаткиИОбороты.Регистратор) = ТИП(Документ.ПриходнаяНакладная)
И ЗапасыОстаткиИОбороты.Регистратор.Дата <= &ДатаЗаказа
Здесь автор частично использовал параметры виртуальной таблицы (для отбора по номенклатуре), но ключевые условия по типу регистратора и дате вынес в секцию ГДЕ. Это неоптимально. Платформа 1С сначала сформирует виртуальную таблицу со всеми движениями по указанной номенклатуре, и только потом к полученному большому набору данных применит фильтры из ГДЕ.
Решение: Максимально возможное количество условий нужно передавать напрямую в параметры виртуальной таблицы. Это позволяет платформе построить на уровне СУБД гораздо более эффективный запрос, который будет выбирать из физических таблиц только нужные данные.
Перенесем условие по типу регистратора в параметры виртуальной таблицы. Для этого используется специальный параметр `Условие`.
Было:
ИЗ
РегистрНакопления.Запасы.ОстаткиИОбороты( , , Регистратор, , Номенклатура В (...)) КАК ЗапасыОстаткиИОбороты
ГДЕ
ТИПЗНАЧЕНИЯ(ЗапасыОстаткиИОбороты.Регистратор) = ТИП(Документ.ПриходнаяНакладная)
...
Стало:
ИЗ
РегистрНакопления.Запасы.ОстаткиИОбороты(
&НачалоПериода,
&КонецПериода,
Регистратор,
,
Регистратор ССЫЛКА Документ.ПриходнаяНакладная И Номенклатура В (...)
) КАК ЗапасыОстаткиИОбороты
Важные моменты:
Регистратор ССЫЛКА Документ.ПриходнаяНакладная прямо в параметры. Это самый эффективный способ фильтрации по типу документа.&НачалоПериода, &КонецПериода), даже если они очень широкие. Получение оборотов за весь период существования базы — плохая практика. Если точный период неизвестен, можно установить, например, начало времен ('00010101') и дату из параметра &ДатаЗаказа.Номенклатура В (...) часто более производительным решением является сначала получить список номенклатуры во временную таблицу, проиндексировать её и затем соединять с виртуальной таблицей.Цель запроса — найти последнюю цену поступления. Автор пытается сделать это следующим выражением:
ВЫРАЗИТЬ(МАКСИМУМ(ЗапасыОстаткиИОбороты.СуммаПриход) / МАКСИМУМ(ЗапасыОстаткиИОбороты.КоличествоПриход) КАК ЧИСЛО(15, 2))
Это выражение содержит логическую ошибку. Представим, что в последнем документе поступления (с максимальной датой) было две строки:
Запрос сгруппирует данные по номенклатуре и дате. Но если бы по одной номенклатуре в один момент времени было несколько движений, агрегатные функции сработали бы некорректно. Они взяли бы максимальную сумму из всех движений и разделили бы её на максимальное количество. Это не цена, а случайное число.
Правильный способ — получить сумму и количество из одной и той же строки движения или, если нужна итоговая цена по документу, использовать СУММА(СуммаПриход) / СУММА(КоличествоПриход).
Алгоритм получения последней цены должен быть таким:
ВТ_Поступления получаем для каждой номенклатуры максимальный Период поступления (как мы уже разобрали выше).ВТ_Поступления) с таблицей оборотов регистра по двум полям: Номенклатура и Период.СУММА() для корректного расчета средней цены за этот последний период.
ВЫБРАТЬ
ЗапасыОбороты.Номенклатура КАК Номенклатура,
ВЫБОР
КОГДА СУММА(ЗапасыОбороты.КоличествоПриход) > 0
ТОГДА СУММА(ЗапасыОбороты.СуммаПриход) / СУММА(ЗапасыОбороты.КоличествоПриход)
ИНАЧЕ 0
КОНЕЦ КАК Цена
ИЗ
РегистрНакопления.Запасы.Обороты( , , , Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_Поступления)) КАК ЗапасыОбороты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Поступления КАК ВТ_Поступления
ПО ЗапасыОбороты.Номенклатура = ВТ_Поступления.Номенклатура
И ЗапасыОбороты.Период = ВТ_Поступления.ДатаПоступления
СГРУППИРОВАТЬ ПО
ЗапасыОбороты.Номенклатура
Обратите внимание: мы использовали виртуальную таблицу Обороты вместо ОстаткиИОбороты, так как нам не нужны остатки, а только обороты за конкретный период, что может быть еще немного быстрее.
Проанализировав запрос, мы выявили несколько классических ошибок, приводящих к снижению производительности. Применение предложенных исправлений позволит значительно ускорить его выполнение.
Запомним основные правила оптимизации запросов в 1С:
Регистратор.Дата используйте Период.ГДЕ.МАКСИМУМ, СУММА) используются корректно и дают тот результат, который вы ожидаете.ПОЛНОЕ СОЕДИНЕНИЕ с осторожностью. Это одна из самых дорогих операций. Часто ее можно заменить на комбинацию ЛЕВОЕ СОЕДИНЕНИЕ и ОБЪЕДИНИТЬ ВСЕ, что может дать более оптимальный план выполнения запроса.Даже небольшое, на первый взгляд, время выполнения запроса (0.4 секунды) может быть признаком серьезных архитектурных проблем. По мере роста базы данных время выполнения такого "неоптимального" запроса будет расти нелинейно, что в итоге приведет к ощутимым "тормозам" в работе пользователей.