Почему СрезПоследних регистра цен номенклатуры возвращает записи за разные периоды и как это исправить?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет IT и автоматизация бизнеса
← На главную

При разработке отчетов по складским остаткам и стоимостной оценке номенклатуры разработчики нередко сталкиваются с ситуацией, когда соединение таблицы остатков с виртуальной таблицей среза последних регистра цен приводит к неожиданному задвоению строк. В результате в выборке для одной и той же номенклатуры появляются цены за разные даты, из-за чего кажется, будто срез последних не сработал и вернул всю историю цен.

Разберем детально, почему возникает такое поведение, как устроена работа среза последних по нескольким измерениям и как правильно составить текст запроса.

Анализ проблемы и механизма работы среза последних

Рассмотрим типичный запрос, в котором остатки номенклатуры соединяются с ценами:


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

При выполнении подобного запроса одна позиция номенклатуры может вывестись 2, 3 и более раз с разными датами установки цены и разными суммами. Выясним причину такого результата.

Виртуальная таблица СрезПоследних в платформе 1С рассчитывает актуальные данные не в разрезе какого-то одного поля (например, Номенклатура), а по каждой уникальной комбинации всех измерений регистра сведений. В типовых конфигурациях регистр ЦеныНоменклатуры, как правило, содержит измерения:

  1. Номенклатура
  2. Характеристика (или ХарактеристикаНоменклатуры)
  3. ВидЦены
  4. Упаковка (в некоторых конфигурациях)

Если для одной номенклатуры цены устанавливались в разрезе характеристик (например, «Размер 42» от 01.09.2023 и «Размер 44» от 15.10.2023), то СрезПоследних вернет обе эти записи, так как для каждой комбинации «Номенклатура + Характеристика + ВидЦены» это будет последняя цена. Когда в условии соединения ПО указана только связь по номенклатуре, левое соединение сопоставит одну строку остатка со всеми строками цен этой позиции по всем ее характеристикам, порождая дубли строк с разными датами.

Решение: добавление всех измерений в условие соединения

Для устранения задвоения и получения корректной цены необходимо добавить проверку характеристик в секцию ПО соединения таблиц:


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

Оптимизация и устранение сопутствующих ошибок

Проанализируем другие важные моменты при работе со срезом последних и остатками, влияющие на производительность и корректность данных:

  1. Неявные соединения через разыменование полей: Конструкция ВидЦены.Ссылка = &ВидЦены заставляет платформу формировать избыточный LEFT JOIN к таблице справочника видов цен. Поле ВидЦены в регистре уже хранит ссылку, поэтому следует писать напрямую: ВидЦены = &ВидЦены. Аналогично в условии соединения недопустимо использовать конструкции вида ТоварыНаСкладахОстатки.Номенклатура.Ссылка.
  2. Правильная фильтрация в ГДЕ: При левом соединении наложение условий в секции ГДЕ на правую таблицу (например, ЦеныНоменклатурыСрезПоследних.Номенклатура В ИЕРАРХИИ(...)) превращает соединение во внутреннее, отсекая товары, по которым цены не были заданы. Фильтры по иерархии и признаку группы необходимо накладывать либо на основную таблицу остатков, либо переносить в параметры виртуальных таблиц.
  3. Корректная агрегация сумм: Если требуется группировка, агрегируемые числовые поля обязательно должны оборачиваться в функцию СУММА(), а вычисляемые выражения цен должны обрабатывать значения NULL через функцию ЕСТЬNULL(): СУММА(ТоварыНаСкладахОстатки.ВНаличииОстаток * ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0)).
  4. Передача границы периода: Для получения остатков и среза на конец дня необходимо передавать в параметр &Период конец дня через КонецПериода(&Дата, "ДЕНЬ") либо объект типа Граница с видом ВидГраницы.Включая. В противном случае расчет выполнится на начало первой секунды даты.

Итоговый оптимизированный вариант запроса

Соберем все рекомендации в готовый шаблон запроса:


ВЫБРАТЬ
    ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
    ТоварыНаСкладахОстатки.Номенклатура.Родитель КАК Родитель,
    ТоварыНаСкладахОстатки.Характеристика КАК Характеристика,
    ЦеныНоменклатурыСрезПоследних.Период КАК ПериодЦены,
    ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена,
    СУММА(ТоварыНаСкладахОстатки.ВНаличииОстаток) КАК Количество,
    СУММА(ТоварыНаСкладахОстатки.ВНаличииОстаток * ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0)) КАК Сумма
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Номенклатура В ИЕРАРХИИ(&Родитель) И НЕ Номенклатура.ЭтоГруппа) КАК ТоварыНаСкладахОстатки
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
        ПО ТоварыНаСкладахОстатки.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
            И ТоварыНаСкладахОстатки.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика
СГРУППИРОВАТЬ ПО
    ТоварыНаСкладахОстатки.Номенклатура,
    ТоварыНаСкладахОстатки.Номенклатура.Родитель,
    ТоварыНаСкладахОстатки.Характеристика,
    ЦеныНоменклатурыСрезПоследних.Период,
    ЦеныНоменклатурыСрезПоследних.Цена
УПОРЯДОЧИТЬ ПО
    Родитель,
    Номенклатура

Такой подход гарантирует выборку актуальной цены для каждой позиции номенклатуры и характеристики без дублирования записей и с оптимальной скоростью выполнения на стороне СУБД.

← На главную