быстрый случайный выбор строки в Postgres

98

У меня есть таблица в postgres, содержащая пару миллионов строк. Я проверил в Интернете и нашел следующие

SELECT myid FROM mytable ORDER BY RANDOM() LIMIT 1;

Это работает, но очень медленно ... есть ли другой способ сделать этот запрос или прямой способ выбрать случайную строку, не читая всю таблицу? Кстати, myid - это целое число, но это может быть пустое поле.

Хуан
источник
1
Если вы хотите выбрать несколько случайных строк, см. Этот вопрос: stackoverflow.com/q/8674718/247696
Flimm

Ответы:

99

Вы можете поэкспериментировать OFFSET, как в

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

Это Nколичество строк в mytable. Возможно, вам сначала потребуется выполнить a, SELECT COUNT(*)чтобы определить значение N.

Обновление (Энтони Хэтчкинс)

Вы должны использовать floorздесь:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

Рассмотрим таблицу из 2-х строк; random()*Nгенерирует 0 <= x < 2и, например, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;возвращает 0 строк из-за неявного округления до ближайшего int.

NPE
источник
имеет смысл использовать N меньше SELECT COUNT(*)?, то есть использовать не все значения в таблице, а только их часть?
Хуан,
@Juan Это зависит от ваших требований.
NPE
использование EXPLAIN SELECT ...с разными значениями N дает одинаковую стоимость для запроса, тогда, я думаю, лучше выбрать максимальное значение N.
Хуан
3
см. исправление в моем ответе ниже
Энтони Хэтчкинс
2
Это отключено на одну ошибку. Он никогда не вернет первую строку и сгенерирует ошибку 1 / COUNT (*), потому что он попытается вернуть строку после последней строки.
Ян
62

PostgreSQL 9.5 представил новый подход для более быстрого выбора образцов: TABLESAMPLE

Синтаксис

SELECT * FROM my_table TABLESAMPLE BERNOULLI(percentage);
SELECT * FROM my_table TABLESAMPLE SYSTEM(percentage);

Это не оптимальное решение, если вы хотите выбрать только одну строку, потому что вам нужно знать КОЛИЧЕСТВО таблицы, чтобы рассчитать точный процент.

Чтобы избежать медленного COUNT и использовать быстрый TABLESAMPLE для таблиц от 1 до миллиардов строк, вы можете:

 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.000001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.00001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.0001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.001) LIMIT 1;
 ...

Это может выглядеть не так элегантно, но, вероятно, быстрее, чем любой другой ответ.

Чтобы решить, хотите ли вы использовать BERNULLI или SYSTEM, прочтите о различиях на http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/

Альфонкс
источник
2
Это намного быстрее и проще, чем любой другой ответ - этот должен быть вверху.
Хайден Шифф
1
Почему нельзя просто использовать подзапрос для подсчета? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
machineghost
2
@machineghost "Чтобы избежать медленного СЧЕТА ..." ... Если ваши данные настолько малы, что вы можете посчитать за разумное время, дерзайте! :-)
alfonx
2
@machineghost Используйте SELECT reltuples FROM pg_class WHERE relname = 'my_table'для подсчета количества.
Гинек-Пичи-Выходил
@ Hynek-Pichi-Vychodil очень хороший вклад! Чтобы гарантировать, что оценка не устарела, она должна быть недавно проанализирована ВАКУУМОМ .. но хорошая база данных должна быть должным образом проанализирована в любом случае .. И все это зависит от конкретного варианта использования. Обычно огромные столы не растут так быстро ... Спасибо!
alfonx
34

Я попробовал это с помощью подзапроса, и он работал нормально. Смещение, по крайней мере, в Postgresql v8.4.4 работает нормально.

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;
Джон Кориат
источник
Фактически, v8.4 необходима для того, чтобы это работало, не работает для <= 8.3.
Энтони Хэтчкинс,
1
см. исправление в моем ответе ниже
Энтони Хэтчкинс,
32

Вам необходимо использовать floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;
Энтони Хэтчкинс
источник
Рассмотрим таблицу из 2-х строк; random()*Nгенерирует 0 <= x <2 и, например, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;возвращает 0 строк из-за неявного округления до ближайшего int.
Энтони Хэтчкинс,
К сожалению, это не сработает, если вы хотите использовать более высокий LIMIT ... Мне нужно получить 3 элемента, поэтому мне нужно использовать синтаксис ORDER BY RANDOM ().
Alexis Wilke
1
Три последовательных запроса все равно будут быстрее, чем один order by random(), примерно как 3*O(N) < O(NlogN)- реальные цифры будут немного отличаться из-за индексов.
Энтони Хэтчкинс,
Моя проблема в том, что 3 элемента должны быть разными, WHERE myid NOT IN (1st-myid)и WHERE myid NOT IN (1st-myid, 2nd-myid)они не будут работать, поскольку решение принимается с помощью OFFSET. Хммм ... Думаю, я мог бы уменьшить N на 1 и 2 во втором и третьем SELECT.
Alexis Wilke
Не могли бы вы или кто-нибудь расширить этот ответ, объяснив, почему мне нужно его использовать floor()? Какие преимущества это дает?
ADTC
14

