Как объединить пересекающиеся интервалы дат в запросе 1С?

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

При решении задач планирования, учета скидок, расчета стажа или резервирования ресурсов разработчики часто сталкиваются с задачей слияния пересекающихся периодов. Например, если у нас есть несколько интервалов, которые перекрывают друг друга полностью или частично, их необходимо «схлопнуть» в непрерывные временные отрезки. Разберем, как реализовать подобное слияние как средствами языка запросов 1С, так и с помощью оптимизированного обхода во встроенном языке.

Постановка задачи

Представим, что во временную таблицу или параметр передается исходный набор интервалов дат:

  1. 01.01.2016 — 10.03.2016
  2. 01.02.2016 — 20.03.2016 (пересекается с первым)
  3. 30.03.2016 — 15.04.2016
  4. 10.04.2016 — 12.04.2016 (полностью внутри третьего)
  5. 11.04.2016 — 25.04.2016 (пересекается с третьим и четвертым)
  6. 30.04.2016 — 10.05.2016 (отдельный период)

На выходе требуется получить результирующий список объединенных интервалов:

  1. 01.01.2016 — 20.03.2016
  2. 30.03.2016 — 25.04.2016
  3. 30.04.2016 — 10.05.2016

Главная сложность заключается в том, что в языке запросов платформы 1С:Предприятие отсутствуют стандартные оконные функции (OVER, LEAD, LAG) и рекурсивные запросы (WITH RECURSIVE). Рассмотрим возможные варианты реализации поставленной цели.

Способ 1. Поиск истинных границ интервалов в запросе

Проанализируем природу результирующих дат. Дата начала объединенного интервала всегда является датой начала одного из исходных периодов, причем она не накрывается ни одним другим интервалом «слева» (то есть не лежит строго внутри другого периода). Аналогично, дата окончания объединенного интервала — это дата окончания исходного периода, которая не перекрывается ни одним другим интервалом «справа».

Используем этот принцип для компактного решения:

  1. Выделим во временную таблицу ЛевыеКрая все даты начала, которые не лежат строго внутри какого-либо другого периода.
  2. Выделим во временную таблицу ПравыеКрая все даты окончания, которые не перекрываются другими периодами.
  3. Соединим каждую левую границу с ближайшей правой границей, которая больше или равна ей.

Посмотрим на практическую реализацию этого алгоритма в запросе:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Край.От КАК От
ПОМЕСТИТЬ ЛевыеКрая
ИЗ
    Интервалы КАК Край
        ЛЕВОЕ СОЕДИНЕНИЕ Интервалы КАК Интервалы
        ПО (Интервалы.От < Край.От)
            И (Край.От <= Интервалы.До)
ГДЕ
    Интервалы.От ЕСТЬ NULL
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ РАЗЛИЧНЫЕ
    Край.До КАК До
ПОМЕСТИТЬ ПравыеКрая
ИЗ
    Интервалы КАК Край
        ЛЕВОЕ СОЕДИНЕНИЕ Интервалы КАК Интервалы
        ПО (Интервалы.От <= Край.До)
            И (Край.До < Интервалы.До)
ГДЕ
    Интервалы.От ЕСТЬ NULL
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ЛевыеКрая.От КАК ДатаНачала,
    МИНИМУМ(ПравыеКрая.До) КАК ДатаОкончания
ИЗ
    ЛевыеКрая КАК ЛевыеКрая
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ПравыеКрая КАК ПравыеКрая
        ПО ЛевыеКрая.От <= ПравыеКрая.До
СГРУППИРОВАТЬ ПО
    ЛевыеКрая.От

Разберем подробнее, как работают условия в этом запросе. В первом пакете левое соединение ищет интервал, который начался раньше текущей даты Край.От и закончился не раньше нее. Если такой период найден, значит, Край.От находится внутри чужого диапазона и не может быть началом объединенной группы. Проверка ГДЕ Интервалы.От ЕСТЬ NULL отсекает такие строки и оставляет только истинные стартовые точки.

Способ 2. Нумерация и сопоставление контрольных точек

Рассмотрим альтернативную технику, которая собирает все граничные точки (как левые, так и правые) в единый список, нумерует их по возрастанию даты через соединение таблицы с собой и разбивает на пары «начало — конец»:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Край.От КАК От
ПОМЕСТИТЬ Края
ИЗ
    Интервалы КАК Край
        ЛЕВОЕ СОЕДИНЕНИЕ Интервалы КАК Интервалы
        ПО (Интервалы.От < Край.От)
            И (Край.От <= Интервалы.До)
ГДЕ
    Интервалы.От ЕСТЬ NULL 

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ РАЗЛИЧНЫЕ
    Край.До
