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

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

В практике разработчика 1С часто возникает задача преобразования дискретных записей в непрерывные периоды. Типичный пример: из регистра сведений, где зафиксированы отдельные дни болезни сотрудника, нужно получить таблицу с колонками «Дата начала» и «Дата окончания» для каждого больничного листа. Эта задача известна в теории баз данных как Gaps and Islands (Разрывы и Острова). В данной статье мы разберем, как решить её максимально эффективно, используя исключительно язык запросов 1С.

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

Представим, что у нас есть набор данных с измерениями Измерение1 (например, Сотрудник) и Дата. Данные идут вразнобой или последовательно, но нам нужно «схлопнуть» идущие подряд дни в один интервал. Если между датами есть пропуск хотя бы в один день, должен начаться новый интервал.

Пример исходных данных:

Результат, который мы должны получить:

Способ 1: Определение границ интервалов (Начала и Концы)

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

Рассмотрим пошаговую реализацию этого алгоритма. Сначала подготовим временную таблицу с исходными данными:


ВЫБРАТЬ
    РС.Сотрудник КАК Сотрудник,
    НАЧАЛОПЕРИОДА(РС.Дата, ДЕНЬ) КАК Дата
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
    РегистрСведений.Больничные КАК РС
ГРУППИРОВАТЬ ПО
    РС.Сотрудник,
    НАЧАЛОПЕРИОДА(РС.Дата, ДЕНЬ)
;

Теперь найдем все «начала» интервалов. Для этого используем левое соединение таблицы самой с собой по условию Т1.Дата = Т2.Дата + 1 день. Если соединение не нашло пару, значит перед нами начало периода.


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

Аналогичным образом вычислим «концы» интервалов:


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

Финальным шагом мы соединяем каждое начало с ближайшим к нему концом, который больше или равен дате начала:


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

Способ 2: Метод порядковых номеров (Математический)

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

Разберем пример:
Даты: 01.01, 02.01, 05.01, 06.01
Номера: 1, 2, 3, 4
Разность (Дата - Номер):
01.01 - 1 = 31.12
02.01 - 2 = 31.12 (Группа 1)
05.01 - 3 = 02.01
06.01 - 4 = 02.01 (Группа 2)

Реализуем этот подход в запросе 1С:


// Шаг 1: Получаем уникальные даты и их порядковые номера
ВЫБРАТЬ
    Т1.Сотрудник,
    Т1.Дата,
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т2.Дата) КАК Номер
ПОМЕСТИТЬ ВТ_ПронумерованныеДаты
ИЗ
    ВТ_ИсходныеДанные КАК Т1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ИсходныеДанные КАК Т2
        ПО Т1.Сотрудник = Т2.Сотрудник
            И Т1.Дата >= Т2.Дата
СГРУППИРОВАТЬ ПО
    Т1.Сотрудник,
    Т1.Дата
;

// Шаг 2: Группируем по разности даты и номера
ВЫБРАТЬ
    Т.Сотрудник,
    МИНИМУМ(Т.Дата) КАК ДатаНачала,
    МАКСИМУМ(Т.Дата) КАК ДатаОкончания
ИЗ
    ВТ_ПронумерованныеДаты КАК Т
СГРУППИРОВАТЬ ПО
    Т.Сотрудник,
    ДОБАВИТЬКДАТЕ(Т.Дата, ДЕНЬ, -Т.Номер)

Важный момент: соединение таблицы самой с собой для получения номера имеет квадратичную сложность O(n²). На очень больших объемах данных (десятки тысяч строк по одному сотруднику) этот запрос может выполняться медленно. В таких случаях лучше использовать Способ 1. Для оптимизации вычислений на больших объемах можно применить метод быстрого определения интервалов в запросе с линейной зависимостью от объема данных.

Способ 3: Объединение пересекающихся периодов

Иногда задача усложняется: периоды могут не просто идти подряд, но и накладываться друг на друга (например, один больничный открыт с 1 по 10 число, а второй — с 5 по 15). Простой поиск Дата + 1 здесь не поможет.

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

Пример логики для объединения пересечений:

  1. Выбираем все начала и концы из исходных данных.
  2. Для каждого начала находим, не «поглощено» ли оно другим периодом.
  3. Используем функцию ВЫБОР для определения принадлежности к текущему «острову».

Рассмотрим фрагмент запроса для поглощенных периодов:


ВЫБРАТЬ
    Дано.Клиент,
    Дано.НачалоПериода,
    Дано.КонецПериода,
    СУММА(РАЗНОСТЬДАТ(ДатыДо.НачалоПериода, 
        ВЫБОР 
            КОГДА ДатыДо.КонецПериода <= Дано.НачалоПериода ТОГДА ДатыДо.КонецПериода 
            ИНАЧЕ Дано.НачалоПериода 
        КОНЕЦ, СЕКУНДА)) КАК Интеграл
ПОМЕСТИТЬ ВТ_Интеграл
ИЗ
    Дано КАК Дано
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Дано КАК ДатыДо 
        ПО ДатыДо.НачалоПериода <= Дано.НачалоПериода
            И ДатыДо.Клиент = Дано.Клиент
СГРУППИРОВАТЬ ПО
    Дано.Клиент, Дано.НачалоПериода, Дано.КонецПериода

Рекомендации по оптимизации

Выясним, как сделать наши запросы максимально быстрыми:

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

← На главную