Почему функция Pow в запросах 1С теряет точность на СУБД MSSQL и как это исправить?

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

При работе с математическими вычислениями в запросах 1С:Предприятие программисты иногда сталкиваются со странным поведением функции Pow() (возведение в степень). Особенно ярко это проявляется, когда результат вычислений — большое число, которое, казалось бы, должно умещаться в стандартные типы данных, но при сравнении или вычитании дает неожиданные результаты. В этой статье мы подробно разберем, почему на СУБД Microsoft SQL Server возникают потери разрядов, как на это влияют версии сервера и какие существуют способы обхода данной проблемы.

Суть проблемы: когда 2 в степени 60 не равно 2 в степени 60

Рассмотрим классическую ситуацию, описанную разработчиками. При выполнении простого запроса, который должен вернуть ноль (так как мы вычитаем число из самого себя), мы получаем ненулевой результат. Проанализируем пример кода:


Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ 60 КАК Число, 1 ПОМЕСТИТЬ Х;
|ВЫБРАТЬ Pow(2, Х.Число) - Pow(2, 60) ИЗ Х";
Результат = Запрос.Выполнить().Выгрузить();
Сообщить(XMLСтрока(Результат[0][0]));

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

Анализ работы транслятора запросов 1С

Платформа 1С при формировании SQL-запроса выполняет приведение типов, чтобы обеспечить максимальную точность. Однако именно здесь кроется нюанс. Типичный SQL-текст, порожденный платформой, выглядит следующим образом:


exec sp_executesql N'SELECT
(CAST(POWER(CAST(@P1 AS NUMERIC(38, 6)), T1._Q_000_F_000) AS NUMERIC(38, 6)) - @P2)
FROM #tt14 T1', N'@P1 numeric(10), @P2 numeric(38,8)', 2, 1152921504606846976.00000000

Обратим внимание на функцию POWER(). Платформа 1С принудительно кастует аргументы к NUMERIC(38, 6). Число 2 в 60-й степени равно 1 152 921 504 606 846 976. Это число состоит из 19 значащих десятичных цифр. Тип NUMERIC(38, 6) теоретически позволяет хранить до 38 знаков, что более чем достаточно для целой части в 19 знаков. Почему же происходит потеря?

Внутренние механизмы MSSQL: ловушка типа FLOAT

Проанализируем поведение функции POWER в самом SQL Server. Согласно документации Microsoft, поведение функции зависит от типа входных данных. Несмотря на то, что мы передаем NUMERIC, старые версии движка MSSQL (например, SQL Server 2012 или 12.0) внутри себя часто используют алгоритмы вычисления, основанные на типе FLOAT (числа с плавающей точкой).

Стандарт FLOAT (двойной точности, 8 байт) гарантирует точность только до 15–17 значащих десятичных цифр. Как только наше число (2 в степени 60) превышает этот порог (а в нем 19 цифр), младшие разряды просто отбрасываются или округляются в процессе самого вычисления степени, еще до того, как сработает финальный CAST обратно в NUMERIC.

Важный момент: в файловой базе 1С используются собственные библиотеки математических вычислений, которые работают с фиксированной точкой иначе и не имеют такого «бутылочного горлышка», как приведение к FLOAT в середине операции.

Влияние версии MSSQL на результат

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

  1. MSSQL 2012 (12.0): Проблема воспроизводится. Точность теряется после 17-го знака.
  2. MSSQL 2016 (13.0) и выше: Проблема не воспроизводится (возвращается 0).

Это означает, что в более современных версиях оптимизатор и математическая библиотека SQL Server были доработаны. Теперь они более корректно обрабатывают тип NUMERIC в составе функции POWER, не прибегая к промежуточному преобразованию в тип с плавающей точкой там, где это критично для точности.

Как гарантированно избежать потери точности?

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

Решение 1: Вынос расчетов на сторону встроенного языка 1С

Встроенный язык 1С работает с типом Число как с десятичным числом с фиксированной точкой (аналог Decimal в .NET или NUMERIC в SQL). Его точность составляет 38 знаков, и она сохраняется при арифметических операциях.

Если вам нужно возвести число в большую степень, лучше сделать это в коде:

РезультатСтепени = Pow(2, 60); // Здесь точность будет сохранена полностью

Решение 2: Использование явного приведения типов во временных таблицах

Иногда 1С создает колонки во временных таблицах с точностью, недостаточной для промежуточных расчетов. Хотя ВЫРАЗИТЬ внутри запроса не всегда спасает (так как ошибка возникает внутри функции Pow), явное определение типа колонки при помещении данных во временную таблицу может помочь SQL-серверу выбрать более правильный план выполнения.

"ВЫБРАТЬ ВЫРАЗИТЬ(60 КАК ЧИСЛО(10, 0)) КАК Число ПОМЕСТИТЬ Х;"

Решение 3: Замена возведения в степень умножением

Для небольших целых степеней можно заменить Pow(X, 3) на X * X * X. В случае с большими степенями (как 60) это непрактично, но для малых значений это гарантирует использование целочисленной арифметики или арифметики с фиксированной точкой в обход FLOAT.

Выводы

Подведем итоги нашего анализа:

Помните, что работа с большими числами в СУБД всегда требует повышенного внимания к типам данных и их внутреннему представлению.

← На главную