sqlpostgresqlroundnumeric

ROUND в SQL: как округлять числа и не ловить копейки-призраки

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

8 мин чтенияСправочникsql · postgresql · round · numeric · money · clickhouse

ROUND кажется одной из самых простых функций в SQL: взяли дробное число — получили ближайшее целое.

Например:

SELECT ROUND(3.14159) AS result;

Результат:

result
3

Но за этой простотой прячется важная деталь: округление бывает разным. Особенно на числах вроде 2.5, 3.5, -2.5, где значение стоит ровно посередине между двумя целыми.

Для обычного отчёта это может быть мелочью. А вот для денег, комиссий, зарплат, лимитов и сверок с внешними системами одна «лишняя» копейка уже становится настоящей проблемой. Причём проблема неприятная: она появляется редко, выглядит случайной и долго не находится.

Разберём, как работает ROUND в PostgreSQL, чем отличается numeric от double precision, почему деньги лучше не хранить в плавающей точке и почему нельзя переносить округление между PostgreSQL, MySQL и ClickHouse вслепую.

Что делает ROUND

ROUND округляет число до ближайшего целого.

Базовый синтаксис:

ROUND(number)

Пример:

SELECT
    ROUND(3.14159) AS a,
    ROUND(9.8) AS b,
    ROUND(10.2) AS c;

Результат:

a b c
3 10 10

То есть функция смотрит на дробную часть и выбирает ближайшее целое число.

Если дробная часть меньше половины, число округляется вниз:

SELECT ROUND(4.4) AS result;

Результат:

result
4

Если дробная часть больше половины, число округляется вверх:

SELECT ROUND(4.6) AS result;

Результат:

result
5

А вот с ровной половиной начинается самое интересное.

Как ROUND округляет половинки в PostgreSQL

В PostgreSQL литерал вроде 2.5 обычно воспринимается как значение точного типа numeric.

Для numeric функция ROUND округляет половинки от нуля.

Это значит:

  • 2.5 превращается в 3;
  • 3.5 превращается в 4;
  • -2.5 превращается в -3;
  • -3.5 превращается в -4.

Пример:

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

Результат:

a b c d e
3 3 4 -3 -4

Фраза «от нуля» звучит немного непривычно, но смысл простой.

Положительное число при половинке уходит дальше вправо:

2.5 -> 3

Отрицательное число при половинке уходит дальше влево:

-2.5 -> -3

То есть число становится дальше от нуля.

ROUND для numeric и double precision

Главная ловушка в PostgreSQL: результат округления может зависеть от типа данных.

Для точного типа numeric половинки округляются от нуля.

А вот для double precision поведение может отличаться. Часто используется банковское округление: половинка уходит к ближайшему чётному числу.

Сравним:

SELECT
    ROUND(2.5::numeric) AS numeric_25,
    ROUND(3.5::numeric) AS numeric_35,
    ROUND(2.5::double precision) AS double_25,
    ROUND(3.5::double precision) AS double_35;

Результат на типичной системе может быть таким:

numeric_25 numeric_35 double_25 double_35
3 4 2 4

Почему 2.5::double precision стало 2, а не 3?

Потому что при банковском округлении ровная половинка округляется к ближайшему чётному числу.

Для 2.5 соседи — 2 и 3. Чётное число — 2, поэтому результат 2.

Для 3.5 соседи — 3 и 4. Чётное число — 4, поэтому результат 4.

Это не ошибка PostgreSQL. Это другая стратегия округления.

Зачем вообще нужно банковское округление

Обычное округление половинок от нуля кажется более привычным: 2.5 стало 3, и всё понятно.

Но если округлять огромное количество чисел, постоянное округление половинок в одну сторону может давать систематическое смещение.

Банковское округление пытается это смещение уменьшить: иногда половинка округляется вниз, иногда вверх — в зависимости от ближайшего чётного числа.

Например:

2.5 -> 2
3.5 -> 4
4.5 -> 4
5.5 -> 6

На длинной дистанции такие округления меньше «тащат» сумму в одну сторону.

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

Почему одно и то же число может округлиться по-разному

Посмотрите на этот запрос:

SELECT
    ROUND(2.5) AS a,
    ROUND(2.5::numeric) AS b,
    ROUND(2.5::double precision) AS c;

Выглядит так, будто везде одно и то же число 2.5.

Но для базы это не совсем одно и то же. В первом и втором случае PostgreSQL работает с точным числом, а в третьем — с приблизительным числом с плавающей точкой.

Именно поэтому результат может отличаться.

Особенно опасно, когда в запросе смешиваются разные типы:

SELECT
    ROUND(amount) AS rounded_amount
FROM orders;

Если amount имеет тип numeric, вы получите одну стратегию. Если amount имеет тип double precision, возможны другие нюансы.

