sqlpostgresqlrandomsampling

RANDOM в PostgreSQL: как получать случайные строки, числа и выборки без боли

RANDOM() возвращает дробь из [0, 1): как тянуть случайные строки, почему ORDER BY RANDOM() тормозит на масштабе и когда брать TABLESAMPLE, floor и setseed.

10 мин чтенияСправочникsql · postgresql · random · sampling · mysql

RANDOM() в PostgreSQL возвращает псевдослучайное число с плавающей точкой в диапазоне от 0 включительно до 1 не включительно.

Проще говоря, это дробь вида 0.8473920192, 0.15412003, 0.9991 — каждый раз новая. На такой маленькой функции держится много полезных вещей: случайная выборка пользователей, перемешивание строк, A/B-группы, демоданные, тестовые сценарии и простые «лотереи» внутри SQL.

Например, вы хотите взять пять случайных пользователей на ручную проверку. Или случайно распределить заказы по трём обработчикам. Или показать на витрине товары в разном порядке, чтобы страница не выглядела одинаково каждый раз. Во всех этих задачах рядом почти всегда появляется random().

Разберёмся спокойно: как она работает, как получать случайные строки и числа, почему ORDER BY random() иногда становится дорогим, чем помогает TABLESAMPLE и как сделать случайность повторяемой для тестов.

Базовый пример: что возвращает RANDOM

Самый простой запрос:

SELECT random();

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

0.8473920192

Точное число каждый раз будет другим. Диапазон у функции такой:

[0, 1)

Это значит:

  • 0 теоретически возможно;
  • 1 уже не входит в диапазон;
  • все значения находятся где-то между 0 и 0.999999....

Важно: каждый вызов random() считается отдельно.

SELECT random() AS r1,
       random() AS r2;

Даже в одной строке r1 и r2 почти всегда будут разными, потому что это два разных вызова функции.

Эта деталь кажется мелкой, но на ней строятся почти все приёмы со случайной сортировкой.

Как выбрать случайные строки из таблицы

Классический способ получить несколько случайных строк — отсортировать таблицу по random() и взять первые строки через LIMIT.

SELECT id, email, country
FROM users
ORDER BY random()
LIMIT 5;

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

  1. PostgreSQL берёт строки из таблицы users.
  2. Для каждой строки вычисляет своё случайное число.
  3. Сортирует строки по этим случайным числам.
  4. Возвращает первые 5 строк.

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

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

Можно перемешать всю таблицу целиком:

SELECT id, email, country
FROM users
ORDER BY random();

А можно использовать этот приём для случайной проверки заказов:

SELECT id, customer_id, amount, status
FROM orders
WHERE status = 'paid'
ORDER BY random()
LIMIT 10;

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

Почему ORDER BY RANDOM может быть дорогим

На маленьких таблицах ORDER BY random() обычно работает быстро и не доставляет проблем. Но на больших данных у этого способа есть неприятная цена.

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

SELECT id, email
FROM users
ORDER BY random()
LIMIT 5;

Кажется, что раз нужен только LIMIT 5, база должна быстро найти пять строк и остановиться. Но нет.

Чтобы честно отсортировать строки по случайному значению, PostgreSQL должен:

  1. пройти по большому количеству строк;
  2. для каждой строки вычислить random();
  3. отсортировать результат;
  4. только потом взять первые 5.

То есть стоимость запроса зависит не только от LIMIT, а от размера таблицы.

Это главный подвох. Вы просите пять строк, но база всё равно делает большую работу.

Индекс здесь обычно не спасает, потому что значение random() каждый раз новое. Нельзя заранее построить полезный индекс по числу, которое пересчитывается при каждом запуске запроса.

На большой таблице такой запрос может превратиться в полный проход по данным и тяжёлую сортировку. В EXPLAIN ANALYZE вы часто увидите что-то вроде полного чтения таблицы и узла сортировки. Это сигнал: для маленькой таблицы нормально, для огромной — уже опасно.

Когда ORDER BY RANDOM подходит

ORDER BY random() — не плохой приём. Он просто не универсальный.

Его удобно использовать, когда:

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

Например, для таблицы из нескольких тысяч строк это вполне нормальный вариант:

SELECT id, title
FROM lessons
ORDER BY random()
LIMIT 3;

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

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

TABLESAMPLE: быстрый способ взять примерную выборку

В PostgreSQL есть конструкция TABLESAMPLE. Она позволяет взять случайную часть таблицы без полной сортировки результата.

Пример:

SELECT id, amount, status
FROM orders TABLESAMPLE SYSTEM (1);

Здесь SYSTEM (1) означает примерно 1% таблицы.

Есть два распространённых метода:

SELECT id, amount, status
FROM orders TABLESAMPLE BERNOULLI (5);
SELECT id, amount, status
FROM orders TABLESAMPLE SYSTEM (5);

Разница между ними важная.

BERNOULLI рассматривает строки более независимо: у каждой строки есть шанс попасть в выборку. Это ближе к «честной» случайной выборке, но может быть дороже.

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

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

TABLESAMPLE не гарантирует точное число строк

