Il modo migliore per selezionare righe casuali PostgreSQL


345

Voglio una selezione casuale di righe in PostgreSQL, ho provato questo:

select * from table where random() < 0.01;

Ma alcuni altri raccomandano questo:

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

Ho una tabella molto grande con 500 milioni di righe, voglio che sia veloce.

Quale approccio è migliore? Quali sono le differenze? Qual è il modo migliore per selezionare righe casuali?


1
Ciao Jack, grazie per la tua risposta, il tempo di esecuzione è più lento in ordine, ma vorrei sapere qual è il diverso se presente ...
nanounanue,

Uhhh ... prego. Quindi, hai provato a confrontare i diversi approcci?

Ci sono anche modi molto più veloci. Tutto dipende dalle tue esigenze e da cosa devi lavorare. Ti servono esattamente 1000 file? La tabella ha un ID numerico? Con nessuna / poche / molte lacune? Quanto è importante la velocità? Quante richieste per unità di tempo? Ogni richiesta ha bisogno di un set diverso o possono essere uguali per un intervallo di tempo definito?
Erwin Brandstetter,

6
La prima opzione "(random () <0,01)" è matematicamente errata in quanto non è possibile ottenere righe in risposta se nessun numero casuale è inferiore a 0,01, ciò potrebbe accadere in ogni caso (anche se meno probabile), indipendentemente dalla dimensione della tabella o superiore alla soglia. La seconda opzione ha sempre ragione
Herme,

1
Se vuoi selezionare solo una riga, vedi questa domanda: stackoverflow.com/q/5297396/247696
Flimm

Risposte:


230

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

  • Hai una colonna ID numerica (numeri interi) con solo poche (o moderatamente poche) lacune.
  • Ovviamente nessuna o poche operazioni di scrittura.
  • La colonna del tuo ID deve essere indicizzata! Una chiave primaria serve bene.

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

Innanzitutto, ottieni 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 è count(*)(per tavoli enormi). Dati sopra indicati, non è necessario. 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é ctnon è molto più piccolo 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 nello idspazio. Hai "poche lacune", quindi aggiungi il 10% (abbastanza per coprire facilmente gli spazi) al numero di righe da recuperare.

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

  • Unisciti alla ids al grande tavolo. Questo dovrebbe essere molto veloce con l'indice in atto.

  • Infine, tagliare le eccedenze idche non sono state consumate da duplicati e lacune. Ogni riga ha una probabilità completamente uguale di essere selezionata.

Versione breve

È possibile semplificare questa query. Il CTE nella query sopra è solo a scopo educativo:

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

Filtra 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
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

Possiamo lavorare con un surplus minore nella query di base. Se ci sono troppi vuoti, quindi non troviamo abbastanza righe nella prima iterazione, l'rCTE continua a iterare con il termine ricorsivo. Abbiamo ancora bisogno di relativamente poche lacune nello spazio ID o la ricorsione potrebbe esaurirsi prima che venga raggiunto il limite - oppure dobbiamo iniziare con un buffer abbastanza grande che sfidi lo scopo di ottimizzare le prestazioni.

I duplicati vengono eliminati da UNIONin rCTE.

L'esterno LIMITfa arrestare il CTE non appena abbiamo abbastanza file.

Questa query viene elaborata con cura per utilizzare l'indice disponibile, generare righe effettivamente casuali e non fermarsi fino a quando non raggiungiamo il limite (a meno che la ricorsione non si esaurisca). Ci sono un certo numero di insidie ​​qui se hai intenzione di riscriverlo.

Avvolgi in funzione

Per uso ripetuto con parametri variabili:

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;

Chiamata:

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

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

Alternativa possibile

Se i tuoi requisiti consentono set identici per chiamate ripetute (e stiamo parlando di chiamate ripetute), prenderei in considerazione una visione materializzata . Eseguire una volta la query sopra e scrivere 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 di tua scelta.

Introduce Postgres 9.5 TABLESAMPLE SYSTEM (n)

Dov'è nuna percentuale. Il manuale:

