schnelle zufällige Zeilenauswahl in Postgres

92

Ich habe eine Tabelle in Postgres, die einige Millionen Zeilen enthält. Ich habe im Internet nachgesehen und Folgendes gefunden

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

Es funktioniert, aber es ist sehr langsam ... gibt es eine andere Möglichkeit, diese Abfrage durchzuführen, oder eine direkte Möglichkeit, eine zufällige Zeile auszuwählen, ohne die gesamte Tabelle zu lesen? Übrigens ist 'myid' eine ganze Zahl, aber es kann ein leeres Feld sein.

Juan
quelle
1
Wenn Sie mehrere zufällige Zeilen auswählen möchten, sehen Sie diese Frage: stackoverflow.com/q/8674718/247696
Flimm

Antworten:

96

Sie könnten mit experimentieren wollen OFFSET, wie in

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

Das Nist die Anzahl der Zeilen in mytable. Möglicherweise müssen Sie zuerst a SELECT COUNT(*)ausführen, um den Wert von herauszufinden N.

Update (von Antony Hatchkins)

Sie müssen floorhier verwenden:

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

Betrachten Sie eine Tabelle mit 2 Zeilen. random()*Ngeneriert 0 <= x < 2und gibt beispielsweise SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;0 Zeilen zurück, da implizit auf das nächste int gerundet wird.

NPE
quelle
macht es Sinn, ein N kleiner als SELECT COUNT(*)? zu verwenden ? Ich meine, nicht alle Werte in der Tabelle zu verwenden, sondern nur einen Teil davon?
Juan
@Juan Das hängt von deinen Anforderungen ab.
NPE
Wenn Sie die EXPLAIN SELECT ...mit unterschiedlichen Werten von N verwenden, ergeben sich die gleichen Kosten für die Abfrage. Dann ist es wahrscheinlich besser, den Maximalwert von N
Juan
3
siehe einen Bugfix in meiner Antwort unten
Antony Hatchkins
2
Dies hat einen Fehler von um eins. Es wird niemals die erste Zeile zurückgeben und einen Fehler 1 / COUNT (*) erzeugen, da versucht wird, die Zeile nach der letzten Zeile zurückzugeben.
Ian
57

PostgreSQL 9.5 führte einen neuen Ansatz für eine viel schnellere Stichprobenauswahl ein: TABLESAMPLE

Die Syntax lautet

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

Dies ist nicht die optimale Lösung, wenn nur eine Zeile ausgewählt werden soll, da Sie den COUNT der Tabelle kennen müssen, um den genauen Prozentsatz zu berechnen.

Um eine langsame Zählung zu vermeiden und schnelles TABLESAMPLE für Tabellen von 1 Zeile bis Milliarden von Zeilen zu verwenden, haben Sie folgende Möglichkeiten:

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

Dies sieht vielleicht nicht so elegant aus, ist aber wahrscheinlich schneller als jede andere Antwort.

Informationen zur Entscheidung, ob Sie BERNULLI oder SYSTEM verwenden möchten, finden Sie unter http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/.

Alfonx
quelle
2
Dies ist viel schneller und einfacher als jede andere Antwort - diese sollte ganz oben stehen.
Hayden Schiff
1
Warum können Sie nicht einfach eine Unterabfrage verwenden, um die Zählung zu erhalten? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
Machineghost
2
@machineghost "Um eine langsame Zählung zu vermeiden ..." ... Wenn Ihre Daten so klein sind, dass Sie in angemessener Zeit zählen können, machen Sie es! :-)
Alfonx
2
@machineghost Zur Zählschätzung verwenden SELECT reltuples FROM pg_class WHERE relname = 'my_table'.
Hynek-Pichi-Vychodil
@ Hynek-Pichi-Vychodil sehr guter Input! Um sicherzustellen, dass die Schätzung nicht veraltet ist, muss sie kürzlich VACUUM ANALYZEd sein. Eine gute Datenbank sollte jedoch trotzdem ordnungsgemäß analysiert werden. Und alles hängt vom jeweiligen Anwendungsfall ab. Normalerweise wachsen riesige Tische nicht so schnell ... Danke!
Alfonx
34

Ich habe dies mit einer Unterabfrage versucht und es hat gut funktioniert. Offset funktioniert zumindest in Postgresql v8.4.4 einwandfrei.

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;
John Coryat
quelle
Tatsächlich ist v8.4 wichtig, damit dies funktioniert, funktioniert nicht für <= 8.3.
Antony Hatchkins
1
siehe einen Bugfix in meiner Antwort unten
Antony Hatchkins
30

Sie müssen verwenden floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;
Antony Hatchkins
quelle
Betrachten Sie eine Tabelle mit 2 Zeilen. random()*Ngeneriert 0 <= x <2 und gibt beispielsweise SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;0 Zeilen zurück, weil implizit auf das nächste int gerundet wird.
Antony Hatchkins
Leider funktioniert dies nicht, wenn Sie ein höheres LIMIT verwenden möchten ... Ich benötige 3 Elemente, daher muss ich die Syntax ORDER BY RANDOM () verwenden.
Alexis Wilke
1
Drei aufeinanderfolgende Abfragen sind immer noch schneller als eine order by random(), ungefähr so, wie die 3*O(N) < O(NlogN)realen Zahlen aufgrund von Indizes leicht abweichen.
Antony Hatchkins
Mein Problem ist, dass die 3 Elemente unterschiedlich sein müssen und a WHERE myid NOT IN (1st-myid)und WHERE myid NOT IN (1st-myid, 2nd-myid)nicht funktionieren würden, da die Entscheidung vom OFFSET getroffen wird. Hmmm ... Ich denke, ich könnte N im zweiten und dritten SELECT um 1 und 2 reduzieren.
Alexis Wilke
Könnten Sie oder jemand diese Antwort mit einer Antwort erweitern, warum ich sie verwenden muss floor()? Welchen Vorteil bietet es?
ADTC
14

Überprüfen Sie diesen Link für einige verschiedene Optionen. http://www.depesz.com/index.php/2007/09/16/my- Thoughts-on-getting-random-row/

Aktualisieren: (A. Hatchkins)

Die Zusammenfassung des (sehr) langen Artikels lautet wie folgt.

Der Autor listet vier Ansätze auf:

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

2) ORDER BY id where id>=random()*N LIMIT 1- ungleichmäßig, wenn es Lücken gibt

3) zufällige Spalte - muss von Zeit zu Zeit aktualisiert werden

4) Benutzerdefinierte Zufallsaggregat - Listmethode, könnte langsam sein: random () muss N-mal generiert werden

und schlägt vor, Methode 2 durch Verwendung zu verbessern

5) ORDER BY id where id=random()*N LIMIT 1 mit nachfolgenden Anfragen, wenn das Ergebnis leer ist.

Kuberchaun
quelle
Ich frage mich, warum sie OFFSET nicht behandelt haben. Die Verwendung einer BESTELLUNG kommt nicht in Frage, nur um eine zufällige Zeile zu erhalten. Glücklicherweise ist OFFSET in den Antworten gut abgedeckt.
AndroidGy
3

Ich habe eine sehr schnelle Lösung ohne gefunden TABLESAMPLE. Viel schneller als OFFSET random()*N LIMIT 1. Es erfordert nicht einmal die Anzahl der Tabellen.

Die Idee ist beispielsweise, einen Ausdrucksindex mit zufälligen, aber vorhersehbaren Daten zu erstellen md5(primary key).

Hier ist ein Test mit Beispieldaten für 1 Million Zeilen:

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;

Ergebnis:

 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

Diese Abfrage kann manchmal (mit einer Wahrscheinlichkeit von ungefähr 1 / Anzahl_der_Zeilen) 0 Zeilen zurückgeben, daher muss sie überprüft und erneut ausgeführt werden. Auch die Wahrscheinlichkeiten sind nicht genau gleich - einige Zeilen sind wahrscheinlicher als andere.

Zum Vergleich:

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

Die Ergebnisse variieren stark, können aber ziemlich schlecht sein:

 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
quelle
2
Schnell, ja. Wirklich zufällig, nein. Ein md5-Wert, der zufällig der nächstgrößere Wert nach einem anderen vorhandenen Wert ist, hat eine sehr geringe Chance, ausgewählt zu werden, während Werte nach einer großen Lücke im Zahlenraum eine viel größere Chance haben (größer um die Anzahl möglicher Werte dazwischen). . Die resultierende Verteilung ist nicht zufällig.
Erwin Brandstetter
Sehr interessant, könnte es in einem Fall einer lotterieähnlichen Abfrage funktionieren: Die Abfrage muss alle verfügbaren Tickets prüfen und zufällig nur EIN einziges Ticket zurückgeben. Kann ich mit Ihrer Technik auch eine pessimistische Sperre verwenden (zum Aktualisieren auswählen)?
Mathieu
Für alles, was mit Lotterie zu tun hat, sollten Sie wirklich eine faire und kryptografisch sichere Zufallsstichprobe verwenden - wählen Sie beispielsweise eine Zufallszahl zwischen 1 und max (ID), bis Sie eine vorhandene ID finden. Die Methode aus dieser Antwort ist weder fair noch sicher - sie ist schnell. Verwendbar für Dinge wie "1% der Zeilen zufällig testen, um etwas zu testen" oder "Zufällige 5 Einträge anzeigen".
Tometzky
3

Der einfachste und schnellste Weg, um zufällige Zeilen abzurufen, ist die Verwendung der tsm_system_rowsErweiterung:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Dann können Sie die genaue Anzahl der gewünschten Zeilen auswählen:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Dies ist mit PostgreSQL 9.5 und höher verfügbar.

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

daamien
quelle
1
Faire Warnung, das ist nicht ganz zufällig. Bei kleineren Tabellen wurden immer die ersten Zeilen der Reihe nach zurückgegeben.
Ben Aubin
1
Ja, dies wird in der Dokumentation (Link oben) klar erklärt: «Wie die integrierte SYSTEM-Stichprobenmethode führt SYSTEM_ROWS eine Stichprobenerfassung auf Blockebene durch, sodass die Stichprobe nicht vollständig zufällig ist, sondern möglicherweise Clustering-Effekten unterliegt, insbesondere wenn sie nur geringfügig ist Anzahl der Zeilen werden angefordert. ». Wenn Sie einen kleinen Datensatz haben, ORDER BY random() LIMIT 1;sollte dieser schnell genug sein.
Daamien
Das habe ich gesehen. Ich wollte nur jedem klar machen, der nicht auf den Link klickt oder ob der Link in Zukunft stirbt.
Ben Aubin
1
Es ist auch erwähnenswert, dass dies nur für die Auswahl zufälliger Zeilen aus einer Tabelle und die DANN-Filterung funktioniert, im Gegensatz zum Ausführen einer Abfrage und dem anschließenden zufälligen Auswählen eines oder mehrerer Datensätze.
Nomen