При работе с запросами в системе 1С:Предприятие разработчики часто сталкиваются с неочевидным поведением при выполнении простейших арифметических операций, особенно деления. Например, деление стоимости на количество в одном случае выдает 6 знаков после запятой, а в другом — 15 или более. Подобная разница может привести к серьезным расхождениям в отчетах и при расчете себестоимости. В данной статье мы подробно разберем, почему это происходит, как СУБД определяет разрядность результата и каким образом мы можем гарантированно получить нужную нам точность.
Рассмотрим типичную ситуацию, с которой столкнулся автор темы. В запросе выполняются два схожих действия:
СтоимостьОстаток / КоличествоОстаток).ВЫРАЗИТЬ (например, ВЫРАЗИТЬ(11000061.87 Как Число(15,2)) / ВЫРАЗИТЬ(45780.000 Как Число(15,3))).Несмотря на то, что исходные числа одинаковы, результат в первом случае может иметь ограниченное число знаков (например, 6), а во втором — огромный «хвост» из 15-20 знаков. Проанализируем причину этого явления. 1С:Предприятие транслирует запросы на язык SQL, и именно СУБД (чаще всего MS SQL Server) принимает решение о том, какой тип данных будет иметь результат операции деления.
Когда мы делим два числа в запросе, СУБД оперирует типами numeric(p, s), где p (precision) — общая точность (всего цифр), а s (scale) — масштаб (цифр после запятой). Для операции деления аргументов e1 / e2 с параметрами (p1, s1) и (p2, s2) итоговая точность и масштаб вычисляются по сложным формулам:
Важно понимать: если итоговая точность (precision) превышает максимально допустимое значение в SQL (38), масштаб (scale) будет принудительно уменьшен, чтобы в итоговое число влезла целая часть. Это критический момент. Когда мы используем поля из регистра накопления (например, 15,2 и 15,3), SQL применяет формулы к этим метаданным. Когда мы используем литералы, SQL может интерпретировать их как float или другие типы с максимальной точностью numeric(38, s), что и порождает разницу в выводе.
Проанализируем ситуацию с виртуальными таблицами (Остатки, Обороты). В SQL-представлении этих таблиц поля Стоимость и Количество не являются обычными колонками. Они представляют собой результат агрегации SUM(...). Чтобы избежать переполнения при суммировании миллионов строк, MS SQL Server часто неявно приводит результат суммы к более высокому типу, например, из numeric(15,2) в numeric(38,2).
Это «невидимое» расширение типов напрямую влияет на деление. Если в одном операнде у нас уже precision = 38, то при делении СУБД будет максимально сокращать количество знаков после запятой, чтобы не «вылететь» за границы разрядности. Именно поэтому в полях регистра мы часто видим меньше знаков, чем при делении констант.
Единственный надежный способ получить предсказуемый результат деления в запросе 1С — это явное ограничение типа результата. Разберем, как это сделать правильно. Если мы хотим получить результат деления с точностью до 6 знаков, мы должны обернуть всю операцию деления в функцию ВЫРАЗИТЬ.
Посмотрим на пример правильного кода:
ВЫБРАТЬ
УчетЗатратОстатки.АналитикаУчетаЗатрат,
ВЫРАЗИТЬ(УчетЗатратОстатки.СтоимостьОстаток / УчетЗатратОстатки.КоличествоОстаток КАК Число(15, 6)) КАК ЦенаЗаЕдиницу
ИЗ
РегистрНакопления.УчетЗатратРегл.Остатки(&Дата, ) КАК УчетЗатратОстатки
Почему это работает? Функция ВЫРАЗИТЬ на уровне SQL превращается в оператор CAST. Согласно документации SQL Server, при приведении к типу с меньшим количеством знаков после запятой, значение округляется. Таким образом, мы убиваем двух зайцев: избавляемся от лишних знаков и получаем математически корректное округление.
Выясним важный нюанс, который часто путает программистов. В 1С существует два способа округления:
ВЫРАЗИТЬ(... КАК ЧИСЛО(X, Y)) выполняет математическое округление (0.5 всегда округляется до 1 в большую сторону по модулю).Округлить() по умолчанию использует банковское округление (0.5 округляется до ближайшего четного).Посмотрим на пример разницы в коде 1С:
// Встроенный язык (банковское по умолчанию)
Результат = Округлить(2.5, 0); // Вернет 2
Результат = Округлить(3.5, 0); // Вернет 4
// Запрос (математическое)
// ВЫРАЗИТЬ(2.5 КАК ЧИСЛО(10, 0)) вернет 3
// ВЫРАЗИТЬ(3.5 КАК ЧИСЛО(10, 0)) вернет 4
Если вам требуется идеальное соответствие итогов в отчете (сделанном на СКД или запросом) и итогов, рассчитанных в коде документа, всегда проверяйте режим округления в функции Округлить(), используя параметр РежимОкругления.Окр15Как10 или РежимОкругления.Окр15Как20.
Если вы столкнулись с непонятным округлением, воспользуйтесь следующим алгоритмом действий:
Запрос.Выполнить().Выгрузить() и посмотрите в отладчике типы полученных колонок. Это покажет, какой тип 1С назначила полю по умолчанию.float.ВЫРАЗИТЬ внутри деления, вы все равно должны обеспечивать проверку знаменателя.Пример комплексного подхода к делению в запросе:
ВЫБРАТЬ
Продажи.Номенклатура,
ВЫБОР
КОГДА Продажи.КоличествоОборот <> 0
ТОГДА ВЫРАЗИТЬ(Продажи.СтоимостьОборот / Продажи.КоличествоОборот КАК Число(15, 2))
ИНАЧЕ 0
КОНЕЦ КАК СредняяЦена
ИЗ
РегистрНакопления.Продажи.Обороты КАК Продажи
Подводя итог, можно сказать, что «странное» поведение округления в 1С — это не ошибка платформы, а следствие строгого следования правилам работы СУБД с фиксированной точностью. Использование ВЫРАЗИТЬ для конечного результата деления является стандартом разработки, позволяющим избежать неопределенности в расчетах.