При работе с периодическими регистрами сведений (например, регистрами курсов валют или цен номенклатуры) часто возникает задача сгруппировать последовательные записи, в которых значение ресурса не менялось. В результате необходимо получить таблицу с периодом действия каждого значения: начальную дату (ДатаС), конечную дату (ДатаПо), валюту и сам курс.
Главная сложность заключается в том, что в языке запросов 1С отсутствуют оконные функции (такие как LAG, LEAD или ROW_NUMBER() OVER (...)), доступные в классическом SQL. Если просто сгруппировать данные по валюте и курсу с получением МИНИМУМ(Период) и МАКСИМУМ(Период), результат окажется ошибочным: если курс валюты через некоторое время вернулся к прежнему значению, отдельные временные интервалы схлопнутся в один общий.
Рассмотрим алгоритм решения этой задачи, известной в теории баз данных как "Gaps and Islands" (пробелы и острова). Нам необходимо разбить записи на группы так, чтобы каждая смена значения курса формировала новый номер интервала.
Разберем процесс по шагам:
МИНИМУМ(Период) в качестве даты начала и МАКСИМУМ(Период) в качестве даты окончания действия курса.Проанализируем практический пример запроса. Для наглядности сформируем исходные данные во временной таблице Дано и выполним их обработку:
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1) КАК Период,
"USD" КАК Валюта,
1.9 КАК Курс
ПОМЕСТИТЬ Дано
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 2), "USD", 1.9
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 3), "USD", 1.95
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 4), "USD", 1.8
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 5), "USD", 1.95
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 1), "EUR", 2
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 2), "EUR", 2
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 3), "EUR", 2
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 4), "EUR", 2
ОБЪЕДИНИТЬ ВЫБРАТЬ ДАТАВРЕМЯ(2000, 1, 5), "EUR", 2
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Дано.Период КАК Период,
Дано.Валюта КАК Валюта,
Дано.Курс КАК Курс,
КОЛИЧЕСТВО(Было.Курс) КАК Номер
ПОМЕСТИТЬ Интервалы
ИЗ
Дано КАК Дано
ЛЕВОЕ СОЕДИНЕНИЕ Дано КАК Было
ПО (Было.Валюта = Дано.Валюта)
И (Было.Период <= Дано.Период)
И (Было.Курс <> Дано.Курс)
СГРУППИРОВАТЬ ПО
Дано.Период,
Дано.Валюта,
Дано.Курс
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
МИНИМУМ(Интервалы.Период) КАК ДатаС,
МАКСИМУМ(Интервалы.Период) КАК ДатаПо,
Интервалы.Валюта КАК Валюта,
Интервалы.Курс КАК Курс
ИЗ
Интервалы КАК Интервалы
СГРУППИРОВАТЬ ПО
Интервалы.Валюта,
Интервалы.Курс,
Интервалы.Номер
УПОРЯДОЧИТЬ ПО
Валюта,
ДатаС
Описанный метод решения запросом имеет важную особенность: из-за неявного соединения таблицы «самой с собой» по условию Период <= Период сложность запроса составляет O(N²). На больших объемах данных в регистре (десятки тысяч записей) такой запрос вызовет высокую нагрузку на СУБД и может выполняться длительное время.
Рассмотрим способы оптимизации и альтернативные подходы:
Валюта и Период:
ИНДЕКСИРОВАТЬ ПО
Валюта,
Период
УПОРЯДОЧИТЬ ПО Валюта, Период). В цикле Пока Выборка.Следующий() КонецЦикла значения текущего курса сравниваются с предыдущим. При смене курса фиксируется ДатаПо предыдущего интервала и открывается новый с ДатаС. Этот подход работает за линейное время O(N) и не нагружает СУБД.ДатаНачала и ДатаОкончания).