Разработка и Конфигурация · 11 мин
Как в 1С получить срез последних на каждую дату
Как в 1С получить срез последних на каждую дату: виртуальная таблица, соединение с периодами, запрос в цикле. Примеры кода, ошибки и оптимизация.
Задача «срез последних на каждую дату» возникает всякий раз, когда нужно восстановить историю изменения периодических данных: цен номенклатуры, курсов валют, кадровых назначений сотрудников, остатков по дням. Стандартная виртуальная таблица СрезПоследних умеет отдавать значения только на один конкретный момент, поэтому для получения динамики по нескольким датам требуются дополнительные приёмы в запросе.
Основных рабочих подхода три: соединение таблицы дат с виртуальной таблицей среза через условие «период меньше или равен», группировка по максимальному периоду и обход дат в цикле с отдельными запросами. Каждый вариант имеет свою цену по производительности и свои ограничения, и выбор зависит от объёма регистра и количества дат.
Ниже разобраны все три способа с готовыми текстами запросов, типичные ошибки, которые приводят к неправильным данным или к падению производительности, и способы проверить корректность результата.
Как устроена виртуальная таблица СрезПоследних
Периодический регистр сведений хранит записи с указанием периода, в котором значение начинает действовать. Виртуальная таблица СрезПоследних — это не физическая таблица, а запрос, который платформа строит автоматически: для каждой комбинации измерений она находит запись с максимальным периодом, не превышающим заданную дату.
Параметры виртуальной таблицы: Период (дата или граница момента времени, на которую строится срез) и Условие (отбор по измерениям и реквизитам). Если параметр периода не указать, срез строится на самую позднюю дату среди всех записей — это частая причина ошибок, когда разработчик ожидает текущую дату, а получает «будущие» значения.
Важно понимать ограничение: параметр Период принимает одно значение. Передать туда таблицу дат напрямую нельзя — платформа не построит «много срезов» одним параметром. Именно поэтому и нужны обходные приёмы.
Способ 1. Соединение таблицы дат со срезом по условию на период
Универсальный приём для произвольного списка дат: сначала формируется временная таблица дат (из параметра-таблицы значений или генерацией), затем она соединяется с физической таблицей регистра по условию «период записи меньше или равен дате среза», после чего группировкой выбирается максимальный период для каждой даты и каждого измерения.
ВЫБРАТЬ
Даты.Дата КАК Дата,
Цены.Номенклатура КАК Номенклатура,
МАКСИМУМ(Цены.Период) КАК Период
ПОМЕСТИТЬ МаксПериоды
ИЗ
ТаблицаДат КАК Даты
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО Цены.Период <= Даты.Дата
СГРУППИРОВАТЬ ПО
Даты.Дата,
Цены.Номенклатура
;
ВЫБРАТЬ
М.Дата,
М.Номенклатура,
Цены.Цена
ИЗ
МаксПериоды КАК М
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО Цены.Номенклатура = М.Номенклатура
И Цены.Период = М.Период
Логика двухшаговая: первый запрос находит для каждой пары «дата + номенклатура» максимальный подходящий период, второй — подтягивает ресурсы (цену) по найденному ключу. Делать это одним запросом без промежуточной группировки нельзя, потому что ресурс нельзя взять из «максимальной» строки напрямую.
Если в регистре на один период возможны несколько записей по одному измерению (для независимого регистра это исключено, но при ручной корректировке данных бывает), добавьте в ключ соединения регистратор или используйте РАЗЛИЧНЫЕ, иначе получите задвоение строк.
Способ 2. Последовательные срезы в цикле
Если дат немного (например, конец каждого месяца за год — 12 итераций) и регистр большой, часто быстрее выполнить отдельный запрос к виртуальной таблице на каждую дату и объединить результаты. Виртуальная таблица внутри использует оптимальный план платформы и индексы регистра.
Результат = Новый ТаблицаЗначений;
Для Каждого СтрокаДата Из ТаблицаДат Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Срез.Номенклатура,
| Срез.Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Срез";
Запрос.УстановитьПараметр("Период", КонецДня(СтрокаДата.Дата));
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Дата = СтрокаДата.Дата;
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
КонецЦикла;
КонецЦикла;
Обратите внимание на КонецДня(): если в регистре записи сделаны в течение дня, а дата среза передана без времени (начало дня), записи текущего дня в срез не попадут. Это одна из самых частых причин «пропавших» значений.
Минус подхода — количество обращений к базе равно количеству дат. При сотнях дат цикл станет узким местом, и лучше перейти на соединение в одном запросе или на пакетный запрос с объединением.
Способ 3. Один запрос с объединением срезов
Компромисс между циклом и соединением: текст запроса строится динамически, для каждой даты добавляется секция ОБЪЕДИНИТЬ ВСЕ с обращением к виртуальной таблице на свою дату. Платформа выполняет это как один запрос, и каждая секция использует индексы среза.
ТекстЗапроса = "";
Для Инд = 0 По ТаблицаДат.Количество() - 1 Цикл
Если Инд > 0 Тогда
ТекстЗапроса = ТекстЗапроса + "
|ОБЪЕДИНИТЬ ВСЕ";
КонецЕсли;
ТекстЗапроса = ТекстЗапроса + "
|ВЫБРАТЬ &Период" + Инд + " КАК Дата, Срез.Номенклатура, Срез.Цена
|ИЗ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период" + Инд + ", ) КАК Срез";
КонецЦикла;
Каждой дате нужен свой именованный параметр, что и показано в примере. Метод хорошо работает до нескольких десятков дат; при большем числе текст запроса раздувается, растёт время компиляции, и выгоднее вернуться к способу с соединением.
Сравнение подходов
Подходит для произвольного и большого числа дат, один запрос. Требует аккуратности с производительностью на больших регистрах.
Простой и предсказуемый код, оптимален при малом числе дат и большом регистре. Много отдельных обращений к базе.
Один запрос с использованием индексов среза; разумный предел — десятки дат, дальше запрос становится громоздким.
Откуда брать таблицу дат
Для всех способов нужен источник дат. Варианты: передать таблицу значений параметром в запрос (ВЫБРАТЬ Т.Дата ПОМЕСТИТЬ ТаблицаДат ИЗ &ТаблицаДат КАК Т), взять даты из периодического регистра сведений или регистра накопления, либо сгенерировать календарь соединением с любым достаточно большим набором чисел.
Если в конфигурации есть регистр сведений с ежедневными записями (например, курсы валют заполняются ежедневно), его можно использовать как источник календаря. В типовых конфигурациях также встречается регламентно заполняемый календарь — уточните наличие в вашей конфигурации.
- 1Определите список дат и сформируйте из него временную таблицу в запросе
- 2Выберите способ: соединение с группировкой — для большого числа дат, цикл или объединение — для малого
- 3Напишите запрос: максимум периода по каждой дате и измерению, затем соединение для получения ресурсов
- 4Проверьте границы: первая дата раньше первой записи регистра даст NULL — решите, как это обрабатывать
- 5Выполните запрос на тестовой копии и сверьте несколько значений с карточкой регистра вручную
Производительность и индексы
Главный фактор скорости — объём соединения. Условие Период <= Дата без дополнительного отбора по измерениям заставляет СУБД перебирать произведение числа дат на число записей регистра. Всегда накладывайте отбор по измерениям, если нужен не весь справочник: например, только по списку номенклатуры из отчёта.
Полезные приёмы: помещайте отфильтрованный список измерений во временную таблицу с индексом (ИНДЕКСИРОВАТЬ ПО Номенклатура), соединяйтесь сначала с ней, а потом с регистром. Для периодических регистров платформа автоматически поддерживает индекс по измерениям и периоду, которым пользуется виртуальная таблица среза, — поэтому способы 2 и 3 на больших данных часто быстрее «ручного» соединения.
Если отчёт строится регулярно по одним и тем же датам (конец месяца), рассмотрите вариант хранить предрасчитанные срезы в отдельном регистре сведений, заполняемом регламентным заданием. Это убирает тяжёлый запрос из интерактивной работы пользователей.
Типичные ошибки и как их проверить
Первая ошибка — сравнение периода с датой без учёта времени. Запись от 15.03 14:30 не попадёт в срез на 15.03 00:00. Решение — использовать КонецДня() или границу с типом «Включая».
Вторая ошибка — забытый параметр периода в виртуальной таблице. Запрос СрезПоследних(, ) вернёт значения на максимальный период в регистре, что при наличии записей будущей датой даст неверные данные, причём молча.
Третья — ожидание строк для дат, предшествующих первой записи. Левое соединение вернёт NULL по ресурсам, и это нормально: значения на эту дату просто не существовало. Обработайте такие строки явно, чтобы не выводить пустые цены или нулевые курсы как реальные данные.
Для проверки результата возьмите 2–3 контрольных объекта, откройте регистр в режиме предприятия с отбором по ним и вручную сопоставьте: какое значение действовало на каждую из проверяемых дат. Совпадение с результатом запроса — достаточный признак корректности логики.
Как проверить результат
В срезе значение этой записи (при срезе на конец дня)
Возвращается предыдущее значение — проверьте время в дате среза
Строка с NULL или отсутствует, это обработано в отчёте
Подставляется произвольное или нулевое значение как реальное
Совпадают с данными регистра при ручной сверке
Расхождения — ищите ошибку в условии соединения или группировке
FAQ
Можно ли передать таблицу дат прямо в параметр виртуальной таблицы СрезПоследних?
Нет. Параметр Период принимает одно значение даты или границу момента времени. Для нескольких дат используйте соединение с группировкой по максимальному периоду, цикл по датам или объединение нескольких срезов в одном запросе.
Почему срез на сегодня не видит записи, сделанные сегодня?
Дата без времени в запросе — это начало дня (00:00:00). Записи, созданные позже в течение дня, имеют больший период и не попадают в срез. Передавайте КонецДня(Дата) или границу с типом «Включая».
Что вернёт запрос для даты раньше первой записи регистра?
При соединении с группировкой максимальный период будет NULL, а ресурсы после соединения — тоже NULL. Это корректное поведение: значения на ту дату не существовало. Обработайте такие строки в отчёте явно.
Какой способ быстрее на большом регистре?
При малом числе дат обычно быстрее цикл или объединение срезов — каждая секция использует индексы виртуальной таблицы. При большом числе дат выигрывает один запрос с соединением и группировкой, но обязательно с отбором по измерениям и индексированными временными таблицами.
Можно ли получить срез последних на каждую дату в СКД?
Да, те же приёмы работают в наборе данных СКД: текст запроса с соединением и группировкой помещается в набор данных, а таблицу дат можно получить из параметра или вложенного запроса. Для динамических дат удобно использовать вычисляемые параметры периода отчёта.
Что важно запомнить
СрезПоследних работает только на одну дату. Для среза на каждую дату соедините таблицу дат с регистром по условию Период <= Дата и сгруппируйте по максимальному периоду, либо выполните срезы в цикле при малом числе дат. Всегда учитывайте время в дате среза и проверяйте результат ручной сверкой с регистром.