Denne artikel er i øjeblikket på russisk — den engelske oversættelse er undervejs.
RANDOM() в PostgreSQL возвращает псевдослучайное число с плавающей точкой в диапазоне [0, 1) — и на этой крошечной функции держится практически любой «случайный» запрос. К ней приходишь, когда нужно вытащить пятёрку пользователей на ручную проверку, развести аудиторию по A/B-группам, перемешать строки или подмешать в демо немного «живых» данных. Разберём, как выжать из неё максимум и где она внезапно превращается в тормоз на ровном месте.
Базовое значение и случайные строки
В PostgreSQL RANDOM() при каждом вызове отдаёт свежее число от 0 включительно и до 1, но саму единицу уже не достаёт:
SELECT random();
SELECT random() AS r1, random() AS r2;
Обратите внимание: два вызова в одной строке дают два разных числа — функция пересчитывается на каждое вхождение. Отсюда и классический трюк: чтобы выдернуть случайные строки, сортируем по 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: он читает случайные страницы, а не перелопачивает всё подряд.
SELECT id, amount, status
FROM orders TABLESAMPLE BERNOULLI (5);
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):
SELECT floor(random() * 100)::int AS n;
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;
После 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 безо всякой сортировки.
SELECT id, email FROM users ORDER BY RAND(42) LIMIT 5;
SELECT id, amount FROM orders SAMPLE 0.1;
Итог короткий. ORDER BY RANDOM() — честный, читаемый и абсолютно корректный приём, который рассыпается на масштабе: он платит полным сканом за каждую выборку. Пока таблица помещается в голове — пользуйтесь смело. Как только речь о миллионах строк — переходите на страничный сэмплинг (TABLESAMPLE, SAMPLE), а setseed и RAND(seed) держите под рукой там, где результат обязан повторяться.
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 синтаксис расходится, и переносить запросы вслепую не выйдет:
RAND(), диапазон тот же —[0, 1). Сэмплинг делается черезORDER BY RAND() LIMIT 5;TABLESAMPLEздесь нет. Сид передаётся прямо в аргумент:RAND(42).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)держите под рукой там, где результат обязан повторяться.