При написании сложных аналитических запросов в платформе 1С:Предприятие разработчики нередко сталкиваются с загадочным поведением платформы: при выполнении операции деления дробных чисел результат неожиданно усекается ровно до 6 знаков после запятой. При этом попытка обернуть все выражение деления в конструкцию ВЫРАЗИТЬ(... КАК ЧИСЛО(15, 8)) не дает желаемого эффекта — запрос возвращает все то же число, дополненное нулями на конце.
В этой статье мы подробно разберем, почему возникает этот эффект, как именно СУБД выполняет расчет типов данных при делении и как правильно строить выражения в языке запросов 1С, чтобы гарантированно сохранить точность вычислений.
Рассмотрим типовую ситуацию, в которой возникает ошибка. Предположим, в запросе выполняется деление данных таблицы на итоговые обороты из регистра сведений или накопления с использованием функции ЕСТЬNULL:
(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество)
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров)
Пусть исходные значения равны:
67,5576 069,81981982При расчете на калькуляторе точный результат равен 0,0008879999999999979.... Однако на выходе запроса 1С мы получаем значение 0,000887. Все последующие знаки отсекаются, что приводит к критическим погрешностям при последующем расчете сумм или записи коэффициентов в регистры.
Чтобы понять первопричину, проанализируем, как платформа 1С транслирует запросы в реляционную СУБД (в частности, Microsoft SQL Server). В базе данных числа с фиксированной точностью хранятся и обрабатываются как тип DECIMAL(p, s) или NUMERIC(p, s), где p (precision) — общая разрядность числа, а s (scale) — количество знаков после запятой.
При выполнении операции деления DECIMAL(p1, s1) / DECIMAL(p2, s2) СУБД вычисляет результирующий тип по следующим формулам:
p = p1 - s1 + s2 + max(6, s1 + p2 + 1)s = max(6, s1 + p2 + 1)Однако в MS SQL Server максимальная разрядность любого числового выражения жестко ограничена 38 знаками. Когда в числителе и знаменателе участвуют составные выражения (сложение полей регистра, таблиц, констант и функции ISNULL), суммарная требуемая разрядность легко превышает 38 знаков.
В этом случае SQL Server вынужден динамически уменьшать масштаб (scale), чтобы гарантированно сохранить целую часть числа и избежать ошибки переполнения. При этом в алгоритме СУБД заложен минимальный гарантированный порог усечения точности: масштаб урезается ровно до 6 знаков. Именно поэтому мы всегда наблюдаем обрезание результата до 6 цифр после запятой.
Посмотрим на типичную ошибку исправления:
ВЫРАЗИТЬ((ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество)
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров) КАК ЧИСЛО(15, 8))
Данная конструкция транслируется в SQL как CAST((Выражение1 / Выражение2) AS NUMERIC(15, 8)). СУБД выполняет операции последовательно:
0.000887).CAST, в результате чего число просто дополняется нулями: 0.00088700.Для того чтобы СУБД не раздувала разрядность промежуточных вычислений и не срабатывал лимит в 38 знаков, необходимо явно указать типы операндов до выполнения операции деления.
Применим функцию ВЫРАЗИТЬ к числителю или его составным частям:
(ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) КАК ЧИСЛО(15, 5)) + Таблица.Количество)
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров)
Или приведем числитель целиком:
ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество КАК ЧИСЛО(15, 5))
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров)
В таком случае СУБД точно знает разрядность делимого, суммарная разрядность не выходит за границы 38 знаков, и операция деления сохраняет максимальную точность (результат вернет 0,000887999999...).
Разберем еще одну частую проблему при расчете себестоимости, сумм списания или долей. Часто в запросах разработчики делят сумму на количество, получая цену, а затем умножают на требуемый объем:
// Некорректный подход (ведет к потере точности в дроби):
СУММА(ВЫРАЗИТЬ(СтоимостьТоваров.Стоимость / Обороты.Количество КАК ЧИСЛО(23, 10)) * СтоимостьТоваров.Количество)
Чтобы избежать промежуточного усечения дроби и накопления погрешностей, всегда следует выполнять умножение до деления, а также явно типизировать промежуточное произведение:
// Корректный подход:
СУММА(ВЫРАЗИТЬ(СтоимостьТоваров.Стоимость * СтоимостьТоваров.Количество КАК ЧИСЛО(23, 10)) / Обороты.Количество)
Выполнение умножения в числителе сохраняет значимые разряды больших чисел и минимизирует погрешность деления.
Если запрос содержит большое количество агрегатных функций, условий КОГДА ... ТОГДА и соединений, надежным решением является вынос промежуточных слагаемых во временную таблицу с явной типизацией полей:
ВЫБРАТЬ
ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество КАК ЧИСЛО(15, 3)) КАК Числитель,
ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров КАК ЧИСЛО(15, 3)) КАК Знаменатель
ПОМЕСТИТЬ ВТ_ПодготовленныеДанные
ИЗ
Таблица КАК Таблица
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Числитель / ВТ.Знаменатель КАК ТочныйКоэффициент
ИЗ
ВТ_ПодготовленныеДанные КАК ВТ
Создание промежуточной временной таблицы гарантирует, что метаданные полей в СУБД будут иметь четко зафиксированный размер, что исключит неявные расширения типов платформой 1С.
ВЫРАЗИТЬ исключительно к итоговому результату деления, если уже произошла потеря точности.ВЫРАЗИТЬ(... КАК ЧИСЛО(...)) операнды деления (делимое и делитель), особенно если они содержат ЕСТЬNULL или литералы.(A * B) / C вместо (A / C) * B.