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