В практике разработчика 1С часто возникает задача преобразования дискретных записей в непрерывные периоды. Типичный пример: из регистра сведений, где зафиксированы отдельные дни болезни сотрудника, нужно получить таблицу с колонками «Дата начала» и «Дата окончания» для каждого больничного листа. Эта задача известна в теории баз данных как Gaps and Islands (Разрывы и Острова). В данной статье мы разберем, как решить её максимально эффективно, используя исключительно язык запросов 1С.
Представим, что у нас есть набор данных с измерениями Измерение1 (например, Сотрудник) и Дата. Данные идут вразнобой или последовательно, но нам нужно «схлопнуть» идущие подряд дни в один интервал. Если между датами есть пропуск хотя бы в один день, должен начаться новый интервал.
Пример исходных данных:
Результат, который мы должны получить:
Проанализируем логику этого метода. Дата является началом интервала, если в исходной таблице отсутствует запись на предыдущий день. Аналогично, дата является концом интервала, если в таблице нет записи на следующий день.
Рассмотрим пошаговую реализацию этого алгоритма. Сначала подготовим временную таблицу с исходными данными:
ВЫБРАТЬ
РС.Сотрудник КАК Сотрудник,
НАЧАЛОПЕРИОДА(РС.Дата, ДЕНЬ) КАК Дата
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
РегистрСведений.Больничные КАК РС
ГРУППИРОВАТЬ ПО
РС.Сотрудник,
НАЧАЛОПЕРИОДА(РС.Дата, ДЕНЬ)
;
Теперь найдем все «начала» интервалов. Для этого используем левое соединение таблицы самой с собой по условию Т1.Дата = Т2.Дата + 1 день. Если соединение не нашло пару, значит перед нами начало периода.
ВЫБРАТЬ
Данные.Сотрудник,
Данные.Дата КАК ДатаНачала
ПОМЕСТИТЬ ВТ_Начала
ИЗ
ВТ_ИсходныеДанные КАК Данные
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеДанные КАК ДанныеДо
ПО Данные.Сотрудник = ДанныеДо.Сотрудник
И Данные.Дата = ДОБАВИТЬКДАТЕ(ДанныеДо.Дата, ДЕНЬ, 1)
ГДЕ
ДанныеДо.Дата ЕСТЬ NULL
;
Аналогичным образом вычислим «концы» интервалов:
ВЫБРАТЬ
Данные.Сотрудник,
Данные.Дата КАК ДатаОкончания
ПОМЕСТИТЬ ВТ_Концы
ИЗ
ВТ_ИсходныеДанные КАК Данные
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеДанные КАК ДанныеПосле
ПО Данные.Сотрудник = ДанныеПосле.Сотрудник
И Данные.Дата = ДОБАВИТЬКДАТЕ(ДанныеПосле.Дата, ДЕНЬ, -1)
ГДЕ
ДанныеПосле.Дата ЕСТЬ NULL
;
Финальным шагом мы соединяем каждое начало с ближайшим к нему концом, который больше или равен дате начала:
ВЫБРАТЬ
Начала.Сотрудник,
Начала.ДатаНачала,
МИНИМУМ(Концы.ДатаОкончания) КАК ДатаОкончания
ИЗ
ВТ_Начала КАК Начала
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Концы КАК Концы
ПО Начала.Сотрудник = Концы.Сотрудник
И Начала.ДатаНачала <= Концы.ДатаОкончания
СГРУППИРОВАТЬ ПО
Начала.Сотрудник,
Начала.ДатаНачала
Этот метод считается более изящным и часто более производительным. Его суть заключается в следующем: если мы пронумеруем даты внутри группы (по сотруднику) и вычтем этот порядковый номер из даты, то для всех дат, идущих подряд, мы получим одну и тот же базовую дату.
Разберем пример:
Даты: 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. Для оптимизации вычислений на больших объемах можно применить метод быстрого определения интервалов в запросе с линейной зависимостью от объема данных.
Иногда задача усложняется: периоды могут не просто идти подряд, но и накладываться друг на друга (например, один больничный открыт с 1 по 10 число, а второй — с 5 по 15). Простой поиск Дата + 1 здесь не поможет.
Проанализируем ситуацию: чтобы объединить пересекающиеся интервалы, нам нужно использовать алгоритм «нарастающего итога» или циклическое объединение. В языке запросов 1С это реализуется через вычисление максимального окончания всех предыдущих периодов. Если начало текущего периода меньше или равно максимальному окончанию предыдущих — периоды пересекаются.
Пример логики для объединения пересечений:
ВЫБОР для определения принадлежности к текущему «острову».Рассмотрим фрагмент запроса для поглощенных периодов:
ВЫБРАТЬ
Дано.Клиент,
Дано.НачалоПериода,
Дано.КонецПериода,
СУММА(РАЗНОСТЬДАТ(ДатыДо.НачалоПериода,
ВЫБОР
КОГДА ДатыДо.КонецПериода <= Дано.НачалоПериода ТОГДА ДатыДо.КонецПериода
ИНАЧЕ Дано.НачалоПериода
КОНЕЦ, СЕКУНДА)) КАК Интеграл
ПОМЕСТИТЬ ВТ_Интеграл
ИЗ
Дано КАК Дано
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Дано КАК ДатыДо
ПО ДатыДо.НачалоПериода <= Дано.НачалоПериода
И ДатыДо.Клиент = Дано.Клиент
СГРУППИРОВАТЬ ПО
Дано.Клиент, Дано.НачалоПериода, Дано.КонецПериода
Выясним, как сделать наши запросы максимально быстрыми:
ПОМЕСТИТЬ) всегда индексируйте поля, по которым происходит соединение (Сотрудник, Дата). Это критично для производительности.НАЧАЛОПЕРИОДА(..., ДЕНЬ). Если в данных окажется время (например, 12:00:00), условие +1 день не сработает корректно.Мы проанализировали основные подходы к решению задачи интервалов. Способ с определением границ является универсальным и надежным, в то время как метод порядковых номеров подкупает своей лаконичностью. Выбор конкретного метода зависит от структуры ваших данных и требований к производительности системы.