it-swarm.com.de

Beste Weg, um zufällige Zeilen PostgreSQL auszuwählen

Ich möchte eine zufällige Auswahl von Zeilen in PostgreSQL, ich habe Folgendes versucht:

select * from table where random() < 0.01;

Aber einige andere empfehlen dies:

select * from table order by random() limit 1000;

Ich habe einen sehr großen Tisch mit 500 Millionen Zeilen, ich möchte, dass er schnell ist.

Welcher Ansatz ist besser? Was sind die Unterschiede? Was ist der beste Weg, um zufällige Zeilen auszuwählen?

290
nanounanue

Angesichts Ihrer Spezifikationen (plus zusätzliche Informationen in den Kommentaren),

  • Sie haben eine numerische ID-Spalte (Ganzzahl) mit nur wenigen (oder mäßig wenigen) Lücken.
  • Offensichtlich keine oder nur wenige Schreibvorgänge.
  • Ihre ID-Spalte muss indiziert sein! Ein Primärschlüssel dient gut.

Die folgende Abfrage benötigt keinen sequentiellen Scan der großen Tabelle, nur einen Index-Scan.

Erhalten Sie zunächst Schätzungen für die Hauptabfrage:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

Der einzig mögliche teure Teil ist die Funktion count(*) (für große Tabellen). In Anbetracht der obigen Spezifikationen benötigen Sie diese nicht. Ein Kostenvoranschlag ist in Ordnung und fast kostenlos erhältlich ( ausführliche Erklärung hier ):

SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;

Solange ct nicht viel kleiner als id_span Ist, wird die Abfrage andere Ansätze übertreffen.

WITH params AS (
    SELECT 1       AS min_id           -- minimum id <= current min id
         , 5100000 AS id_span          -- rounded up. (max_id - min_id + buffer)
    )
SELECT *
FROM  (
    SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
    FROM   params p
          ,generate_series(1, 1100) g  -- 1000 + buffer
    GROUP  BY 1                        -- trim duplicates
    ) r
JOIN   big USING (id)
LIMIT  1000;                           -- trim surplus
  • Generiere Zufallszahlen im id Raum. Sie haben "wenige Lücken", also addieren Sie 10% (genug, um die Leerzeichen leicht abzudecken) zur Anzahl der abzurufenden Zeilen.

  • Jedes id kann mehrmals zufällig ausgewählt werden (obwohl dies mit einem großen ID-Bereich sehr unwahrscheinlich ist). Gruppieren Sie daher die generierten Zahlen (oder verwenden Sie DISTINCT).

  • Verbinden Sie die ids mit dem großen Tisch. Dies sollte bei vorhandenem Index sehr schnell gehen.

  • Schließlich schneiden Sie überschüssige ids, die nicht von Dupes und Lücken gefressen wurden. Jede Zeile hat eine völlig gleiche Chance , ausgewählt zu werden.

Kurze Version

Sie können diese Abfrage vereinfachen . Der CTE in der obigen Abfrage dient nur zu Bildungszwecken:

SELECT *
FROM  (
    SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
    FROM   generate_series(1, 1100) g
    ) r
JOIN   big USING (id)
LIMIT  1000;

Verfeinern Sie mit rCTE

Vor allem, wenn Sie sich bei Lücken und Schätzungen nicht so sicher sind.