ИЗ
    Интервалы КАК Край
        ЛЕВОЕ СОЕДИНЕНИЕ Интервалы КАК Интервалы
        ПО (Интервалы.От <= Край.До)
            И (Край.До < Интервалы.До)
ГДЕ
    Интервалы.От ЕСТЬ NULL
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    МИНИМУМ(Края.От) КАК ДатаНачала,
    МАКСИМУМ(Края.От) КАК ДатаОкончания
ИЗ
    (ВЫБРАТЬ
        КОЛИЧЕСТВО(Раньше.От) КАК Номер,
        Позже.От КАК От
    ИЗ
        Края КАК Позже
            ВНУТРЕННЕЕ СОЕДИНЕНИЕ Края КАК Раньше
            ПО (Раньше.От <= Позже.От)
    СГРУППИРОВАТЬ ПО
        Позже.От) КАК Края
СГРУППИРОВАТЬ ПО
    ВЫРАЗИТЬ(Края.Номер / 2 КАК ЧИСЛО(10, 0))

В этом варианте объединение левых и правых точек дает упорядоченный список парных границ: [Начало1, Конец1, Начало2, Конец2, ...]. Деление порядкового номера на 2 с округлением позволяет сгруппировать точки строго по парам.

Подводные камни при работе в запросах

При использовании запросов для склеивания диапазонов важно учитывать технические особенности СУБД и платформы:

  1. Квадратичная вычислительная сложность: Неэквивалентные соединения («меньше или равно», «больше или равно») приводят к вычислению декартова произведения таблиц. На объемах в тысячи интервалов такой запрос может выполняться недопустимо долго.
  2. Пустые даты: Если правая граница периода не заполнена (неограниченный период со значением 0001-01-01), сравнения <= и >= отработают некорректно. В таких случаях пустую дату необходимо заменять на максимальную возможную дату через конструкцию ВЫБОР КОГДА ... ТОГДА ДАТАВРЕМЯ(3999, 12, 31).
  3. Стыковка «день в день»: Если интервалы задаются датами без учета времени (например, 01.01–10.01 и 11.01–20.01), строгое условие пересечения посчитает их двумя разными периодами. Для склейки смежных дней условие соединения необходимо модифицировать с помощью функции ДОБАВИТЬКДАТЕ(..., ДЕНЬ, 1).
  4. Разрез аналитики: Если объединение выполняется для конкретных объектов (сотрудники, договоры, номенклатура), все соединения таблиц должны обязательно содержать жесткое равенство измерений (например, Интервалы1.Договор = Интервалы2.Договор), а во временных таблицах следует применять директиву ИНДЕКСИРОВАТЬ ПО.

Альтернатива: последовательная обработка во встроенном языке

Если объем исходных периодов велик (сотни или тысячи записей), выгрузка данных в коллекцию ТаблицаЗначений с последующей обработкой в цикле будет работать на порядки быстрее запроса. Сортировка таблицы выполняется со сложностью $O(N \log N)$, а итоговый проход — за линейное время $O(N)$.

Посмотрим на пример кода с алгоритмом слияния в цикле:


// Предположим, что ТаблицаИнтервалов содержит колонки: ДатаНачала, ДатаОкончания
ТаблицаИнтервалов.Сортировать("ДатаНачала Возр, ДатаОкончания Возр");

ТаблицаРезультат = ТаблицаИнтервалов.СкопироватьКолонки();

Если ТаблицаИнтервалов.Количество() > 0 Тогда
    ТекущаяСтрока = ТаблицаРезультат.Добавить();
    ЗаполнитьЗначенияСвойств(ТекущаяСтрока, ТаблицаИнтервалов[0]);
    
    Для Индекс = 1 По ТаблицаИнтервалов.Количество() - 1 Цикл
        СтрокаИсходная = ТаблицаИнтервалов[Индекс];
        
        // Проверяем пересечение или стыковку текущего накопленного периода со следующим
        Если СтрокаИсходная.ДатаНачала <= ТекущаяСтрока.ДатаОкончания Тогда
            // Расширяем правую границу, если следующий период выходит за текущий
            Если СтрокаИсходная.ДатаОкончания > ТекущаяСтрока.ДатаОкончания Тогда
                ТекущаяСтрока.ДатаОкончания = СтрокаИсходная.ДатаОкончания;
            КонецЕсли;
        Иначе
            // Обнаружен разрыв: фиксируем новый независимый интервал
            ТекущаяСтрока = ТаблицаРезультат.Добавить();
            ЗаполнитьЗначенияСвойств(ТекущаяСтрока, СтрокаИсходная);
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

Таким образом, для небольших массивов данных и задач внутри сложных отчетов СКД отлично подходит вариант решения на чистом языке запросов через определение граничных точек. Для высоконагруженных участков и больших списков документов оптимальным выбором остается предварительная сортировка и обработка в цикле средствами встроенного языка.

← На главную