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

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

При написании сложных аналитических запросов в платформе 1С:Предприятие разработчики нередко сталкиваются с загадочным поведением платформы: при выполнении операции деления дробных чисел результат неожиданно усекается ровно до 6 знаков после запятой. При этом попытка обернуть все выражение деления в конструкцию ВЫРАЗИТЬ(... КАК ЧИСЛО(15, 8)) не дает желаемого эффекта — запрос возвращает все то же число, дополненное нулями на конце.

В этой статье мы подробно разберем, почему возникает этот эффект, как именно СУБД выполняет расчет типов данных при делении и как правильно строить выражения в языке запросов 1С, чтобы гарантированно сохранить точность вычислений.

Анализ проблемы: откуда берется потеря точности

Рассмотрим типовую ситуацию, в которой возникает ошибка. Предположим, в запросе выполняется деление данных таблицы на итоговые обороты из регистра сведений или накопления с использованием функции ЕСТЬNULL:


(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество) 
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров)

Пусть исходные значения равны:

  1. Числитель: 67,55
  2. Знаменатель: 76 069,81981982

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

Механизм работы СУБД: математика типов DECIMAL и MS SQL Server

Чтобы понять первопричину, проанализируем, как платформа 1С транслирует запросы в реляционную СУБД (в частности, Microsoft SQL Server). В базе данных числа с фиксированной точностью хранятся и обрабатываются как тип DECIMAL(p, s) или NUMERIC(p, s), где p (precision) — общая разрядность числа, а s (scale) — количество знаков после запятой.

При выполнении операции деления DECIMAL(p1, s1) / DECIMAL(p2, s2) СУБД вычисляет результирующий тип по следующим формулам:

  1. Общая разрядность: p = p1 - s1 + s2 + max(6, s1 + p2 + 1)
  2. Количество знаков после запятой: 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)). СУБД выполняет операции последовательно:

  1. Сначала вычисляется операция деления в скобках, где из-за переполнения 38 знаков результат сразу обрезается до 6 знаков (получается 0.000887).
  2. Затем к уже усеченному значению применяется CAST, в результате чего число просто дополняется нулями: 0.00088700.

Способ 1. Явное приведение типов операндов до деления (Рекомендуемый)

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

Применим функцию ВЫРАЗИТЬ к числителю или его составным частям:


(ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) КАК ЧИСЛО(15, 5)) + Таблица.Количество) 
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров)

Или приведем числитель целиком:


ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество КАК ЧИСЛО(15, 5)) 
/ (ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров)

В таком случае СУБД точно знает разрядность делимого, суммарная разрядность не выходит за границы 38 знаков, и операция деления сохраняет максимальную точность (результат вернет 0,000887999999...).

Способ 2. Оптимизация порядка математических операций

Разберем еще одну частую проблему при расчете себестоимости, сумм списания или долей. Часто в запросах разработчики делят сумму на количество, получая цену, а затем умножают на требуемый объем:


// Некорректный подход (ведет к потере точности в дроби):
СУММА(ВЫРАЗИТЬ(СтоимостьТоваров.Стоимость / Обороты.Количество КАК ЧИСЛО(23, 10)) * СтоимостьТоваров.Количество)

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


// Корректный подход:
СУММА(ВЫРАЗИТЬ(СтоимостьТоваров.Стоимость * СтоимостьТоваров.Количество КАК ЧИСЛО(23, 10)) / Обороты.Количество)

Выполнение умножения в числителе сохраняет значимые разряды больших чисел и минимизирует погрешность деления.

Способ 3. Подготовка данных во временных таблицах

Если запрос содержит большое количество агрегатных функций, условий КОГДА ... ТОГДА и соединений, надежным решением является вынос промежуточных слагаемых во временную таблицу с явной типизацией полей:


ВЫБРАТЬ
    ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОстатковОборот, 0) + Таблица.Количество КАК ЧИСЛО(15, 3)) КАК Числитель,
    ВЫРАЗИТЬ(ЕСТЬNULL(Обороты.КоличествоВЕдиницеОтчетовОборот, 0) + Таблица.КоличествоМетров КАК ЧИСЛО(15, 3)) КАК Знаменатель
ПОМЕСТИТЬ ВТ_ПодготовленныеДанные
ИЗ
    Таблица КАК Таблица
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ.Числитель / ВТ.Знаменатель КАК ТочныйКоэффициент
ИЗ
    ВТ_ПодготовленныеДанные КАК ВТ

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

Сводка правил для разработчика 1С

  1. Никогда не применяйте ВЫРАЗИТЬ исключительно к итоговому результату деления, если уже произошла потеря точности.
  2. Всегда явно типизируйте через ВЫРАЗИТЬ(... КАК ЧИСЛО(...)) операнды деления (делимое и делитель), особенно если они содержат ЕСТЬNULL или литералы.
  3. При выполнении комплексных расчетов производите умножение перед делением: (A * B) / C вместо (A / C) * B.
  4. Помните об особенностях используемой СУБД: ограничения в 38 разрядов характерны для MS SQL Server, в то время как PostgreSQL ведет себя мягче, однако единый стандарт написания запросов защитит конфигурацию от сбоев при миграции между СУБД.
← На главную