Как в 1С с помощью запроса получить периоды действия записей периодического регистра сведений?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с периодическими регистрами сведений в платформе 1С:Предприятие часто возникает задача сформировать отчет или выборку в виде интервалов: с какой даты по какую за объектом (например, контрагентом или сотрудником) было закреплено определенное значение ресурса (менеджер, должность, оклад). В самом регистре хранится только дата начала действия (Период), а дата окончания следующей записью определяется неявно. Разберем различные способы решения этой задачи — как исключительно средствами языка запросов, так и комбинированным методом с обработкой результата в коде 1С.

Способ 1. Классическое соединение таблицы самой с собой (без генерации календаря)

Рассмотрим наиболее универсальный и канонический способ получения интервалов с помощью языка запросов 1С. Поскольку в платформе 1С отсутствуют аналитические оконные функции (такие как LEAD или LAG в реляционных СУБД), дату завершения периода определяют путем соединения таблицы со своей же копией по условию превышения даты.

Разберем по шагам логику построения такого запроса:

  1. Помещаем исходные данные среза или физической таблицы регистра во временную таблицу. Обязательно индексируем поля группировки и даты для быстродействия соединений.
  2. Выполняем левое соединение временной таблицы самой с собой по совпадению ключевого измерения (Контрагент) и условию строгого неравенства периодов: дата во второй таблице должна быть больше даты в первой таблице.
  3. Группируем результат по данным первой таблицы, а для определения даты окончания вычисляем минимальный период из второй таблицы с помощью функции МИНИМУМ().
  4. Если интервал должен быть строго закрытым (чтобы дата окончания не совпадала секунда в секунду с началом следующего периода), вычитаем одну секунду или один день (в зависимости от периодичности регистра) с помощью функции ДОБАВИТЬКДАТЕ().
  5. Для последней актуальной записи дата окончания будет равна 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. Всегда используйте явную инструкцию ИНДЕКСИРОВАТЬ ПО для временных таблиц.

Способ 2. Поиск границ непрерывных периодов через смежные даты

Проанализируем альтернативный подход, предложенный участниками обсуждения. Он применяется в задачах расчета повременной занятости или графиков, где данные разворачиваются с фиксированным шагом (например, подневно через производственный календарь).

Идея метода заключается в поиске «разрывов» цепочки: мы соединяем дату записи с датой следующего дня (ДОБАВИТЬКДАТЕ(Период, ДЕНЬ, 1)) и датой предыдущего дня (ДОБАВИТЬКДАТЕ(Период, ДЕНЬ, -1)). Если предыдущего дня нет — это точка входа (начало интервала). Если нет следующего дня — это точка выхода (конец интервала).

Посмотрим на пример запроса, определяющего такие границы:


ВЫБРАТЬ
    вт_Данные.Период КАК Период,
    вт_Данные.Контрагент КАК Контрагент,
    вт_Данные.Менеджер КАК Менеджер,
    вт_ДанныеП.Период ЕСТЬ NULL КАК ЭтоНачало,
    вт_ДанныеС.Период ЕСТЬ NULL КАК ЭтоКонец
ПОМЕСТИТЬ вт_ПоискГраниц
ИЗ
    вт_Данные КАК вт_Данные
    ПОЛНОЕ СОЕДИНЕНИЕ вт_Данные КАК вт_ДанныеС
        ПО (ДОБАВИТЬКДАТЕ(вт_Данные.Период, ДЕНЬ, 1) = вт_ДанныеС.Период)
        И вт_Данные.Контрагент = вт_ДанныеС.Контрагент
        И вт_Данные.Менеджер = вт_ДанныеС.Менеджер
    ПОЛНОЕ СОЕДИНЕНИЕ вт_Данные КАК вт_ДанныеП
        ПО (ДОБАВИТЬКДАТЕ(вт_Данные.Период, ДЕНЬ, -1) = вт_ДанныеП.Период)
        И вт_Данные.Контрагент = вт_ДанныеП.Контрагент
        И вт_Данные.Менеджер = вт_ДанныеП.Менеджер