I metodi di campionamento BERNOULLIe SYSTEMaccettano ciascuno un singolo argomento che è la frazione della tabella da campionare, espressa in percentuale tra 0 e 100 . Questo argomento può essere qualsiasi realespressione valutata.

Enorme enfasi sulla mia. È molto veloce , ma il risultato non è esattamente casuale . Il manuale di nuovo:

Il SYSTEMmetodo è significativamente più veloce del BERNOULLImetodo quando vengono specificate piccole percentuali di campionamento, ma può restituire un campione meno casuale della tabella a causa degli effetti di clustering.

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

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

Relazionato:

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.


Dove viene definita la tabella t ? Nel caso in cui r invece di t ?
Luc M

1
@LucM: è definito qui:, JOIN bigtbl tche è l'abbreviazione di JOIN bigtbl AS t. tè un alias di tabella per bigtbl. Il suo scopo è abbreviare la sintassi, ma non sarebbe necessario in questo caso particolare. Ho semplificato la query nella mia risposta e ho aggiunto una versione semplice.
Erwin Brandstetter,

Qual è lo scopo dell'intervallo di valori di generate_series (1.1100)?
Fantastico-o

@ Awesome-o: l'obiettivo è recuperare 1000 righe, inizio con un ulteriore 10% per compensare qualche lacuna o (improbabile ma possibile) duplicare numeri casuali ... la spiegazione è nella mia risposta.
Erwin Brandstetter,

Erwin, ho pubblicato una variante della tua "Possibile alternativa": stackoverflow.com/a/23634212/430128 . Sarebbe interessato ai tuoi pensieri.
Raman,

100

È possibile esaminare e confrontare il piano di esecuzione di entrambi utilizzando

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

Un test rapido su una grande tabella 1 mostra che il ORDER BYprimo ordina la tabella completa e quindi seleziona i primi 1000 articoli. L'ordinamento di una tabella di grandi dimensioni non solo legge quella tabella, ma comporta anche la lettura e la scrittura di file temporanei. L' where random() < 0.1unico esegue una scansione del tavolo completo una volta.

Per le tabelle di grandi dimensioni ciò potrebbe non essere quello desiderato, poiché anche una scansione completa delle tabelle potrebbe richiedere troppo tempo.

Una terza proposta sarebbe

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

Questo interrompe la scansione della tabella non appena sono state trovate 1000 righe e quindi restituisce prima. Ovviamente questo annulla un po 'la casualità, ma forse questo è abbastanza buono nel tuo caso.

Modifica: oltre a queste considerazioni, è possibile verificare le domande già poste per questo. L'uso della query [postgresql] randomrestituisce alcuni risultati.

E un articolo collegato di depez che delinea diversi altri approcci:


1 "grande" come in "la tabella completa non si adatta alla memoria".


1
Un buon punto per scrivere il file temporaneo per eseguire l'ordinamento. Questo è davvero un grande successo. Immagino che potremmo fare random() < 0.02e poi mescolare quell'elenco, quindi limit 1000! L'ordinamento sarà meno costoso su alcune migliaia di righe (lol).
Donald Miner,

Il "seleziona * dalla tabella dove random () <0,05 limite 500;" è uno dei metodi più semplici per postgresql. Ne abbiamo fatto uso in uno dei nostri progetti in cui dovevamo selezionare il 5% dei risultati e non più di 500 righe alla volta per l'elaborazione.
martedì

Perché nel mondo dovresti mai considerare una scansione completa O (n) per il recupero di un campione su una tabella di file di 500m? È ridicolmente lento su grandi tavoli e completamente inutile.
mafu,

77

ordine postgresql per random (), selezionare le righe in ordine casuale:

select your_columns from your_table ORDER BY random()

ordine postgresql per random () con un distinto:

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

ordine postgresql per limite casuale di una riga:

select your_columns from your_table ORDER BY random() limit 1

1
select your_columns from your_table ORDER BY random() limit 1impiega ~ 2 minuti per eseguire su 45mil file
nguyên

c'è un modo per accelerare questo?
CpILL,

