При работе с математическими вычислениями в запросах 1С:Предприятие программисты иногда сталкиваются со странным поведением функции Pow() (возведение в степень). Особенно ярко это проявляется, когда результат вычислений — большое число, которое, казалось бы, должно умещаться в стандартные типы данных, но при сравнении или вычитании дает неожиданные результаты. В этой статье мы подробно разберем, почему на СУБД Microsoft SQL Server возникают потери разрядов, как на это влияют версии сервера и какие существуют способы обхода данной проблемы.
Рассмотрим классическую ситуацию, описанную разработчиками. При выполнении простого запроса, который должен вернуть ноль (так как мы вычитаем число из самого себя), мы получаем ненулевой результат. Проанализируем пример кода:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ 60 КАК Число, 1 ПОМЕСТИТЬ Х;
|ВЫБРАТЬ Pow(2, Х.Число) - Pow(2, 60) ИЗ Х";
Результат = Запрос.Выполнить().Выгрузить();
Сообщить(XMLСтрока(Результат[0][0]));
В файловой базе данных этот код вернет ожидаемый «0». Однако при работе в клиент-серверном варианте на определенных версиях MSSQL результат будет отличаться. Выясним причину, заглянув «под капот» — в тот SQL-код, который платформа 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 знаков. Почему же происходит потеря?
Проанализируем поведение функции POWER в самом SQL Server. Согласно документации Microsoft, поведение функции зависит от типа входных данных. Несмотря на то, что мы передаем NUMERIC, старые версии движка MSSQL (например, SQL Server 2012 или 12.0) внутри себя часто используют алгоритмы вычисления, основанные на типе FLOAT (числа с плавающей точкой).
Стандарт FLOAT (двойной точности, 8 байт) гарантирует точность только до 15–17 значащих десятичных цифр. Как только наше число (2 в степени 60) превышает этот порог (а в нем 19 цифр), младшие разряды просто отбрасываются или округляются в процессе самого вычисления степени, еще до того, как сработает финальный CAST обратно в NUMERIC.
Важный момент: в файловой базе 1С используются собственные библиотеки математических вычислений, которые работают с фиксированной точкой иначе и не имеют такого «бутылочного горлышка», как приведение к FLOAT в середине операции.
В ходе практических тестов было выявлено, что проблема сильно зависит от версии используемой СУБД:
Это означает, что в более современных версиях оптимизатор и математическая библиотека SQL Server были доработаны. Теперь они более корректно обрабатывают тип NUMERIC в составе функции POWER, не прибегая к промежуточному преобразованию в тип с плавающей точкой там, где это критично для точности.
Рассмотрим несколько стратегий решения этой проблемы, если вы вынуждены работать на старых версиях СУБД или хотите сделать ваш код максимально универсальным.
Встроенный язык 1С работает с типом Число как с десятичным числом с фиксированной точкой (аналог Decimal в .NET или NUMERIC в SQL). Его точность составляет 38 знаков, и она сохраняется при арифметических операциях.
Если вам нужно возвести число в большую степень, лучше сделать это в коде:
РезультатСтепени = Pow(2, 60); // Здесь точность будет сохранена полностью
Иногда 1С создает колонки во временных таблицах с точностью, недостаточной для промежуточных расчетов. Хотя ВЫРАЗИТЬ внутри запроса не всегда спасает (так как ошибка возникает внутри функции Pow), явное определение типа колонки при помещении данных во временную таблицу может помочь SQL-серверу выбрать более правильный план выполнения.
"ВЫБРАТЬ ВЫРАЗИТЬ(60 КАК ЧИСЛО(10, 0)) КАК Число ПОМЕСТИТЬ Х;"
Для небольших целых степеней можно заменить Pow(X, 3) на X * X * X. В случае с большими степенями (как 60) это непрактично, но для малых значений это гарантирует использование целочисленной арифметики или арифметики с фиксированной точкой в обход FLOAT.
Подведем итоги нашего анализа:
Pow() запроса 1С — это не ошибка самой платформы, а особенность реализации математических функций в старых версиях MSSQL.FLOAT, который ограничен 15–17 значащими цифрами.Помните, что работа с большими числами в СУБД всегда требует повышенного внимания к типам данных и их внутреннему представлению.