PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Il modo migliore per selezionare righe casuali PostgreSQL

Date le vostre specifiche (più informazioni aggiuntive nei commenti),

  • Hai una colonna ID numerico (numeri interi) con solo pochi (o moderatamente pochi) spazi vuoti.
  • Ovviamente nessuna o poche operazioni di scrittura.
  • La tua colonna ID deve essere indicizzata! Una chiave primaria funziona bene.

La query seguente non richiede una scansione sequenziale della tabella grande, solo una scansione dell'indice.

Innanzitutto, ottieni le stime per la query principale:

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

L'unica parte forse costosa è il count(*) (per tavoli enormi). Date le specifiche sopra, non ne hai bisogno. Un preventivo andrà benissimo, disponibile quasi a costo zero (spiegazione dettagliata qui):

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

Finché ct non è molto minore di id_span , la query supererà gli altri approcci.

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
  • Genera numeri casuali nel id spazio. Hai "pochi spazi vuoti", quindi aggiungi il 10% (abbastanza per coprire facilmente gli spazi vuoti) al numero di righe da recuperare.

  • Ogni id può essere selezionato più volte per caso (anche se molto improbabile con un grande spazio ID), quindi raggruppa i numeri generati (o usa DISTINCT ).

  • Unisciti all'id s al grande tavolo. Questo dovrebbe essere molto veloce con l'indice in atto.

  • Infine taglia l'eccesso id s che non sono stati mangiati da duplicati e lacune. Ogni riga ha una opportunità completamente uguale da raccogliere.

Versione breve

Puoi semplificare questa domanda. Il CTE nella query sopra è solo a scopo didattico:

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

Perfeziona con rCTE

Soprattutto se non sei così sicuro di lacune e stime.

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

Possiamo lavorare con un avanzo inferiore nella query di base. Se ci sono troppi spazi vuoti, quindi non troviamo abbastanza righe nella prima iterazione, rCTE continua a scorrere con il termine ricorsivo. Ne servono ancora relativamente pochi le lacune nello spazio ID o la ricorsione potrebbero esaurirsi prima che venga raggiunto il limite, oppure dobbiamo iniziare con un buffer sufficientemente grande che sfugge allo scopo di ottimizzare le prestazioni.

I duplicati vengono eliminati da UNION nel rCTE.

Il LIMIT esterno fa fermare il CTE non appena abbiamo abbastanza righe.

Questa query è stata accuratamente redatta per utilizzare l'indice disponibile, generare effettivamente righe casuali e non interrompersi finché non si raggiunge il limite (a meno che la ricorsione non si esaurisca). Ci sono una serie di insidie ​​qui se hai intenzione di riscriverlo.

Avvolgi nella funzione

Per uso ripetuto con parametri variabili:

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

Chiama:

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

Potresti anche fare in modo che questo generico funzioni per qualsiasi tabella:prendi il nome della colonna PK e della tabella come tipo polimorfico e usa EXECUTE ... Ma questo va oltre lo scopo di questa domanda. Vedi:

  • Refactoring di una funzione PL/pgSQL per restituire l'output di varie query SELECT

Possibile alternativa

SE i tuoi requisiti consentono set identici per ripetuti chiamate (e stiamo parlando di chiamate ripetute) io considererei una visione materializzata . Esegui la query sopra una volta e scrivi il risultato in una tabella. Gli utenti ottengono una selezione quasi casuale alla velocità della luce. Aggiorna la tua scelta casuale a intervalli o eventi a tua scelta.

Postgres 9.5 introduce TABLESAMPLE SYSTEM (n)

Dove n è una percentuale. Il manuale:

Il BERNOULLI e SYSTEM ciascuno dei metodi di campionamento accetta un singolo argomento che è la frazione della tabella da campionare, espressa come una percentuale compresa tra 0 e 100 . Questo argomento può essere qualsiasi real -espressione valutata.

Enfasi in grassetto mio. È molto veloce , ma il risultato è non esattamente casuale . Il manuale di nuovo:

Il SYSTEM è significativamente più veloce del BERNOULLI metodoquando vengono specificate piccole percentuali di campionamento, ma può restituire un campione non casuale della tabella a causa degli effetti di raggruppamento.

Il numero di righe restituite può variare notevolmente. Per il nostro esempio, per ottenere approssimativamente 1000 righe:

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

Correlati:

  • Un modo rapido per scoprire il conteggio delle righe di una tabella in PostgreSQL

Oppure installa il modulo aggiuntivo tsm_system_rows per ottenere esattamente il numero di righe richieste (se ce ne sono abbastanza) e consentire la sintassi più conveniente:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Vedi la risposta di Evan per i dettagli.

Ma non è ancora esattamente casuale.