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 SieDISTINCT
). -
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.