В практике разработчика 1С часто возникает задача «наложить» один временной интервал на другой. Типичный пример: у сотрудника есть период начисления зарплаты (например, месяц), и внутри этого месяца менялись условия стажа или процента надбавки. Нам необходимо получить таблицу, где каждый отрезок времени будет сопоставлен с актуальным для него значением. В этой статье мы подробно разберем, как реализовать это «красиво» с помощью одного запроса, избегая перебора в цикле на языке 1С:Предприятие.
Прежде чем переходить к написанию кода, проанализируем условие, при котором два периода вообще имеют общие точки. Пусть первый период задан границами [ДатаНачала1, ДатаОкончания1], а второй — [ДатаНачала2, ДатаОкончания2].
Периоды пересекаются тогда и только тогда, когда выполняется условие: Начало одного периода меньше или равно окончанию другого. В терминах 1С это выглядит так:
ДатаНачала1 <= ДатаОкончания2 И ДатаОкончания1 >= ДатаНачала2
Если это условие выполняется, то у периодов есть общая часть. Границы этого пересечения определяются по следующему правилу:
Рассмотрим ситуацию, когда у нас есть временная таблица ТаблицаПериодов (основной период) и ТаблицаПериодовСтажа (вспомогательные периоды с данными). Нам нужно соединить их так, чтобы в результат попали только пересекающиеся части.
Посмотрим на пример базового соединения:
ВЫБРАТЬ
ТаблицаПериодов.ДатаНачалаНачисления КАК ДатаНачалаНачисления,
ТаблицаПериодов.ДатаОкончанияНачисления КАК ДатаОкончанияНачисления,
ТаблицаПериодовСтажа.ДатаНачалаСтажа КАК ДатаНачалаСтажа,
ТаблицаПериодовСтажа.ДатаОкончанияСтажа КАК ДатаОкончанияСтажа,
ТаблицаПериодовСтажа.Процент КАК Процент
ИЗ
ТаблицаПериодов КАК ТаблицаПериодов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТаблицаПериодовСтажа КАК ТаблицаПериодовСтажа
ПО ТаблицаПериодов.ДатаНачалаНачисления <= ТаблицаПериодовСтажа.ДатаОкончанияСтажа
И ТаблицаПериодов.ДатаОкончанияНачисления >= ТаблицаПериодовСтажа.ДатаНачалаСтажа
Важный нюанс: Такое соединение просто выберет строки, которые пересекаются. Если мы оставим выбор полей в таком виде, мы получим исходные даты, а не границы «разрезанного» периода. Например, для периода начисления с 15.05 по 29.05 и периода стажа с 01.05 по 18.05 мы получим строку с исходными датами, что не решит задачу.
Поскольку в языке запросов 1С отсутствуют скалярные функции МИН и МАКС для сравнения двух значений в одной строке (агрегатные функции здесь не подходят), мы должны использовать конструкцию ВЫБОР.
Разберем, как вычислить правильные даты пересечения:
ВЫБРАТЬ
// Вычисляем Максимум из двух дат начала
ВЫБОР
КОГДА ТаблицаПериодов.ДатаНачалаНачисления > ТаблицаПериодовСтажа.ДатаНачалаСтажа
ТОГДА ТаблицаПериодов.ДатаНачалаНачисления
ИНАЧЕ ТаблицаПериодовСтажа.ДатаНачалаСтажа
КОНЕЦ КАК ДатаНачалаРезультат,
// Вычисляем Минимум из двух дат окончания
ВЫБОР
КОГДА ТаблицаПериодов.ДатаОкончанияНачисления < ТаблицаПериодовСтажа.ДатаОкончанияСтажа
ТОГДА ТаблицаПериодов.ДатаОкончанияНачисления
ИНАЧЕ ТаблицаПериодовСтажа.ДатаОкончанияСтажа
КОНЕЦ КАК ДатаОкончанияРезультат,
ТаблицаПериодовСтажа.Процент КАК Процент
ИЗ
ТаблицаПериодов КАК ТаблицаПериодов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТаблицаПериодовСтажа КАК ТаблицаПериодовСтажа
ПО ТаблицаПериодов.ДатаНачалаНачисления <= ТаблицаПериодовСтажа.ДатаОкончанияСтажа
И ТаблицаПериодов.ДатаОкончанияНачисления >= ТаблицаПериодовСтажа.ДатаНачалаСтажа
Проанализируем работу этого кода на примере. Если период начисления 15.05–29.05 «встречается» с периодом стажа 01.05–18.05:
МАКС(15.05, 01.05) = 15.05МИН(29.05, 18.05) = 18.05Тип Дата в 1С всегда включает время. При работе с периодами «по день включительно» крайне важно следить за тем, чтобы границы не «разъехались». Если одна дата — это начало дня (00:00:00), а другая — произвольное время, результат может быть некорректным.
Рекомендуем всегда приводить даты к началу или концу периода. Выясним причину возможных ошибок: если период стажа заканчивается 18.05 в 00:00:00 (фактически это начало дня), а начисление начинается 18.05 в 09:00:00, то по условию <= и >= они пересекутся, но фактически это могут быть разные логические периоды.
Для надежности используйте функции НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА:
ПО НАЧАЛОПЕРИОДА(ТаблицаПериодов.ДатаНачалаНачисления, ДЕНЬ) <= КОНЕЦПЕРИОДА(ТаблицаПериодовСтажа.ДатаОкончанияСтажа, ДЕНЬ)
И КОНЕЦПЕРИОДА(ТаблицаПериодов.ДатаОкончанияНачисления, ДЕНЬ) >= НАЧАЛОПЕРИОДА(ТаблицаПериодовСтажа.ДатаНачалаСтажа, ДЕНЬ)
Когда мы соединяем таблицы по условиям «больше-меньше», СУБД выполняет более тяжелую работу, чем при соединении по точному равенству. Чтобы запрос работал быстро на больших объемах данных, следует соблюдать правила:
ГДЕ или при выборе из физической таблицы в виртуальную, чтобы не соединять лишние данные.ВНУТРЕННЕЕ само отсечет ситуации, когда пересечения нет. Однако, если вы используете ЛЕВОЕ СОЕДИНЕНИЕ, обязательно проверяйте результат на NULL и корректность полученных дат (Начало не должно быть больше Окончания).В некоторых сложных случаях (например, когда нужно исключать праздничные дни внутри пересекающихся периодов) проще использовать вспомогательную таблицу чисел или дат. Разберем по шагам этот алгоритм:
День МЕЖДУ Начало И Конец.МИНИМУМ(День) и МАКСИМУМ(День).Хотя этот метод более ресурсоемкий, он позволяет решать задачи с пропусками и сложными графиками гораздо нагляднее.
Подводя итог, можно сказать, что решение через INNER JOIN и ВЫБОР является наиболее универсальным и производительным способом работы с периодами в 1С. Оно позволяет получить чистый и понятный результат без использования циклов в коде Объектной модели.