Как в запросе 1С получить цены последнего поступления по номенклатуре?

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

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

Анализ исходной проблемы: почему запрос возвращает все цены

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


ВЫБРАТЬ
    Закупки.Регистратор,
    Закупки.Стоимость,
    Закупки.Номенклатура,
    Закупки.Количество
ИЗ
    РегистрНакопления.Закупки КАК Закупки
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ (
        ВЫБРАТЬ
            ПоследниеДатыЗакупки.Номенклатура КАК Номенклатура,
            МАКСИМУМ(ПоследниеДатыЗакупки.Период) КАК Период,
            ПоследниеДатыЗакупки.Регистратор КАК Регистратор
        ИЗ
            РегистрНакопления.Закупки КАК ПоследниеДатыЗакупки
        СГРУППИРОВАТЬ ПО
            ПоследниеДатыЗакупки.Номенклатура,
            ПоследниеДатыЗакупки.Регистратор
    ) КАК ДатыПоследнихДокументов
    ПО Закупки.Период = ДатыПоследнихДокументов.Период
    И Закупки.Номенклатура = ДатыПоследнихДокументов.Номенклатура

Проанализируем, почему этот запрос выводит все документы и все цены вместо одной последней по каждой позиции. Причина кроется во вложенном запросе ДатыПоследнихДокументов.

В секции СГРУППИРОВАТЬ ПО указаны два поля: Номенклатура и Регистратор. СУБД выполняет группировку по уникальной комбинации этих полей. Поскольку поле Регистратор указывает на конкретный документ, агрегатная функция МАКСИМУМ(Период) отрабатывает в разрезе каждого отдельного документа. В итоге вложенный запрос возвращает список абсолютно всех документов поступления для каждой позиции номенклатуры. После внутреннего соединения основной таблицы с таким подзапросом результирующая выборка содержит все движения без какой-либо фильтрации по последней дате.

Решение 1: Исправление группировки во вложенном запросе

Выясним, как быстро исправить исходный запрос. Для этого необходимо убрать поле Регистратор из выборки и секции СГРУППИРОВАТЬ ПО вложенного запроса, оставив группировку исключительно по полю Номенклатура.

Разберем исправленный вариант запроса:


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

В данном варианте подзапрос возвращает строго одну запись на каждую позицию номенклатуры — с самой поздней датой движения. Также мы добавили безопасный расчет цены за единицу с проверкой деления на ноль через конструкцию ВЫБОР КОГДА ... ТОГДА.

Проблема одинаковых дат и нюанс с моментом времени

Рассмотрим скрытую проблему, с которой можно столкнуться при использовании поля Период. Поле Период в регистре накопления хранит значение даты и времени с точностью до секунды. Если в базе данных в одну и ту же секунду будут проведены два документа поступления по одной номенклатуре (например, в результате автоматического обмена данными, группового перепроведения или пакетной загрузки), соединение по полям Период и Номенклатура вернет обе строки, что снова приведет к задвоению данных в отчете.

Разберем, почему нельзя просто написать МАКСИМУМ(Регистратор.МоментВремени):

  1. У таблицы регистра накопления нет собственного поля МоментВремени. Это свойство присуще ссылочным объектам информационной базы (документам).
  2. Обращение через точку к реквизитам регистратора вида Закупки.Регистратор.МоментВремени приводит к неявному левому соединению с таблицами всех типов документов, которые могут быть регистраторами данного регистра. Это существенно замедляет выполнение запроса.

Чтобы исключить коллизию одинаковых дат и гарантировать выборку строго одного документа, применяют двухэтапный поиск: сначала находят максимальную дату, а затем среди записей с этой датой выбирают максимальный Регистратор (ссылку на документ):


ВЫБРАТЬ
    Закупки.Номенклатура КАК Номенклатура,
    МАКСИМУМ(Закупки.Период) КАК Период
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
    РегистрНакопления.Закупки КАК Закупки
СГРУППИРОВАТЬ ПО
    Закупки.Номенклатура
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Закупки.Номенклатура КАК Номенклатура,
    МАКСИМУМ(Закупки.Регистратор) КАК Регистратор
ПОМЕСТИТЬ ВТ_ПоследниеДокументы
ИЗ
    РегистрНакопления.Закупки КАК Закупки
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_МаксимальныеДаты КАК МаксДаты
        ПО Закупки.Номенклатура = МаксДаты.Номенклатура
        И Закупки.Период = МаксДаты.Период
