sqlpostgresqlroundnumeric

ROUND в SQL: как округлять числа, цены и проценты без сюрпризов

ROUND(x, n) округляет до n знаков после запятой; в PostgreSQL двухаргументная форма требует numeric. Разбираем отрицательное n, валютный вывод и поведение ClickHouse.

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

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

Например, база может честно посчитать средний чек как 1487.6666666667, но пользователю, менеджеру или бухгалтерскому отчёту обычно нужен аккуратный вид: 1487.67.

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

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

Что делает ROUND

Чаще всего ROUND используют так:

ROUND(x, n)

Здесь:

  • x — число, которое нужно округлить;
  • n — сколько знаков оставить после запятой.

Например, ROUND(3.14159, 2) оставит два знака после запятой и вернёт 3.14.

SELECT
  ROUND(3.14159, 2) AS pi_2,
  ROUND(3.14159, 0) AS pi_0,
  ROUND(2.5, 0)     AS half_value;

Результат будет таким:

pi_2 pi_0 half_value
3.14 3 3

Если второй аргумент не указать, PostgreSQL округлит число до целого:

SELECT ROUND(12.78) AS rounded_value;

Результат:

rounded_value
13

То есть эти два выражения по смыслу близки:

SELECT
  ROUND(12.78)    AS without_scale,
  ROUND(12.78, 0) AS with_zero_scale;

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

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

Это значит:

  • 2.5 превращается в 3;
  • -2.5 превращается в -3.
SELECT
  ROUND(2.5, 0)  AS positive_half,
  ROUND(-2.5, 0) AS negative_half;

Такое поведение важно помнить, если вы считаете деньги, бонусы, комиссии или любые суммы, где значение .5 может влиять на итог.

ROUND — это не усечение

Новички иногда путают округление с отбрасыванием лишних знаков.

ROUND округляет число по правилам математики:

SELECT ROUND(3.99, 1) AS rounded_value;

Результат:

rounded_value
4.0

А TRUNC просто отрезает лишнее:

SELECT TRUNC(3.99, 1) AS truncated_value;

Результат:

truncated_value
3.9

Разница простая:

  • ROUND смотрит на следующий знак и может увеличить число;
  • TRUNC ничего не увеличивает, он просто отбрасывает лишние цифры.

Если вы показываете пользователю средний чек, чаще нужен ROUND. Если строите техническую границу или хотите именно «отрезать хвост», тогда может подойти TRUNC.

Округление до двух знаков: цены, суммы и проценты

Самый частый сценарий — округлить сумму до двух знаков после запятой.

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

SELECT
  id,
  amount,
  ROUND(amount, 2) AS rounded_amount
FROM orders;

Такой запрос удобен, когда сумма может быть рассчитана через деление, скидки, комиссии или среднее значение.

Например, посчитаем средний чек по оплаченным заказам:

SELECT
  ROUND(AVG(amount), 2) AS avg_paid_order
FROM orders
WHERE status = 'paid';

Без ROUND база может вернуть длинную дробь. С ROUND результат будет выглядеть аккуратно и привычно для отчёта.

То же самое с процентами:

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

Если paid_orders = 37, а total_orders = 125, результат будет 29.60.

Отрицательный второй аргумент: округление до десятков, сотен и тысяч

У ROUND есть возможность, о которой часто забывают: второй аргумент может быть отрицательным.

Если положительное n округляет вправо от запятой, то отрицательное n округляет влево от запятой.

SELECT
  ROUND(12345.678, -1) AS to_tens,
  ROUND(12345.678, -2) AS to_hundreds,
  ROUND(12345.678, -3) AS to_thousands;

Результат:

to_tens to_hundreds to_thousands
12350 12300 12000

Что здесь происходит:

  • -1 — округление до десятков;
  • -2 — до сотен;
  • -3 — до тысяч.

Это очень удобно для аналитики. Например, можно быстро разложить заказы по «корзинам» стоимости.

SELECT
  ROUND(amount, -2) AS amount_bucket,
  COUNT(*)          AS orders_count