Важный момент: TABLESAMPLE возвращает примерный процент, а не точное количество строк.

Вот запрос:

SELECT id, email
FROM users TABLESAMPLE SYSTEM (10);

Он не означает «верни ровно десять строк». Он означает «возьми примерно 10% таблицы по выбранному методу сэмплинга».

Поэтому результат может быть разным:

  • в одном запуске вернулось 958 строк;
  • в другом 1031;
  • в третьем 990.

Это нормально.

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

SELECT id, email
FROM users TABLESAMPLE SYSTEM (10)
ORDER BY random()
LIMIT 5;

Идея такая:

  1. Сначала TABLESAMPLE SYSTEM (10) быстро берёт примерную часть таблицы.
  2. Потом ORDER BY random() сортирует уже не всю таблицу, а только эту меньшую выборку.
  3. LIMIT 5 оставляет ровно пять строк.

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

Как получить случайное целое число

random() возвращает дробь. Но в реальных задачах часто нужно целое число.

Например:

  • случайный номер от 1 до 6, как бросок кубика;
  • случайная скидка;
  • случайный бакет;
  • случайный номер группы.

Базовая формула такая:

SELECT floor(random() * 100)::int AS n;

Этот запрос вернёт целое число от 0 до 99.

Почему именно так?

random() даёт число от 0 до почти 1.

Умножаем на 100:

0 <= random() * 100 < 100

Получаем число от 0 до 99.999....

Затем floor() отбрасывает дробную часть:

0, 1, 2, 3, ..., 99

В итоге получается диапазон:

[0, 100)

То есть 0 включается, 100 не включается.

Пример с кубиком

Чтобы получить число от 1 до 6, используем такую формулу:

SELECT floor(random() * 6)::int + 1 AS dice;

Разбор:

random() * 6

даёт число от 0 до почти 6.

floor(random() * 6)::int

даёт целое число от 0 до 5.

floor(random() * 6)::int + 1

даёт целое число от 1 до 6.

То есть получается обычный шестигранный кубик.

Случайное распределение по группам

Допустим, у нас есть сотрудники, и мы хотим случайно разложить их по трём группам: 0, 1, 2.

SELECT id,
       name,
       dept,
       floor(random() * 3)::int AS bucket
FROM employees;

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

id | name  | dept    | bucket
---+-------+---------+--------
1  | Alex  | sales   | 2
2  | Maria | support | 0
3  | Ivan  | qa      | 1
4  | Nina  | sales   | 2

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

Такой подход удобно использовать для демоданных или разовых случайных распределений. Но если вы делаете серьёзный A/B-тест, где пользователь должен всегда попадать в одну и ту же группу, лучше использовать детерминированный способ: например, хеш от user_id.

Иначе сегодня пользователь попадёт в группу A, завтра в B, а послезавтра снова в A. Для эксперимента это плохо.

Почему лучше не использовать ROUND для случайных целых

Иногда хочется написать так:

SELECT round(random() * 6)::int AS n;

Но для равномерного случайного целого числа это плохая идея.

Причина в том, что round() округляет к ближайшему целому. Из-за этого крайние значения получают меньше шансов.

Например, если вы делаете:

SELECT round(random() * 6)::int AS n;

то результат может быть от 0 до 6. Но числа 0 и 6 выпадают реже, чем 1, 2, 3, 4, 5.

Почему?

Для 0 подходит только маленький кусок диапазона: примерно от 0 до 0.5.

Для 1 подходит уже больший кусок: примерно от 0.5 до 1.5.

То есть середина получает больше места, а края — меньше.

Если вам нужно равномерное распределение, используйте floor():

SELECT floor(random() * 6)::int + 1 AS n;

Это привычная и правильная формула.

Случайные значения в INSERT

random() удобно использовать, когда нужно быстро накидать демоданные.

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

INSERT INTO payments (user_id, amount)
SELECT id,
       floor(random() * 9000)::int + 1000 AS amount
FROM users
LIMIT 20;

Здесь сумма будет от 1000 до 9999.

Разбор формулы:

floor(random() * 9000)::int

даёт число от 0 до 8999.

floor(random() * 9000)::int + 1000

даёт число от 1000 до 9999.

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

Случайное обновление строк

random() можно использовать и в UPDATE.

Например, назначим пользователям случайный рейтинг от 1 до 5:

UPDATE users
SET rating = floor(random() * 5)::int + 1;

Каждая строка получит своё значение, потому что random() будет вызван для каждой обновляемой строки.

Можно обновить не всех, а только часть:

UPDATE users
SET rating = floor(random() * 5)::int + 1
WHERE country = 'BR';

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

Но с обновлениями будьте аккуратны: случайность легко испортит данные, если забыть WHERE. Перед настоящим UPDATE лучше сначала проверить выборку через SELECT.

Повторяемая случайность через SETSEED

Случайность хороша, пока вам не нужны стабильные тесты.

Представьте, что у вас есть автотест. Он запускает запрос со случайной сортировкой и проверяет результат. Сегодня тест прошёл, завтра упал, потому что строки пришли в другом порядке. Неприятно.

