швидкий вибір випадкових рядків у Postgres


98

У мене є таблиця в postgres, яка містить пару мільйонів рядків. Я перевірив в Інтернеті і виявив наступне

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

Це працює, але це дуже повільно ... чи існує інший спосіб зробити цей запит, або прямий спосіб вибрати випадковий рядок, не читаючи всю таблицю? До речі, 'myid' - це ціле число, але це може бути порожнє поле.


1
Якщо ви хочете вибрати кілька випадкових рядки, побачити це питання: stackoverflow.com/q/8674718/247696
Флімм

Відповіді:


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.


має сенс використовувати N менше, ніж SELECT COUNT(*)?, я маю на увазі не використовувати всі значення в таблиці, а лише їх частину?
Хуан

@Juan Це залежить від ваших вимог.
NPE

використання EXPLAIN SELECT ...значень N з різними значеннями дає однакову вартість запиту, тоді, мабуть, краще вибрати максимальне значення N.
Хуан

3
див. виправлення в моїй відповіді нижче
Antony Hatchkins

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 oder, прочитайте про різницю на веб-сайті 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 "Щоб уникнути повільної COUNT ..." ... Якщо ваші дані настільки малі, що ви можете порахувати за розумний час, сміливо! :-)
alfonx

2
@machineghost Використовувати SELECT reltuples FROM pg_class WHERE relname = 'my_table'для оцінки підрахунку.
Hynek -Pichi- Vychodil

@ Hynek-Pichi-Vychodil дуже хороший вхід! Щоб гарантувати, що оцінка не застаріла, її потрібно недавно ВАКУУМАТИЧНО АНАЛІЗУВАТИ, але хорошу базу даних слід аналізувати належним чином. І все залежить від конкретного випадку використання. Зазвичай величезні столи ростуть не так швидко ... Дякую!
альфонкс

34

Я спробував це з підзапитом, і він спрацював нормально. Зсув, принаймні в Postgresql v8.4.4 працює нормально.

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;

Насправді v8.4 необхідний для того, щоб це працювало, але не працює для <= 8.3.
Antony Hatchkins

1
див. виправлення в моїй відповіді нижче
Antony Hatchkins

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.
Antony Hatchkins

На жаль, це не спрацьовує, якщо ви хочете використати більш високий LIMIT ... Мені потрібно отримати 3 елементи, тому мені потрібно використовувати синтаксис ORDER BY RANDOM ().
Alexis Wilke

1
Три послідовних запити все одно будуть швидшими, ніж один order by random(), приблизно так само 3*O(N) < O(NlogN)- показники реального життя будуть дещо відрізнятися через індекси.
Antony Hatchkins

Моя проблема полягає в тому , що 3 елементів повинні бути різні і WHERE myid NOT IN (1st-myid)і WHERE myid NOT IN (1st-myid, 2nd-myid)не працюватиме , тому що рішення прийнято зміщеним. Хм-м-м-м ... Я думаю, я міг би зменшити N на 1 і 2 у другому та третьому SELECT.
Alexis Wilke

Чи можете ви або хтось розширити цю відповідь, відповівши на питання, чому мені потрібно використовувати floor()? Яку перевагу він пропонує?
ADTC

14

Перегляньте це посилання, щоб переглянути різні варіанти. http://www.depesz.com/index.php/2007/09/16/my-oughts-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 з подальшими запитами, якщо результат порожній.


Цікаво, чому вони не покрили OFFSET? Про використання ЗАМОВЛЕННЯ не може бути й мови, щоб отримати випадковий рядок. На щастя, OFFSET добре висвітлено у відповідях.
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


1
Чесне попередження, це не зовсім випадково. У менших таблицях у мене завжди було повернення перших рядків по порядку.
Бен Обен,

1
так, це чітко пояснюється в документації (посилання вище): «Як і вбудований метод вибірки SYSTEM, SYSTEM_ROWS виконує вибірку на рівні блоку, так що вибірка не є повністю випадковою, але може зазнавати ефектів кластеризації, особливо якщо лише невеликий кількість рядків. ». Якщо у вас невеликий набір даних, він ORDER BY random() LIMIT 1;повинен бути досить швидким.
daamien

Я побачив, що. Просто хотів дати зрозуміти кожному, хто не натискає посилання або якщо посилання в майбутньому загине.
Бен Обен,

1
Також варто зазначити, що це буде працювати лише для виділення випадкових рядків із таблиці та ПОТІМ фільтрації, на відміну від порівняння із запуском запиту та вибором одного чи кількох записів навмання.
номен

3

Я придумав дуже швидке рішення без TABLESAMPLE. Набагато швидше, ніж OFFSET random()*N LIMIT 1. Це навіть не вимагає підрахунку таблиць.

Ідея полягає в тому, щоб створити індекс виразу, наприклад, із випадковими, але передбачуваними даними md5(primary key).

Ось тест із 1М рядками зразків даних:

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, що є наступним великим значенням після іншого існуючого значення, має дуже малий шанс бути вибраним, тоді як значення після великого розриву в просторі чисел мають набагато більший шанс (більше на кількість можливих значень між ними) . Отриманий розподіл не є випадковим.
Ервін Брандштеттер

дуже цікаво, чи може це спрацювати у випадку використання лотерейного запиту: запит повинен переглянути всі доступні квитки і випадково повернути лише ОДИН один квиток. також я можу використовувати песимістичний замок (вибрати ... для оновлення) з вашою технікою?
Матьє

Для будь-чого, що пов’язане з лотереєю, ви дійсно повинні використовувати чесну та криптографічно захищену випадкову вибірку - наприклад, виберіть випадкове число від 1 до max (id), поки не знайдете існуючий id. Метод з цієї відповіді не є ні справедливим, ні безпечним - він швидкий. Використовується для таких речей, як "отримати випадкові 1% рядків, щоб щось перевірити", або "показати випадкові 5 записів".
Томецький
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.