Для финансовых расчётов лучше не оставлять это на волю случая. Приводите данные к точному типу явно:

SELECT
    ROUND(amount::numeric) AS rounded_amount
FROM orders;

Но ещё лучше — изначально хранить деньги в numeric, а не в double precision.

Почему double precision опасен для денег

Тип double precision хранит числа приблизительно. Он быстрый и полезный для научных расчётов, измерений, координат, статистики, но для денег подходит плохо.

Причина в том, что многие десятичные дроби нельзя точно представить в двоичной плавающей точке.

Для человека число выглядит как 19.99.

А внутри оно может быть чем-то вроде:

19.989999999999998

Или наоборот:

19.990000000000002

Чаще всего это незаметно. Но при суммировании, расчётах комиссий и последующем округлении такие хвосты могут вылезти наружу.

Например:

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

Если amount — это numeric(12,2), сумма считается точно.

Если amount — это double precision, сумма может накопить маленькую погрешность. И ROUND будет округлять уже не красивое человеческое число, а число с техническим хвостом.

Для денег правило простое: храните суммы в numeric.

Например:

CREATE TABLE payments (
    id integer,
    amount numeric(12, 2)
);

Так вы заранее убираете целый класс странных проблем.

Пример: округлить среднюю зарплату

Допустим, есть таблица сотрудников:

CREATE TABLE employees (
    id integer,
    dept text,
    salary numeric(12, 2)
);

Нужно посчитать среднюю зарплату по отделам и округлить до целого.

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

Результат может быть таким:

dept avg_salary
Engineering 185000
Sales 142000
Support 98000

Если salary хранится как numeric, то AVG(salary) тоже будет точным числовым результатом, и ROUND отработает предсказуемо.

Это хороший пример нормального применения ROUND: мы не меняем исходные данные, а готовим удобное значение для отчёта.

ROUND — не всегда просто косметика

Иногда разработчики воспринимают округление как украшение результата: мол, просто убрали лишние знаки после запятой.

Но в бизнес-логике округление может быть частью расчёта.

Например:

  • округление комиссии;
  • округление суммы к оплате;
  • округление лимита;
  • округление бонусов;
  • округление налоговой базы;
  • округление зарплатного отчёта.

Если база округляет одним способом, а приложение потом другим, могут появиться расхождения.

Например, база посчитала:

2.5 -> 3

А приложение округлило похожее значение по банковскому правилу:

2.5 -> 2

На одной строке это выглядит мелко. Но в отчёте на тысячи операций такие различия превращаются в вопросы от бухгалтерии, финансового отдела или внешней системы.

Поэтому для важных расчётов стоит заранее договориться:

  • где именно происходит округление: в базе или в приложении;
  • какой тип данных используется;
  • какая стратегия округления считается правильной;
  • можно ли округлять промежуточные значения или только финальный результат.

Не округляйте слишком рано

Ещё одна частая ошибка — округлять каждую строку отдельно, а потом суммировать.

Например:

SELECT
    SUM(ROUND(amount)) AS total
FROM orders;

Это не то же самое, что сначала суммировать, а потом округлить:

SELECT
    ROUND(SUM(amount)) AS total
FROM orders;

Разница может быть заметной.

Представим три суммы:

10.4
10.4
10.4

Если округлить каждую отдельно:

10 + 10 + 10 = 30

Если сначала сложить:

10.4 + 10.4 + 10.4 = 31.2

А потом округлить:

31

Получились разные итоги.

Поэтому перед округлением задайте себе вопрос: нужно округлять каждую операцию отдельно или только общий результат?

В финансовых и аналитических отчётах это принципиально.

ROUND с количеством знаков после запятой

У ROUND есть вариант со вторым аргументом:

ROUND(number, digits)

Он округляет не до целого, а до нужного количества знаков после запятой.

Например:

SELECT
    ROUND(3.14159, 2) AS pi_2,
    ROUND(9.876, 1) AS value_1;

Результат:

pi_2 value_1
3.14 9.9

Это удобно для процентов, средних значений и денежных отчётов, где нужно показать две цифры после запятой.

Например:

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

Но логика остаётся той же: важно понимать тип данных и стратегию округления.

ROUND и отображение результата

Иногда округление используют только для красивого вывода:

SELECT
    ROUND(conversion_rate * 100, 2) AS conversion_percent
FROM daily_metrics;

Это нормальный сценарий для отчёта.

Но не стоит путать округление для отображения и округление для хранения.

Если вы округлили значение в SELECT, исходные данные в таблице не изменились.

SELECT ROUND(amount, 2) AS amount_for_report
FROM payments;

Такой запрос просто показывает значение в удобном виде.