43

A partire da PostgreSQL 9.5, c'è una nuova sintassi dedicata all'ottenimento di elementi casuali da una tabella:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Questo esempio ti darà il 5% di elementi da mytable.

Vedi ulteriori spiegazioni su questo post del blog: http://www.postgresql.org/docs/current/static/sql-select.html


3
Una nota importante della documentazione: "Il metodo SYSTEM esegue il campionamento a livello di blocco con ogni blocco che ha la possibilità specificata di essere selezionato; vengono restituite tutte le righe in ciascun blocco selezionato. Il metodo SYSTEM è significativamente più veloce del metodo BERNOULLI quando le piccole percentuali di campionamento sono specificati, ma può restituire un campione meno casuale della tabella a causa di effetti di clustering. "
Tim

1
Esiste un modo per specificare un numero di righe anziché una percentuale?
Flimm,

4
È possibile utilizzare TABLESAMPLE SYSTEM_ROWS(400)per ottenere un campione di 400 righe casuali. È necessario abilitare l' estensione integratatsm_system_rows per utilizzare questa istruzione.
Mickaël Le Baillif,

27

Quello con ORDER BY sarà più lento.

select * from table where random() < 0.01;va record per record e decide di filtrarlo casualmente o no. Ciò avverrà O(N)perché deve solo controllare ogni record una volta.

select * from table order by random() limit 1000;sta per ordinare l'intero tavolo, quindi scegliere il primo 1000. A parte qualsiasi magia voodoo dietro le quinte, l'ordine è O(N * log N).

L'aspetto negativo di random() < 0.01quello è che otterrai un numero variabile di record di output.


Nota, c'è un modo migliore per mescolare un set di dati piuttosto che l'ordinamento casuale: The Fisher-Yates Shuffle , che si avvia O(N). L'implementazione dello shuffle in SQL sembra tuttavia piuttosto una sfida.


3
Non c'è motivo per cui non puoi aggiungere un limite 1 alla fine del tuo primo esempio. L'unico problema è che esiste il potenziale che non si ottenga alcun record, quindi è necessario considerarlo nel proprio codice.
Relequestual

Il problema con Fisher-Yates è che devi avere l'intero set di dati in memoria per poterlo selezionare. Non possibile per set di dati molto grandi :(
CpILL

16

Ecco una decisione che funziona per me. Immagino sia molto semplice da capire ed eseguire.

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;

6
Penso che questa soluzione funzioni come ORDER BY random()funziona ma potrebbe non essere efficiente quando si lavora con una tabella di grandi dimensioni.
Anh Cao,

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

Se sai quante righe vuoi, dai un'occhiata tsm_system_rows.

tsm_system_rows

Il modulo fornisce il metodo di campionamento della tabella SYSTEM_ROWS, che può essere utilizzato nella clausola TABLESAMPLE di un comando SELECT.

Questo metodo di campionamento della tabella accetta un singolo argomento intero che rappresenta il numero massimo di righe da leggere. L'esempio risultante conterrà sempre esattamente quel numero di righe, a meno che la tabella non contenga abbastanza righe, nel qual caso viene selezionata l'intera tabella. Come il metodo di campionamento SYSTEM integrato, SYSTEM_ROWS esegue il campionamento a livello di blocco, in modo che il campione non sia completamente casuale ma possa essere soggetto a effetti di raggruppamento, specialmente se è richiesto solo un numero limitato di righe.

Innanzitutto installa l'estensione

CREATE EXTENSION tsm_system_rows;

Quindi la tua query,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);

2
Ho aggiunto un link alla tua risposta aggiunta, è un notevole miglioramento rispetto al SYSTEMmetodo integrato .
Erwin Brandstetter,

Ho appena risposto a una domanda qui (disco singolo casuale) durante il quale ho eseguito un considerevole benchmarking e test delle estensioni tsm_system_rowse tsm_system_time. Per quanto posso vedere, sono praticamente inutili per tutto tranne che per la selezione assolutamente minima di righe casuali. Le sarei grato se potessi dare una rapida occhiata e commentare la validità o meno della mia analisi.
Vérace,

