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