Для таких случаев в PostgreSQL есть setseed().

SELECT setseed(0.42);

SELECT id
FROM users
ORDER BY random()
LIMIT 3;

setseed() задаёт начальное состояние генератора случайных чисел для текущей сессии. После этого последовательность random() становится воспроизводимой.

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

SELECT setseed(0.42);

SELECT random() AS r1,
       random() AS r2,
       random() AS r3;

Если повторить это в той же среде, последовательность будет одинаковой.

Важные ограничения SETSEED

У setseed() есть несколько важных деталей.

Во-первых, значение должно быть в диапазоне от -1 до 1.

SELECT setseed(0.7);

Так можно.

SELECT setseed(42);

Так нельзя: 42 выходит за допустимый диапазон.

Во-вторых, сид действует только в рамках текущего соединения с базой. Если вы открыли новое подключение, оно не обязано помнить старый setseed().

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

RANDOM и A/B-тесты

Для простого одноразового распределения можно написать так:

SELECT id,
       email,
       CASE
           WHEN random() < 0.5 THEN 'A'
           ELSE 'B'
       END AS test_group
FROM users;

Запрос случайно разложит пользователей на две группы.

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

Сегодня пользователь может попасть в A, а завтра в B. Для настоящего A/B-теста это обычно неправильно. Пользователь должен оставаться в своей группе, иначе результаты эксперимента смешаются.

Для реального A/B-теста лучше один раз записать группу в таблицу:

UPDATE users
SET test_group = CASE
    WHEN random() < 0.5 THEN 'A'
    ELSE 'B'
END
WHERE test_group IS NULL;

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

MySQL: RAND вместо RANDOM

В MySQL похожая функция называется RAND().

SELECT RAND();

Диапазон такой же: от 0 включительно до 1 не включительно.

Случайные строки выбирают похожим способом:

SELECT id, email
FROM users
ORDER BY RAND()
LIMIT 5;

И проблема та же: на большой таблице ORDER BY RAND() может быть дорогим, потому что базе нужно вычислить случайное значение для строк и отсортировать результат.

Для повторяемой случайности в MySQL можно передать сид прямо в функцию:

SELECT RAND(42);

А пример со случайной выборкой может выглядеть так:

SELECT id, email
FROM users
ORDER BY RAND(42)
LIMIT 5;

Но при переносе запросов между PostgreSQL и MySQL важно помнить: в PostgreSQL функция называется random(), а в MySQL — RAND().

ClickHouse: rand возвращает целое число

В ClickHouse тоже есть случайные функции, но поведение отличается.

Функция rand() возвращает не дробь от 0 до 1, а целое число типа UInt32.

SELECT rand();

Если нужно получить значение примерно в диапазоне от 0 до 1, можно разделить результат:

SELECT rand() / 4294967295.0 AS r;

Для случайной выборки в ClickHouse часто используют нативный SAMPLE, если таблица это поддерживает:

SELECT id, amount
FROM orders
SAMPLE 0.1;

Это означает выборку примерно 10% данных. Такой подход обычно лучше, чем сортировать всё по случайному значению.

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

random() часто применяют не ради «математики», а ради обычных рабочих задач.

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

SELECT id, email
FROM users
ORDER BY random()
LIMIT 20;

Случайно перемешать список:

SELECT id, title
FROM articles
ORDER BY random();

Сгенерировать случайную скидку от 5 до 30:

SELECT floor(random() * 26)::int + 5 AS discount_percent;

Разложить строки по десяти бакетам:

SELECT id,
       floor(random() * 10)::int AS bucket
FROM events;

Взять примерную выборку из большой таблицы:

SELECT id, created_at, amount
FROM orders TABLESAMPLE SYSTEM (2);

Получить ровно несколько строк из предварительной выборки:

SELECT id, created_at, amount
FROM orders TABLESAMPLE SYSTEM (5)
ORDER BY random()
LIMIT 10;

Главное, что нужно запомнить

random() в PostgreSQL возвращает псевдослучайную дробь в диапазоне от 0 включительно до 1 не включительно.

Для случайных строк самый понятный способ — ORDER BY random() LIMIT n.

SELECT id, email
FROM users
ORDER BY random()
LIMIT 5;

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

Для больших данных смотрите в сторону TABLESAMPLE, особенно если вам подходит примерная выборка.

SELECT id, amount
FROM orders TABLESAMPLE SYSTEM (1);

Для случайных целых чисел используйте floor(), а не round().

SELECT floor(random() * 6)::int + 1 AS dice;

Для повторяемых тестов используйте setseed().

SELECT setseed(0.42);

А при переносе между базами не забывайте про различия: в PostgreSQL — random(), в MySQL — RAND(), в ClickHouse — rand() и отдельный механизм SAMPLE.

Главная мысль простая: random() отлично подходит для понятной случайности в SQL, но на больших таблицах случайность тоже нужно проектировать аккуратно. Чем больше данных, тем важнее не только «получить случайно», но и не заставить базу сортировать весь мир ради пяти строк.

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

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

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