sqlpostgresqlrandomsampling

RANDOM() in SQL: Random Sampling, Integers, and Seeding

How RANDOM() works in PostgreSQL, why ORDER BY RANDOM() is slow at scale, and what to use instead.

3 min qariReferencesql · postgresql · random · sampling · mysql
Dan l-artiklu bħalissa huwa bir-Russu — it-traduzzjoni bl-Ingliż għaddejja.

RANDOM() в PostgreSQL возвращает псевдослучайное число с плавающей точкой в диапазоне [0, 1) — и на этой крошечной функции держится практически любой «случайный» запрос. К ней приходишь, когда нужно вытащить пятёрку пользователей на ручную проверку, развести аудиторию по A/B-группам, перемешать строки или подмешать в демо немного «живых» данных. Разберём, как выжать из неё максимум и где она внезапно превращается в тормоз на ровном месте.

Базовое значение и случайные строки

В PostgreSQL RANDOM() при каждом вызове отдаёт свежее число от 0 включительно и до 1, но саму единицу уже не достаёт:

SELECT random();              -- e.g. 0.8473920192
SELECT random() AS r1, random() AS r2;  -- two different values

Обратите внимание: два вызова в одной строке дают два разных числа — функция пересчитывается на каждое вхождение. Отсюда и классический трюк: чтобы выдернуть случайные строки, сортируем по RANDOM() и снимаем верхушку.

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

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

Почему ORDER BY RANDOM() дорог

Вся красота держится ровно до тех пор, пока таблица маленькая. Подвох — в цене. Чтобы отсортировать по RANDOM(), движок обязан присвоить случайное число каждой строке и отсортировать таблицу целиком, и только потом отрезать LIMIT. На миллионах строк это оборачивается полным сканом и сортировкой, которая запросто уезжает на диск.

  • Индекс тут бессилен: ключ свежий на каждый вызов, кэшировать нечего.
  • Стоимость растёт с размером таблицы, а не с LIMIT — хоть пять строк просите, хоть пятьсот.
  • EXPLAIN ANALYZE на таком запросе покажет узел Sort поверх полного Seq Scan — это и есть красный флаг на больших данных.

Для тяжёлых таблиц берите TABLESAMPLE: он читает случайные страницы, а не перелопачивает всё подряд.

-- roughly 5% of physical pages, very cheap
SELECT id, amount, status
FROM orders TABLESAMPLE BERNOULLI (5);

-- block sampling: even faster, coarser
SELECT * FROM orders TABLESAMPLE SYSTEM (1);

А вот и грабли. TABLESAMPLE возвращает примерный процент, а не ровное число строк, и сэмплит постранично — значит, строки внутри одной страницы коррелируют, и идеально равномерного распределения вы не получите. Когда нужно строго N строк, складывайте оба подхода: грубо отбираем горстку страниц, а потом доводим до точного количества обычным ORDER BY random().

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

Случайные целые числа

RANDOM() отдаёт дробь, но в жизни чаще нужен int в заданном диапазоне. Рабочая формула — floor(random() * n), она кроет полуинтервал [0, n):

-- integer in [0, 100)
SELECT floor(random() * 100)::int AS n;

-- integer in [1, 6] like a dice roll
SELECT floor(random() * 6)::int + 1 AS dice;

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

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

Ещё одни грабли, и на них наступают регулярно: не пишите round(random() * n), если хотите равномерное целое. round округляет к ближайшему, и крайние значения (0 и n) ловят лишь половину интервала каждое — то есть выпадают вдвое реже середины. Перекос едва заметен, но он есть. Берите floor — всегда.

Воспроизводимость через setseed

Случайность отлично работает в продакшене и отвратительно — в тестах: результат скачет на каждом прогоне, и assert'ы плывут вместе с ним. Лекарство — setseed(). Он фиксирует генератор на текущую сессию, принимая число строго из [-1, 1]:

SELECT setseed(0.42);
SELECT id FROM users ORDER BY random() LIMIT 3;  -- same order on replay

После setseed последовательность RANDOM() становится детерминированной — вплоть до следующего setseed или конца сессии. Ровно то, что нужно для повторяемых тестов и предсказуемых демо. Пара оговорок: значение сида обязано лежать в [-1, 1], иначе PostgreSQL швырнёт ошибку, а сама привязка живёт только внутри текущего соединения — новый коннект про ваш сид ничего не знает.

MySQL и ClickHouse

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

  • MySQL зовёт функцию RAND(), диапазон тот же — [0, 1). Сэмплинг делается через ORDER BY RAND() LIMIT 5; TABLESAMPLE здесь нет. Сид передаётся прямо в аргумент: RAND(42).
  • ClickHouse выдаёт rand(), но это UInt32 из [0, 2^32), а вовсе не дробь. Хотите [0, 1) — делите на 4294967295.0. Зато для сэмплинга есть нативный SELECT ... SAMPLE 0.1 безо всякой сортировки.
-- MySQL: random rows + reproducible seed
SELECT id, email FROM users ORDER BY RAND(42) LIMIT 5;

-- ClickHouse: native sampling, no full sort
SELECT id, amount FROM orders SAMPLE 0.1;

Итог короткий. ORDER BY RANDOM() — честный, читаемый и абсолютно корректный приём, который рассыпается на масштабе: он платит полным сканом за каждую выборку. Пока таблица помещается в голове — пользуйтесь смело. Как только речь о миллионах строк — переходите на страничный сэмплинг (TABLESAMPLE, SAMPLE), а setseed и RAND(seed) держите под рукой там, где результат обязан повторяться.

Ipprattika fuq eżerċizzji reali

Solvi eżerċizzji fit-taħriġ tal-SQL b'valutazzjoni u għajnuniet istantanji.

Iftaħ it-taħriġ