Довольно часто разработчики сталкиваются с ситуацией, когда запрос к базе данных превосходно отрабатывает на небольшом объеме строк, но при незначительном росте объема входных данных время выполнения возрастает в сотни раз. Например, при расчете наценки по 45 позициям запрос выполняется за несколько секунд, а при 46 строках зависает на десятки минут. Проанализируем причины такого поведения и разберем варианты решения данной проблемы.
Рассмотрим исходный вариант запроса, вызвавший существенное замедление:
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.Цена КАК ЦенаПродажи
ПОМЕСТИТЬ ТаблицаТоваров
ИЗ
&ТаблицаТоваров КАК Товары
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ПериодыЦен.Номенклатура КАК Номенклатура,
ВЫБОР
КОГДА НЕ &ЦенаСНДС
ТОГДА МАКСИМУМ(ЕСТЬNULL(ПоследниеЦеныЗакупокСрезПоследних.ЦенаБезНДС, 0))
ИНАЧЕ МАКСИМУМ(ЕСТЬNULL(ПоследниеЦеныЗакупокСрезПоследних.ЦенаСНДС, 0))
КОНЕЦ КАК ЦенаЗакупки
ПОМЕСТИТЬ ЦеныЗакупки
ИЗ
(ВЫБРАТЬ
ТаблицаТоваров.Номенклатура КАК Номенклатура,
МАКСИМУМ(ПоследниеЦеныЗакупокСрезПоследних.Период) КАК Период
ИЗ
ТаблицаТоваров КАК ТаблицаТоваров
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПоследниеЦеныЗакупок.СрезПоследних(&Дата, ВидДоговора = &ВидДоговора) КАК ПоследниеЦеныЗакупокСрезПоследних
ПО ТаблицаТоваров.Номенклатура = ПоследниеЦеныЗакупокСрезПоследних.Номенклатура
СГРУППИРОВАТЬ ПО
ТаблицаТоваров.Номенклатура) КАК ПериодыЦен
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПоследниеЦеныЗакупок.СрезПоследних(&Дата, ВидДоговора = &ВидДоговора) КАК ПоследниеЦеныЗакупокСрезПоследних
ПО ПериодыЦен.Номенклатура = ПоследниеЦеныЗакупокСрезПоследних.Номенклатура
И ПериодыЦен.Период = ПоследниеЦеныЗакупокСрезПоследних.Период
СГРУППИРОВАТЬ ПО
ПериодыЦен.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.ЦенаПродажи КАК ЦенаПродажи,
ЦеныЗакупки.ЦенаЗакупки КАК ЦенаЗакупки,
ВЫБОР
КОГДА ЕСТЬNULL(ЦеныЗакупки.ЦенаЗакупки, 0) = 0
ТОГДА 999999999
ИНАЧЕ (Товары.ЦенаПродажи - ЦеныЗакупки.ЦенаЗакупки) / ЦеныЗакупки.ЦенаЗакупки * 100
КОНЕЦ КАК Наценка
ИЗ
ТаблицаТоваров КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ ЦеныЗакупки КАК ЦеныЗакупки
ПО Товары.Номенклатура = ЦеныЗакупки.Номенклатура
ГДЕ
ЕСТЬNULL(ЦеныЗакупки.ЦенаЗакупки, 0) <> 0
Выясним причину, почему этот код работает неэффективно:
СрезПоследних без отбора по номенклатуре в параметрах таблицы. В СУБД это разворачивается в расчет среза по всему объему регистра.ПериодыЦен с виртуальной таблицей среза мешает оптимизатору СУБД построить эффективный план.tempdb и не дает преимуществ.Номенклатура не типизировано строго ссылочным типом, на стороне СУБД происходит неявное приведение типов (Implicit Conversion), что исключает использование индексов.Упростим запрос: избавимся от двойного обращения к виртуальной таблице, передадим список отбора непосредственно в параметры виртуальной таблицы и уберем лишние вложенные конструкции.
Посмотрим на пример переписанного запроса:
ВЫБРАТЬ
ВЫРАЗИТЬ(Товары.Номенклатура КАК Справочник.Номенклатура) КАК Номенклатура,
Товары.Цена КАК ЦенаПродажи
ПОМЕСТИТЬ ТаблицаТоваров
ИЗ
&ТаблицаТоваров КАК Товары
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
ВЫБОР
КОГДА НЕ &ЦенаСНДС
ТОГДА МАКСИМУМ(ЕСТЬNULL(ЦеныСрез.ЦенаБезНДС, 0))
ИНАЧЕ МАКСИМУМ(ЕСТЬNULL(ЦеныСрез.ЦенаСНДС, 0))
КОНЕЦ КАК ЦенаЗакупки
ПОМЕСТИТЬ ЦеныЗакупки
ИЗ
ТаблицаТоваров КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПоследниеЦеныЗакупок.СрезПоследних(
&Дата,
ВидДоговора = &ВидДоговора
И Номенклатура В (ВЫБРАТЬ РАЗЛИЧНЫЕ Т.Номенклатура ИЗ ТаблицаТоваров КАК Т)
) КАК ЦеныСрез
ПО Товары.Номенклатура = ЦеныСрез.Номенклатура
СГРУППИРОВАТЬ ПО
Товары.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.ЦенаПродажи КАК ЦенаПродажи,
ЦеныЗакупки.ЦенаЗакупки КАК ЦенаЗакупки,
(Товары.ЦенаПродажи - ЦеныЗакупки.ЦенаЗакупки) / ЦеныЗакупки.ЦенаЗакупки * 100 КАК Наценка
ИЗ
ТаблицаТоваров КАК Товары
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ЦеныЗакупки КАК ЦеныЗакупки
ПО Товары.Номенклатура = ЦеныЗакупки.Номенклатура
ГДЕ
ЦеныЗакупки.ЦенаЗакупки > 0
Разберем внесенные изменения:
ВЫРАЗИТЬ для колонки Номенклатура, что гарантирует точное совпадение типов с измерением регистра на уровне СУБД.ИНДЕКСИРОВАТЬ ПО для таблицы на несколько десятков строк.СрезПоследних.ЛЕВОЕ СОЕДИНЕНИЕ на ВНУТРЕННЕЕ СОЕДИНЕНИЕ, так как строки с отсутствующей ценой закупки всё равно отсекались блоком ГДЕ, а сложная конструкция ВЫБОР для деления на ноль стала избыточной.Если регистр содержит большое количество измерений (помимо номенклатуры), виртуальная таблица СрезПоследних вычисляет срез по всем измерениям сразу. В таких случаях гораздо быстрее обратиться напрямую к реальной таблице регистра и самостоятельно получить запись с максимальным периодом.
Рассмотрим, как реализовать этот подход:
ВЫБРАТЬ
ВЫРАЗИТЬ(Товары.Номенклатура КАК Справочник.Номенклатура) КАК Номенклатура,
МАКСИМУМ(Цены.Период) КАК Период
ПОМЕСТИТЬ МаксимальныеПериоды
ИЗ
&ТаблицаТоваров КАК Товары
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ПоследниеЦеныЗакупок КАК Цены
ПО (Цены.Период <= &Дата)
И (Цены.ВидДоговора = &ВидДоговора)
И (Товары.Номенклатура = Цены.Номенклатура)
СГРУППИРОВАТЬ ПО
Товары.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Периоды.Номенклатура КАК Номенклатура,
ВЫБОР
КОГДА НЕ &ЦенаСНДС
ТОГДА МАКСИМУМ(Цены.ЦенаБезНДС)
ИНАЧЕ МАКСИМУМ(Цены.ЦенаСНДС)
КОНЕЦ КАК ЦенаЗакупки
ПОМЕСТИТЬ ИтоговыеЦены
ИЗ
МаксимальныеПериоды КАК Периоды
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ПоследниеЦеныЗакупок КАК Цены
ПО Периоды.Номенклатура = Цены.Номенклатура
И Периоды.Период = Цены.Период
И (Цены.ВидДоговора = &ВидДоговора)
СГРУППИРОВАТЬ ПО
Периоды.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.Цена КАК ЦенаПродажи,
ИтоговыеЦены.ЦенаЗакупки КАК ЦенаЗакупки,
(Товары.Цена - ИтоговыеЦены.ЦенаЗакупки) / ИтоговыеЦены.ЦенаЗакупки * 100 КАК Наценка
ИЗ
&ТаблицаТоваров КАК Товары
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ИтоговыеЦены КАК ИтоговыеЦены
ПО Товары.Номенклатура = ИтоговыеЦены.Номенклатура
ГДЕ
ИтоговыеЦены.ЦенаЗакупки > 0
Такой вариант избавляет СУБД от необходимости разворачивать подзапросы виртуальной таблицы по всем незадействованным измерениям регистра сведений.
Начиная с версии платформы 1С 8.3.20, доступен механизм оконных функций, позволяющий получить последнюю запись без дополнительных самосоединений и виртуальных таблиц с максимальной скоростью:
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
Товары.Цена КАК ЦенаПродажи,
ВЫБОР
КОГДА НЕ &ЦенаСНДС ТОГДА Цены.ЦенаБезНДС
ИНАЧЕ Цены.ЦенаСНДС
КОНЕЦ КАК ЦенаЗакупки,
ROW_NUMBER() OVER (
PARTITION BY Цены.Номенклатура
ORDER BY Цены.Период УБЫВ
) КАК НомерСтрокиЦены
ПОМЕСТИТЬ ДанныеСреза
ИЗ
&ТаблицаТоваров КАК Товары
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ПоследниеЦеныЗакупок КАК Цены
ПО Товары.Номенклатура = Цены.Номенклатура
И (Цены.Период <= &Дата)
И (Цены.ВидДоговора = &ВидДоговора)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Данные.Номенклатура КАК Номенклатура,
Данные.ЦенаПродажи КАК ЦенаПродажи,
Данные.ЦенаЗакупки КАК ЦенаЗакупки,
(Данные.ЦенаПродажи - Данные.ЦенаЗакупки) / Данные.ЦенаЗакупки * 100 КАК Наценка
ИЗ
ДанныеСреза КАК Данные
ГДЕ
Данные.НомерСтрокиЦены = 1
И Данные.ЦенаЗакупки > 0
Если после модификации текста запроса сохраняются просадки производительности, обратим внимание на настройки регистра и СУБД:
Номенклатура должно стоять на первом месте в дереве конфигурации. Основной индекс регистра строится по порядку следования измерений. Если первым стоит Контрагент, поиск по одной лишь номенклатуре не сможет задействовать основной кластерный индекс.ВидДоговора рекомендуется установить свойство Индексировать, если по нему регулярно накладывается фильтр.