WITH RECURSIVE random_pick AS (
   SELECT *
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   generate_series(1, 1030)  -- 1000 + few percent - adapt to your needs
      LIMIT  1030                      -- hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss

   UNION                               -- eliminate dupe
   SELECT b.*
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   random_pick r             -- plus 3 percent - adapt to your needs
      LIMIT  999                       -- less than 1000, hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

Wir können mit einem kleineren Überschuss in der Basisabfrage arbeiten. Wenn zu viele Lücken vorhanden sind, sodass in der ersten Iteration nicht genügend Zeilen gefunden werden, wird die Iteration des rCTE mit dem rekursiven Term fortgesetzt. Wir brauchen noch relativ wenige Lücken im ID-Bereich, oder die Rekursion läuft möglicherweise leer, bevor das Limit erreicht ist - oder wir müssen mit einem ausreichend großen Puffer beginnen, der dem Zweck der Leistungsoptimierung widerspricht.

Duplikate werden durch das UNION im rCTE entfernt.

Das äußere LIMIT bewirkt, dass der CTE stoppt, sobald wir genug Zeilen haben.

Diese Abfrage wurde sorgfältig entworfen, um den verfügbaren Index zu verwenden, tatsächlich zufällige Zeilen zu generieren und nicht anzuhalten, bis wir das Limit erreicht haben (es sei denn, die Rekursion läuft trocken). Es gibt hier eine Reihe von Fallstricken, wenn Sie es umschreiben wollen.

In Funktion einwickeln

Zur wiederholten Verwendung mit unterschiedlichen Parametern:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT c.reltuples * _gaps
      FROM   pg_class c
      WHERE  c.oid = 'big'::regclass);
BEGIN

   RETURN QUERY
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   generate_series(1, _surplus) g
         LIMIT  _surplus           -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  _limit             -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

Anruf:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

Sie können diese generische Methode sogar für jede Tabelle verwenden: Nehmen Sie den Namen der PK-Spalte und der Tabelle als polymorphen Typ und verwenden Sie EXECUTE ... Dies würde den Rahmen dieser Frage sprengen. Sehen:

Mögliche Alternative

WENN Ihre Anforderungen identische Mengen für wiederholte Anrufe zulassen (und es sich um wiederholte Anrufe handelt), würde ich eine materialisierte Ansicht in Betracht ziehen. Führen Sie die obige Abfrage einmal aus und schreiben Sie das Ergebnis in eine Tabelle. Benutzer erhalten eine quasi zufällige Auswahl mit Blitzgeschwindigkeit. Aktualisieren Sie Ihre zufällige Auswahl in Abständen oder bei Ereignissen Ihrer Wahl.

In Postgres 9.5 wird TABLESAMPLE SYSTEM (n) eingeführt

Wobei n ein Prozentsatz ist. Das Handbuch:

Die Stichprobenmethoden BERNOULLI und SYSTEM akzeptieren jeweils ein einzelnes Argument, das den Bruchteil der zu untersuchenden Tabelle darstellt, ausgedrückt als Prozentsatz zwischen 0 und 100 . Dieses Argument kann ein beliebiger Ausdruck mit dem Wert real sein.

Meine kühne Betonung. Es ist sehr schnell , aber das Ergebnis ist nicht genau zufällig . Das Handbuch nochmal:

Die SYSTEM -Methode ist erheblich schneller als die BERNOULLI -Methode, wenn kleine Stichprobenprozentsätze angegeben werden. Aufgrund von Clustering-Effekten wird jedoch möglicherweise eine weniger zufällige Stichprobe der Tabelle zurückgegeben.

Die Anzahl der zurückgegebenen Zeilen kann stark variieren. In unserem Beispiel erhalten Sie ungefähr 1000 Zeilen:

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

Verbunden:

Oder installieren Sie das Zusatzmodul tsm_system_rows , um die Anzahl der angeforderten Zeilen genau zu ermitteln (wenn es genug gibt) und lassen Sie die bequemere Syntax zu:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Siehe Evans Antwort für Details.

Das ist aber noch nicht ganz zufällig.

204

Sie können den Ausführungsplan von beiden untersuchen und vergleichen, indem Sie verwenden

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

Ein kurzer Test auf einem großen Tisch1 zeigt, dass der ORDER BY zuerst die komplette Tabelle sortiert und dann die ersten 1000 Items auswählt. Beim Sortieren einer großen Tabelle wird nicht nur diese Tabelle gelesen, sondern auch temporäre Dateien gelesen und geschrieben. Die Funktion where random() < 0.1 durchsucht die gesamte Tabelle nur einmal.

Bei großen Tabellen ist dies möglicherweise nicht das, was Sie möchten, da selbst ein vollständiger Tabellenscan zu lange dauern kann.

Ein dritter Vorschlag wäre

select * from table where random() < 0.01 limit 1000;

