PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Beste Möglichkeit, zufällige Zeilen PostgreSQL auszuwählen

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

  • Sie haben eine numerische ID-Spalte (Ganzzahlen) mit nur wenigen (oder mäßig wenigen) Lücken.
  • Offensichtlich keine oder wenige Schreibvorgänge.
  • Ihre ID-Spalte muss indiziert werden! Ein Primärschlüssel leistet gute Dienste.

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

Rufen Sie zuerst Schätzungen für die Hauptabfrage ab:

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öglicherweise teure Teil ist der count(*) (für riesige Tische). Angesichts der obigen Spezifikationen brauchen Sie es nicht. Ein Kostenvoranschlag reicht völlig aus und ist fast kostenlos erhältlich (detaillierte Erklärung hier):

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

Solange ct ist nicht viel kleiner als id_span , übertrifft die Abfrage andere Ansätze.

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
  • Generieren Sie Zufallszahlen in der id Platz. Sie haben "wenige Lücken", also addieren Sie 10 % (genug, um die Lücken leicht abzudecken) zu der Anzahl der abzurufenden Zeilen hinzu.

  • Jede id kann zufällig mehrmals ausgewählt werden (obwohl sehr unwahrscheinlich bei einem großen ID-Raum), also gruppieren Sie die generierten Zahlen (oder verwenden Sie DISTINCT ).

  • Treten Sie der id bei s zum großen Tisch. Dies sollte mit dem vorhandenen Index sehr schnell gehen.

  • Trimmen Sie schließlich überschüssige id s, die nicht von Dupes und Lücken gefressen wurden. Jede Reihe hat eine völlig gleiche Chance abgeholt werden.

Kurzfassung

Sie können vereinfachen diese Abfrage. 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;

Mit rCTE verfeinern

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
   )
TABLE  random_pick
LIMIT  1000;  -- actual limit

Wir können mit einem kleineren Überschuss arbeiten in der Basisabfrage. Wenn es zu viele Lücken gibt, sodass wir in der ersten Iteration nicht genügend Zeilen finden, iteriert der rCTE weiter mit dem rekursiven Term. Wir brauchen noch relativ wenige Lücken im ID-Raum oder die Rekursion kann versiegen, 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 die UNION eliminiert im rCTE.

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

Diese Abfrage wurde sorgfältig entworfen, um den verfügbaren Index zu verwenden, tatsächlich zufällige Zeilen zu generieren und nicht aufzuhören, 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.

Wrap in Funktion

Für wiederholte Verwendung mit variierenden Parametern:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big
  LANGUAGE plpgsql VOLATILE ROWS 1000 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
   )
   TABLE  random_pick
   LIMIT  _limit;
END
$func$;

Aufruf:

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

Sie könnten dies sogar so generisch machen, dass es für jede Tabelle funktioniert:Nehmen Sie den Namen der PK-Spalte und der Tabelle als polymorphen Typ und verwenden Sie EXECUTE ... Aber das geht über den Rahmen dieser Frage hinaus. Siehe:

  • Refaktorisieren Sie eine PL/pgSQL-Funktion, um die Ausgabe verschiedener SELECT-Abfragen zurückzugeben

Mögliche Alternative

WENN Ihre Anforderungen identische Sets für Wiederholungen zulassen Anrufe (und wir sprechen von wiederholten Anrufen) Ich würde eine materialisierte Ansicht in Betracht ziehen . Führen Sie die obige Abfrage einmal aus und schreiben Sie das Ergebnis in eine Tabelle. Benutzer erhalten blitzschnell eine quasi zufällige Auswahl. Aktualisieren Sie Ihre Zufallsauswahl in Intervallen oder bei Ereignissen Ihrer Wahl.

Postgres 9.5 führt TABLESAMPLE SYSTEM (n) ein

Wobei n ist ein Prozentsatz. Das Handbuch:

Der BERNOULLI und SYSTEM Stichprobenmethoden akzeptieren jeweils ein einziges Argument, nämlich den Bruchteil der Tabelle, für den Stichproben genommen werden sollen, ausgedrückt als Prozentsatz zwischen 0 und 100 . Dieses Argument kann ein beliebiger real sein -wertiger Ausdruck.

Fette Hervorhebung von mir. Es ist sehr schnell , aber das Ergebnis ist nicht ganz zufällig . Nochmal das Handbuch:

Das SYSTEM Methode ist deutlich schneller als die BERNOULLI -Methode, wenn kleine Stichprobenprozentsätze angegeben sind, aber aufgrund von Clustering-Effekten möglicherweise eine weniger zufällige Stichprobe der Tabelle zurückgegeben wird.

Die Anzahl der zurückgegebenen Zeilen kann stark variieren. Für unser Beispiel, um ungefähr zu erhalten 1000 Zeilen:

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

Verwandte:

  • Schneller Weg, um die Zeilenanzahl einer Tabelle in PostgreSQL zu ermitteln

Oder Installieren Sie das zusätzliche Modul tsm_system_rows um die Anzahl der angeforderten Zeilen genau zu erhalten (falls genug vorhanden sind) und die bequemere Syntax zu ermöglichen:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Einzelheiten finden Sie in Evans Antwort.

Aber das ist immer noch nicht ganz zufällig.