А вот если вы используете ROUND в UPDATE, вы уже меняете данные:

UPDATE payments
SET amount = ROUND(amount, 2);

С такими запросами нужно быть осторожнее: после обновления вы можете потерять исходную точность.

MySQL: ROUND и типы данных

В MySQL функция тоже называется ROUND.

Базовый пример:

SELECT ROUND(2.5) AS result;

Для точных типов вроде DECIMAL половинки обычно округляются от нуля.

Например:

SELECT ROUND(CAST(2.5 AS DECIMAL(10, 1))) AS result;

Результат:

result
3

А вот для приблизительных типов вроде DOUBLE поведение может зависеть от реализации и платформы. На практике можно встретить банковское округление.

Например:

SELECT
    ROUND(CAST(2.5 AS DECIMAL(10, 1))) AS decimal_result,
    ROUND(2.5e0) AS double_result;

На одной системе результат может выглядеть так:

decimal_result double_result
3 2

Главный вывод такой же, как в PostgreSQL: для денег и точных бизнес-расчётов используйте точные типы, а не DOUBLE.

ClickHouse: ROUND и банковское округление

В ClickHouse тоже есть функция round.

Но при переносе логики из PostgreSQL нужно быть особенно внимательным: стратегия округления половинок может отличаться от привычной.

Например, банковское округление даёт такие результаты:

SELECT
    round(2.5) AS a,
    round(3.5) AS b;

Результат может быть таким:

a b
2 4

То есть 2.5 округляется к 2, потому что 2 — ближайшее чётное. А 3.5 округляется к 4, потому что 4 — ближайшее чётное.

В ClickHouse также есть функция roundBankers, которая явно подчёркивает банковскую стратегию:

SELECT
    roundBankers(2.5) AS a,
    roundBankers(3.5) AS b;

Результат:

a b
2 4

Практическое правило: если переносите расчёты между PostgreSQL, MySQL и ClickHouse, обязательно проверьте контрольные значения:

2.5
3.5
-2.5
-3.5

И отдельно проверьте типы: точные числа и числа с плавающей точкой могут вести себя по-разному.

Как безопасно использовать ROUND в отчётах

Для обычных отчётов можно держаться такого шаблона.

Если считаете деньги:

SELECT
    ROUND(SUM(amount), 2) AS total_amount
FROM payments
WHERE status = 'paid';

При этом amount лучше хранить как numeric.

Если считаете средние значения:

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

Если считаете проценты:

SELECT
    ROUND(
        paid_orders::numeric / total_orders * 100,
        2
    ) AS paid_percent
FROM daily_stats;

Здесь ::numeric помогает избежать целочисленного деления и сделать расчёт более предсказуемым.

Частые ошибки

Первая ошибка — использовать double precision для денег.

Плохо:

CREATE TABLE payments (
    id integer,
    amount double precision
);

Лучше:

CREATE TABLE payments (
    id integer,
    amount numeric(12, 2)
);

Вторая ошибка — не понимать, как округляются половинки.

Перед важным расчётом проверьте:

SELECT
    ROUND(2.5::numeric) AS numeric_25,
    ROUND(2.5::double precision) AS double_25,
    ROUND(-2.5::numeric) AS numeric_minus_25,
    ROUND(-2.5::double precision) AS double_minus_25;

Третья ошибка — округлять слишком рано.

Обычно лучше сначала посчитать итог, а потом округлить:

SELECT ROUND(SUM(amount), 2) AS total_amount
FROM payments;

А не округлять каждую строку без необходимости:

SELECT SUM(ROUND(amount, 2)) AS total_amount
FROM payments;

Иногда второй вариант нужен по бизнес-правилам. Но он должен быть осознанным, а не случайным.

Четвёртая ошибка — округлять в базе, потом ещё раз в приложении.

Если база и приложение используют разные правила, вы получите редкие и неприятные расхождения.

Главное

ROUND округляет число до ближайшего целого или до нужного количества знаков после запятой.

Базовый пример:

SELECT ROUND(3.14159) AS result;

Для PostgreSQL важно помнить:

  • ROUND(2.5::numeric) даёт 3;
  • ROUND(-2.5::numeric) даёт -3;
  • numeric округляет половинки от нуля;
  • double precision может использовать другую стратегию, часто банковскую;
  • деньги лучше хранить в numeric, а не в double precision;
  • не округляйте промежуточные значения без необходимости;
  • для важных расчётов заранее зафиксируйте тип данных и правило округления.

ROUND — это не просто «сделать красиво». В отчётах, платежах, комиссиях и зарплатах округление становится частью бизнес-логики. Поэтому хороший SQL-разработчик не просто пишет ROUND, а понимает, какое число, какого типа и по какому правилу он округляет.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр