sqlpostgresqlmysqlmath

Целочисленное деление в SQL: почему 7 / 2 иногда равно 3, а иногда 3.5

Когда оператор / в PostgreSQL отсекает дробь, почему лишний cast превращает 7/2 в 3.5, чем выручает функция div(a, b), как с тем же справляются DIV в MySQL и intDiv в ClickHouse и почему рядом почти всегда стоит MOD.

8 мин чтенияСправочникsql · postgresql · mysql · math · division

Целочисленное деление — это деление без дробного хвоста.

В обычной математике мы привыкли, что 7 / 2 равно 3.5. Но в программировании и SQL часто нужен другой ответ: сколько раз одно целое число полностью помещается в другом.

Например:

7 / 2 = 3 and remainder 1

То есть двойка помещается в семёрку три полных раза, а ещё один остаётся сверху.

В SQL такая логика нужна постоянно:

  • разложить пользователей по корзинам;
  • разбить строки на блоки по 100, 1000 или 10000;
  • превратить секунды в минуты и остаток секунд;
  • распределить записи по шардам;
  • посчитать, сколько полных сотен, десятков или упаковок помещается в числе.

Звучит просто, почти как школьная арифметика. Но именно здесь часто ломаются отчёты: один и тот же запрос в разных СУБД может дать разные результаты.

В PostgreSQL выражение 7 / 2 вернёт 3, если оба числа целые.

В MySQL выражение 7 / 2 вернёт 3.5.

В ClickHouse для целого частного обычно используют отдельную функцию intDiv.

Поэтому целочисленное деление — это не «мелочь». Это место, где важно понимать типы данных и поведение конкретной базы.

Что такое целочисленное деление

Целочисленное деление возвращает только целую часть результата.

Например:

7 / 2 = 3
100 / 30 = 3
10 / 5 = 2

Дробная часть просто отбрасывается.

Но есть важный нюанс: в PostgreSQL при делении целых чисел дробная часть отбрасывается в сторону нуля.

Это особенно заметно на отрицательных числах:

-7 / 2 = -3

Математическое округление вниз дало бы -4, потому что -4 меньше, чем -3.5. Но PostgreSQL при целочисленном делении не делает floor. Он именно отсекает дробную часть.

Можно представить, что база берёт результат -3.5 и просто убирает .5. Получается -3.

Деление двух целых в PostgreSQL

В PostgreSQL оператор / смотрит на типы операндов.

Если оба операнда целые, результат тоже будет целым:

SELECT 7 / 2;

SELECT -7 / 2;

SELECT 100 / 30;

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

3
-3
3

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

Например, у нас есть таблица users, и мы хотим сгруппировать пользователей по диапазонам идентификаторов: 0-99, 100-199, 200-299 и так далее.

SELECT
    id / 100 AS bucket,
    count(*) AS users_count
FROM users
GROUP BY id / 100
ORDER BY bucket;

Если id равен 17, корзина будет 0.

Если id равен 145, корзина будет 1.

Если id равен 203, корзина будет 2.

То есть выражение id / 100 превращает каждый идентификатор в номер блока.

Усечение к нулю — не то же самое, что floor

Для положительных чисел разницы почти не видно:

7 / 2 = 3
floor(7 / 2) = 3

Но для отрицательных чисел разница есть:

-7 / 2 = -3
floor(-7 / 2) = -4

Целочисленное деление в PostgreSQL усекает дробную часть к нулю.

Функция floor округляет вниз.

Это разные операции.

Если вам нужно именно округление вниз, особенно при отрицательных значениях, используйте floor явно:

SELECT floor(-7.0 / 2.0);

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

Главная ловушка: типы данных меняют результат

Самая частая ошибка — случайно превратить одно из чисел в дробное.

В PostgreSQL результат зависит от типов.

SELECT 7 / 2;

SELECT 7 / 2.0;

SELECT 7::float / 2;

В первом случае оба числа целые, поэтому результат будет 3.

Во втором случае 2.0 — уже не целое число, а число с дробной частью. Поэтому результат будет 3.5.

В третьем случае левый операнд явно приведён к float, поэтому результат тоже будет 3.5.

Именно это часто ломает аналитику. Человек думает: «Я делю одно число на другое и хочу целое частное». А база видит: «Один операнд дробный, значит деление тоже будет дробным».

Почему NUMERIC может неожиданно дать дробь

Особенно часто это случается с денежными колонками.

Допустим, в таблице orders колонка amount имеет тип numeric.

Тогда такой запрос:

SELECT amount / 100 AS hundreds
FROM orders;

не означает «сколько полных сотен помещается в сумме».

Он означает обычное дробное деление.

Если amount равен 250, результат будет 2.5.