ГДЕ
    НЕ вт_Данные.Контрагент ЕСТЬ NULL
    И (вт_ДанныеП.Период ЕСТЬ NULL ИЛИ вт_ДанныеС.Период ЕСТЬ NULL)
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    вт_Начало.Контрагент КАК Контрагент,
    вт_Начало.Менеджер КАК Менеджер,
    вт_Начало.Период КАК ДатаНачала,
    МИНИМУМ(вт_Конец.Период) КАК ДатаОкончания
ИЗ
    вт_ПоискГраниц КАК вт_Начало
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ вт_ПоискГраниц КАК вт_Конец
        ПО вт_Начало.ЭтоНачало
        И вт_Конец.ЭтоКонец
        И вт_Начало.Контрагент = вт_Конец.Контрагент
        И вт_Начало.Менеджер = вт_Конец.Менеджер
        И вт_Конец.Период >= вт_Начало.Период
СГРУППИРОВАТЬ ПО
    вт_Начало.Контрагент,
    вт_Начало.Менеджер,
    вт_Начало.Период

Обратите внимание: данный способ корректно работает только в том случае, если исходная таблица уже плотно заполнена данными на каждый день без «дыр» во времени. Если между записями регистра интервал составляет несколько месяцев, использовать смещение на 1 ДЕНЬ нельзя без предварительного заполнения календаря.

Способ 3. Программная обработка выборки в коде 1С

Если выборка содержит большой массив данных, сложные соединения по неравенству дат могут вызвать существенную просадку производительности СУБД. В таком случае оптимальным и легко читаемым решением является последовательный обход отсортированной таблицы средствами встроенного языка 1С.

Разберем по шагам реализацию на встроенном языке:

  1. Выполняем простой запрос к регистру сведений, выбирая поля Период, Контрагент, Менеджер с обязательной сортировкой УПОРЯДОЧИТЬ ПО Контрагент, Период.
  2. Выгружаем результат в таблицу значений и добавляем колонку ДатаОкончания.
  3. Проходим по строкам в цикле: для каждой текущей строки датой окончания будет являться дата следующей строки (за вычетом секунды), при условии, что контрагент в следующей строке не изменился.
  4. Для последней записи по каждому контрагенту устанавливаем пустую дату или дату окончания отчета.

Посмотрим на пример кода на встроенном языке 1С:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    Рег.Контрагент КАК Контрагент,
    |    Рег.Менеджер КАК Менеджер,
    |    Рег.Период КАК ДатаНачала
    |ИЗ
    |    РегистрСведений.ОтветственныеМенеджеры КАК Рег
    |УПОРЯДОЧИТЬ ПО
    |    Контрагент,
    |    ДатаНачала";

ТаблицаИнтервалов = Запрос.Выполнить().Выгрузить();
ТаблицаИнтервалов.Колонки.Добавить("ДатаОкончания", Новый ОписаниеТипов("Дата"));

ВсегоСтрок = ТаблицаИнтервалов.Количество();
Для Индекс = 0 По ВсегоСтрок - 1 Цикл
    СтрокаТекущая = ТаблицаИнтервалов[Индекс];
    
    Если Индекс + 1 < ВсегоСтрок Тогда
        СтрокаСледующая = ТаблицаИнтервалов[Индекс + 1];
        
        Если СтрокаТекущая.Контрагент = СтрокаСледующая.Контрагент Тогда
            СтрокаТекущая.ДатаОкончания = СтрокаСледующая.ДатаНачала - 1; // вычитаем 1 секунду
        Иначе
            СтрокаТекущая.ДатаОкончания = Дата("39991231235959");
        КонецЕсли;
    Иначе
        СтрокаТекущая.ДатаОкончания = Дата("39991231235959");
    КонецЕсли;
КонецЦикла;

Такой подход выполняется линейно за $O(N)$, совершенно не нагружает оперативную память сервера СУБД блокировками и тяжелыми объединениями и отлично подходит для фоновых регламентных обработок больших массивов истории.

← На главную