В практике разработки на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с ситуацией, когда один и тот же текст запроса ведет себя по-разному в консоли запросов и в составе системы компоновки данных (СКД). Особенно остро это проявляется при расчете вычисляемых показателей, таких как себестоимость единицы товара или маржинальность, где используется операция деления. Разберем подробно, почему СКД кажется более «устойчивой» к математическим ошибкам и как добиться идентичности данных в разных механизмах построения отчетов.
Первое, на что необходимо обратить внимание — это фундаментальное различие между значениями NULL и 0. В языке запросов 1С это совершенно разные сущности. NULL означает отсутствие значения как такового (например, при левом соединении, когда во второй таблице нет соответствующей записи), а 0 — это конкретное числовое значение.
Проанализируем фрагмент кода, который часто вызывает проблемы:
КОГДА Себест.СуммаРасход ЕСТЬ NULL
И Себест.КоличествоРасход ЕСТЬ NULL
ТОГДА 0
ИНАЧЕ Себест.СуммаРасход / Себест.КоличествоРасход
Многие ошибочно полагают, что проверка на NULL защищает от деления на ноль. Однако, если в регистре накопления зафиксирована запись с нулевым количеством (что технически возможно в некоторых конфигурациях или при ошибках учета), условие ЕСТЬ NULL не сработает, так как значение 0 не является NULL. Обычный запрос в этом случае немедленно вернет ошибку "Деление на 0 значения типа Numeric".
Выясним причину, по которой один и тот же код в СКД выполняется без ошибок. Это происходит благодаря специфическим механизмам работы компоновщика данных:
Для того чтобы отчет работал стабильно в любом режиме, рассмотрим правильный способ написания условий. Проверять нужно и на NULL, и на 0 одновременно. Наиболее надежный вариант выглядит так:
ВЫБОР
КОГДА ЕСТЬNULL(Себест.КоличествоРасход, 0) = 0
ТОГДА 0
ИНАЧЕ Себест.СуммаРасход / Себест.КоличествоРасход
КОНЕЦ
Или, если мы интегрируем это в сложный расчет валовой прибыли, как в примере автора:
СУММА(ВЫБОР
КОГДА Продажи.КоличествоОборот > 0
ТОГДА ВЫРАЗИТЬ(Продажи.СтоимостьОборот - ВЫБОР
КОГДА ЕСТЬNULL(Себест.КоличествоРасход, 0) <> 0
ТОГДА Себест.СуммаРасход / Себест.КоличествоРасход
ИНАЧЕ 0
КОНЕЦ * Продажи.КоличествоОборот КАК ЧИСЛО(15, 2))
ИНАЧЕ 0
КОНЕЦ)
Ситуация, когда отчет на СКД и обычный запрос выдают разные суммы (например, разница в 80 рублей на определенную дату), часто ставит программиста в тупик. Рассмотрим, почему это происходит:
Во-первых, роль агрегатных функций и округления. В СКД вы часто используете функцию ВЫРАЗИТЬ(... КАК ЧИСЛО(15, 2)). Если в обычном запросе вы округляете каждую строку перед суммированием, а СКД (согласно настройкам ресурсов) сначала суммирует неокругленные данные, а затем округляет итог — вы получите разницу за счет накопленной погрешности.
Во-вторых, настройки ролей полей. Проверим вкладку «Наборы данных» в СКД. Если для полей себестоимости неправильно указаны роли (например, поле помечено как остаток, хотя является оборотом), СКД применит специфическую логику расчета итогов, которая будет отличаться от простого сложения строк в консоли запросов.
В-третьих, параметры виртуальных таблиц. СКД автоматически подставляет параметры периодов в виртуальные таблицы (например, РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода)). Если в обычном запросе параметры переданы иначе или не учитывают границы (включая или исключая секунду границы), результаты неизбежно разойдутся.
Рассмотрим ситуацию: отчет за каждый месяц по отдельности строится 5 минут (итого 60 минут за год), но запуск отчета сразу за год занимает 1.5 часа. Почему время растет нелинейно? Проанализируем работу оптимизатора СУБД:
TempDB на диске, что медленнее в десятки раз.Для диагностики расхождений нам нужно увидеть не тот текст, который мы написали в конфигураторе, а тот, который уходит на SQL-сервер. Посмотрим, как это сделать программно. Обычное обращение к СхемаКомпоновкиДанных.НаборыДанных.НаборДанных1.Запрос не поможет, так как там хранится исходный шаблон.
Разберем алгоритм получения финального запроса:
// 1. Инициализируем компоновщик макета
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
// 2. Генерируем макет компоновки (именно здесь происходит трансформация запроса)
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки());
// 3. Перебираем наборы данных в макете
Для Каждого НаборДанных Из МакетКомпоновки.НаборыДанных Цикл
Если ТипЗнч(НаборДанных) = Тип("НаборДанныхЗапросМакетаКомпоновкиДанных") Тогда
Сообщить("Финальный запрос:");
Сообщить(НаборДанных.Запрос);
КонецЕсли;
КонецЦикла;
Важный момент: полученный таким образом запрос будет содержать все вставленные системой условия ГДЕ, параметры и дополнительные соединения. Именно его нужно скопировать в консоль запросов для поиска тех самых «потерянных рублей» в итоговых суммах.
Для создания надежных отчетов всегда придерживайтесь следующих правил:
ЕСТЬ NULL при делении, всегда добавляйте явное сравнение с нулем.ВЫРАЗИТЬ.