В практике разработки на платформе 1С часто возникает задача построения отчета, который должен отображать состояние объекта (например, сотрудника) на каждую календарную дату внутри определенного периода. Основная сложность заключается в том, что регистры сведений хранят данные только в моменты изменения состояний, а нам необходимо «развернуть» эти записи в непрерывную последовательность дней. Ситуация осложняется, когда один период может «вытеснять» другой — например, когда командировка прерывает период простоя.
Разберем подробно, как решить эту задачу исключительно средствами языка запросов 1С, проанализируем различные подходы и выберем наиболее эффективный.
Для того чтобы получить данные на каждый день, нам сначала нужна «опорная» таблица, содержащая все даты целевого периода. В 1С нет готовой функции, которая возвращает таблицу дат, поэтому мы сформируем её с помощью соединения нескольких таблиц с цифрами. Это классический прием, позволяющий получить последовательность чисел, которую затем можно преобразовать в даты.
Рассмотрим пример формирования таблицы дат на 1000 дней вперед от даты начала:
ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4
ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, Единицы.Цифра + Десятки.Цифра * 10 + Сотни.Цифра * 100), ДЕНЬ) КАК ПериодДень
ПОМЕСТИТЬ ВТ_Календарь
ИЗ
ВТ_Цифры КАК Единицы
КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Десятки
КРОСС СОЕДИНЕНИЕ ВТ_Цифры КАК Сотни
ГДЕ
ДОБАВИТЬКДАТЕ(&ДатаНачала, ДЕНЬ, Единицы.Цифра + Десятки.Цифра * 10 + Сотни.Цифра * 100) <= &ДатаОкончания;
В данном фрагменте мы используем КРОСС СОЕДИНЕНИЕ (декартово произведение), чтобы получить комбинации цифр от 0 до 999. Условие в секции ГДЕ ограничивает выборку только нужным нам интервалом дат. Это гораздо производительнее, чем передавать готовую таблицу значений из кода 1С в запрос.
Теперь, когда у нас есть таблица ВТ_Календарь, нам необходимо соединить её с регистром сведений СостояниеФизическихЛиц. Основная проблема здесь — получить именно ту запись, которая является «последней» для каждой конкретной даты. Виртуальная таблица СрезПоследних не подходит напрямую, так как она не умеет принимать поле из другой таблицы в качестве параметра периода.
Проанализируем метод имитации среза последних через подзапрос. Мы ищем максимальную дату записи в регистре, которая меньше или равна нашей текущей дате из календаря:
ВЫБРАТЬ
Календарь.ПериодДень КАК ПериодДень,
&Сотрудник КАК ФизическоеЛицо,
МАКСИМУМ(Регистр.Период) КАК ПериодЗаписи
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
ВТ_Календарь КАК Календарь
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СостояниеФизическихЛиц КАК Регистр
ПО Регистр.ФизическоеЛицо = &Сотрудник
И Регистр.Период <= Календарь.ПериодДень
СГРУППИРОВАТЬ ПО
Календарь.ПериодДень;
Важный момент: если ваш регистр содержит реквизит ДатаОкончания, его необходимо учитывать. В противном случае статус будет «тянуться» бесконечно, пока не появится новая запись. Если же статус ограничен во времени, условие соединения должно быть дополнено проверкой: Календарь.ПериодДень МЕЖДУ Регистр.Период И Регистр.ДатаОкончания.
Ситуация, описанная автором (Простой вытесняется Командировкой), указывает на то, что в один и тот же день для сотрудника могут действовать две записи. В типовых конфигурациях вроде ЗУП 3.1 эта проблема решается с помощью интервальных регистров, где периоды уже «нарезаны» без пересечений при записи документа. Если мы работаем с обычным регистром, нам нужно определить приоритет.
Рассмотрим, как можно применить логику выбора приоритетного состояния. Мы можем добавить к статусам числовой приоритет (например, Командировка = 2, Простой = 1). Тогда финальный запрос будет выглядеть так:
ВЫБРАТЬ
Календарь.ПериодДень,
Состояния.Статус
ИЗ
ВТ_Календарь КАК Календарь
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СостояниеФизическихЛиц КАК Состояния
ПО Состояния.ФизическоеЛицо = &Сотрудник
И Календарь.ПериодДень МЕЖДУ Состояния.Период И Состояния.ДатаОкончания
ГДЕ
// Выбираем только одну запись с максимальным приоритетом или самой поздней датой начала
Состояния.Период = (
ВЫБРАТЬ МАКСИМУМ(Внутр.Период)
ИЗ РегистрСведений.СостояниеФизическихЛиц КАК Внутр
ГДЕ Внутр.ФизическоеЛицо = &Сотрудник
И Календарь.ПериодДень МЕЖДУ Внутр.Период И Внутр.ДатаОкончания
)
Проанализируем ситуацию, если записи вносятся «в пределах секунды». Использование МАКСИМУМ(Период) в подзапросе гарантирует, что если командировка была введена позже простоя и их периоды пересекаются, запрос выберет именно её. Это и реализует механизм «вытеснения» в простейшем виде.
При работе с большими объемами данных (много сотрудников и длинные периоды) такие запросы могут выполняться медленно из-за большого количества соединений. Выясним причины и способы оптимизации:
ФизическоеЛицо и Период проиндексированы. Если используется ДатаОкончания, желательно также включить её в индекс.ВТ. Это помогает планировщику SQL-сервера построить оптимальный план выполнения.В современных конфигурациях 1С рекомендуется по возможности использовать архитектуру интервальных регистров. Это специальные регистры сведений, в которые при проведении документа записываются уже «чистые» интервалы: если документ «Командировка» пересекается с существующим «Простоем», программный код при записи разделяет одну запись простоя на две части (до и после командировки). В этом случае ваш запрос превратится в простое соединение с условием МЕЖДУ, что будет работать мгновенно.
Подводя итог, мы рассмотрели универсальный способ получения данных на каждую дату. Используя комбинацию генерации дат через кросс-соединение цифр и корректное соединение с физической таблицей регистра с учетом максимального периода, можно решить задачу вытеснения состояний даже в сложных кастомных конфигурациях.