Почему запрос на МоментВремени возвращает пустой результат или ломает сортировку партий в 1С?

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

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

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

Исходная ситуация и анализ запроса

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

Для этого используется следующий запрос:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    РасходнаяНакладнаяСписокНоменклатуры.Номенклатура,
    |    СУММА(РасходнаяНакладнаяСписокНоменклатуры.Количество) КАК Количество
    |ПОМЕСТИТЬ ТДок
    |ИЗ
    |    Документ.РасходнаяНакладная.СписокНоменклатуры КАК РасходнаяНакладнаяСписокНоменклатуры
    |ГДЕ
    |    РасходнаяНакладнаяСписокНоменклатуры.Ссылка = &Ссылка
    |
    |СГРУППИРОВАТЬ ПО
    |    РасходнаяНакладнаяСписокНоменклатуры.Номенклатура
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ТДок.Номенклатура,
    |    ТДок.Количество,
    |    СебестоимостьПродажОстатки.Партия,
    |    СебестоимостьПродажОстатки.КоличествоОстаток,
    |    СебестоимостьПродажОстатки.СуммаОстаток
    |ИЗ
    |    ТДок КАК ТДок
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.SetестоимостьПродаж.Остатки(&МоментВремени, ) КАК СебестоимостьПродажОстатки
    |        ПО ТДок.Номенклатура = СебестоимостьПродажОстатки.Номенклатура
    |
    |УПОРЯДОЧИТЬ ПО
    |    СебестоимостьПродажОстатки.Партия.МоментВремени УБЫВ";

Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ВидГраницы.Включая));
Запрос.УстановитьПараметр("Ссылка", Ссылка);

На первый взгляд, код написан по стандартам. Однако при тестировании разработчик замечает две критические проблемы:

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

Выясним причины этого поведения и найдем правильные решения.

Причина 1. Смещение на оси времени при оперативном и неоперативном проведении

Для того чтобы понять, почему при изменении времени документа «исчезают» остатки, проанализируем, как платформа работает с осью времени.

Когда вы создаете новый документ и проводите его в режиме реального времени, платформа по умолчанию запускает оперативное проведение. При этом система автоматически сдвигает время документа на текущую физическую секунду сервера. Если ваши приходные накладные были проведены в 15:52:42, а новый расходный документ проводится оперативно в 16:26:35, то момент времени расхода (16:26:35) на оси времени находится строго после прихода. Запрос остатков на эту точку успешно находит партии.

Ситуация кардинально меняется, когда вы открываете уже записанный (но еще не проведенный или распроведенный) документ, вручную меняете его время (например, на 15:00:00) и нажимаете кнопку «Провести». В этот момент документ проводится неоперативно (задним числом). Платформа не сдвигает время документа вперед, а записывает его ровно на ту секунду, которую вы указали — 15:00:00.

Теперь сопоставим хронологию событий на оси времени:

Когда СУБД выполняет расчет виртуальной таблицы остатков на момент времени 15:00:00, она видит состояние склада на эту конкретную секунду. Очевидно, что в 15:00:00 ни одной из трех приходных накладных в базе еще физически не существовало (они появятся только через 52 минуты). В результате запрос остатков абсолютно логично возвращает пустую таблицу.

Причина 2. Ошибка использования границы времени при проведении

Вторая методологическая ошибка кроется в передаче параметра границы времени:


Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ВидГраницы.Включая));

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

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


// Правильный вариант для получения чистых остатков до проведения текущего документа
Запрос.УстановитьПараметр("МоментВремени", МоментВремени()); 
// Или:
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ВидГраницы.Исключая));

Причина 3. Почему ломается сортировка по Моменту Времени?

Теперь разберем, почему указание параметра МоментВремени в виртуальной таблице ломало сортировку в предложении УПОРЯДОЧИТЬ ПО СебестоимостьПродажОстатки.Партия.МоментВремени УБЫВ.

Здесь срабатывают сразу два фактора:

1. Обработка значений NULL при Левом Соединении.
В запросе автора используется ЛЕВОЕ СОЕДИНЕНИЕ таблицы документа с таблицей остатков. Если из-за нарушения хронологии (описанного в Причине 1) остатков по конкретной номенклатуре нет, вся строка со стороны таблицы остатков заполняется значениями NULL. Поле СебестоимостьПродажОстатки.Партия тоже становится равным NULL. Попытка СУБД упорядочить пустые ссылки и неопределенные значения приводит к тому, что порядок строк в результирующей выборке становится хаотичным.

2. Специфика трансляции «Момента Времени» в SQL-запрос.
Объект МоментВремени — это прикладной объект платформы «1С:Предприятие», состоящий из даты/времени и уникального идентификатора ссылки (GUID). В физических таблицах СУБД такого единого поля нет. Когда вы пишете в запросе 1С обращение через точку к виртуальному полю:

СебестоимостьПродажОстатки.Партия.МоментВремени

Платформа транслирует это для СУБД как сложную конструкцию сортировки по двум физическим полям:

  1. Дата документа-партии (Партия.Дата).
  2. Уникальный идентификатор ссылки (Партия.Ссылка).

Если реквизит Партия имеет составной тип данных (может ссылаться на разные виды документов прихода), то СУБД вынуждена выполнять неявные соединения (LEFT JOIN) со всеми физическими таблицами этих документов, чтобы вытащить их даты. Это не только колоссально снижает производительность запроса, но и может приводить к ошибкам сортировки, особенно если в выборку попадают значения NULL.

Решение проблемы: правильное построение запроса и кода

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

Выполним оптимизацию по шагам:

Шаг 1. Контроль времени документов при тестировании.
Убедитесь, что документы прихода (партии) на оси времени всегда находятся раньше, чем документы расхода. При ручном тестировании в консоли запросов или учебной базе всегда проверяйте не только дату, но и секунды документов. Расходный документ должен иметь время больше, чем приходный.

Шаг 2. Отказ от обращения к виртуальным свойствам через точку.
Вместо использования тяжелой конструкции Партия.МоментВремени мы будем сортировать результат по реальным физическим полям: дате партии и самой ссылке на партию. Это исключит лишние соединения таблиц на уровне СУБД и ускорит выполнение запроса.

Шаг 3. Обработка значений NULL.
Чтобы строки, по которым нет остатков на складе (где значение партии равно NULL), корректно обрабатывались СУБД и не ломали сортировку остальных строк, мы можем использовать функцию ЕСТЬNULL или изменить логику соединения (например, использовать ВНУТРЕННЕЕ СОЕДИНЕНИЕ, если нам нужны только те товары, которые реально есть на остатке).

Посмотрим на финальный, оптимизированный вариант кода проведения документа:


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

// Передаем МоментВремени() без "Включая", чтобы не захватить движения текущего документа (для старой методики)
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ВидГраницы.Исключая));
Запрос.УстановитьПараметр("Ссылка", Ссылка);

РезультатЗапроса = Запрос.Выполнить();

Подведем итоги

Мы выяснили, что "мистика" с исчезновением остатков и неправильным порядком партий в 1С всегда имеет под собой строгое логическое объяснение:

← На главную