Почему зависает проведение документа при контроле остатков по партиям в 1С?

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

При реализации партионного учета и контроля остатков в документе «Расходная накладная» разработчики нередко сталкиваются с ситуацией: если остатков товара хватает, проведение проходит мгновенно, но при попытке списать больше товара, чем числится на выбранной партии, система надолго «подвисает» или перестает отвечать. Разберем по шагам, почему возникает эта проблема, как СУБД обрабатывает сложные условия в виртуальных таблицах и какие скрытые ошибки в коде модуля проведения могут приводить к сбоям.

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

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


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

Выясним причину зависания. На первый взгляд условие в параметрах виртуальной таблицы выглядит логично — мы хотим отобрать только те пары номенклатуры и партий, которые есть в документе. Однако конструкция (Номенклатура, Партия) В (ВЫБРАТЬ ...) при трансляции платформой 1С на язык СУБД (MS SQL Server, PostgreSQL) превращается в сложный запрос с предикатами EXISTS или серией подзапросов к физическим таблицам итогов и движений.

В результате оптимизатор СУБД не может эффективно использовать стандартные индексы таблицы итогов регистра накопления и вместо быстрого поиска по индексу переходит к полному сканированию таблиц (Table Scan / Index Scan). На реальной или тестовой базе с достаточным объемом движений такой запрос выполняется крайне долго, вызывая иллюзию зависания платформы или бесконечного цикла.

Исправление запроса: правильная работа с виртуальными таблицами

Согласно стандартам разработки фирмы «1С» и требованиям к сертификации специалистов, в параметры виртуальных таблиц регистров накопления следует передавать фильтр по ведущему измерению — Номенклатура, а связь по дополнительным измерениям (партиям, характеристикам, складам) производить в секции ПО левого соединения.

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


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

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

Скрытые ошибки в обработке выборки

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

  1. Опасность деления на ноль: Если остаток товара по партии полностью отсутствует, то благодаря функции ЕСТЬNULL поле КоличествоОстаток вернет значение 0. Если расчет цены Цена = СебестоимостьОстаток / КоличествоОстаток стоит перед условием проверки нехватки, выполнение процедуры упадет с критической ошибкой Деление на 0. Расчет себестоимости единицы должен выполняться только тогда, когда остатка гарантированно хватает.
  2. Разыменование полей через точку в цикле: Вызов ВыборкаДетальныеЗаписи.Номенклатура при формировании текста сообщения пользователю заставляет платформу делать неявный запрос к базе для получения строкового представления. Представление необходимо получать заранее в тексте запроса через Номенклатура.Представление.
  3. Управление записью движений: Флаг Движения.ОстаткиНоменклатуры.Записывать = Истина; должен устанавливаться только в том случае, если ошибок не обнаружено (Не Отказ). В противном случае платформа будет выполнять лишнюю работу по передаче пустых или ошибочных наборов записей.
  4. Прерывание цикла или продолжение: Оператор Продолжить в блоке проверки остатка является корректным, если мы хотим показать пользователю полный список всех позиций, по которым обнаружена нехватка, а не останавливаться на первой же ошибке.

Итоговый корректный модуль проведения

Объединим все исправления в единый оптимизированный блок кода процедуры обработки проведения:


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

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    
    КолвоКСписанию = ВыборкаДетальныеЗаписи.Количество;
    Остаток = ВыборкаДетальныеЗаписи.КоличествоОстаток;
    
    Если КолвоКСписанию > Остаток Тогда
        Сообщить("Не хватает товара " + ВыборкаДетальныеЗаписи.НоменклатураПредставление +
            " в количестве " + (КолвоКСписанию - Остаток));
        Отказ = Истина;
        Продолжить;
    КонецЕсли;
    
    // Если проведение уже заблокировано предыдущими строками с нехваткой, 
    // формировать движения для корректных строк нет необходимости
    Если Отказ Тогда
        Продолжить;
    КонецЕсли;
    
    Движение = Движения.ОстаткиНоменклатуры.Добавить();
    Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
    Движение.Период = Дата;
    Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
    Движение.Партия = ВыборкаДетальныеЗаписи.Партия;
    Движение.Количество = КолвоКСписанию;
    
    Если КолвоКСписанию = Остаток Тогда
        // При полном списании остатка списываем всю себестоимость, чтобы избежать копеечных погрешностей
        Движение.Себестоимость = ВыборкаДетальныеЗаписи.СебестоимостьОстаток;
    Иначе
        Цена = ВыборкаДетальныеЗаписи.СебестоимостьОстаток / Остаток;
        Движение.Себестоимость = Окр(Цена * КолвоКСписанию, 2);
    КонецЕсли;

КонецЦикла;

Если Не Отказ Тогда
    Движения.ОстаткиНоменклатуры.Записывать = Истина;
КонецЕсли;

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

← На главную