sqlpostgresqlmysqlclickhouse

Что такое MOD в SQL: остаток от деления, чётность и корзины

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

7 мин чтенияСправочникsql · postgresql · mysql · clickhouse · math · sharding

MOD(a, b) возвращает остаток от деления a на b.

Проще говоря: SQL делит одно число на другое, берёт целую часть, а всё, что «не поместилось», возвращает как остаток.

Например, 10 не делится на 3 нацело:

  • 3 помещается в 10 три раза;
  • 3 × 3 = 9;
  • остаётся 1.

Значит, MOD(10, 3) вернёт 1.

В SQL этот приём используют очень часто: проверить чётность числа, найти каждую десятую строку, разложить пользователей по корзинам, распределить данные по шардам или построить любую циклическую логику.

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

В PostgreSQL и MySQL можно писать и функцию MOD(a, b), и оператор a % b. Обычно они означают одно и то же: остаток от деления.

SELECT
  MOD(10, 3) AS a,
  10 % 3 AS b,
  MOD(9, 3) AS c,
  MOD(7, 10) AS d;

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

a b c d
1 1 0 7

Разберём каждое значение:

MOD(10, 3) возвращает 1, потому что 10 делится на 3 с остатком 1.

10 % 3 тоже возвращает 1. Это просто другой синтаксис для той же идеи.

MOD(9, 3) возвращает 0, потому что 9 делится на 3 нацело.

MOD(7, 10) возвращает 7, потому что 10 ни разу не помещается в 7, и весь остаток — это само число 7.

Проверка делимости

Самое важное правило:

если число делится на другое число без остатка, MOD возвращает 0.

Например, заказ с количеством товаров 12 можно ровно разложить по коробкам по 4 товара:

SELECT MOD(12, 4) AS remainder;

Результат:

remainder
0

А вот 14 товаров по коробкам по 4 уже не раскладываются идеально:

SELECT MOD(14, 4) AS remainder;

Результат:

remainder
2

Осталось 2 товара.

Именно поэтому MOD удобен для проверок вида «делится или не делится»:

SELECT id, amount
FROM orders
WHERE MOD(amount, 1000) = 0;

Такой запрос найдёт заказы, у которых сумма ровно кратна 1000.

Деление на ноль

С делителем нужно быть осторожным.

SELECT MOD(5, 0) AS result;

Так делать нельзя. Деление на ноль вызовет ошибку.

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

В реальных запросах это особенно важно, когда делитель приходит из столбца:

SELECT
  value,
  divider,
  MOD(value, divider) AS remainder
FROM numbers;

Если в divider окажется 0, запрос упадёт с ошибкой. Поэтому перед такими вычислениями делитель часто проверяют:

SELECT
  value,
  divider,
  MOD(value, divider) AS remainder
FROM numbers
WHERE divider <> 0;

Как проверить чётность числа

Один из самых популярных сценариев для MOD — проверка чётности.

Чётное число делится на 2 без остатка. Значит:

  • если MOD(id, 2) = 0, число чётное;
  • если MOD(id, 2) = 1, число нечётное.

Например, пометим заказы по чётности id:

SELECT
  id,
  amount,
  CASE
    WHEN MOD(id, 2) = 0 THEN 'even'
    ELSE 'odd'
  END AS parity
FROM orders
ORDER BY id;

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

id amount parity
1 1200 odd
2 850 even
3 430 odd
4 2100 even

Для новичка это хороший способ почувствовать MOD: мы не «угадываем» чётность, а задаём SQL простой математический вопрос — есть ли остаток при делении на 2.

Как выбрать каждую N-ю строку

MOD удобно использовать, когда нужно взять каждую N-ю запись.

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

SELECT id, email, country
FROM users
WHERE MOD(id, 10) = 0
ORDER BY id;

Такой запрос вернёт пользователей с id, которые делятся на 10 без остатка:

id email country
10 user10@example.com DE
20 user20@example.com PL
30 user30@example.com ES

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

Но здесь есть важный нюанс.

Такой подход хорошо работает, если id идут более-менее равномерно: 1, 2, 3, 4, 5 и так далее. Если в последовательности много дыр или идентификаторы устроены сложно, выборка может получиться кривой.

Например, если в таблице остались только такие id:

id
10
20
30
40

то условие MOD(id, 10) = 0 выберет вообще всех. Это уже не «примерно каждый десятый», а просто вся таблица.

А если используются UUID, делить сам идентификатор через MOD обычно нельзя напрямую. В таких случаях берут хэш от значения, а уже потом считают остаток от хэша.

Корзины и шардинг через MOD

Ещё один полезный сценарий — разложить строки по корзинам.

Представьте, что у нас есть пользователи, и мы хотим распределить их по 4 группам. Например, чтобы обрабатывать данные частями или постепенно включать новую фичу.

Для этого можно взять остаток от деления id на 4:

SELECT
  MOD(id, 4) AS bucket,
  COUNT(*) AS users
FROM users
GROUP BY MOD(id, 4)
ORDER BY bucket;

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

bucket users
0 250
1 249
2 251
3 250

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

MOD(id, 4) всегда возвращает один из четырёх остатков:

  • 0;
  • 1;
  • 2;
  • 3.

То есть каждый пользователь попадает в одну из четырёх корзин.

Например:

id MOD(id, 4)
1 1
2 2
3 3
4 0
5 1
6 2

Так можно стабильно делить пользователей на группы. Пользователь с одним и тем же id всегда попадёт в одну и ту же корзину.

Например, можно обработать только одну корзину:

SELECT id, email
FROM users
WHERE MOD(id, 4) = 2
ORDER BY id;

Такой запрос выберет пользователей из корзины 2.

Этот приём часто используют для пакетной обработки: сегодня обработали корзину 0, потом 1, потом 2, потом 3.

Важная ловушка: отрицательные числа

С положительными числами всё обычно понятно. Но с отрицательными есть ловушка.

Во многих случаях результат MOD сохраняет знак первого аргумента, то есть делимого.

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

SELECT
  MOD(-10, 3) AS a,
  MOD(10, -3) AS b,
  MOD(-10, -3) AS c;

Результат:

a b c
-1 1 -1

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

Это не страшно, если вы проверяете делимость:

SELECT MOD(-10, 2) AS remainder;

Результат будет 0, потому что -10 делится на 2 без остатка.

Но это может быть проблемой, если вы используете MOD как номер корзины.

Например:

SELECT MOD(-10, 3) AS bucket;

Результат:

bucket
-1

А корзина -1 нам обычно не нужна. Если мы хотели получить одну из корзин 0, 1, 2, отрицательное значение ломает идею.

Как всегда получить неотрицательный остаток

Чтобы гарантированно получить остаток в диапазоне от 0 до n - 1, можно применить такой приём:

SELECT MOD(MOD(-10, 3) + 3, 3) AS bucket;

Результат:

bucket
2

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

сначала MOD(-10, 3) даёт -1.

Потом мы прибавляем делитель:

-1 + 3

Получается 2.

Потом ещё раз берём остаток от деления на 3. В итоге значение аккуратно попадает в диапазон 0..2.

В общем виде формула выглядит так:

SELECT MOD(MOD(value, n) + n, n) AS bucket
FROM numbers;

Она полезна, когда value может быть отрицательным, а вам нужен нормальный номер корзины.

Что будет с NULL

MOD не превращает NULL в ноль.

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

SELECT
  MOD(NULL, 3) AS a,
  MOD(10, NULL) AS b;

Результат:

a b
NULL NULL

Это стандартное поведение SQL: если значение неизвестно, результат вычисления тоже неизвестен.

Поэтому такой запрос не найдёт строки с NULL:

SELECT id, value
FROM numbers
WHERE MOD(value, 2) = 0;

Если value равен NULL, выражение MOD(value, 2) = 0 не станет истинным. Оно даст неизвестный результат, а WHERE пропускает только строки, где условие истинно.

Если нужно отдельно обработать NULL, добавьте явную проверку:

SELECT id, value
FROM numbers
WHERE value IS NULL OR MOD(value, 2) = 0;

MOD в разных СУБД

В PostgreSQL и MySQL обычно можно использовать оба варианта:

SELECT MOD(10, 3) AS a;

Или так:

SELECT 10 % 3 AS a;

Оба варианта возвращают остаток от деления.

В PostgreSQL оператор % работает с целыми числами и с типом numeric. Для дробных значений можно использовать MOD.

Например:

SELECT MOD(5.5, 2) AS remainder;

Результат:

remainder
1.5

В ClickHouse тоже есть оператор %, а ещё есть специальные функции.

Например, moduloOrZero возвращает 0, если делитель равен нулю:

SELECT moduloOrZero(10, 0) AS result;

А positiveModulo помогает получить неотрицательный остаток:

SELECT positiveModulo(-10, 3) AS bucket;

Результат:

bucket
2

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

Когда использовать MOD

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

Например:

  • проверить, чётный или нечётный id;
  • найти значения, которые делятся на число без остатка;
  • выбрать каждую десятую строку;
  • разложить пользователей по корзинам;
  • обрабатывать данные частями;
  • распределить пользователей по шардам;
  • включать фичу только для части аудитории.

Главное — помнить три правила.

Первое: если остаток равен 0, значит число делится нацело.

Второе: деление на ноль вызовет ошибку.

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

Короткий пример из жизни

Допустим, у нас есть таблица пользователей, и мы хотим включить новую фичу только для четверти аудитории.

Можно выбрать одну корзину из четырёх:

SELECT id, email
FROM users
WHERE MOD(id, 4) = 0
ORDER BY id;

Если пользователей много и id распределены равномерно, это даст примерно 25% пользователей.

Потом можно включить ещё одну корзину:

SELECT id, email
FROM users
WHERE MOD(id, 4) IN (0, 1)
ORDER BY id;

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

А потом можно включить все корзины:

SELECT id, email
FROM users
WHERE MOD(id, 4) IN (0, 1, 2, 3)
ORDER BY id;

Так MOD превращается из простой математической функции в удобный инструмент для управляемого запуска изменений.

Главное из статьи

MOD(a, b) возвращает остаток от деления a на b.

Если результат равен 0, значит a делится на b нацело.

Для проверки чётности используют деление на 2:

SELECT id
FROM orders
WHERE MOD(id, 2) = 0;

Для выборки каждой N-й строки можно использовать деление на N:

SELECT id, email
FROM users
WHERE MOD(id, 10) = 0;

Для корзин и шардов удобно брать остаток от количества корзин:

SELECT MOD(id, 4) AS bucket
FROM users;

Но если исходное значение может быть отрицательным, обычный MOD может вернуть отрицательный остаток. Тогда результат лучше нормализовать:

SELECT MOD(MOD(value, n) + n, n) AS bucket
FROM numbers;

MOD — маленькая функция, но очень практичная. Она помогает SQL-запросам отвечать не только на вопрос «сколько?», но и на вопрос «в какую группу это попадает?».

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

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

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