Перейдите по этой ссылке, чтобы узнать о различных вариантах. http://www.depesz.com/index.php/2007/09/16/my-oughtts-on-getting-random-row/

Обновить: (А.Хэтчкинс)

Резюме (очень) длинной статьи таково.

Автор перечисляет четыре подхода:

1) ORDER BY random() LIMIT 1; - медленный

2) ORDER BY id where id>=random()*N LIMIT 1 - неравномерно, если есть зазоры

3) случайный столбец - необходимо время от времени обновлять

4) произвольный произвольный агрегат - хитрый метод, может быть медленным: random () нужно сгенерировать N раз

и предлагает улучшить метод №2, используя

5) ORDER BY id where id=random()*N LIMIT 1 с последующими запросами, если результат пуст.

Kuberchaun
источник
Интересно, почему они не накрыли OFFSET? Использование ORDER недопустимо, чтобы получить случайную строку. К счастью, смещение хорошо освещено в ответах.
androidguy
4

Самый простой и быстрый способ получить случайную строку - использовать tsm_system_rowsрасширение:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Затем вы можете выбрать точное количество строк, которое хотите:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Это доступно в PostgreSQL 9.5 и новее.

См. Https://www.postgresql.org/docs/current/static/tsm-system-rows.html

Daamien
источник
1
Честное предупреждение, это не совсем случайно. На меньших таблицах он всегда возвращал первые строки по порядку.
Бен Обин
1
да, это четко объяснено в документации (ссылка выше): «Подобно встроенному методу выборки SYSTEM, SYSTEM_ROWS выполняет выборку на уровне блоков, поэтому выборка не является полностью случайной, но может подвергаться эффектам кластеризации, особенно если только небольшой количество строк запрашивается. ». Если у вас небольшой набор данных, он ORDER BY random() LIMIT 1;должен работать достаточно быстро.
daamien
Я видел это. Просто хотел дать понять всем, кто не нажимает на ссылку, или если ссылка умрет в будущем.
Бен Обин
1
Также стоит отметить, что это будет работать только для выбора случайных строк из таблицы и фильтрации THEN, в отличие от выполнения запроса и последующего выбора одной или нескольких записей наугад.
номен
3

Я придумал очень быстрое решение без TABLESAMPLE. Намного быстрее, чем OFFSET random()*N LIMIT 1. Это даже не требует подсчета таблиц.

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

Вот тест с образцами данных 1M строк:

create table randtest (id serial primary key, data int not null);

insert into randtest (data) select (random()*1000000)::int from generate_series(1,1000000);

create index randtest_md5_id_idx on randtest (md5(id::text));

explain analyze
select * from randtest where md5(id::text)>md5(random()::text)
order by md5(id::text) limit 1;

Результат:

 Limit  (cost=0.42..0.68 rows=1 width=8) (actual time=6.219..6.220 rows=1 loops=1)
   ->  Index Scan using randtest_md5_id_idx on randtest  (cost=0.42..84040.42 rows=333333 width=8) (actual time=6.217..6.217 rows=1 loops=1)
         Filter: (md5((id)::text) > md5((random())::text))
         Rows Removed by Filter: 1831
 Total runtime: 6.245 ms

Этот запрос может иногда (с вероятностью примерно 1 / Number_of_rows) возвращать 0 строк, поэтому его необходимо проверить и запустить повторно. Кроме того, вероятности не совсем одинаковы - некоторые строки более вероятны, чем другие.

Для сравнения:

explain analyze SELECT id FROM randtest OFFSET random()*1000000 LIMIT 1;

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

 Limit  (cost=1442.50..1442.51 rows=1 width=4) (actual time=179.183..179.184 rows=1 loops=1)
   ->  Seq Scan on randtest  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.016..134.835 rows=915702 loops=1)
 Total runtime: 179.211 ms
(3 rows)
Томецкий
источник
2
Быстро, да. Действительно случайно, нет. Значения md5, которые являются следующим большим значением после другого существующего значения, имеют очень небольшой шанс быть выбранным, в то время как значения после большого разрыва в числовом пространстве имеют гораздо больший шанс (больше на количество возможных значений между ними) . Полученное распределение не случайно.
Эрвин Брандштеттер,
очень интересно, может ли это сработать в случае использования запроса, подобного лотерее: запрос должен просмотреть все доступные билеты и случайным образом вернуть только ОДИН билет. также могу ли я использовать пессимистическую блокировку (выберите ... для обновления) с вашей техникой?
Mathieu
Для всего, что связано с лотереей, вам действительно следует использовать справедливую и криптографически безопасную случайную выборку - например, выбирать случайное число от 1 до max (id), пока не найдете существующий идентификатор. Метод из этого ответа не является ни справедливым, ни безопасным - он быстрый. Используется для таких вещей, как «получить случайный 1% строк для проверки чего-либо» или «показать 5 случайных записей».
Tometzky