Почему отчет на СКД работает без ошибок при делении на ноль, а обычный запрос завершается аварийно?

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

В практике разработки на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с ситуацией, когда один и тот же текст запроса ведет себя по-разному в консоли запросов и в составе системы компоновки данных (СКД). Особенно остро это проявляется при расчете вычисляемых показателей, таких как себестоимость единицы товара или маржинальность, где используется операция деления. Разберем подробно, почему СКД кажется более «устойчивой» к математическим ошибкам и как добиться идентичности данных в разных механизмах построения отчетов.

Различие между NULL и 0 в языке запросов

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

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


КОГДА Себест.СуммаРасход ЕСТЬ NULL
    И Себест.КоличествоРасход ЕСТЬ NULL
ТОГДА 0
ИНАЧЕ Себест.СуммаРасход / Себест.КоличествоРасход

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

Почему СКД «прощает» отсутствие проверки на ноль

Выясним причину, по которой один и тот же код в СКД выполняется без ошибок. Это происходит благодаря специфическим механизмам работы компоновщика данных:

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

Корректная обработка деления в запросах

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


ВЫБОР
    КОГДА ЕСТЬNULL(Себест.КоличествоРасход, 0) = 0
        ТОГДА 0
    ИНАЧЕ Себест.СуммаРасход / Себест.КоличествоРасход
КОНЕЦ

Или, если мы интегрируем это в сложный расчет валовой прибыли, как в примере автора:


СУММА(ВЫБОР
    КОГДА Продажи.КоличествоОборот > 0
        ТОГДА ВЫРАЗИТЬ(Продажи.СтоимостьОборот - ВЫБОР
            КОГДА ЕСТЬNULL(Себест.КоличествоРасход, 0) <> 0
                ТОГДА Себест.СуммаРасход / Себест.КоличествоРасход
            ИНАЧЕ 0
        КОНЕЦ * Продажи.КоличествоОборот КАК ЧИСЛО(15, 2))
    ИНАЧЕ 0
КОНЕЦ)

Причины расхождения итоговых сумм

Ситуация, когда отчет на СКД и обычный запрос выдают разные суммы (например, разница в 80 рублей на определенную дату), часто ставит программиста в тупик. Рассмотрим, почему это происходит:

Во-первых, роль агрегатных функций и округления. В СКД вы часто используете функцию ВЫРАЗИТЬ(... КАК ЧИСЛО(15, 2)). Если в обычном запросе вы округляете каждую строку перед суммированием, а СКД (согласно настройкам ресурсов) сначала суммирует неокругленные данные, а затем округляет итог — вы получите разницу за счет накопленной погрешности.

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

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

Проблема производительности: Год против Месяца

Рассмотрим ситуацию: отчет за каждый месяц по отдельности строится 5 минут (итого 60 минут за год), но запуск отчета сразу за год занимает 1.5 часа. Почему время растет нелинейно? Проанализируем работу оптимизатора СУБД:

  1. Объем временных таблиц. При запросе за месяц временные таблицы помещаются в оперативную память сервера. При запросе за год объем данных превышает лимиты, и СУБД начинает использовать TempDB на диске, что медленнее в десятки раз.
  2. Индексация. На малых объемах СУБД может использовать поиск по индексу (Index Seek). При огромном периоде оптимизатор может решить, что проще сканировать всю таблицу целиком (Index Scan), что резко снижает скорость.
  3. Механизм автозаполнения СКД. СКД при большом периоде может генерировать избыточные связи между таблицами, пытаясь обеспечить детальность по всем измерениям.

Как увидеть реальный SQL-запрос, который выполняет СКД

Для диагностики расхождений нам нужно увидеть не тот текст, который мы написали в конфигураторе, а тот, который уходит на SQL-сервер. Посмотрим, как это сделать программно. Обычное обращение к СхемаКомпоновкиДанных.НаборыДанных.НаборДанных1.Запрос не поможет, так как там хранится исходный шаблон.

Разберем алгоритм получения финального запроса:


// 1. Инициализируем компоновщик макета
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;

// 2. Генерируем макет компоновки (именно здесь происходит трансформация запроса)
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки());

// 3. Перебираем наборы данных в макете
Для Каждого НаборДанных Из МакетКомпоновки.НаборыДанных Цикл
    Если ТипЗнч(НаборДанных) = Тип("НаборДанныхЗапросМакетаКомпоновкиДанных") Тогда
        Сообщить("Финальный запрос:");
        Сообщить(НаборДанных.Запрос);
    КонецЕсли;
КонецЦикла;

Важный момент: полученный таким образом запрос будет содержать все вставленные системой условия ГДЕ, параметры и дополнительные соединения. Именно его нужно скопировать в консоль запросов для поиска тех самых «потерянных рублей» в итоговых суммах.

Резюме для разработчика

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

← На главную