При разработке сложных отчетов или механизмов распределения затрат программисты 1С часто сталкиваются с проблемой «потери» точности в запросах. Ситуация, когда при делении одного числа на другое итоговый результат «гуляет» на несколько процентов, встречается довольно часто, особенно при работе с большими суммами или временными таблицами. Разберем подробно, почему это происходит и как добиться максимальной точности вычислений, не выходя за рамки языка запросов.
Прежде всего, проанализируем, как СУБД (например, MS SQL Server или PostgreSQL) обрабатывает арифметические операции. Когда мы пишем простое деление в запросе, система автоматически определяет тип результата на основе типов операндов. Если операнды имеют стандартную точность (например, 2 знака после запятой), промежуточный результат может быть округлен слишком рано.
Важный нюанс: В MS SQL Server существует лимит точности в 38 знаков. Если при умножении чисел общее количество разрядов превышает этот предел, СУБД начинает уменьшать масштаб (количество знаков после запятой), чтобы сохранить целую часть. Именно поэтому популярный совет «умножить на миллион» может не сработать или даже ухудшить ситуацию, если числа и так велики.
Рассмотрим классическую ошибку в порядке вычислений. Часто разработчики пишут формулу так:
(Сумма / Итог) * Надбавка
В этом случае сначала происходит деление Сумма / Итог. Если Сумма значительно меньше Итога, результат деления будет очень маленьким числом (например, 0.000123). Если СУБД ограничит точность этого промежуточного значения, то последующее умножение на Надбавка даст огромную погрешность.
Правильный подход: Сначала выполняем умножение, а затем деление. Это позволит сохранить значимые разряды в целой части до самого последнего момента.
// Вместо: Сумма / Итог * Надбавка
// Используем:
Выбрать
Таблица.Надбавка * Таблица.Сумма / ТаблицаИтоговая.Сумма КАК Результат
Для управления точностью в запросах 1С существует оператор ВЫРАЗИТЬ. Распространенная ошибка — применять его только к финальному результату. Чтобы СУБД выделила достаточно памяти под промежуточные вычисления, необходимо приводить к нужному типу сами операнды.
Проанализируем пример, где мы принудительно задаем точность до 10 знаков после запятой для делимого и делителя:
ВЫБРАТЬ
ВЫРАЗИТЬ(Таблица.Сумма КАК ЧИСЛО(25, 10)) / ВЫРАЗИТЬ(ТаблицаИтоговая.Сумма КАК ЧИСЛО(25, 10)) КАК ТочнаяПропорция
ИЗ
...
Почему это помогает? Когда мы явно указываем ЧИСЛО(25, 10), мы заставляем систему использовать соответствующий тип данных (Numeric/Decimal) с фиксированной точностью. Это предотвращает неявное приведение к стандартным типам с 2 знаками.
Если вы используете временные таблицы (ПОМЕСТИТЬ), помните, что типы колонок определяются при создании таблицы. Если в первую временную таблицу попали данные с низкой точностью, все последующие запросы к ней будут ограничены этими типами.
Разберем по шагам правильную схему работы:
ВЫРАЗИТЬ для агрегатных функций.
// Сначала считаем итоги с высокой точностью
ВЫБРАТЬ
СУММА(ВЫРАЗИТЬ(Данные.Сумма КАК ЧИСЛО(25, 5))) КАК ОбщийИтог
ПОМЕСТИТЬ ВТ_Итоги
ИЗ
Данные;
// Затем считаем распределение
ВЫБРАТЬ
Данные.Менеджер,
ВЫРАЗИТЬ(Данные.Сумма * Надбавки.Сумма / ВТ_Итоги.ОбщийИтог КАК ЧИСЛО(25, 10)) КАК РаспределеннаяНадбавка
ИЗ
Данные
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Итоги ПО ИСТИНА
Даже при идеальной точности (например, 10 знаков) при распределении 100 рублей на 3 человек мы получим по 33.3333333333 рубля. В сумме это даст 99.9999999999, но не 100. Если ваша задача — чтобы сумма частей строго равнялась целому, необходимо распределить погрешность (дельту).
Рассмотрим более сложный пример запроса, который позволяет вычислить погрешность:
// 1. Считаем предварительные проценты и их отклонение
ВЫБРАТЬ
Т.Назначение,
Т.Сумма,
ВЫРАЗИТЬ(Т.Сумма / Т_Итог.Сумма КАК ЧИСЛО(15, 2)) КАК Процент,
Т.Сумма / Т_Итог.Сумма - (ВЫРАЗИТЬ(Т.Сумма / Т_Итог.Сумма КАК ЧИСЛО(15, 2))) КАК ДельтаПроцент
ПОМЕСТИТЬ ТТ
ИЗ
Т, Т_Итог;
// 2. Считаем общую накопленную дельту
ВЫБРАТЬ
СУММА(ТТ.ДельтаПроцент) КАК ОбщаяДельта,
СУММА(ТТ.Сумма) КАК ОбщаяСумма
ПОМЕСТИТЬ Т_Дельта
ИЗ
ТТ;
// 3. Возвращаем результат, корректируя его на долю дельты
ВЫБРАТЬ
ТТ.Назначение,
ВЫРАЗИТЬ(ТТ.Процент + Т_Дельта.ОбщаяДельта * ТТ.Сумма / Т_Дельта.ОбщаяСумма КАК ЧИСЛО(15, 2)) КАК ИтоговыйПроцент
ИЗ
ТТ
ЛЕВОЕ СОЕДИНЕНИЕ Т_Дельта ПО ИСТИНА
Проанализируем ситуацию: если вам нужно распределить сумму до копейки так, чтобы итоговый баланс сошелся идеально, использование только запросов может быть избыточно сложным. В практике 1С-разработки для таких задач часто применяется комбинированный подход:
ТаблицаЗначений.Остаток = ОбщаяСумма - УжеРаспределено.Это гарантирует отсутствие копеек «в воздухе». Однако, если вы работаете в рамках СКД или больших массивов данных, где выгрузка в память нежелательна, описанные выше методы с ВЫРАЗИТЬ и изменением порядка операций (Умножение -> Деление) являются стандартом решения проблемы точности.
Подведем итог: Для повышения точности в запросе не используйте простое умножение на миллион. Вместо этого контролируйте типы через ВЫРАЗИТЬ для каждого операнда и всегда выполняйте умножение на коэффициент до операции деления. Это позволит минимизировать погрешность до пренебрежимо малых величин.