А если вам нужно именно количество полных сотен, такой результат уже опасен. Он может попасть в группировку, отчёт или расчёт комиссии и испортить цифры.

Для целого частного лучше выразить намерение явно.

Функция div в PostgreSQL

В PostgreSQL есть функция div, которая возвращает целое частное.

SELECT div(7, 2);

SELECT div(7.9, 2.0);

SELECT div(-7, 2);

Результаты:

3
3
-3

Преимущество div в том, что намерение видно сразу: нам нужно именно целочисленное деление, а не обычное деление с дробной частью.

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

SELECT
    id,
    amount,
    div(amount, 100) AS full_hundreds
FROM orders;

Если amount равен 250, результат будет 2.

Если amount равен 999, результат будет 9.

Если amount равен 1000, результат будет 10.

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

Когда использовать /, а когда div

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

Например, средняя цена одной позиции:

SELECT amount / quantity AS unit_price
FROM orders
WHERE quantity > 0;

Если amount или quantity дробные, результат тоже может быть дробным — и это нормально.

Используйте div, когда вам нужно именно целое частное.

Например:

SELECT div(total_seconds, 60) AS full_minutes
FROM events;

Или:

SELECT div(id, 1000) AS shard_group
FROM users;

Правило простое:

если дробная часть важна — используйте обычное деление;

если дробную часть нужно отбросить — используйте целочисленное деление явно.

Целочисленное деление в MySQL

В MySQL поведение отличается.

Оператор / выполняет обычное деление и возвращает дробный результат:

SELECT 7 / 2;

Результат:

3.5000

Для целочисленного деления в MySQL используется оператор DIV:

SELECT 7 DIV 2;

SELECT 100 DIV 30;

Результаты:

3
3

Пример с корзинами пользователей:

SELECT
    id DIV 100 AS bucket,
    count(*) AS users_count
FROM users
GROUP BY id DIV 100
ORDER BY bucket;

Это важное отличие. Запрос, который в PostgreSQL даёт целое частное через /, в MySQL может дать дробь.

Целочисленное деление в ClickHouse

В ClickHouse для целочисленного деления обычно используют функцию intDiv.

SELECT intDiv(7, 2);

Результат:

3

А обычное деление через / — это не то же самое, что целочисленное деление.

Поэтому при переносе запросов между PostgreSQL, MySQL и ClickHouse нельзя автоматически считать, что 7 / 2 везде означает одно и то же.

Лучше прямо проверить синтаксис нужной СУБД и записать намерение явно.

DIV и MOD: частное и остаток

Целочисленное деление часто используют вместе с остатком от деления.

Целое частное отвечает на вопрос:

сколько полных раз делитель помещается в число?

Остаток отвечает на вопрос:

что осталось?

Например:

7 = 2 * 3 + 1

Здесь:

  • 3 — целое частное;
  • 1 — остаток.

В PostgreSQL остаток можно получить через mod.

SELECT
    amount,
    div(amount, 100) AS whole_hundreds,
    mod(amount, 100) AS remainder
FROM orders;

Если amount равен 275, результат будет такой:

whole_hundreds = 2
remainder = 75

То есть в числе есть две полные сотни и ещё семьдесят пять сверху.

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

Допустим, в таблице orders есть дата создания заказа created_at. Хотим узнать возраст заказа и красиво разложить его на минуты и секунды.

Сначала получим возраст в секундах:

SELECT
    id,
    extract(epoch FROM (now() - created_at))::int AS age_seconds
FROM orders;

Теперь добавим минуты и остаток секунд:

SELECT
    id,
    age_seconds,
    div(age_seconds, 60) AS minutes,
    mod(age_seconds, 60) AS seconds
FROM (
    SELECT
        id,
        extract(epoch FROM (now() - created_at))::int AS age_seconds
    FROM orders
) t;

Если age_seconds равен 125, получим:

minutes = 2
seconds = 5

Потому что 125 секунд — это две полные минуты и пять секунд.

Пример: корзины по идентификаторам

Допустим, нужно разбить пользователей на блоки по 1000 идентификаторов.

SELECT
    div(id, 1000) AS bucket,
    count(*) AS users_count
FROM users
GROUP BY div(id, 1000)
ORDER BY bucket;

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

id 1       -> bucket 0
id 999     -> bucket 0
id 1000    -> bucket 1
id 1999    -> bucket 1
id 2000    -> bucket 2

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

Пример: распределение по сменам

Остаток от деления хорошо подходит для распределения по кругу.

Например, хотим раскидать сотрудников на три смены по их id:

SELECT
    name,
    dept,
    mod(id, 3) AS shift
FROM employees
ORDER BY shift, name;

