В практике разработки на платформе 1С программисты часто сталкиваются с ситуацией, когда запрос, успешно работающий на большинстве документов, внезапно "падает" с ошибкой «Arithmetic overflow error converting numeric to data type numeric» на конкретном наборе данных. Особенно часто это происходит в отчетах или запросах, где производятся сложные расчеты: пересчет валют, вычисление себестоимости с учетом коэффициентов или расчет маржинальности.
Разберем детально, почему возникает эта ошибка и как ее правильно устранить, сохранив точность вычислений.
Многие ошибочно полагают, что переполнение возникает только тогда, когда итоговый результат числа слишком велик (например, превышает 15 знаков). Однако в контексте SQL-сервера (MS SQL Server или PostgreSQL), который выполняет запрос, проблема кроется в промежуточной точности.
Проанализируем ситуацию: когда вы перемножаете два числа типа numeric(15, 2) и numeric(15, 3), СУБД не просто выдает результат, она резервирует под него контейнер, размер которого рассчитывается по специальным формулам. В MS SQL Server при умножении точность результата (общее количество цифр) становится равной p1 + p2 + 1. Если в запросе идет длинная цепочка умножений и делений, общая точность очень быстро достигает предела в 38 знаков. Как только этот предел превышен, система пытается сохранить целую часть за счет отбрасывания знаков после запятой. Если же даже целая часть не помещается в 38 знаков, возникает ошибка Arithmetic overflow.
Рассмотрим пример проблемного кода, который часто становится причиной падения:
ВЫБРАТЬ
ЕСТЬNULL(СУММА(ЗаказПокупателяТовары.Сумма) -
СУММА(ЗаказПокупателяТовары.Количество * ЗаказПокупателяТовары.Коэффициент / ЗаказПокупателяТовары.КоэффициентЕдХр
* ЦеныНоменклатурыСрезПоследних.Цена * ЦеныНоменклатурыСрезПоследних.ЕдиницаИзмерения.Коэффициент
/ ЗаказПокупателяТовары.КоэффициентЕдХр * КурсыВалютСрезПоследних.Курс
/ КурсыВалютСрезПоследних.Кратность), 0) КАК Маржа
В данной формуле мы видим цепочку из множества операций. Каждое умножение расширяет разрядность промежуточного значения. Если в данных окажется очень маленькое число (например, коэффициент 0,000333333) или, наоборот, очень большое, SQL-движок может запутаться в распределении разрядов и выдать ошибку.
Самый надежный способ борьбы с переполнением в 1С — это принудительное ограничение разрядности промежуточного результата. Для этого используется оператор ВЫРАЗИТЬ. Он работает как явное приведение типа (CAST) на уровне СУБД, сбрасывая накопленную "виртуальную" точность.
Рассмотрим, как правильно применить этот метод. Рекомендуется ограничивать разрядность не только всей формулы целиком, но и отдельных ее частей, если формула очень длинная. Посмотрим на примере:
ВЫБРАТЬ
ВЫРАЗИТЬ(ЗаказПокупателяТовары.Количество * ЗаказПокупателяТовары.Коэффициент / ЗаказПокупателяТовары.КоэффициентЕдХр КАК ЧИСЛО(15, 6)) КАК КолвоЕдХр,
ВЫРАЗИТЬ(ЦеныНоменклатурыСрезПоследних.Цена * ЦеныНоменклатурыСрезПоследних.ЕдиницаИзмерения.Коэффициент / ЗаказПокупателяТовары.КоэффициентЕдХр КАК ЧИСЛО(15, 6)) КАК ЦенаЕдХр
Важный нюанс: использование ВЫРАЗИТЬ(x КАК ЧИСЛО(15, 6)) гарантирует, что СУБД будет использовать одинаковую точность как на PostgreSQL, так и на MS SQL Server, что делает ваш код более стабильным и предсказуемым.
Проанализируем влияние порядка вычислений. В математике от перестановки множителей произведение не меняется, но в вычислительной среде СУБД это не совсем так.
Попробуем перегруппировать множители так, чтобы не допускать экстремального роста промежуточного числа. Вместо (A * B * C * D) / (E * F) лучше использовать (A * B / E) * (C * D / F), оборачивая каждую логическую часть в ВЫРАЗИТЬ.
Использование функции СУММА() существенно усугубляет ситуацию. Когда SQL Server выполняет агрегацию, он автоматически пытается увеличить точность результата суммы, чтобы избежать переполнения при сложении миллионов строк. Если выражение внутри СУММА() уже имеет точность, близкую к критическим 38 знакам, добавление агрегата гарантированно вызовет ошибку.
Чтобы этого избежать, выполним следующие шаги:
ВЫРАЗИТЬ к каждой строке.СУММА() уже по подготовленным и ограниченным по разрядности данным.Посмотрим на пример структуры такого запроса:
ВЫБРАТЬ
СУММА(Т.Сумма) - СУММА(Т.СтоимостьКурсовая) КАК Маржа
ИЗ (
ВЫБРАТЬ
ЗаказПокупателяТовары.Сумма КАК Сумма,
ВЫРАЗИТЬ(ЗаказПокупателяТовары.Количество * Цена * Курс / Кратность КАК ЧИСЛО(15, 2)) КАК СтоимостьКурсовая
ИЗ
Документ.ЗаказПокупателя.Товары КАК ЗаказПокупателяТовары
// ... соединения с ценами и курсами
) КАК Т
Проанализируем еще несколько важных моментов, которые помогут избежать ошибки:
ВЫБОР КОГДА Кратность = 0 ТОГДА 1 ИНАЧЕ Кратность КОНЕЦ.ЕСТЬNULL(..., 0) применяется уже к результату вычисления. Если переполнение случилось в процессе умножения внутри функции, ЕСТЬNULL вас не спасет. Сначала нужно "вылечить" само выражение.Сумма * 0.2 (расчет НДС), СУБД может интерпретировать 0.2 с избыточной точностью. Безопаснее также оборачивать такие константы в ВЫРАЗИТЬ, если они участвуют в длинных цепочках расчетов.Подведем итог: для решения проблемы переполнения необходимо "дробить" длинные формулы, ограничивать разрядность промежуточных результатов через ВЫРАЗИТЬ(Курс * Цена КАК ЧИСЛО(15, 2)) и следить за тем, чтобы агрегатные функции работали с уже типизированными данными.