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