СГРУППИРОВАТЬ ПО
    Закупки.Номенклатура
ИНДЕКСИРОВАТЬ ПО
    Номенклатура,
    Регистратор
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Закупки.Регистратор КАК Регистратор,
    Закупки.Номенклатура КАК Номенклатура,
    Закупки.Количество КАК Количество,
    Закупки.Стоимость КАК Стоимость,
    ВЫБОР
        КОГДА Закупки.Количество <> 0
            ТОГДА Закупки.Стоимость / Закупки.Количество
        ИНАЧЕ 0
    КОНЕЦ КАК Цена
ИЗ
    РегистрНакопления.Закупки КАК Закупки
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследниеДокументы КАК ПослДокументы
        ПО Закупки.Номенклатура = ПослДокументы.Номенклатура
        И Закупки.Регистратор = ПослДокументы.Регистратор

Решение 2: Оптимизация производительности через временные таблицы

Проанализируем производительность запросов к СУБД. Соединение физических таблиц с вложенными подзапросами считается антипаттерном в разработке 1С. Оптимизатор СУБД (например, в MS SQL Server или PostgreSQL) часто не может построить эффективный план выполнения для сложных вложенных выборок, что приводит к полному сканированию таблиц (Table Scan) и блокировкам данных.

Стандарты разработки 1С предписывают использовать пакетные запросы с временными таблицами и их обязательным индексированием. Разберем оптимизированный вариант:

  1. Первый пакет: выбираем максимальный период по номенклатуре и помещаем данные во временную таблицу ВТ_ПоследниеДаты с созданием индекса по полю Номенклатура.
  2. Второй пакет: соединяем физическую таблицу регистра с проиндексированной временной таблицей.

ВЫБРАТЬ
    Закупки.Номенклатура КАК Номенклатура,
    МАКСИМУМ(Закупки.Период) КАК Период
ПОМЕСТИТЬ ВТ_ПоследниеДаты
ИЗ
    РегистрНакопления.Закупки КАК Закупки
СГРУППИРОВАТЬ ПО
    Закупки.Номенклатура
ИНДЕКСИРОВАТЬ ПО
    Номенклатура
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Закупки.Регистратор КАК Регистратор,
    Закупки.Номенклатура КАК Номенклатура,
    Закупки.Количество КАК Количество,
    Закупки.Стоимость КАК Стоимость,
    ВЫБОР
        КОГДА Закупки.Количество <> 0
            ТОГДА Закупки.Стоимость / Закупки.Количество
        ИНАЧЕ 0
    КОНЕЦ КАК Цена
ИЗ
    РегистрНакопления.Закупки КАК Закупки
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследниеДаты КАК ПоследниеДаты
        ПО Закупки.Номенклатура = ПоследниеДаты.Номенклатура
        И Закупки.Период = ПоследниеДаты.Период

Решение 3: Архитектурный подход через регистры сведений

Посмотрим на задачу с точки зрения архитектуры типовых конфигураций (1С:Управление торговлей, 1С:ERP, 1С:Комплексная автоматизация, 1С:УНФ). Регистры накопления предназначены для учета оборотов и остатков (количественных и суммовых), но не для оперативного получения актуальных цен.

Для хранения цен предназначены периодические регистры сведений (например, ЦеныНоменклатуры, ЦеныНоменклатурыПоставщиков или ЦеныНоменклатурыДокументов). Главное преимущество регистра сведений — наличие готовой виртуальной таблицы СрезПоследних.

Пример запроса к виртуальной таблице СрезПоследних:


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

Преимущества использования виртуальной таблицы СрезПоследних:

  1. Платформа 1С формирует максимально оптимизированный SQL-запрос на уровне СУБД.
  2. Нет необходимости вручную выполнять агрегацию МАКСИМУМ(Период) и делить стоимость на количество.
  3. Исключены проблемы с делением на ноль и задвоением записей при совпадении секунд у разных документов.
  4. Код становится более компактным, читаемым и простым в сопровождении.

Если конфигурация предусматривает фиксацию цен поступления при проведении документов (например, регистрацию цен поставщиков), наиболее правильным решением будет получение актуальной цены именно из регистра сведений, а не из регистра накопления.

← На главную