Результат mod(id, 3) будет одним из трёх значений:

0
1
2

Это простая схема round-robin: первая группа, вторая группа, третья группа, потом снова первая.

Осторожно с отрицательным остатком

В PostgreSQL знак результата mod повторяет знак делимого.

SELECT mod(-7, 3);

Результат будет:

-1

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

Для обычных положительных id это не проблема.

А вот если вы работаете с хешами, отрицательными числами или техническими ключами, это может привести к ошибке. Например, при распределении по шардам отрицательный остаток может дать номер корзины -1, а такой корзины у вас нет.

Чтобы получить гарантированно неотрицательный остаток, используют такую обёртку:

SELECT mod(mod(x, n) + n, n) AS positive_remainder
FROM values_table;

Если n равно 3, результат всегда будет в диапазоне от 0 до 2.

Ошибка деления на ноль

Целочисленное деление не отменяет обычное правило: делить на ноль нельзя.

Такой запрос завершится ошибкой:

SELECT div(10, 0);

То же самое касается обычного деления:

SELECT 10 / 0;

Если делитель приходит из таблицы, лучше защититься через NULLIF.

SELECT
    amount,
    div(amount, nullif(quantity, 0)) AS amount_per_item
FROM orders;

Если quantity равен 0, выражение nullif(quantity, 0) вернёт NULL, и результат деления тоже станет NULL, а не ошибкой.

Для отчётов это часто лучше, чем падение всего запроса.

Типовая ошибка в аналитике

Допустим, аналитик хочет разложить заказы по диапазонам суммы: 0-99, 100-199, 200-299.

Он пишет:

SELECT
    amount / 100 AS amount_bucket,
    count(*) AS orders_count
FROM orders
GROUP BY amount / 100
ORDER BY amount_bucket;

Если amount имеет тип integer, в PostgreSQL всё будет похоже на ожидаемое.

Но если amount имеет тип numeric, результат может стать дробным:

1.25
1.50
1.99
2.00

И вместо нормальных корзин получится множество мелких групп.

Лучше написать явно:

SELECT
    div(amount, 100) AS amount_bucket,
    count(*) AS orders_count
FROM orders
GROUP BY div(amount, 100)
ORDER BY amount_bucket;

Теперь корзина будет именно целой:

0
1
2
3

Код стал понятнее и безопаснее.

Чем целочисленное деление отличается от округления

Иногда кажется, что можно заменить целочисленное деление функциями округления. Но это не одно и то же.

div отбрасывает дробную часть.

floor округляет вниз.

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

ceil округляет вверх.

Посмотрим на идею:

7 / 2 = 3.5
div result = 3
floor result = 3
round result = 4
ceil result = 4

Для положительных чисел div и floor часто совпадают.

Но для отрицательных значений:

-7 / 2 = -3.5
div result = -3
floor result = -4
round result = -4
ceil result = -3

Поэтому не заменяйте одно другим автоматически. Сначала решите, какой смысл вам нужен.

Как писать переносимые запросы

Если запрос живёт только в PostgreSQL, можно использовать / для двух целых чисел, но для явного намерения часто лучше использовать div.

Если запрос может переехать в MySQL, помните: там для целочисленного деления используется DIV.

Если запрос может переехать в ClickHouse, смотрите в сторону intDiv.

Сравнение выглядит так:

СУБД Обычное деление Целочисленное деление
PostgreSQL 7.0 / 2 7 / 2 или div(7, 2)
MySQL 7 / 2 7 DIV 2
ClickHouse 7 / 2 intDiv(7, 2)

Главный вывод: не переносите выражение 7 / 2 между СУБД вслепую. Сначала проверьте, что именно оно означает в вашем движке.

Главное

Целочисленное деление возвращает целое частное и отбрасывает дробную часть.

В PostgreSQL обычный оператор / даёт целочисленный результат только тогда, когда оба операнда целые:

SELECT 7 / 2;

Результат:

3

Но если хотя бы один операнд становится дробным, результат тоже становится дробным:

SELECT 7 / 2.0;

Результат:

3.5

Для явного целочисленного деления в PostgreSQL удобно использовать div:

SELECT div(7, 2);

Результат:

3

Остаток от деления можно получить через mod:

SELECT
    div(7, 2) AS quotient,
    mod(7, 2) AS remainder;

Целочисленное деление полезно для корзин, блоков, шардов, разложения времени и любых задач, где важно количество полных частей.

Но всегда помните три правила:

  • результат зависит от типов операндов;
  • поведение отличается между PostgreSQL, MySQL и ClickHouse;
  • для отрицательных чисел усечение к нулю, floor и mod могут вести себя не так, как вы ожидаете.

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

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

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