szybki losowy wybór wierszy w Postgres

98

Mam tabelę w postgres, która zawiera kilka milionów wierszy. Sprawdziłem w internecie i znalazłem następujące

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

Działa, ale jest naprawdę powolny ... czy istnieje inny sposób wykonania tego zapytania lub bezpośredni sposób na wybranie losowego wiersza bez czytania całej tabeli? Nawiasem mówiąc, „myid” jest liczbą całkowitą, ale może to być puste pole.

Juan
źródło
1
Jeśli chcesz wybrać wiele losowych wierszy, zobacz to pytanie: stackoverflow.com/q/8674718/247696
Flimm

Odpowiedzi:

99

Możesz chcieć poeksperymentować OFFSET, jak w

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

To Nliczba wierszy w mytable. Być może będziesz musiał najpierw wykonać a, SELECT COUNT(*)aby obliczyć wartość N.

Aktualizacja (Antony Hatchkins)

Musisz użyć floortutaj:

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

Rozważ tabelę z 2 wierszami; random()*Ngeneruje 0 <= x < 2i na przykład SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;zwraca 0 wierszy z powodu niejawnego zaokrąglenia do najbliższej liczby całkowitej.

NPE
źródło
czy ma sens użycie N mniejszego niż SELECT COUNT(*)?, to znaczy, nie używaj wszystkich wartości w tabeli, ale tylko część z nich?
Juan
@Juan To zależy od Twoich wymagań.
NPE
używanie EXPLAIN SELECT ...z różnymi wartościami N daje taki sam koszt dla zapytania, więc myślę, że lepiej jest wybrać maksymalną wartość N.
Juan
3
zobacz poprawkę w mojej odpowiedzi poniżej
Antony Hatchkins
2
To ma jeden błąd. Nigdy nie zwróci pierwszego wiersza i wygeneruje błąd 1 / COUNT (*), ponieważ spróbuje zwrócić wiersz po ostatnim wierszu.
Ian,
62

PostgreSQL 9.5 wprowadził nowe podejście do znacznie szybszego wyboru próbek: TABLESAMPLE

Składnia to

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

Nie jest to optymalne rozwiązanie, jeśli chcesz wybrać tylko jeden wiersz, ponieważ musisz znać LICZBĘ tabeli, aby obliczyć dokładną wartość procentową.

Aby uniknąć powolnego COUNT i użyć szybkiego TABLESAMPLE dla tabel od 1 wiersza do miliardów wierszy, możesz wykonać:

 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;
 ...

To może nie wyglądać tak elegancko, ale prawdopodobnie jest szybsze niż jakakolwiek inna odpowiedź.

Aby zdecydować, czy chcesz używać BERNULLI oder SYSTEM, przeczytaj o różnicy na http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/

alfonx
źródło
2
Jest to znacznie szybsze i łatwiejsze niż jakakolwiek inna odpowiedź - ta powinna być na górze.
Hayden Schiff,
1
Dlaczego nie możesz po prostu użyć podzapytania, aby uzyskać liczbę? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
machineghost
2
@machineghost "Aby uniknąć powolnego LICZENIA ..." ... Jeśli twoje dane są tak małe, że możesz policzyć w rozsądnym czasie, zrób to! :-)
alfonx
2
@machineghost Użyj SELECT reltuples FROM pg_class WHERE relname = 'my_table'do oszacowania liczby.
Hynek -Pichi- Vychodil
@ Hynek-Pichi-Vychodil bardzo dobry wkład! Aby oszacowanie nie było nieaktualne, należy je ostatnio przeprowadzić VACUUM ANALYZEd .. ale dobra baza danych i tak powinna zostać odpowiednio przeanalizowana .. I wszystko zależy od konkretnego przypadku użycia. Zwykle ogromne stoły nie rosną tak szybko ... Dzięki!
alfonx
34

Wypróbowałem to z podzapytaniem i zadziałało dobrze. Offset, przynajmniej w Postgresql v8.4.4 działa dobrze.

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;
John Coryat
źródło
W rzeczywistości wersja 8.4 jest niezbędna, aby to działało, nie działa dla <= 8.3.
Antony Hatchkins
1
zobacz poprawkę w mojej odpowiedzi poniżej
Antony Hatchkins
32

Musisz użyć floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;
Antony Hatchkins
źródło
Rozważ tabelę z 2 wierszami; random()*Ngeneruje 0 <= x <2 i na przykład SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;zwraca 0 wierszy z powodu niejawnego zaokrąglenia do najbliższej liczby całkowitej.
Antony Hatchkins
Niestety to nie działa, jeśli chcesz użyć wyższego LIMITU ... Potrzebuję 3 pozycji, więc muszę użyć składni ORDER BY RANDOM ().
Alexis Wilke
1
Trzy kolejne zapytania nadal będą szybsze niż jedno order by random(), w przybliżeniu 3*O(N) < O(NlogN)- rzeczywiste dane dotyczące życia będą się nieco różnić ze względu na wskaźniki.
Antony Hatchkins
Mój problem polega na tym, że te 3 elementy muszą być różne, a WHERE myid NOT IN (1st-myid)i WHERE myid NOT IN (1st-myid, 2nd-myid)nie działają, ponieważ decyzję podejmuje PRZESUNIĘCIE. Hmmm ... Myślę, że mógłbym zmniejszyć N o 1 i 2 w drugim i trzecim SELECT.
Alexis Wilke
Czy mógłbyś lub ktokolwiek rozszerzyć tę odpowiedź o odpowiedź, dlaczego muszę użyć floor()? Jakie korzyści daje?
ADTC,
14

Sprawdź ten link, aby uzyskać różne opcje. http://www.depesz.com/index.php/2007/09/16/my-hardts-on-getting-random-row/

Aktualizacja: (A. Hatchkins)

Podsumowanie (bardzo) długiego artykułu jest następujące.

Autor wymienia cztery podejścia:

1) ORDER BY random() LIMIT 1; - wolno

2) ORDER BY id where id>=random()*N LIMIT 1 - niejednolite, jeśli występują luki

3) kolumna losowa - wymaga od czasu do czasu aktualizacji

4) niestandardowy agregat losowy - przebiegła metoda, może być powolna: random () należy wygenerować N razy

i sugeruje ulepszenie metody nr 2 za pomocą

5) ORDER BY id where id=random()*N LIMIT 1 z kolejnymi zapytaniami, jeśli wynik jest pusty.

Kuberchaun
źródło
Zastanawiam się, dlaczego nie zakryli OFFSET? Użycie ZAMÓWIENIA nie wchodzi w grę tylko po to, aby uzyskać losowy wiersz. Na szczęście OFFSET jest dobrze uwzględniony w odpowiedziach.
androidguy
4

Najłatwiejszym i najszybszym sposobem na pobranie losowego wiersza jest użycie tsm_system_rowsrozszerzenia:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Następnie możesz wybrać dokładną liczbę wierszy, które chcesz:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Jest to dostępne w PostgreSQL 9.5 i nowszych.

Zobacz: https://www.postgresql.org/docs/current/static/tsm-system-rows.html

daamien
źródło
1
Uczciwe ostrzeżenie, to nie jest całkowicie przypadkowe. W przypadku mniejszych tabel zawsze zwracałem pierwsze wiersze w kolejności.
Ben Aubin
1
tak, jest to jasno wyjaśnione w dokumentacji (link powyżej): «Podobnie jak wbudowana metoda próbkowania SYSTEM, SYSTEM_ROWS wykonuje próbkowanie na poziomie bloków, tak że próbka nie jest całkowicie losowa, ale może podlegać efektom klastrowania, zwłaszcza jeśli tylko mały liczba rzędów jest wymagana. ». Jeśli masz mały zestaw danych, ORDER BY random() LIMIT 1;powinien on być wystarczająco szybki.
daamien
Widziałem to. Chciałem tylko wyjaśnić każdemu, kto nie kliknie linku lub jeśli w przyszłości zostanie on usunięty.
Ben Aubin
1
Warto również zauważyć, że będzie to działać tylko przy wybieraniu losowych wierszy z tabeli i NASTĘPNIE filtrowaniu, w przeciwieństwie do / w porównaniu do uruchamiania zapytania, a następnie losowego wybierania jednego lub kilku rekordów.
nomen
3

Wymyśliłem bardzo szybkie rozwiązanie bez TABLESAMPLE. Dużo szybciej niż OFFSET random()*N LIMIT 1. Nie wymaga nawet liczby stolików.

Chodzi na przykład o utworzenie indeksu wyrażeń z losowymi, ale przewidywalnymi danymi md5(primary key).

Oto test z przykładowymi danymi 1 mln wierszy:

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;

Wynik:

 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

To zapytanie może czasami (z prawdopodobieństwem około 1 / Number_of_rows) zwrócić 0 wierszy, więc należy je sprawdzić i ponownie uruchomić. Również prawdopodobieństwa nie są dokładnie takie same - niektóre wiersze są bardziej prawdopodobne niż inne.

Dla porownania:

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

Wyniki są bardzo różne, ale mogą być dość złe:

 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)
Tometzky
źródło
2
Szybko, tak. Naprawdę losowe, nie. Wartości md5, które są następną większą wartością po innej istniejącej wartości, mają bardzo małą szansę na wybranie, podczas gdy wartości po dużej luce w przestrzeni liczbowej mają znacznie większą szansę (większa o liczbę możliwych wartości pomiędzy) . Wynikowy rozkład nie jest losowy.
Erwin Brandstetter
bardzo interesujące, czy mogłoby to zadziałać w przypadku zapytania podobnego do loterii: zapytanie musi przejrzeć wszystkie dostępne bilety i losowo zwrócić tylko JEDEN bilet pojedynczy. czy mogę również użyć pesymistycznej blokady (wybierz ... do aktualizacji) z twoją techniką?
Mathieu
W przypadku wszystkiego, co dotyczy loterii, powinieneś naprawdę używać uczciwego i bezpiecznego pod względem kryptograficznym losowego próbkowania - na przykład wybierz losową liczbę od 1 do max (id), aż znajdziesz istniejący identyfikator. Metoda z tej odpowiedzi nie jest ani uczciwa, ani bezpieczna - jest szybka. Przydatne do takich rzeczy, jak „pobierz losowy 1% wierszy, aby coś przetestować” lub „pokaż 5 losowych wpisów”.
Tometzky