При написании запросов в 1С разработчики часто сталкиваются с необъяснимым на первый взгляд поведением арифметических операций. Классический пример: мы берем два числа, делим их друг на друга напрямую в запросе в виде констант и получаем длинную дробную часть с высокой точностью. Но как только мы заменяем константы на реальные поля ресурсов из виртуальной таблицы (например, остатки или обороты), результат деления внезапно обрезается ровно до 6 знаков после запятой, даже если исходные числа абсолютно идентичны. Разберемся подробно, почему так происходит, как под капотом взаимодействуют платформа 1С и СУБД MS SQL Server, и как гарантировать получение нужной точности вычислений.
Рассмотрим практическую ситуацию. В запросе выполняются два аналогичных математических вычисления. В первом случае мы делим ресурсы виртуальной таблицы остатков СтоимостьОстаток на КоличествоОстаток. Во втором случае мы подставляем те же самые числовые значения, но жестко типизируем их через функцию ВЫРАЗИТЬ.
ВЫБРАТЬ
УчетЗатратОстатки.СтоимостьОстаток / УчетЗатратОстатки.КоличествоОстаток КАК Поле1,
Выразить(11000061.87 Как Число(15,2)) / Выразить(45780.000 Как Число(15,3)) Как Поле2,
УчетЗатратОстатки.КоличествоОстаток,
УчетЗатратОстатки.СтоимостьОстаток
ИЗ
РегистрНакопления.УчетЗатратРегл.Остатки(&ДатаДокумента, АналитикаУчетаЗатрат = &Аналитика) КАК УчетЗатратОстатки
При выполнении этого запроса со значениями СтоимостьОстаток = 11000061.87 и КоличествоОстаток = 45780.000 мы получаем следующие результаты:
240,280949 (ровно 6 знаков после запятой).240,2809495412844036697247 (высокая точность вычислений).Казалось бы, исходные типы данных в конфигураторе для ресурсов регистра накопления настроены стандартно: для стоимости это Число(15, 2), для количества — Число(15, 3). В выражении для Поля 2 мы явно указали точно такие же типы. Почему же СУБД возвращает результаты с настолько разной точностью?
Для того чтобы понять причину разницы, проанализируем, как платформа 1С транслирует обращения к виртуальным таблицам в реальные SQL-запросы к СУБД. Виртуальная таблица остатков не существует в базе данных физически. При обращении к ней платформа выполняет сложный динамический запрос, собирая и суммируя данные из таблиц актуальных итогов и таблиц движений документов (через конструкции UNION ALL и агрегатную функцию SUM).
Согласно стандартам разработки 1С и механизмам платформы, при выполнении агрегатных функций (в частности, СУММА) СУБД должна гарантировать отсутствие переполнения разрядов при сложении огромного количества записей. Для этого платформа 1С принудительно увеличивает разрядность целой части суммируемых полей. Правило увеличения разрядности на уровне платформы выглядит так:
Количество знаков целой части результата суммирования увеличивается на 7 знаков относительно исходного типа ресурса. Дробная часть при этом сохраняет свою исходную длину.
Давайте посчитаем, какими становятся типы наших полей в промежуточном SQL-запросе, который генерирует 1С:
СтоимостьОстаток в конфигураторе имеет тип Число(15, 2). Длина целой части составляет 13 знаков (15 - 2). После агрегации и суммирования платформа увеличивает целую часть на 7 знаков. Длина целой части становится равна 20 знакам (13 + 7). Таким образом, в СУБД это поле преобразуется в тип numeric(22, 2) (где 22 — общая разрядность precision, а 2 — масштаб scale).КоличествоОстаток в конфигураторе имеет тип Число(15, 3). Длина целой части составляет 12 знаков (15 - 3). Платформа увеличивает целую часть на 7 знаков до 19. В СУБД поле транслируется как тип numeric(22, 3).В зависимости от версии платформы, сложности запроса и режима совместимости конфигурации, эти типы могут увеличиваться еще сильнее, вплоть до максимального значения numeric(38, s).
Теперь обратимся к математическим алгоритмам работы самой СУБД MS SQL Server. Когда СУБД выполняет операцию деления двух чисел с типами данных decimal(p1, s1) / decimal(p2, s2), где p — общая разрядность (precision), а s — масштаб дробной части (scale), она рассчитывает параметры результирующего типа по строгим формулам:
ДлинаЦелойЧасти = p1 - s1 + s2.МасштабРезультата = max(6, s1 + p2 + 1).ИтоговаяРазрядность = ДлинаЦелойЧасти + МасштабРезультата.Однако в MS SQL Server существует жесткое ограничение: максимальная общая разрядность любого числа типа decimal или numeric не может превышать 38 знаков. Если в ходе расчета по формулам значение ИтоговаяРазрядность превышает 38, СУБД начинает принудительно уменьшать (обрезать) масштаб дробной части результата вычислений, чтобы число уложилось в рамки стандарта СУБД и не произошло переполнения целой части.
Официальная документация MS SQL Server описывает это поведение следующим образом: если результирующая разрядность превышает 38, а длина целой части требует хранения большого количества знаков, масштаб дробной части результата принудительно уменьшается до минимального гарантированного лимита, равного 6 знакам после запятой (результат приводится к типу decimal(38, 6)). Это предотвращает ошибку переполнения целой части, но сильно снижает точность деления.
Проанализируем детально, почему наши два поля из примера дали принципиально разный результат.
В выражении для Поля 2 мы явно применили функцию ВЫРАЗИТЬ, зафиксировав типы операндов:
numeric(15, 2) (т.е. p1 = 15, s1 = 2)numeric(15, 3) (т.е. p2 = 15, s2 = 3)Применим формулы MS SQL Server:
15 - 2 + 3 = 16 знаков.max(6, 2 + 15 + 1) = 18 знаков.16 + 18 = 34 знака.Поскольку итоговая расчетная разрядность (34) меньше максимально допустимого предела СУБД (38), MS SQL Server не применяет механизмы урезания точности. Вычисления производятся с полным масштабом в 18 знаков после запятой. Нам возвращается число с длинным "хвостом" дробной части: 240,2809495412844036...
В случае с Полем 1 платформа 1С сгенерировала виртуальную таблицу, раздув разрядность полей для безопасного суммирования агрегатом СУММА. Как мы выяснили выше, операнды получили типы numeric(22, 2) и numeric(22, 3). В зависимости от внутренней структуры запроса, оптимизатор 1С может транслировать эти поля на СУБД с еще более высокой разрядностью, вплоть до предельной.
Допустим, типы полей составили:
numeric(22, 2) (т.е. p1 = 22, s1 = 2)numeric(22, 3) (т.е. p2 = 22, s2 = 3)Рассчитаем параметры деления:
22 - 2 + 3 = 23 знака.max(6, 2 + 22 + 1) = 25 знаков.23 + 25 = 48 знаков!Итоговая разрядность 48 знаков существенно превышает лимит СУБД в 38 знаков. СУБД MS SQL Server вынуждена запустить алгоритм снижения точности. Чтобы гарантированно сохранить целую часть числа (которой требуется до 23 знаков), СУБД уменьшает масштаб дробной части до минимально возможного стандарта — 6 знаков после запятой. Итоговым типом результата деления становится numeric(38, 6). Именно поэтому мы получили обрезанное значение 240,280949.
Понимание физики этого процесса позволяет нам легко управлять поведением СУБД. Рассмотрим эффективные практические способы решения этой проблемы.
Самый правильный и надежный способ — принудительно уменьшить разрядность делимого и делителя непосредственно перед операцией деления. Для этого мы оборачиваем каждое поле в функцию ВЫРАЗИТЬ. Это заставит платформу 1С передать в СУБД жестко ограниченные типы данных, математический расчет разрядности уложится в рамки 38 знаков, и СУБД вернет максимально точный результат.
Рекомендуется использовать этот подход во всех сложных расчетах цен, себестоимости и долей в запросах 1С:
ВЫБРАТЬ
ВЫРАЗИТЬ(УчетЗатратОстатки.СтоимостьОстаток КАК Число(15, 2)) /
ВЫРАЗИТЬ(УчетЗатратОстатки.КоличествоОстаток КАК Число(15, 3)) КАК ТочнаяСебестоимость
ИЗ
РегистрНакопления.УчетЗатратРегл.Остатки(&ДатаДокумента, АналитикаУчетаЗатрат = &Аналитика) КАК УчетЗатратОстатки
После такого приведения типов операндов СУБД вернет результат вычислений с высокой точностью (до 18 знаков после запятой), аналогично нашему Полю 2.
Если вам на выходе требуется строго определенное количество знаков после запятой (например, 2 или 4 знака), необходимо обернуть в функцию ВЫРАЗИТЬ все математическое выражение целиком. Однако здесь кроется важная архитектурная ловушка, которую нужно детально разобрать.
Если вы напишете запрос следующим образом:
// Внимание: Этот вариант может вернуть неточный результат!
ВЫБРАТЬ
ВЫРАЗИТЬ(УчетЗатратОстатки.СтоимостьОстаток / УчетЗатратОстатки.КоличествоОстаток КАК Число(15, 10)) КАК Себестоимость
ИЗ
РегистрНакопления.УчетЗатратРегл.Остатки(&ДатаДокумента, АналитикаУчетаЗатрат = &Аналитика) КАК УчетЗатратОстатки
Вы можете получить неожиданный результат вида 240,2809490000. Почему появились нули в конце? Потому что СУБД сначала выполнила деление "раздутых" полей виртуальной таблицы, на шаге деления урезала точность результата до 6 знаков после запятой (получив 240,280949), а затем внешняя функция ВЫРАЗИТЬ( ... КАК Число(15, 10)) просто дополнила уже обрезанное число четырьмя пустыми нулями в конце.
Чтобы избежать этой проблемы при расчетах с высокой точностью, необходимо комбинировать оба подхода: сначала приводить к компактному типу сами операнды, а затем округлять итоговый результат деления:
// Правильный вариант расчета с гарантированной точностью 10 знаков
ВЫБРАТЬ
ВЫРАЗИТЬ(
ВЫРАЗИТЬ(УчетЗатратОстатки.СтоимостьОстаток КАК Число(15, 2)) /
ВЫРАЗИТЬ(УчетЗатратОстатки.КоличествоОстаток КАК Число(15, 3))
КАК Число(15, 10)) КАК ТочнаяСебестоимость
ИЗ
РегистрНакопления.УчетЗатратРегл.Остатки(&ДатаДокумента, АналитикаУчетаЗатрат = &Аналитика) КАК УчетЗатратОстатки
В данном случае вычисления на стороне СУБД пройдут по следующему сценарию:
numeric(15, 2) и numeric(15, 3).240,2809495412844...).ВЫРАЗИТЬ корректно округлит этот точный результат до 10 знаков после запятой, и мы получим корректное значение: 240,2809495413.Разница в округлении чисел при делении в запросах 1С — это не ошибка или "глюк" платформы. Это закономерный результат работы математических алгоритмов оптимизации типов данных в СУБД MS SQL Server при взаимодействии с динамическими запросами виртуальных таблиц 1С. Для предотвращения потери точности при делении ресурсов всегда соблюдайте простое золотое правило разработки: явно типизируйте операнды деления с помощью функции ВЫРАЗИТЬ перед выполнением математических операций в запросе.