FROM orders
WHERE status = 'paid'
GROUP BY ROUND(amount, -2)
ORDER BY amount_bucket;

Так заказы с суммами около 1000, 1100, 1200 и дальше попадут в понятные группы. Получится простая гистограмма без отдельной таблицы диапазонов.

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

amount_bucket orders_count
500 18
1000 42
1500 31
2000 14

Такой отчёт сразу показывает, в каком ценовом диапазоне у вас больше всего заказов.

Главная ловушка PostgreSQL: numeric против double precision

Теперь самый важный момент.

В PostgreSQL есть функция:

ROUND(numeric, integer)

Но нет функции:

ROUND(double precision, integer)

Из-за этого запрос может неожиданно упасть.

Например:

SELECT ROUND(salary / 12.0, 2) AS monthly_pay
FROM employees;

PostgreSQL может выдать ошибку:

ERROR: function round(double precision, integer) does not exist

На первый взгляд странно: ведь ROUND существует. Проблема не в самой функции, а в типе данных.

Выражение salary / 12.0 может стать значением типа double precision. А PostgreSQL не умеет применять двухаргументный ROUND к double precision.

Решение — явно привести число к numeric:

SELECT
  name,
  ROUND(salary::numeric / 12, 2) AS monthly_pay
FROM employees;

Теперь PostgreSQL понимает, какую именно версию функции нужно вызвать.

Почему ошибка кажется особенно коварной

Одноаргументный ROUND для double precision в PostgreSQL есть.

То есть такой запрос может работать:

SELECT ROUND(salary / 12.0) AS monthly_pay
FROM employees;

А такой — уже нет:

SELECT ROUND(salary / 12.0, 2) AS monthly_pay
FROM employees;

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

Поэтому для PostgreSQL полезно запомнить простое правило:

если используете ROUND(x, n), особенно для денег, приводите значение к numeric или храните его как numeric изначально.

Почему для денег лучше numeric

double precision хранит число приближённо. Это тип с плавающей точкой, и он не всегда может точно представить десятичные дроби.

Например, значение 0.1 в таком формате может храниться не как идеально точное 0.1, а как очень близкое к нему двоичное приближение.

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

Для денежных сумм лучше использовать десятичный тип:

CREATE TABLE orders (
  id     bigint,
  amount numeric(12, 2),
  status text
);

numeric(12, 2) означает:

  • всего до 12 цифр;
  • 2 цифры после запятой.

Например, туда хорошо ложатся суммы вроде 9999999999.99.

Если сумма изначально хранится как numeric, запросы становятся проще:

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

Не нужно каждый раз думать, почему PostgreSQL не нашёл подходящую функцию.

ROUND не отвечает за красивый внешний вид

ROUND округляет число. Но он не делает из числа красивую денежную строку.

Он не обязан:

  • добавлять знак валюты;
  • ставить разделители тысяч;
  • красиво выравнивать число;
  • превращать результат в текст для интерфейса.

Для внешнего вида в PostgreSQL используют to_char.

Посчитаем общую сумму заказов пользователя:

SELECT
  u.name,
  SUM(o.amount)                                   AS raw_total,
  ROUND(SUM(o.amount), 2)                         AS rounded_total,
  to_char(ROUND(SUM(o.amount), 2), 'FM999G999D00') AS pretty_total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.name;

Что здесь происходит:

  • SUM(o.amount) считает исходную сумму;
  • ROUND(SUM(o.amount), 2) округляет её до двух знаков;
  • to_char превращает число в красиво отформатированную строку.

Маска FM999G999D00 читается так:

  • FM убирает лишние пробелы;
  • G ставит разделитель групп разрядов;
  • D обозначает десятичный разделитель;
  • 00 фиксирует два знака после запятой.

Хорошая привычка: отдельно считайте число и отдельно форматируйте его для показа.

ROUND — для точности.
to_char — для внешнего вида.

Пример из реального отчёта

Допустим, нужно показать по каждому пользователю:

  • сколько заказов он оплатил;
  • общую сумму;
  • средний чек;
  • красивую сумму для интерфейса.
