При построении отчетов и анализе динамики данных часто возникает задача сопоставить для каждой записи периодического регистра сведений ее текущий период и предыдущий период изменения. Это требуется, например, при расчете интервалов действия цен, окладов сотрудников или курсов валют, а также при определении даты завершения состояния объекта.
Разберем основные варианты решения этой задачи на языке запросов 1С: от простого соединения подзапросом до высокопроизводительного алгоритма на временных таблицах и процедурной обработки.
Допустим, в периодическом регистре сведений хранятся записи по измерениям со следующими датами. Нам необходимо получить выборку, в которой для каждой записи будет присутствовать текущая дата и дата предыдущей записи по этому же измерению:
Рассмотрим вариант решения задачи через прямое левое соединение таблицы регистра сведений с самой собой с отбором через условие ГДЕ. Этот способ наиболее прост для понимания логики сопоставления.
Проанализируем пример запроса на базе регистра сведений КурсыВалют:
ВЫБРАТЬ
КурсыВалют1.Валюта КАК Валюта,
КурсыВалют1.Период КАК Период,
КурсыВалют1.Курс КАК Курс,
ЕСТЬНУЛЛ(КурсыВалют2.Период, КурсыВалют1.Период) КАК ПредПериод,
КурсыВалют2.Курс КАК ПредКурс
ИЗ
РегистрСведений.КурсыВалют КАК КурсыВалют1
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют КАК КурсыВалют2
ПО КурсыВалют1.Валюта = КурсыВалют2.Валюта
И КурсыВалют1.Период >= КурсыВалют2.Период
ГДЕ
КурсыВалют2.Период В
(ВЫБРАТЬ ПЕРВЫЕ 1
КурсыВалют3.Период КАК Период
ИЗ
РегистрСведений.КурсыВалют КАК КурсыВалют3
ГДЕ
КурсыВалют3.Валюта = КурсыВалют1.Валюта
И КурсыВалют3.Период < КурсыВалют1.Период
УПОРЯДОЧИТЬ ПО
Период УБЫВ)
ИЛИ КурсыВалют1.Период = КурсыВалют2.Период
Разберем подробно, как работает данный запрос:
КурсыВалют1 с таблицей КурсыВалют2 по совпадению измерения Валюта и условию КурсыВалют1.Период >= КурсыВалют2.Период.ГДЕ мы ограничиваем выбираемый предыдущий период, запрашивая через ВЫБРАТЬ ПЕРВЫЕ 1 с сортировкой ПО Период УБЫВ самую свежую дату, которая строго меньше текущей даты.ИЛИ КурсыВалют1.Период = КурсыВалют2.Период позволяет сохранить самую первую запись регистра, у которой нет более ранних дат, обеспечивая подстановку первоначального периода.NULL первой записи применяется функция ЕСТЬНУЛЛ(КурсыВалют2.Период, КурсыВалют1.Период).В языке запросов 1С отсутствуют оконные функции (такие как LAG или LEAD). Конструкции с подзапросами ВЫБРАТЬ ПЕРВЫЕ 1 внутри условий соединения или фильтрации ГДЕ создают коррелированные подзапросы. На больших объемах данных в СУБД это приводит к сложности порядка $O(N^2)$ и сильному падению производительности.
Чтобы избежать проблем с замедлением работы, разберем более производительный алгоритм с использованием временных таблиц и агрегатной функции МАКСИМУМ.
// Шаг 1: Помещаем исходные данные во временную таблицу
ВЫБРАТЬ
КурсыВалют.Валюта КАК Валюта,
КурсыВалют.Период КАК Период,
КурсыВалют.Курс КАК Курс
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
РегистрСведений.КурсыВалют КАК КурсыВалют
;
// Шаг 2: Находим максимальную дату, меньшую текущей
ВЫБРАТЬ
ВТ1.Валюта КАК Валюта,
ВТ1.Период КАК Период,
ВТ1.Курс КАК Курс,
МАКСИМУМ(ВТ2.Период) КАК ПредПериод
ПОМЕСТИТЬ ВТ_ПериодыСДатами
ИЗ
ВТ_ИсходныеДанные КАК ВТ1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеДанные КАК ВТ2
ПО ВТ1.Валюта = ВТ2.Валюта
И ВТ1.Период > ВТ2.Период
СГРУППИРОВАТЬ ПО
ВТ1.Валюта,
ВТ1.Период,
ВТ1.Курс
;
// Шаг 3: Формируем итоговый результат
ВЫБРАТЬ
ВТ.Валюта КАК Валюта,
ЕСТЬНУЛЛ(ВТ.ПредПериод, ВТ.Период) КАК ПредПериод,
ВТ.Период КАК ТекущийПериод,
ВТ.Курс КАК Курс
ИЗ
ВТ_ПериодыСДатами КАК ВТ
УПОРЯДОЧИТЬ ПО
ВТ.Валюта,
ВТ.Период
Выполнение задачи через группировку по МАКСИМУМ(ВТ2.Период) дает устойчивую производительность и оптимальный план выполнения запроса на стороне СУБД.
Если данные подготавливаются для выгрузки во внешнюю систему или итоговую таблицу значений, обработка выборки в цикле на сервере может работать значительно быстрее даже самого оптимального запроса.
Проанализируем пример процедурной обработки:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КурсыВалют.Валюта КАК Валюта,
| КурсыВалют.Период КАК Период,
| КурсыВалют.Курс КАК Курс
|ИЗ
| РегистрСведений.КурсыВалют КАК КурсыВалют
|УПОРЯДОЧИТЬ ПО
| Валюта,
| Период ВОЗР";
Выборка = Запрос.Выполнить().Выбрать();
ТЗРезультат = Новый ТаблицаЗначений;
ТЗРезультат.Колонки.Добавить("Валюта");
ТЗРезультат.Колонки.Добавить("ПредПериод");
ТЗРезультат.Колонки.Добавить("ТекущийПериод");
ТЗРезультат.Колонки.Добавить("Курс");
ТекущаяВалюта = Неопределено;
ПредыдущийПериод = Неопределено;
Пока Выборка.Следующий() Цикл
Если Выборка.Валюта <> ТекущаяВалюта Тогда
ТекущаяВалюта = Выборка.Валюта;
ПредыдущийПериод = Выборка.Период;
КонецЕсли;
НоваяСтрока = ТЗРезультат.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
НоваяСтрока.ТекущийПериод = Выборка.Период;
НоваяСтрока.ПредПериод = ПредыдущийПериод;
ПредыдущийПериод = Выборка.Период;
КонецЦикла;
Подчинен регистратору, в одну секунду может быть записано несколько документов. В таких случаях одного поля Период недостаточно для определения предыдущей записи — необходимо дополнительно связывать и сравнивать поля Регистратор и НомерСтроки.Период > ВТ2.Период убедитесь, что в регистре нет повторяющихся записей на одну и ту же дату по одинаковому ключевому набору измерений.