В практике разработки на платформе 1С:Предприятие 8.3 часто возникает задача преобразования набора дискретных записей (например, поминутной занятости оборудования или сотрудников) в наглядные временные интервалы. Типичный пример: в регистре сведений хранятся записи по минутам с признаком Занято (Булево). Нам же требуется получить результат в виде: «с 1-й по 10-ю минуту — Свободно, с 11-й по 25-ю — Занято». В SQL эта задача известна как Gaps and Islands («Разрывы и острова»).
Разберем подробно, как реализовать такой алгоритм в языке запросов 1С, проанализируем возможные подходы и выберем наиболее оптимальный вариант, который позволит избежать избыточности данных и корректно обработать границы периодов.
Предположим, у нас имеется непериодический регистр сведений ПланированиеВремени со следующей структурой:
РабочееМесто, День, НомерМинуты (тип Число, от 1 до 1440).Занято (тип Булево).Проблема прямого выбора заключается в том, что обычная группировка по значению Занято схлопнет все минуты дня в две строки, потеряв хронологическую последовательность. Нам же нужно сохранить порядок и выделить каждый непрерывный блок отдельно. Рассмотрим пошагово, как это сделать.
Прежде всего, ограничим выборку интересующим нас периодом и разрезом (например, конкретным рабочим местом и днем). Поместим эти данные во временную таблицу ВТ_ИсходныеДанные. Это позволит нам обращаться к данным несколько раз без повторного обращения к физической таблице базы данных, что значительно ускорит выполнение запроса.
ВЫБРАТЬ
Т.НомерМинуты КАК НомерМинуты,
Т.Занято КАК Занято,
Т.РабочееМесто КАК РабочееМесто
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
РегистрСведений.ПланированиеВремени КАК Т
ГДЕ
Т.День = &День
И Т.РабочееМесто = &РабочееМесто;
Основная идея решения заключается в определении моментов, когда значение ресурса Занято меняется по сравнению с предыдущей записью. Проанализируем ситуацию: если для текущей минуты значение Занято отличается от значения в предыдущей минуте, значит, начался новый интервал.
Рассмотрим вариант соединения таблицы самой с собой для поиска «предыдущего» значения. Мы ищем максимальный номер минуты, который меньше текущего, и смотрим состояние в этой точке. Однако есть более изящный способ — найти для каждой записи «точку отсчета», то есть начало её «острова».
Для каждой минуты нам нужно найти ближайшую предшествующую минуту, в которой состояние было иным. Все записи, имеющие одну и ту же «ближайшую границу изменения», относятся к одному интервалу.
Посмотрим на пример логики формирования временной таблицы ВТ_Границы:
ВЫБРАТЬ
М1.НомерМинуты КАК НомерМинуты,
М1.Занято КАК Занято,
МАКСИМУМ(М11.НомерМинуты) КАК НомерМинутыПредыдущегоИзменения
ПОМЕСТИТЬ ВТ_РазмеченныеДанные
ИЗ
ВТ_ИсходныеДанные КАК М1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеДанные КАК М11
ПО М1.НомерМинуты > М11.НомерМинуты
И М1.Занято <> М11.Занято
ГРУППИРОВАТЬ ПО
М1.НомерМинуты,
М1.Занято;
В этой таблице поле НомерМинутыПредыдущегоИзменения будет содержать NULL для самого первого интервала в дне и номер последней минуты предыдущего интервала для всех последующих. Это идеальный маркер для группировки.
Теперь нам остается сгруппировать данные по найденному маркеру границы и значению ресурса. Минимальный номер минуты в группе станет началом периода, а максимальный — концом.
ВЫБРАТЬ
ЕСТЬNULL(ВТ.НомерМинутыПредыдущегоИзменения + 1, 1) КАК МинутаНачала,
МАКСИМУМ(ВТ.НомерМинуты) КАК МинутаОкончания,
ВТ.Занято КАК СтатусЗанятости
ИЗ
ВТ_РазмеченныеДанные КАК ВТ
ГРУППИРОВАТЬ ПО
ВТ.Занято,
ВТ.НомерМинутыПредыдущегоИзменения
УПОРЯДОЧИТЬ ПО
МинутаНачала;
Важный нюанс: Обратите внимание на конструкцию ЕСТЬNULL(ВТ.НомерМинутыПредыдущегоИзменения + 1, 1). Она корректно обрабатывает самый первый интервал дня, устанавливая его начало в 1-ю минуту, а для всех последующих интервалов прибавляет единицу к моменту последнего изменения статуса.
Рассмотрим ситуацию, когда данных очень много (например, тысячи рабочих мест). Соединение таблицы ВТ_ИсходныеДанные самой с собой по условию М1.НомерМинуты > М11.НомерМинуты имеет квадратичную сложность O(n²). Если в сутках 1440 минут, то для одного рабочего места запрос отработает быстро, но при массовом расчете за месяц производительность может упасть.
В таких случаях рекомендуется:
ROW_NUMBER(), её можно имитировать. Разность между общим порядковым номером записи и порядковым номером внутри группы (где Занято = Истина) будет постоянной для одного непрерывного интервала.Проанализируем пример кода для обработки в цикле, если вы решите отказаться от сложного запроса:
ТекущееСостояние = Неопределено;
НачалоИнтервала = 0;
Пока Выборка.Следующий() Цикл
Если Выборка.Занято <> ТекущееСостояние Тогда
Если ТекущееСостояние <> Неопределено Тогда
// Фиксируем законченный интервал
НоваяСтрока = ТаблицаРезультат.Добавить();
НоваяСтрока.Начало = НачалоИнтервала;
НоваяСтрока.Конец = Выборка.НомерМинуты - 1;
НоваяСтрока.Статус = ТекущееСостояние;
КонецЕсли;
НачалоИнтервала = Выборка.НомерМинуты;
ТекущееСостояние = Выборка.Занято;
КонецЕсли;
КонецЦикла;
Мы разобрали классический способ решения задачи группировки последовательностей в 1С. Использование временных таблиц и корреляционных соединений позволяет получить результат в виде компактного списка интервалов прямо в запросе. Это удобно для последующего вывода в отчеты (СКД) или использования в других запросах. Однако всегда помните о нагрузке на сервер БД: для очень больших массивов данных алгоритм «поиска предыдущего» через соединение может потребовать оптимизации или переноса логики на сторону встроенного языка.