При построении аналитических отчетов часто возникает необходимость найти не только первую операцию контрагента за указанный интервал, но и предшествующую ей дату совершения продажи или взаиморасчетов. Рассмотрим типичные ошибки, возникающие при попытке решить эту задачу через самосоединение виртуальных таблиц, и разберем корректные и оптимальные подходы.
Проанализируем типичный ошибочный запрос, в котором используется соединение виртуальной таблицы оборотов самой с собой:
ВЫБРАТЬ
МАКСИМУМ(ПродажиОбороты1.Период) КАК ПредыдущаяДата,
МИНИМУМ(ВложенныйЗапрос.ПерваяДата) КАК ПерваяДата,
ВложенныйЗапрос.Контрагент
ИЗ
(ВЫБРАТЬ
МИНИМУМ(ПродажиОбороты.Период) КАК ПерваяДата,
ПродажиОбороты.Контрагент КАК Контрагент
ИЗ
РегистрНакопления.Продажи.Обороты(&НП, &КП, Регистратор, ) КАК ПродажиОбороты
СГРУППИРОВАТЬ ПО
ПродажиОбороты.Контрагент) КАК ВложенныйЗапрос
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Продажи.Обороты(, , Регистратор, ) КАК ПродажиОбороты1
ПО ВложенныйЗапрос.Контрагент = ПродажиОбороты1.Контрагент
И ВложенныйЗапрос.ПерваяДата > ПродажиОбороты1.Период
СГРУППИРОВАТЬ ПО
ВложенныйЗапрос.Контрагент
Выясним причину некорректного поведения и низкой производительности такого запроса:
Обороты предназначена для подсчета агрегированных ресурсов. Если нам требуются только даты движений без ресурсов, обращение к таблице оборотов с периодичностью Регистратор создает неоправданную нагрузку на СУБД.РегистрНакопления.Продажи.Обороты(, , Регистратор, ) без указания параметров дат считывает всю историю регистра. Соединение по нестрогому равенству > приводит к квадратичному росту объема обрабатываемых данных.Период хранит дату и время с точностью до секунды. Если в одну секунду было несколько документов, строгое условие ПерваяДата > Период отсекает все записи этой секунды. Строгое понятие «предыдущей записи» в регистре накопления определяется только с учетом полей Период, Регистратор (или его моментом времени) и НомерСтроки.Если нам требуется найти дату последней продажи, предшествующей началу расчетного периода &НП, нет необходимости связывать первую дату периода с историей. Достаточно ограничить выборку параметров даты &НП напрямую. Разберем по шагам реализацию через временные таблицы и обращение к физической таблице движений РегистрНакопления.Продажи.
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
МИНИМУМ(Продажи.Период) КАК ПерваяДата
ПОМЕСТИТЬ ВТ_ПервыеДаты
ИЗ
РегистрНакопления.Продажи КАК Продажи
ГДЕ
Продажи.Период МЕЖДУ &НП И &КП
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
МАКСИМУМ(Продажи.Период) КАК ПредыдущаяДата
ПОМЕСТИТЬ ВТ_ПредыдущиеДаты
ИЗ
РегистрНакопления.Продажи КАК Продажи
ГДЕ
Продажи.Период < &НП
И Продажи.Контрагент В
(ВЫБРАТЬ
ВТ_ПервыеДаты.Контрагент
ИЗ
ВТ_ПервыеДаты)
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_ПервыеДаты.Контрагент КАК Контрагент,
ВТ_ПервыеДаты.ПерваяДата КАК ПерваяДата,
ЕСТЬNULL(ВТ_ПредыдущиеДаты.ПредыдущаяДата, ДАТАВРЕМЯ(1, 1, 1)) КАК ПредыдущаяДата
ИЗ
ВТ_ПервыеДаты КАК ВТ_ПервыеДаты
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПредыдущиеДаты КАК ВТ_ПредыдущиеДаты
ПО ВТ_ПервыеДаты.Контрагент = ВТ_ПредыдущиеДаты.Контрагент
В этом запросе СУБД использует индексы по полям Период и Контрагент, исключая полное сканирование таблицы.
Если платформа поддерживает оконные функции, задачу получения предыдущей даты можно решить без тяжелых группировок и самосоединений. Посмотрим на пример с использованием функции LAG():
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
Продажи.Период КАК Период,
LAG(Продажи.Период, 1) OVER (PARTITION BY Продажи.Контрагент ORDER BY Продажи.Период, Продажи.Регистратор) КАК ПредыдущаяДата
ПОМЕСТИТЬ ВТ_ДвиженияСПредыдущейДатой
ИЗ
РегистрНакопления.Продажи КАК Продажи
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Контрагент КАК Контрагент,
МИНИМУМ(ВТ.Период) КАК ПерваяДата,
МИНИМУМ(ВТ.ПредыдущаяДата) КАК ПредыдущаяДата
ИЗ
ВТ_ДвиженияСПредыдущейДатой КАК ВТ
ГДЕ
ВТ.Период МЕЖДУ &НП И &КП
СГРУППИРОВАТЬ ПО
ВТ.Контрагент
Рассмотрим преимущества данного подхода: оконная функция LAG() берет значение даты из физически предшествующей строки в рамках разреза Контрагент с корректной сортировкой по полям Период и Регистратор, что полностью исключает коллизии при совпадении времени записей.