При формировании аналитических выборок и отчетов разработчики часто сталкиваются с задачей: для каждого контрагента (или участника) необходимо получить ровно три последних документа по дате. В стандартном SQL для этого традиционно применяются оконные функции, однако в языке запросов 1С функционал вроде ROW_NUMBER() OVER (PARTITION BY ...) отсутствует. Кроме того, конструкция ПЕРВЫЕ N применяется исключительно ко всему результату запроса или подзапроса, а не к отдельным группам записей.
Разберем детально, какими способами можно решить данную задачу, какие подводные камни с производительностью могут возникнуть и как правильно оптимизировать выборку для обычных запросов и Системы компоновки данных (СКД).
Проанализируем базовый вариант, который чаще всего приходит на ум разработчикам: выполнить левое соединение списка уникальных участников с выборкой документов, где в условии соединения используется вложенный подзапрос с конструкцией ПЕРВЫЕ 3.
Посмотрим на пример такого запроса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ЗаказТураУчастники.Участник КАК Участник
ПОМЕСТИТЬ ВТ_Участники
ИЗ
Документ.ЗаказТура.Участники КАК ЗаказТураУчастники
;
ВЫБРАТЬ РАЗЛИЧНЫЕ
ВТ_Участники.Участник КАК Участник,
ЗаказТураУчастники.Ссылка КАК Ссылка
ИЗ
ВТ_Участники КАК ВТ_Участники
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказТура.Участники КАК ЗаказТураУчастники
ПО (ЗаказТураУчастники.Ссылка В
(ВЫБРАТЬ ПЕРВЫЕ 3
Подзапрос.Ссылка
ИЗ
Документ.ЗаказТура.Участники КАК Подзапрос
ГДЕ
Подзапрос.Участник = ВТ_Участники.Участник
И Подзапрос.Ссылка.Проведен
И НЕ Подзапрос.Ссылка.Аннулирован
И НЕ Подзапрос.Ссылка.ПометкаУдаления
УПОРЯДОЧИТЬ ПО
Подзапрос.Ссылка.Дата УБЫВ))
Рассмотрим подробнее особенности и слабые места этого подхода:
Подзапрос.Участник = ВТ_Участники.Участник связывает внутренний запрос с текущей строкой внешней таблицы. СУБД вынуждена выполнять внутреннюю выборку с сортировкой и ограничением строк индивидуально для каждой записи, что на больших объемах данных приводит к катастрофическому падению скорости.Подзапрос.Ссылка.Проведен и Подзапрос.Ссылка.Дата при обращении к табличной части создают скрытые неявные соединения с шапкой документа. Это исключает эффективное использование индексов СУБД и приводит к полному сканированию таблиц.Если требуется получить фиксированное небольшое количество документов (ровно три) в одном запросе без потери производительности, применим технику последовательного вычисления максимумов во временных таблицах.
Разберем процесс по шагам:
Участник, Дата, Ссылка.ОБЪЕДИНИТЬ ВСЕ.
ВЫБРАТЬ
Заказ.Ссылка КАК Ссылка,
Заказ.Дата КАК Дата
ПОМЕСТИТЬ ВТ_ЗаказыШапка
ИЗ
Документ.ЗаказТура КАК Заказ
ГДЕ
Заказ.Проведен
И НЕ Заказ.Аннулирован
И НЕ Заказ.ПометкаУдаления
ИНДЕКСИРОВАТЬ ПО
Ссылка
;
ВЫБРАТЬ РАЗЛИЧНЫЕ
ТЧ.Участник КАК Участник,
Шапка.Дата КАК Дата,
Шапка.Ссылка КАК Ссылка
ПОМЕСТИТЬ ВТ_ВсеЗаказы
ИЗ
ВТ_ЗаказыШапка КАК Шапка
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаказТура.Участники КАК ТЧ
ПО Шапка.Ссылка = ТЧ.Ссылка
ИНДЕКСИРОВАТЬ ПО
Участник,
Дата,
Ссылка
;
// Шаг 1: Самый последний заказ
ВЫБРАТЬ
ВТ.Участник КАК Участник,
МАКСИМУМ(ВТ.Дата) КАК Дата
ПОМЕСТИТЬ ВТ_Заказ1_Дата
ИЗ
ВТ_ВсеЗаказы КАК ВТ
СГРУППИРОВАТЬ ПО
ВТ.Участник
;
ВЫБРАТЬ
ВТ.Участник КАК Участник,
ВТ.Дата КАК Дата,
МАКСИМУМ(ВТ.Ссылка) КАК Ссылка
ПОМЕСТИТЬ ВТ_Заказ1
ИЗ
ВТ_ВсеЗаказы КАК ВТ
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Заказ1_Дата КАК Даты
ПО ВТ.Участник = Даты.Участник
И ВТ.Дата = Даты.Дата
СГРУППИРОВАТЬ ПО
ВТ.Участник,
ВТ.Дата
ИНДЕКСИРОВАТЬ ПО
Ссылка
;
// Шаг 2: Предпоследний заказ
ВЫБРАТЬ
ВТ.Участник КАК Участник,
МАКСИМУМ(ВТ.Дата) КАК Дата
ПОМЕСТИТЬ ВТ_Заказ2_Дата
ИЗ
ВТ_ВсеЗаказы КАК ВТ
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказ1 КАК Искл
ПО ВТ.Ссылка = Искл.Ссылка
ГДЕ
Искл.Ссылка ЕСТЬ NULL
СГРУППИРОВАТЬ ПО
ВТ.Участник
;
ВЫБРАТЬ
ВТ.Участник КАК Участник,
ВТ.Дата КАК Дата,
МАКСИМУМ(ВТ.Ссылка) КАК Ссылка
ПОМЕСТИТЬ ВТ_Заказ2
ИЗ
ВТ_ВсеЗаказы КАК ВТ
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Заказ2_Дата КАК Даты
ПО ВТ.Участник = Даты.Участник
И ВТ.Дата = Даты.Дата
СГРУППИРОВАТЬ ПО
ВТ.Участник,
ВТ.Дата
ИНДЕКСИРОВАТЬ ПО
Ссылка
;
// Шаг 3: Третий с конца заказ
ВЫБРАТЬ
ВТ.Участник КАК Участник,
МАКСИМУМ(ВТ.Дата) КАК Дата
ПОМЕСТИТЬ ВТ_Заказ3_Дата
ИЗ
ВТ_ВсеЗаказы КАК ВТ
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказ1 КАК Искл1
ПО ВТ.Ссылка = Искл1.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказ2 КАК Искл2
ПО ВТ.Ссылка = Искл2.Ссылка
ГДЕ
Искл1.Ссылка ЕСТЬ NULL
И Искл2.Ссылка ЕСТЬ NULL
СГРУППИРОВАТЬ ПО
ВТ.Участник
;
ВЫБРАТЬ
ВТ.Участник КАК Участник,
ВТ.Дата КАК Дата,
МАКСИМУМ(ВТ.Ссылка) КАК Ссылка
ПОМЕСТИТЬ ВТ_Заказ3
ИЗ
ВТ_ВсеЗаказы КАК ВТ
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Заказ3_Дата КАК Даты
ПО ВТ.Участник = Даты.Участник
И ВТ.Дата = Даты.Дата
СГРУППИРОВАТЬ ПО
ВТ.Участник,
ВТ.Дата
;
// Итоговое объединение
ВЫБРАТЬ Участник, Ссылка, Дата ИЗ ВТ_Заказ1
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ Участник, Ссылка, Дата ИЗ ВТ_Заказ2
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ Участник, Ссылка, Дата ИЗ ВТ_Заказ3
Такой каскадный метод на больших базах отрабатывает значительно быстрее коррелированных подзапросов, так как поиск максимумов по индексам выполняется сервером баз данных практически мгновенно.
Если количество отбираемых строк N является переменным параметром, можно применить нумерацию строк в запросе через соединение таблицы с самой собой.
Выясним принцип работы:
ВЫБРАТЬ
Т1.Участник КАК Участник,
Т1.Ссылка КАК Ссылка,
Т1.Дата КАК Дата
ИЗ
ВТ_ВсеЗаказы КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ВсеЗаказы КАК Т2
ПО Т1.Участник = Т2.Участник
И (Т1.Дата < Т2.Дата ИЛИ (Т1.Дата = Т2.Дата И Т1.Ссылка <= Т2.Ссылка))
СГРУППИРОВАТЬ ПО
Т1.Участник,
Т1.Ссылка,
Т1.Дата
ИМЕЮЩИЕ
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т2.Ссылка) <= 3
В этом запросе для каждого документа подсчитывается, сколько существует документов с датой, большей либо равной текущей. Условие ИМЕЮЩИЕ КОЛИЧЕСТВО(...) <= 3 оставляет ровно три самых свежих заказа. Важно помнить, что квадратичная сложность такого соединения может приводить к замедлению, если по одному участнику накоплены тысячи заказов.
Если задача решается в рамках отчета на Системе компоновки данных, не обязательно усложнять текст запроса. В СКД есть штатные механизмы ограничения записей.
УПОРЯДОЧИТЬ ПО Дата УБЫВ. В настройках структуры отчета создаем родительскую группировку по полю Участник, а подчиненной группировкой делаем детальные записи (или группировку по полю Ссылка). В свойствах подчиненной группировки находим параметр «Количество записей» и указываем значение 3. При выводе СКД сама ограничит вывод тремя записями для каждого родительского элемента.ГДЕ Участник = &ПараметрУчастник, сортировкой по убыванию даты и директивой ВЫБРАТЬ ПЕРВЫЕ 3. На вкладке «Связи наборов данных» связываем наборы по полю Участник с передачей значения в параметр. СКД будет параметрически запрашивать ровно по 3 строки на каждого участника.Если результат требуется получить во встроенном языке для дальнейшей обработки алгоритмом, оптимально выполнить один запрос с итогами и прерывать внутренний цикл при достижении трех записей:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Участник КАК Участник,
| Шапка.Ссылка КАК Ссылка,
| Шапка.Дата КАК Дата
|ИЗ
| Документ.ЗаказТура КАК Шапка
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ЗаказТура.Участники КАК ТЧ
| ПО Шапка.Ссылка = ТЧ.Ссылка
|ГДЕ
| Шапка.Проведен
| И НЕ Шапка.Аннулирован
| И НЕ Шапка.ПометкаУдаления
|УПОРЯДОЧИТЬ ПО
| Шапка.Дата УБЫВ
|ИТОГИ ПО
| Участник";
ВыборкаУчастников = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
ПоследниеЗаказы = Новый Соответствие;
Пока ВыборкаУчастников.Следующий() Цикл
МассивДокументов = Новый Массив;
ВыборкаДокументов = ВыборкаУчастников.Выбрать();
Пока ВыборкаДокументов.Следующий() Цикл
МассивДокументов.Добавить(ВыборкаДокументов.Ссылка);
Если МассивДокументов.Количество() = 3 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
ПоследниеЗаказы.Вставить(ВыборкаУчастников.Участник, МассивДокументов);
КонецЦикла;
Такой подход обеспечивает высокую скорость выполнения запроса, прозрачен в отладке и полностью исключает перегрузку СУБД тяжелыми соединениями.