Dieser stoppt den Tabellenscan, sobald 1000 Zeilen gefunden wurden und kehrt daher früher zurück. Natürlich verzerrt dies die Zufälligkeit ein wenig, aber vielleicht ist das in Ihrem Fall gut genug.

Bearbeiten: Abgesehen von diesen Überlegungen können Sie sich die bereits gestellten Fragen dazu ansehen. Die Verwendung der Abfrage [postgresql] random Liefert einige Treffer.

Und ein verknüpfter Artikel von depez, der mehrere weitere Ansätze umreißt:


1 "groß" wie in "Die gesamte Tabelle passt nicht in den Speicher".

89
A.H.

postgresql order by random (), Zeilen in zufälliger Reihenfolge auswählen:

select your_columns from your_table ORDER BY random()

postgresql order by random () mit einem eindeutigen:

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

postgresql Bestellung per Zufallslimit eine Zeile:

select your_columns from your_table ORDER BY random() limit 1
70
Eric Leschinski

Ab PostgreSQL 9.5 gibt es eine neue Syntax, mit der zufällige Elemente aus einer Tabelle abgerufen werden können:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

In diesem Beispiel erhalten Sie 5% der Elemente aus mytable.

Weitere Erklärungen finden Sie in diesem Blog-Beitrag: http://www.postgresql.org/docs/current/static/sql-select.html

36

Der mit dem ORDER BY wird der langsamere sein.

select * from table where random() < 0.01; geht Datensatz für Datensatz und entscheidet, ob er zufällig gefiltert wird oder nicht. Dies wird O(N) sein, da jeder Datensatz nur einmal überprüft werden muss.

select * from table order by random() limit 1000; sortiert die gesamte Tabelle und wählt dann die ersten 1000 aus. Abgesehen von jeglicher Voodoo-Magie hinter den Kulissen lautet die Reihenfolge nach O(N * log N).

Der Nachteil von random() < 0.01 ist, dass Sie eine variable Anzahl von Ausgabedatensätzen erhalten.


Beachten Sie, dass es eine bessere Möglichkeit gibt, einen Datensatz zu mischen, als nach dem Zufallsprinzip zu sortieren: The Fisher-Yates Shuffle , das in O(N) ausgeführt wird. Die Implementierung des Shuffle in SQL klingt jedoch nach einer ziemlichen Herausforderung.

27
Donald Miner

Hier ist eine Entscheidung, die für mich funktioniert. Ich denke, es ist sehr einfach zu verstehen und auszuführen.

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
15
Bogdan Surai
select * from table order by random() limit 1000;

Wenn Sie wissen, wie viele Zeilen Sie möchten, lesen Sie tsm_system_rows .

tsm_system_rows

das Modul stellt die Tabellenabtastmethode SYSTEM_ROWS zur Verfügung, die in der TABLESAMPLE-Klausel eines SELECT-Befehls verwendet werden kann.

Diese Tabellen-Stichprobenmethode akzeptiert ein einzelnes ganzzahliges Argument, bei dem es sich um die maximale Anzahl der zu lesenden Zeilen handelt. Das resultierende Beispiel enthält immer genau so viele Zeilen, es sei denn, die Tabelle enthält nicht genügend Zeilen. In diesem Fall wird die gesamte Tabelle ausgewählt. Wie die integrierte SYSTEM-Abtastmethode führt SYSTEM_ROWS eine Abtastung auf Blockebene durch, sodass die Abtastung nicht vollständig zufällig ist, sondern möglicherweise Cluster-Effekten unterliegt, insbesondere wenn nur eine geringe Anzahl von Zeilen vorhanden ist angefordert.

Installieren Sie zuerst die Erweiterung

CREATE EXTENSION tsm_system_rows;

Dann Ihre Anfrage,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
9
Evan Carroll

Wenn Sie nur eine Zeile wünschen, können Sie ein berechnetes offset verwenden, das von count abgeleitet ist.

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
7
Nelo Mitranim

Eine Variation der materialisierten Ansicht "Mögliche Alternative" skizziert von Erwin Brandstetter ist möglich.

