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

Программист Управленческий учет Торговля и дистрибуция
← На главную

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

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

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

  1. Соединение с виртуальными таблицами: Прямое ЛЕВОЕ СОЕДИНЕНИЕ с виртуальной таблицей остатков заставляет СУБД выполнять вложенный запрос для каждой строки, что крайне неэффективно.
  2. Группировка по вычисляемым полям: Попытка группировать данные по результату деления (Сумма / Количество) создает лишнюю нагрузку и может привести к ошибкам округления или дублированию строк, если данные в ТЧ документа не свернуты.
  3. Деление на ноль: Если на складе числится сумма, но нулевое количество (так называемые «хвосты»), прямой расчет себестоимости вызовет ошибку выполнения запроса.

Шаг 1. Оптимизация получения данных из документа

Первым делом нам необходимо подготовить данные из табличной части документа. Важно свернуть их по номенклатуре, чтобы избежать дублей и лишних расчетов. Мы будем использовать менеджер временных таблиц (МенеджерВременныхТаблиц) для передачи данных между этапами запроса.

Рассмотрим пример кода для первой временной таблицы ВТТабличнаяЧасть:


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

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

Шаг 2. Получение остатков и расчет себестоимости

Теперь проанализируем, как правильно получить данные из регистра. Вместо соединения в параметрах виртуальной таблицы мы используем условие В (ВЫБРАТЬ ...) из нашей первой временной таблицы. Это позволяет системе получить остатки только по тем позициям, которые реально присутствуют в документе.

Также добавим проверку на деление на ноль с помощью конструкции ВЫБОР:


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

Шаг 3. Итоговая сборка данных для движений

На последнем этапе мы соединяем наши временные таблицы. Обратите внимание на использование функции ЕСТЬNULL. Если товара нет на остатках, соединение вернет NULL, и расчет суммы превратится в пустое значение. Мы должны явно заменить его на 0.


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

Реализация логики проведения в коде

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

Рассмотрим структуру процедуры ОбработкаПроведения:


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

Дополнительные рекомендации по оптимизации

Для создания действительно надежной системы стоит учитывать еще несколько важных факторов, которые часто остаются за рамками простых примеров:

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

← На главную