СрезПоследних

Учимся брать «последнее известное значение на дату» — цену, курс, ставку — одной виртуальной таблицей вместо ручного перебора истории.

СрезПоследних — виртуальная таблица регистра сведений, которая для каждой комбинации измерений возвращает ровно одну, самую свежую запись на указанную дату (или на текущий момент, если дату не задавать).

Представьте регистр сведений Цены: он периодический, и в нём хранится история — 1 января телефон стоил 30000, 15 марта подорожал до 32000, 1 июня — до 35000. Когда мы оформляем документ 20 марта, нам нужна цена 32000: та, что действовала на эту дату. Перебирать всю историю руками и искать «последнюю запись не позже 20 марта» — долго и больно. Ровно эту работу за нас делает виртуальная таблица СрезПоследних.

Регистр сведений — таблица для хранения значений, привязанных к измерениям (и, если регистр периодический, к дате). Классика: цены номенклатуры, курсы валют, ставки НДС, ответственные по складам. Измерение — то, в разрезе чего хранятся данные (Номенклатура, Валюта). Ресурс — само хранимое значение (Цена, Курс).

Зачем это нужно на практике

«Актуальное значение на дату» — одна из самых частых задач в 1С. Цена в чеке, курс валюты в проводке, тариф в начислении, текущий ответственный по складу — всё это периодические регистры сведений, и почти всегда вам нужен срез на конкретный момент. Без виртуальной таблицы пришлось бы писать подзапрос с группировкой по максимальной дате и соединением — громоздко и легко ошибиться. СрезПоследних решает это одной строкой.

Синтаксис: имя таблицы и параметр Период

В запросе виртуальная таблица пишется как РегистрСведений.ИмяРегистра.СрезПоследних(<Период>, <Отбор>). Оба параметра необязательны, но Период — ключевой: это дата, на которую берётся срез.

ВЫБРАТЬ
    ЦеныСрезПоследних.Номенклатура КАК Номенклатура,
    ЦеныСрезПоследних.Цена КАК Цена
ИЗ
    РегистрСведений.Цены.СрезПоследних(&ДатаСреза, ) КАК ЦеныСрезПоследних

Здесь &ДатаСреза — параметр запроса, который мы зададим из кода через УстановитьПараметр. Второй аргумент (отбор) оставлен пустым — вернутся цены на все товары.

Как задать дату из встроенного языка

Запрос = Новый Запрос;
Запрос.Текст =
    "ВЫБРАТЬ
    |    ЦеныСрезПоследних.Номенклатура КАК Номенклатура,
    |    ЦеныСрезПоследних.Цена КАК Цена
    |ИЗ
    |    РегистрСведений.Цены.СрезПоследних(&ДатаСреза, ) КАК ЦеныСрезПоследних";

Запрос.УстановитьПараметр("ДатаСреза", '20240320235959');

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    Сообщить(Выборка.Номенклатура.Наименование + ": " + Выборка.Цена);
КонецЦикла;

Результат:

Смартфон Alpha: 32000
Ноутбук Beta: 54900
Мышь Gamma: 990

Обратите внимание на литерал даты '20240320235959' — в 1С дата пишется без кавычек-строк, в апострофах, форматом ГГГГММДДЧЧММСС. Секунды до конца дня (235959) взяты специально: если товар подорожал ровно 20 марта, мы хотим захватить и эту новую цену, а не остаться на старой (об этой ловушке — в конце урока).

Отбор внутри виртуальной таблицы

Чаще всего цена нужна не по всем товарам, а по одному-двум. Второй параметр СрезПоследних — это отбор, и передавать условие лучше именно сюда, а не в ГДЕ (почему — отдельный урок раздела). Отбор пишется как условие с полями измерений регистра:

ВЫБРАТЬ
    ЦеныСрезПоследних.Цена КАК Цена
ИЗ
    РегистрСведений.Цены.СрезПоследних(
        &ДатаСреза,
        Номенклатура = &Товар
            И ТипЦен = &РозничныйТип) КАК ЦеныСрезПоследних

Если у регистра несколько измерений (например, Номенклатура и ТипЦен), срез вернёт последнюю запись для каждой уникальной комбинации. Отбор по ТипЦен = &РозничныйТип оставит только розничные цены.

Классическая задача: цена на дату документа

Самый частый сценарий — при проведении документа подставить цену, актуальную на дату этого документа. Вот как это выглядит в модуле объекта:

Функция ПолучитьЦенуНаДату(Товар, ТипЦен, ДатаДок) Экспорт
    Запрос = Новый Запрос;
    Запрос.Текст =
        "ВЫБРАТЬ
        |    ЦеныСрезПоследних.Цена КАК Цена
        |ИЗ
        |    РегистрСведений.Цены.СрезПоследних(
        |        &ДатаДок,
        |        Номенклатура = &Товар
        |            И ТипЦен = &ТипЦен) КАК ЦеныСрезПоследних";

    Запрос.УстановитьПараметр("ДатаДок", ДатаДок);
    Запрос.УстановитьПараметр("Товар",  Товар);
    Запрос.УстановитьПараметр("ТипЦен", ТипЦен);

    Выборка = Запрос.Выполнить().Выбрать();
    Если Выборка.Следующий() Тогда
        Возврат Выборка.Цена;
    Иначе
        Возврат 0; // цена ещё не установлена
    КонецЕсли;
КонецФункции

Функция вернёт цену, действовавшую на момент документа. Проверка Если Выборка.Следующий() обязательна: если для товара цену вообще ни разу не заводили, срез вернёт пустую выборку, и без проверки мы получим ошибку обращения к Выборка.Цена.

Как это работает

Под капотом СрезПоследних — это НЕ реальная таблица в базе. Платформа 1С в момент выполнения запроса разворачивает виртуальную таблицу в настоящий SQL-запрос к таблице регистра сведений: группирует записи по измерениям, находит максимальную дату не позже переданного Периода и подтягивает ресурсы именно этой записи. Проще говоря, за красивым коротким синтаксисом стоит сгенерированный подзапрос с MAX(Период) и соединением — но писать и оптимизировать его платформа берёт на себя.

Важное следствие: чем точнее отбор внутри виртуальной таблицы, тем меньше данных платформа поднимает из базы. Если отобрать один товар прямо в параметрах, СУБД отработает по индексу и вернёт одну запись. Если же вытащить срез по всем товарам, а потом отфильтровать в ГДЕ — база сначала посчитает срез по десяткам тысяч позиций и лишь затем отбросит лишнее.

Если Период не указать вовсеРегистрСведений.Цены.СрезПоследних() — срез берётся на текущий момент (фактически на «конец времён»), то есть возвращаются самые последние из имеющихся значений. Это удобно для отчёта «текущие цены», но опасно внутри проведения документа задним числом: подставится не та цена, что была на дату документа, а самая свежая из справочника.

Частые ошибки

  • Забыть про время в дате. Литерал '20240320' — это 20.03.2024 00:00:00, то есть самое начало дня. Если цена изменилась 20 марта в течение дня, срез на 00:00:00 вернёт вчерашнюю. Для «на весь день» берите конец дня: КонецДня(ДатаДок) в коде или '20240320235959' в тексте запроса.
  • Путать СрезПоследних и СрезПервых. У регистра сведений есть и СрезПервых — она вернёт первую запись начиная с даты. Для «актуальной цены» нужен именно СрезПоследних; СрезПервых применяют реже (например, «первая цена после акции»).
  • Применять к непериодическому регистру. Виртуальные таблицы СрезПоследних/СрезПервых есть только у периодических регистров сведений. У непериодического их просто нет — платформа не даст обратиться к .СрезПоследних(...).
  • Не проверять пустой результат. Если по товару значение не заводили, выборка пуста. Всегда оборачивайте чтение в Если Выборка.Следующий() Тогда ... КонецЕсли;.
  • Тащить весь срез и фильтровать в ГДЕ. Условие по измерению кладите во второй параметр виртуальной таблицы, а не в ГДЕ — это радикально быстрее (подробно в уроке «Параметры виртуальной таблицы vs ГДЕ»).

Итоги

  • СрезПоследних — виртуальная таблица периодического регистра сведений: одна самая свежая запись на дату для каждой комбинации измерений.
  • Первый параметр — Период (дата среза); без него срез берётся на текущий момент.
  • Второй параметр — отбор по измерениям; кладите условия сюда, а не в ГДЕ.
  • Классика — «цена/курс/ставка на дату документа»; всегда проверяйте пустую выборку.
  • Помните про время в дате: '20240320' — это начало дня; для всего дня берите КонецДня(...).
Проверьте себя
1. Что возвращает виртуальная таблица СрезПоследних периодического регистра сведений?
AВсе записи регистра начиная с указанной даты
BОдну самую свежую запись на указанную дату для каждой комбинации измерений
CСумму значений ресурса за период
DПервую по времени запись для каждого измерения
2. В запросе параметру ДатаСреза передали литерал '20240320' (без времени). Товар подорожал 20 марта в 14:00. Какую цену вернёт СрезПоследних?
AНовую цену от 20 марта
BСтарую цену (действовавшую до 20 марта), потому что '20240320' — это 20.03.2024 00:00:00
CПустое значение
DСреднее между старой и новой ценой