Срез последних на каждую дату — это задача, когда значение периодического регистра сведений нужно получить не на одну общую дату, а на свою дату для каждой строки: на дату документа, на годовщину приёма, на каждый день периода. Стандартная виртуальная таблица СрезПоследних принимает только одну дату, поэтому задачу решают либо «классическим» запросом через группировку по максимальному периоду, либо «интервальным» — превращая точечные записи регистра в интервалы «с даты по дату» и соединяясь с ними по условию МЕЖДУ.
Второй подход в 1С:ЗУП вырос в отдельный механизм — интервальные регистры: интервалы хранятся во вспомогательном регистре и обновляются при записи основного. Ниже разберём оба запроса на простом примере, посмотрим, как устроен механизм ЗУП, и сделаем собственный интервальный регистр.
Модельный пример и стандартные средства платформы
Возьмём периодический регистр сведений КадроваяИстория, подчинённый регистратору:
| Поле | Вид | Тип |
|---|---|---|
| Период | стандартный | Дата |
| Регистратор | стандартный | ДокументСсылка.ПеремещениеВПодразделение |
| Сотрудник | измерение | СправочникСсылка.Сотрудники |
| Подразделение | ресурс | СправочникСсылка.Подразделения |
У справочника Сотрудники есть реквизит ДатаРождения.
Пока дата одна, платформа даёт всё необходимое:
МенеджерРегистра = РегистрыСведений.КадроваяИстория;
ОтборПоСотруднику = Новый Структура("Сотрудник", Сотрудник);
// Действующее значение на дату — последняя запись с периодом не позже даты.
ДанныеНаДату = МенеджерРегистра.ПолучитьПоследнее(ДатаСреза, ОтборПоСотруднику);
Подразделение = ДанныеНаДату.Подразделение;
// Срез по всем сотрудникам на одну дату.
ТаблицаСреза = МенеджерРегистра.СрезПоследних(ДатаСреза);Метод Получить возвращает запись ровно на указанный период, ПолучитьПоследнее — действующую на дату. В запросах тот же смысл у виртуальной таблицы:
ВЫБРАТЬ
КадроваяИсторияСрез.Сотрудник КАК Сотрудник,
КадроваяИсторияСрез.Подразделение КАК Подразделение
ИЗ
РегистрСведений.КадроваяИстория.СрезПоследних(&ДатаСреза, ) КАК КадроваяИсторияСрезПроблема начинается, когда даты разные. Пример задачи: для каждого сотрудника найти подразделение, в котором он работал в день рождения в выбранном году. ПолучитьПоследнее решает её только в цикле — запрос на каждого сотрудника, что противоречит стандартам и плохо масштабируется. СрезПоследних с одной датой не подходит вовсе.
Классический вариант: максимум периода не позже даты
Идея: для каждой пары «сотрудник — дата среза» найти максимальный период записи регистра, не превышающий эту дату, и затем взять запись с этим периодом.
// 1. Дата среза для каждого сотрудника — день рождения в выбранном году.
ВЫБРАТЬ
Сотрудники.Ссылка КАК Сотрудник,
ДОБАВИТЬКДАТЕ(Сотрудники.ДатаРождения, ГОД,
ГОД(&ДатаГода) - ГОД(Сотрудники.ДатаРождения)) КАК ДатаСреза
ПОМЕСТИТЬ ВТДатыСреза
ИЗ
Справочник.Сотрудники КАК Сотрудники
;
////////////////////////////////////////////////////////////////////////////////
// 2. Для каждой даты — последний период изменения не позже неё.
ВЫБРАТЬ
ДатыСреза.Сотрудник КАК Сотрудник,
ДатыСреза.ДатаСреза КАК ДатаСреза,
МАКСИМУМ(История.Период) КАК Период
ПОМЕСТИТЬ ВТПоследниеПериоды
ИЗ
ВТДатыСреза КАК ДатыСреза
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИстория КАК История
ПО ДатыСреза.Сотрудник = История.Сотрудник
И История.Период <= ДатыСреза.ДатаСреза
СГРУППИРОВАТЬ ПО
ДатыСреза.Сотрудник,
ДатыСреза.ДатаСреза
;
////////////////////////////////////////////////////////////////////////////////
// 3. Значение ресурса на найденный период.
ВЫБРАТЬ
ПоследниеПериоды.Сотрудник КАК Сотрудник,
ПоследниеПериоды.ДатаСреза КАК ДатаСреза,
История.Подразделение КАК Подразделение
ИЗ
ВТПоследниеПериоды КАК ПоследниеПериоды
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИстория КАК История
ПО ПоследниеПериоды.Сотрудник = История.Сотрудник
И ПоследниеПериоды.Период = История.ПериодВажно, что соединение во втором пакете идёт и по сотруднику, и по условию на период: без равенства по измерению запрос вернёт чепуху. Если сотрудник родился 29 февраля, ДОБАВИТЬКДАТЕ в невисокосный год даст 28 февраля — это стоит учесть в требованиях. Сотрудники, у которых на дату ещё нет записей, во внутреннее соединение не попадут; если они нужны в результате, используйте левое соединение от ВТДатыСреза.
Интервальный вариант: превращаем записи в отрезки
Второй подход сначала строит таблицу интервалов: для каждой записи регистра — дата начала (её период) и дата окончания (период следующей записи того же сотрудника минус секунда). Дальше любой срез — это одно соединение по условию ДатаСреза МЕЖДУ ДатаНачала И ДатаОкончания.

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

