Как сгруппировать последовательные интервалы времени в 1С и SQL?

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

В практике разработки на платформе 1С часто встречается задача, когда необходимо объединить («схлопнуть») несколько идущих друг за другом записей, если они образуют непрерывную цепочку по времени и имеют одинаковые аналитические признаки (например, длина слота или подразделение). В программировании и теории баз данных эта задача классифицируется как «Gaps and Islands» (Разрывы и острова). Наша цель — найти эти «острова» непрерывности и сгруппировать их в одну запись с общим временем начала и окончания.

В этой статье мы подробно разберем несколько подходов к решению данной задачи: от использования современных оконных функций в SQL до классических методов построения запросов в 1С и обработки данных в коде.

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

Представим таблицу, где каждая строка содержит НачалоПериода, КонецПериода, ДлинаСлота и КоличествоСлотов. Нам необходимо объединить идущие подряд записи, у которых КонецПериода текущей записи совпадает с НачаломПериода следующей, при условии, что ДлинаСлота у них одинакова. При этом КоличествоСлотов должно суммироваться.

Проанализируем основные сложности: простое использование СГРУППИРОВАТЬ ПО не сработает, так как записи в течение дня могут иметь разрывы, и нам важно объединять только те, что идут «стык в стык».

Решение через оконные функции (SQL-подход)

Если мы работаем напрямую с SQL-запросами или используем современные версии платформы 1С (начиная с 8.3.21), нам доступны оконные функции LAG и LEAD. Рассмотрим логику, предложенную опытными разработчиками: мы сравниваем текущую запись с предыдущей, чтобы понять, является ли текущая запись продолжением предыдущей или началом нового «острова».

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


WITH PreparedData AS (
    SELECT
        StartPeriod,
        EndPeriod,
        SlotLength,
        SlotsCount,
        LAG(EndPeriod) OVER (PARTITION BY SlotLength ORDER BY StartPeriod) AS PrevEnd
    FROM
        TableData
),
IslandsStart AS (
    SELECT
        StartPeriod,
        EndPeriod,
        SlotLength,
        SlotsCount,
        CASE 
            WHEN PrevEnd = StartPeriod THEN 0 
            ELSE 1 
        END AS IsNewIsland
    FROM
        PreparedData
),
GroupedIslands AS (
    SELECT
        StartPeriod,
        EndPeriod,
        SlotLength,
        SlotsCount,
        SUM(IsNewIsland) OVER (PARTITION BY SlotLength ORDER BY StartPeriod) AS IslandID
    FROM
        IslandsStart
)
SELECT
    MIN(StartPeriod) AS RealStart,
    MAX(EndPeriod) AS RealEnd,
    SlotLength,
    SUM(SlotsCount) AS TotalSlots
FROM
    GroupedIslands
GROUP BY
    IslandID, SlotLength

Как это работает по шагам:

  1. В блоке PreparedData мы получаем значение конца предыдущего интервала для каждой строки с помощью функции LAG.
  2. В блоке IslandsStart мы сравниваем НачалоПериода текущей строки с КонцомПериода предыдущей. Если они не равны, значит, начался новый блок (остров) и мы ставим флаг 1.
  3. В блоке GroupedIslands мы рассчитываем накопительный итог по флагу IsNewIsland. Этот итог становится уникальным номером группы для всех строк одного непрерывного периода.
  4. На финальном этапе мы просто группируем данные по этому номеру.

Решение в 1С:Предприятие (классический метод запросов)

Поскольку в языке запросов 1С оконные функции реализованы не в полном объеме или могут быть недоступны в старых конфигурациях, рассмотрим алгоритм через временные таблицы и соединения. Этот метод основан на определении «начал» и «концов» интервалов.

Рассмотрим логику поиска границ:

  1. Поиск начал интервалов: Запись является началом интервала, если в таблице нет другой записи, конец которой равен началу текущей (при условии совпадения разрезов группировки).
  2. Поиск окончаний: Запись является окончанием, если в таблице нет записи, начало которой равно концу текущей.
  3. Стыковка: Для каждого «начала» ищется минимальное «окончание», которое больше или равно этому началу.

Проанализируем пример построения такого запроса в 1С:


// 1. Помещаем данные во временную таблицу
ВЫБРАТЬ
    Т.НачалоПериода,
    Т.КонецПериода,
    Т.ДлинаСлота,
    Т.КоличествоСлотов
ПОМЕСТИТЬ ВТ_Данные
ИЗ
    &ИсходныеДанные КАК Т;

////////////////////////////////////////////////////////////////////////////////
// 2. Находим все начала цепочек
ВЫБРАТЬ
    Т1.НачалоПериода,
    Т1.ДлинаСлота
ПОМЕСТИТЬ ВТ_Начала
ИЗ
    ВТ_Данные КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Данные КАК Т2
    ПО Т1.НачалоПериода = Т2.КонецПериода
    И Т1.ДлинаСлота = Т2.ДлинаСлота
ГДЕ
    Т2.НачалоПериода ЕСТЬ NULL;

////////////////////////////////////////////////////////////////////////////////
// 3. Для каждого начала ищем соответствующий ему конец и суммируем показатели
// (Это упрощенный подход, на больших данных лучше использовать промежуточную таблицу групп)

Метод разности последовательностей (Tabibitosan)

Существует элегантный математический способ группировки «островов», называемый методом Tabibitosan. Он основан на том, что если вычесть из последовательности дат порядковый номер строки, то внутри непрерывного интервала результат будет константой.

Рассмотрим применение этого метода:

  1. Пронумеруем все строки в пределах одного дня, отсортировав их по времени начала.
  2. Для каждой строки вычислим разность: Разность = ВремяНачала - (ПорядковыйНомер * ДлинаСлота).
  3. Все строки, у которых эта разность совпала, принадлежат одному непрерывному периоду.

Этот метод очень эффективен, так как позволяет избежать сложных соединений «самой с собой» и выполняется за один проход по данным.

Решение через объектную модель 1С (Таблица значений)

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

Рассмотрим алгоритм «упаковки» в коде:


// Предположим, ТЗДанные уже отсортирована по НачалоПериода
ТЗРезультат = ТЗДанные.СкопироватьКолонки();

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

Важные нюансы при работе с типом Дата:

При сравнении НачалоПериода = КонецПериода в 1С нужно учитывать точность до секунды. Если в ваших данных между интервалами может быть зазор в 1 секунду (например, 12:00:00 - 12:05:00 и 12:05:01 - 12:10:00), условие равенства не сработает. В таких случаях используйте функцию ДОБАВИТЬКДАТЕ(КонецПериода, СЕКУНДА, 1) для корректной стыковки в запросах.

Подведение итогов

Мы проанализировали несколько мощных инструментов для решения задачи упаковки интервалов. Выбор конкретного метода зависит от версии платформы и объема данных:

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

← На главную