При работе с периодическими регистрами сведений в 1С разработчики часто сталкиваются с классической задачей: получить полный список объектов (сотрудников, контрагентов, детей в группах), которые были активны в течение определенного периода времени. Простая выборка из физической таблицы регистра или использование только виртуальных таблиц срезов по отдельности не дают полной картины.
Если мы берем только физическую таблицу за период (движения), то мы теряем тех, кто начал работать (или был зачислен) до начала этого периода и не имел никаких движений внутри него. Если же мы используем только виртуальную таблицу СрезПоследних или СрезПервых, мы гарантированно теряем «обороты» — например, сотрудников, которые были приняты и уволены внутри одного и того же расчетного месяца.
В этой статье мы подробно разберем, как правильно составить запрос, объединяющий начальные данные и обороты за период, на примере регистра сведений СоставГруппДетскогоУчреждения. Мы проанализируем типичные ошибки, оптимизируем решение и рассмотрим профессиональную альтернативу — метод интервалов.
Представим, что у нас есть периодический регистр сведений СоставГруппДетскогоУчреждения (периодичность — в пределах дня). У него присутствуют следующие реквизиты, измерения и ресурсы:
ГруппаДетскогоУчреждения (СправочникСсылка)Ребенок (СправочникСсылка)ВыбылИзГруппы (Булево) — принимает значение Истина, когда ребенок покидает группу, и Ложь при зачислении.Период (Дата)Нам необходимо получить список всех детей, которые посещали выбранную группу в интервале между &ДатаН и &ДатаК. В этот список должны попасть:
&ДатаН и не выбыли на момент начала периода.Рассмотрим детально первый подход. Он заключается в том, чтобы выбрать данные из двух разных источников и объединить их с помощью оператора ОБЪЕДИНИТЬ ВСЕ, после чего сгруппировать полученный результат во избежание дублирования строк.
Разберем этот запрос по шагам.
Для того чтобы узнать, кто уже состоял в группе на момент начала нашего периода (на дату &ДатаН), мы обращаемся к виртуальной таблице СрезПоследних. При этом нам нужно наложить два важных фильтра:
ВыбылИзГруппы = ЛОЖЬ необходимо накладывать в секции ГДЕ. Если наложить его в параметрах виртуальной таблицы, СУБД сначала отберет записи, где этот ресурс равен ЛОЖЬ, а уже потом сделает срез. Это приведет к тому, что в выборку попадут дети, которые фактически давно выбыли, но их последняя «активная» запись все еще висит в истории.Вторая часть запроса выбирает данные напрямую из физической таблицы регистра сведений. Здесь мы накладываем условие на период: запись должна находиться строго в границах от &ДатаН до &ДатаК, а также соответствовать нашей целевой группе.
Мы используем оператор ОБЪЕДИНИТЬ ВСЕ вместо стандартного ОБЪЕДИНИТЬ. Почему это важно? Оператор ОБЪЕДИНИТЬ заставляет СУБД сравнивать каждую строчку и удалять дубликаты непосредственно в момент объединения. Это крайне ресурсоемкая операция. Гораздо быстрее объединить все строки «как есть» с помощью ОБЪЕДИНИТЬ ВСЕ, поместить их во вложенный запрос (или временную таблицу) и уже там применить оператор ВЫБРАТЬ РАЗЛИЧНЫЕ или СГРУППИРОВАТЬ ПО.
Посмотрим на готовый программный код реализации этого решения:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ВТ_Объединение.Ребенок КАК Ребенок
|ИЗ
| (ВЫБРАТЬ
| СрезПоследних.Ребенок КАК Ребенок
| ИЗ
| РегистрСведений.СоставГруппДетскогоУчреждения.СрезПоследних(&ДатаН, ГруппаДетскогоУчреждения = &ГруппаДетскогоУчреждения) КАК СрезПоследних
| ГДЕ
| СрезПоследних.ВыбылИзГруппы = ЛОЖЬ
|
| ОБЪЕДИНИТЬ ВСЕ
|
| ВЫБРАТЬ
| СоставГрупп.Ребенок КАК Ребенок
| ИЗ
| РегистрСведений.СоставГруппДетскогоУчреждения КАК СоставГрупп
| ГДЕ
| СоставГрупп.Период >= &ДатаН
| И СоставГрупп.Период <= &ДатаК
| И СоставГрупп.ГруппаДетскогоУчреждения = &ГруппаДетскогоУчреждения) КАК ВТ_Объединение";
Запрос.УстановитьПараметр("ГруппаДетскогоУчреждения", ГруппаДетскогоУчреждения);
Запрос.УстановитьПараметр("ДатаН", ДатаНач);
// Применяем КонецДня, чтобы захватить все возможные движения на границе конечной даты
Запрос.УстановитьПараметр("ДатаК", КонецДня(ДатаКон));
РезультатЗапроса = Запрос.Выполнить();
ТаблицаРезультата = РезультатЗапроса.Выгрузить();
Хотя первый метод прост и понятен, в сложных конфигурациях со множественными и быстрыми изменениями состояний (например, зачисление и выбытие несколько раз за один день или в одну секунду) он может приводить к коллизиям. Для решения таких задач в профессиональной разработке 1С применяется метод интервалов.
Суть метода заключается в том, чтобы программно в запросе для каждой записи о приеме (где ВыбылИзГруппы = ЛОЖЬ) найти сопоставленную ей ближайшую хронологическую запись о выбытии (где ВыбылИзГруппы = ИСТИНА). Таким образом мы получаем непрерывные временные интервалы вида [ДатаПриема, ДатаВыбытия].
После формирования этих интервалов мы можем легко проверить, пересекается ли интервал нахождения ребенка в группе с нашим отчетным периодом [&ДатаН, &ДатаК]. Математически пересечение двух отрезков времени описывается простым условием:
ДатаПриема <= ДатаК И (ДатаВыбытия >= ДатаН ИЛИ ДатаВыбытия ЕСТЬ NULL)
Проанализируем этот алгоритм на примере следующего запроса:
ЗапросИнтервалы = Новый Запрос;
ЗапросИнтервалы.Текст =
"ВЫБРАТЬ
| РегПрием.Ребенок КАК Ребенок,
| РегПрием.Период КАК ДатаПриема,
| МИНИМУМ(РегВыбытие.Period) КАК ДатаВыбытия
|ПОМЕСТИТЬ ВТ_Интервалы
|ИЗ
| РегистрСведений.СоставГруппДетскогоУчреждения КАК РегПрием
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СоставГруппДетскогоУчреждения КАК РегВыбытие
| ПО РегПрием.Ребенок = РегВыбытие.Ребенок
| И РегПрием.ГруппаДетскогоУчреждения = РегВыбытие.ГруппаДетскогоУчреждения
| И РегВыбытие.ВыбылИзГруппы = ИСТИНА
| И РегВыбытие.Период > РегПрием.Период
|ГДЕ
| РегПрием.ВыбылИзГруппы = ЛОЖЬ
| И РегПрием.ГруппаДетскогоУчреждения = &ГруппаДетскогоУчреждения
|
|СГРУППИРОВАТЬ ПО
| РегПрием.Ребенок,
| РегПрием.Период
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| ВТ_Интервалы.Ребенок КАК Ребенок
|ИЗ
| ВТ_Интервалы КАК ВТ_Интервалы
|ГДЕ
| ВТ_Интервалы.ДатаПриема <= &ДатаК
| И (ВТ_Интервалы.ДатаВыбытия >= &ДатаН
| ИЛИ ВТ_Интервалы.ДатаВыбытия ЕСТЬ NULL)";
ЗапросИнтервалы.УстановитьПараметр("ГруппаДетскогоУчреждения", ГруппаДетскогоУчреждения);
ЗапросИнтервалы.УстановитьПараметр("ДатаН", ДатаНач);
ЗапросИнтервалы.УстановитьПараметр("ДатаК", КонецДня(ДатаКон));
РезультатЗапросаИнтервалы = ЗапросИнтервалы.Выполнить();
ТаблицаИнтервалов = РезультатЗапросаИнтервалы.Выгрузить();
Выбор конкретного метода зависит от архитектуры вашей базы данных. Для простых отчетов вполне достаточно использовать Вариант 1 с объединением среза и физической таблицы. Для построения сложных аналитических систем и систем расчета стоимости услуг рекомендуется сразу внедрять Вариант 2.