6

Se si desidera solo una riga, è possibile utilizzare un offsetderivato calcolato da count.

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

2

È possibile una variante della visione materializzata "Possibile alternativa" delineata da Erwin Brandstetter .

Supponiamo, ad esempio, che non desideri duplicati nei valori randomizzati che vengono restituiti. Quindi dovrai impostare un valore booleano nella tabella principale contenente il tuo set di valori (non randomizzato).

Supponendo che questa sia la tabella di input:

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

Popolare il ID_VALUEStavolo secondo necessità. Quindi, come descritto da Erwin, crea una vista materializzata che randomizza la ID_VALUEStabella una volta:

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

Si noti che la vista materializzata non contiene la colonna utilizzata, poiché questa diventerà rapidamente obsoleta. Né la vista deve contenere altre colonne che potrebbero trovarsi nella id_valuestabella.

Per ottenere (e "consumare") valori casuali, utilizzare un AGGIORNAMENTO-RITORNO su id_values, selezionando id_valuesda id_values_randomizedcon un join e applicando i criteri desiderati per ottenere solo le possibilità pertinenti. Per esempio:

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;

Cambia LIMITse necessario - se hai bisogno di un solo valore casuale alla volta, cambia LIMITin 1.

Con gli indici corretti attivi id_values, credo che l'UPDATE-RETURNING dovrebbe essere eseguito molto rapidamente con poco carico. Restituisce valori casuali con un round-trip del database. I criteri per le righe "idonee" possono essere complessi quanto richiesto. Nuove righe possono essere aggiunte alla id_valuestabella in qualsiasi momento e diventeranno accessibili all'applicazione non appena viene aggiornata la vista materializzata (che può essere probabilmente eseguita in un momento non di punta). La creazione e l'aggiornamento della vista materializzata saranno lenti, ma devono essere eseguiti solo quando vengono aggiunti nuovi ID alla id_valuestabella.


molto interessante. Funzionerebbe se avessi bisogno non solo di selezionare, ma anche di aggiornare usando select..per aggiornare con un pg_try_advisory_xact_lock? (cioè ho bisogno di molte letture E scritture simultanee)
Mathieu,

1

Una lezione dalla mia esperienza:

offset floor(random() * N) limit 1non è più veloce di order by random() limit 1.

Ho pensato che l' offsetapproccio sarebbe stato più veloce perché avrebbe risparmiato il tempo di smistamento in Postgres. Si scopre che non lo era.


0

Aggiungi una colonna chiamata rcon type serial. Indice r.

Supponiamo di avere 200.000 righe, genereremo un numero casuale n, dove 0 n<<= 200, 000.

Seleziona le righe con r > n, ASCordinale e seleziona la più piccola.

Codice:

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

Il codice si spiega da sé. La subquery al centro viene utilizzata per stimare rapidamente i conteggi delle righe della tabella da https://stackoverflow.com/a/7945274/1271094 .

A livello di applicazione è necessario eseguire nuovamente l'istruzione se n> il numero di righe o è necessario selezionare più righe.


Mi piace perché è breve ed elegante :) E ho anche trovato un modo per migliorarlo: EXPLAIN ANALYZE mi dice che in questo modo, un indice PKEY non verrà utilizzato perché random () restituisce un doppio, mentre PKEY ha bisogno di un BIGINT.
fxtentacle,

seleziona * da YOUR_TABLE dove r> (seleziona (seleziona reltuples :: bigint come stima AS da pg_class dove oid = 'public.YOUR_TABLE' :: regclass) * random ()) :: BIGINT ordina per r asc limit (1);
fxtentacle,

0

So di essere un po 'in ritardo alla festa, ma ho appena trovato questo fantastico strumento chiamato pg_sample :

pg_sample - estrarre un piccolo set di dati di esempio da un database PostgreSQL più grande mantenendo l'integrità referenziale.

Ho provato questo con un database di 350 milioni di righe ed è stato davvero veloce, non so della casualità .

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.