sqlpostgresqlroundnumeric

SQL ROUND: Rounding to the Nearest Integer, Banker's Rounding and Money

How ROUND rounds to the nearest integer, why half-away-from-zero differs from banker's rounding, and why numeric beats double for money.

3 min. skaitymoReferencesql · postgresql · round · numeric · money · clickhouse
Šis straipsnis šiuo metu yra rusų kalba — vertimas į anglų kalbą rengiamas.

ROUND округляет число до ближайшего целого: вы вызываете его, когда нужно превратить дробный итог суммы, среднего или процента в целое значение для отчёта, лимита или платёжки. За простым названием прячутся две разные стратегии округления половинок и коварная разница между точным типом numeric и приблизительным double precision, на которой регулярно горят финансовые отчёты.

Округление ROUND редко бывает «просто косметикой»: его результат попадает в суммы оплат, комиссии, лимиты, отчёты по зарплатам и сверки с внешними системами. Если PostgreSQL и приложение округляют половинку 0.5 по-разному, расхождение в одну копейку будет редким — и именно поэтому такие редкие копейки потом труднее всего объяснять.

Базовое округление до целого

С одним аргументом ROUND отбрасывает дробную часть и приближает значение к ближайшему целому. Ровные половинки (.5) уходят от нуля: 2.5 становится 3, а -2.5 становится -3. То есть ROUND всегда выбирает сторону, дальше от нуля, как только дробная часть равна ровно половине.

SELECT
  ROUND(3.14159) AS a,   -- 3
  ROUND(2.5)     AS b,   -- 3
  ROUND(3.5)     AS c,   -- 4
  ROUND(-2.5)    AS d;   -- -3

Ключевые свойства ROUND с одним аргументом:

  • Для numeric результат сохраняет тот же тип, дробная часть просто обнуляется.
  • Стратегия для литералов вроде 2.5 — half-away-from-zero, симметричная относительно нуля.
  • Чтобы оставить знаки после запятой, у ROUND есть второй аргумент ROUND(x, n) — он разбирается в отдельной статье.

numeric против double: где прячется банковское округление

Главная ловушка ROUND в том, что PostgreSQL округляет половинки по-разному в зависимости от типа аргумента. Литерал 2.5 — это numeric, и здесь ROUND идёт от нуля. А вот для double precision тот же ROUND использует банковское округление (round-half-to-even): половинки уходят к ближайшему чётному.

SELECT
  ROUND(2.5::numeric)          AS num_25,   -- 3
  ROUND(3.5::numeric)          AS num_35,   -- 4
  ROUND(2.5::double precision) AS dbl_25,   -- 2
  ROUND(3.5::double precision) AS dbl_35;   -- 4

Для double значения 2.5 и 3.5 округляются ROUND к 2 и 4 соответственно — потому что по правилу half-to-even выигрывает чётный сосед. Это не баг, а способ уменьшить систематическое смещение, когда суммируется множество округлённых значений.

Грабли: одно и то же число даёт у ROUND разный результат только из-за типа. Если в запросе встречаются и константы numeric, и колонки float, итоги могут «не сойтись» на копейку. Приводите данные к numeric явно, когда от ROUND нужна предсказуемость.

Отдельно проверьте границу между PostgreSQL и приложением. Если ROUND срабатывает на этапе агрегации, а сервис потом ещё раз округляет уже полученную сумму, вы получаете двойное правило и труднообъяснимые расхождения. Лучше зафиксировать один слой, один тип и одну стратегию округления.

Округление денег

Когда ROUND применяется к деньгам, double опасен ещё и потому, что не все десятичные дроби представимы в двоичной плавающей точке. Сравните накопленную сумму, которую вы потом передадите в ROUND:

SELECT
  SUM(amount)            AS raw_total,
  ROUND(SUM(amount))     AS rounded_total
FROM orders
WHERE status = 'paid';

Если колонка amount объявлена как numeric(12,2), SUM накапливается точно и ROUND отрабатывает честно. Если как double precision, у суммы уже могут появиться «хвосты» вроде 19.999999998, и ROUND округлит именно их. Правило простое: храните деньги в numeric, а ROUND применяйте к numeric.

Округлить через ROUND среднюю зарплату по отделу до целого:

SELECT
  dept,
  ROUND(AVG(salary)) AS avg_salary
FROM employees
GROUP BY dept
ORDER BY avg_salary DESC;

AVG над numeric-колонкой возвращает numeric, поэтому здесь ROUND предсказуемо использует half-away-from-zero, без сюрпризов с банковским округлением.

Отличия MySQL и ClickHouse

Функция ROUND есть везде, но стратегия для половинок различается, и переносить логику округления между СУБД вслепую нельзя.

  • MySQL: ROUND(x) для точных типов (DECIMAL) округляет от нуля, а для приблизительных (DOUBLE) поведение зависит от платформенной реализации C — обычно банковское. Тот же раскол по типу аргумента, что и в PostgreSQL.
SELECT ROUND(2.5), ROUND(2.5e0);
-- 3 (DECIMAL, away from zero), often 2 (DOUBLE, to even)
  • ClickHouse: здесь главное отличие — round() по умолчанию делает банковское округление к ближайшему чётному, а не уходит от нуля, как ROUND над numeric в PostgreSQL. Поэтому round(2.5) даёт 2, а не 3. Если нужно явно подчеркнуть банковскую стратегию, есть roundBankers(), а за привычным округлением от нуля придётся идти к floor/ceil с поправкой. Не переносите финансовую логику из PostgreSQL вслепую: проверьте тип аргумента и нужную стратегию на контрольных половинках.
SELECT round(2.5), round(3.5), roundBankers(2.5);
-- 2, 4, 2

Запомните одно: для ROUND «округлить до целого» — это не единственная операция, а семейство стратегий. Прежде чем доверять ROUND финансовый отчёт, проверьте тип аргумента и то, как именно ваша СУБД обходится с ровными половинками.

Praktikuokitės su realiomis užduotimis

Spręskite užduotis SQL treniruoklyje su momentiniu vertinimu ir užuominomis.

Atverti treniruoklį