Модуль набора записей основного регистра запоминает сотрудников до и после записи и просит пересчитать их интервалы:
Процедура ПередЗаписью(Отказ, Замещение)
// Сотрудники из прежних записей регистратора: их интервалы тоже меняются.
Запрос = Новый Запрос(
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| История.Сотрудник КАК Сотрудник
|ИЗ
| РегистрСведений.КадроваяИстория КАК История
|ГДЕ
| История.Регистратор = &Регистратор");
Запрос.УстановитьПараметр("Регистратор", Отбор.Регистратор.Значение);
ДополнительныеСвойства.Вставить("СотрудникиДоЗаписи",
Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Сотрудник"));
КонецПроцедуры
Процедура ПриЗаписи(Отказ, Замещение)
// Проверку ОбменДанными.Загрузка намеренно не делаем:
// интервалы должны быть актуальны и после загрузки из обмена.
Сотрудники = Выгрузить(, "Сотрудник").ВыгрузитьКолонку("Сотрудник");
Для Каждого Сотрудник Из ДополнительныеСвойства.СотрудникиДоЗаписи Цикл
Если Сотрудники.Найти(Сотрудник) = Неопределено Тогда
Сотрудники.Добавить(Сотрудник);
КонецЕсли;
КонецЦикла;
РегистрыСведений.КадроваяИсторияИнтервальный.ПерестроитьИнтервалы(Сотрудники);
КонецПроцедурыВ модуле менеджера интервального регистра — пересчёт по полной истории выбранных сотрудников:
#Область ПрограммныйИнтерфейс
// Пересчитывает интервалы действия значений для указанных сотрудников.
//
// Параметры:
// Сотрудники - Массив из СправочникСсылка.Сотрудники
//
Процедура ПерестроитьИнтервалы(Сотрудники) Экспорт
Если Сотрудники.Количество() = 0 Тогда
Возврат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Сотрудники", Сотрудники);
Запрос.Текст =
"ВЫБРАТЬ
| История.Сотрудник КАК Сотрудник,
| История.Период КАК Период,
| История.Подразделение КАК Подразделение
|ПОМЕСТИТЬ ВТИстория
|ИЗ
| РегистрСведений.КадроваяИстория КАК История
|ГДЕ
| История.Сотрудник В (&Сотрудники)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Начало.Сотрудник КАК Сотрудник,
| Начало.Подразделение КАК Подразделение,
| Начало.Период КАК ДатаНачала,
| ЕСТЬNULL(ДОБАВИТЬКДАТЕ(МИНИМУМ(Следующее.Период), СЕКУНДА, -1),
| ДАТАВРЕМЯ(3999, 12, 31, 23, 59, 59)) КАК ДатаОкончания
|ИЗ
| ВТИстория КАК Начало
| ЛЕВОЕ СОЕДИНЕНИЕ ВТИстория КАК Следующее
| ПО Начало.Сотрудник = Следующее.Сотрудник
| И Начало.Период < Следующее.Период
|СГРУППИРОВАТЬ ПО
| Начало.Сотрудник,
| Начало.Подразделение,
| Начало.Период";
Интервалы = Запрос.Выполнить().Выгрузить();
Для Каждого Сотрудник Из Сотрудники Цикл
НаборЗаписей = СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Сотрудник.Установить(Сотрудник);
Для Каждого СтрокаИнтервала Из Интервалы.НайтиСтроки(Новый Структура("Сотрудник", Сотрудник)) Цикл
ЗаполнитьЗначенияСвойств(НаборЗаписей.Добавить(), СтрокаИнтервала);
КонецЦикла;
// Пустой набор с отбором удалит интервалы сотрудника, у которого не осталось истории.
НаборЗаписей.Записать();
КонецЦикла;
КонецПроцедуры
#КонецОбластиПересчёт идёт в той же транзакции, что и запись основного регистра, поэтому интервальный регистр не рассинхронизируется. После этого срез на каждую дату становится тривиальным:
ВЫБРАТЬ
ДатыСреза.Сотрудник КАК Сотрудник,
ДатыСреза.ДатаСреза КАК ДатаСреза,
Интервалы.Подразделение КАК Подразделение
ИЗ
ВТДатыСреза КАК ДатыСреза
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияИнтервальный КАК Интервалы
ПО ДатыСреза.Сотрудник = Интервалы.Сотрудник
И (ДатыСреза.ДатаСреза МЕЖДУ Интервалы.ДатаНачала И Интервалы.ДатаОкончания)Не забудьте первичное заполнение: для уже существующих данных вызовите ПерестроитьИнтервалы порциями по всем сотрудникам — например, обработчиком обновления.
Типичные ошибки
- Срез в цикле.
ПолучитьПоследнееилиСрезПоследнихдля каждой строки — самая частая причина медленных отчётов по кадровым данным. - Нет равенства по измерениям при соединении по периоду в классическом варианте — результат перемешивается между сотрудниками.
- Перекрытие интервалов. Если в регистре несколько записей на один период и сотрудника (разные регистраторы), интервалы задублируются. Это нужно исключить на уровне данных или правил записи.
- Граница интервала. «Минус секунда» корректна, когда период хранится с точностью до секунды; для периодичности «день» и выше удобнее вычитать день.
- Забытые удалённые записи. При отмене проведения сотрудник исчезает из нового набора — его надо запомнить в
ПередЗаписью, иначе интервалы останутся старыми.
Если отчёт по кадровым или ценовым данным заметно тормозит, причину почти всегда видно в техжурнале — это типичная находка аудита производительности 1С. Собственный интервальный механизм в нетиповой конфигурации делаю в рамках доработки 1С.
Итог
СрезПоследнихработает на одну дату; срез на индивидуальные даты требует отдельного приёма.- Классический вариант — максимум периода не позже даты с соединением по измерениям, затем выбор записи по найденному периоду.
- Интервальный вариант превращает записи в отрезки «с — по» и решает срез одним соединением через
МЕЖДУ. - В ЗУП интервалы хранятся во вспомогательных регистрах, а выборка идёт через функции
ЗарплатаКадрыПериодическиеРегистры. - Для своей конфигурации интервальный регистр делается за пару объектов: пересчёт в
ПриЗаписиосновного набора и простое соединение в запросах.