SELECT
  u.name,
  COUNT(o.id)                                      AS paid_orders,
  ROUND(SUM(o.amount), 2)                          AS total_amount,
  ROUND(AVG(o.amount), 2)                          AS avg_order_amount,
  to_char(ROUND(SUM(o.amount), 2), 'FM999G999D00') AS pretty_total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.name
ORDER BY total_amount DESC;

Такой запрос уже похож на настоящий кусок аналитики для личного кабинета, админки или финансового отчёта.

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

Отличия MySQL

В MySQL двухаргументный ROUND(x, n) работает привычно и принимает разные числовые типы без такого сюрприза, как в PostgreSQL с double precision.

SELECT
  ROUND(amount, 2) AS rounded_amount
FROM orders;

Отрицательный второй аргумент тоже работает:

SELECT
  ROUND(amount, -2) AS amount_bucket,
  COUNT(*)          AS orders_count
FROM orders
GROUP BY ROUND(amount, -2)
ORDER BY amount_bucket;

Для красивого денежного формата с двумя знаками после запятой в MySQL можно использовать FORMAT.

SELECT
  FORMAT(ROUND(amount, 2), 2) AS pretty_amount
FROM orders;

Но идея остаётся той же: округление и форматирование — разные задачи.

Отличия ClickHouse

В ClickHouse у ROUND тоже есть форма с двумя аргументами:

SELECT round(amount, 2) AS rounded_amount
FROM orders;

Но у ClickHouse есть важная особенность: для чисел с плавающей точкой округление половины может быть банковским, то есть к ближайшему чётному числу.

Например:

SELECT
  round(2.5)        AS rounded_2_5,
  roundBankers(3.5) AS bankers_3_5;

Результат может отличаться от привычного ожидания в PostgreSQL или MySQL:

rounded_2_5 bankers_3_5
2 4

Банковское округление называют half to even: если число ровно посередине, выбирается ближайшее чётное значение.

Поэтому при переносе запросов между PostgreSQL, MySQL и ClickHouse не стоит слепо доверять одному названию ROUND. Лучше проверить поведение на простых значениях:

SELECT
  round(2.5) AS test_1,
  round(3.5) AS test_2,
  round(4.5) AS test_3;

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

Частые сценарии использования ROUND

ROUND чаще всего встречается в таких задачах:

  1. Округлить средний чек:
SELECT
  ROUND(AVG(amount), 2) AS avg_order_amount
FROM orders
WHERE status = 'paid';
  1. Посчитать процент:
SELECT
  ROUND(success_count::numeric / total_count * 100, 2) AS success_percent
FROM metrics;
  1. Сгруппировать суммы по сотням:
SELECT
  ROUND(amount, -2) AS amount_bucket,
  COUNT(*)          AS orders_count
FROM orders
GROUP BY ROUND(amount, -2)
ORDER BY amount_bucket;
  1. Округлить результат деления:
SELECT
  name,
  ROUND(total_score::numeric / attempts_count, 2) AS avg_score
FROM students;
  1. Подготовить число для отчёта:
SELECT
  category,
  ROUND(SUM(revenue), 2) AS revenue
FROM sales
GROUP BY category
ORDER BY revenue DESC;

Что важно запомнить

ROUND(x, n) округляет число x до n знаков после запятой.

Если n не указать, число округляется до целого:

SELECT ROUND(9.7) AS rounded_value;

Если n положительное, округление идёт вправо от запятой:

SELECT ROUND(9.876, 2) AS rounded_value;

Если n отрицательное, округление идёт влево от запятой:

SELECT ROUND(9876, -2) AS rounded_value;

В PostgreSQL двухаргументная форма ROUND(x, n) работает с numeric, но не с double precision.

Поэтому для таких выражений лучше явно делать приведение:

SELECT ROUND(value::numeric, 2) AS rounded_value
FROM measurements;

Для денег лучше хранить суммы как numeric(12, 2) или другой подходящий numeric, а не как double precision.

ROUND округляет число, но не форматирует его для красивого вывода. Для внешнего вида в PostgreSQL используйте to_char.

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

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

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

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