Angenommen, Sie möchten keine Duplikate in den zurückgegebenen zufälligen Werten. Sie müssen also einen booleschen Wert für die Primärtabelle festlegen, die Ihre (nicht randomisierten) Werte enthält.

Angenommen, dies ist die Eingabetabelle:

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

Füllen Sie die Tabelle ID_VALUES Nach Bedarf aus. Erstellen Sie dann, wie von Erwin beschrieben, eine materialisierte Ansicht, die die Tabelle ID_VALUES Einmal zufällig sortiert:

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

Beachten Sie, dass die materialisierte Ansicht die verwendete Spalte nicht enthält, da diese schnell veraltet sein wird. Die Ansicht muss auch keine anderen Spalten enthalten, die sich möglicherweise in der Tabelle id_values Befinden.

Um zufällige Werte zu erhalten (und zu "verbrauchen"), verwenden Sie ein UPDATE-RETURNING für id_values, Wählen Sie id_values Unter id_values_randomized Mit einem Join aus und wenden Sie die gewünschten Kriterien an Erhalten Sie nur relevante Möglichkeiten. Zum Beispiel:

UPDATE id_values
SET used = TRUE
WHERE id_values.id IN 
  (SELECT i.id
    FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
    WHERE (NOT i.used)
    LIMIT 5)
RETURNING id;

Ändern Sie LIMIT nach Bedarf - wenn Sie jeweils nur einen Zufallswert benötigen, ändern Sie LIMIT in 1.

Mit den richtigen Indizes für id_values Sollte das UPDATE-RETURNING meiner Meinung nach sehr schnell und mit wenig Last ausgeführt werden. Bei einem Datenbank-Roundtrip werden zufällige Werte zurückgegeben. Die Kriterien für "berechtigte" Zeilen können beliebig komplex sein. Neue Zeilen können jederzeit zur Tabelle id_values Hinzugefügt werden und sind für die Anwendung verfügbar, sobald die materialisierte Ansicht aktualisiert wird (was wahrscheinlich außerhalb der Spitzenzeiten ausgeführt werden kann). Das Erstellen und Aktualisieren der materialisierten Ansicht ist langsam, muss jedoch nur ausgeführt werden, wenn der Tabelle id_values Neue IDs hinzugefügt werden.

2
Raman

Ich weiß, dass ich ein bisschen zu spät zur Party komme, aber ich habe gerade dieses großartige Tool namens pg_sample gefunden:

pg_sample - Extrahieren eines kleinen Beispieldatensatzes aus einer größeren PostgreSQL-Datenbank unter Beibehaltung der referenziellen Integrität.

Ich habe dies mit einer 350M-Zeilendatenbank versucht und es war sehr schnell, ich weiß nichts über die Zufälligkeit.

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
0
Daniel Gerber

Fügen Sie eine Spalte mit dem Namen r mit dem Typ serial hinzu. Index r.

Angenommen, wir haben 200.000 Zeilen und generieren eine Zufallszahl n, wobei 0 <n <= 200.000.

Zeilen auswählen mit r > n, sortiere sie ASC und wähle die kleinste aus.

Code:

select * from YOUR_TABLE 
where r > (
    select (
        select reltuples::bigint AS estimate
        from   pg_class
        where  oid = 'public.YOUR_TABLE'::regclass) * random()
    )
order by r asc limit(1);

Der Code ist selbsterklärend. Die Unterabfrage in der Mitte wird verwendet, um die Anzahl der Tabellenzeilen schnell von https://stackoverflow.com/a/7945274/1271094 zu schätzen.

Auf Anwendungsebene müssen Sie die Anweisung erneut ausführen, wenn n> die Anzahl der Zeilen oder mehrere Zeilen ausgewählt werden müssen.

0
MK Yung

Eine Lektion aus meiner Erfahrung:

offset floor(random() * N) limit 1 ist nicht schneller als order by random() limit 1.

Ich dachte, der offset -Ansatz wäre schneller, weil er die Zeit für das Sortieren in Postgres sparen sollte. Es stellte sich heraus, dass es nicht war.

0
user10375