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

Tablesample e altri metodi per ottenere tuple casuali

TABLESAMPLE di PostgreSQL offre alcuni vantaggi in più rispetto ad altri metodi tradizionali per ottenere tuple casuali.

TABLESAMPLE è una clausola SQL SELECT e fornisce due metodi di campionamento che sono SYSTEM e BERNOULLI . Con l'aiuto di TABLESAMPLE possiamo facilmente recuperare righe casuali da una tabella. Per ulteriori informazioni su TABLESAMPLE puoi controllare il post del blog precedente .

In questo post del blog parleremo di modi alternativi per ottenere righe casuali. Come le persone selezionavano righe casuali prima di TABLESAMPLE , quali sono i pro ei contro degli altri metodi e cosa abbiamo guadagnato con TABLESAMPLE ?

Ci sono fantastici post sul blog sulla selezione di righe casuali, puoi iniziare a leggere i seguenti post sul blog per acquisire una profonda comprensione di questo argomento.

I miei pensieri su come ottenere una riga casuale

Ottenere righe casuali da una tabella del database

casuale_agg()

Confrontiamo i metodi tradizionali per ottenere righe casuali da una tabella con i nuovi metodi forniti da TABLESAMPLE.

Prima del TABLESAMPLE clausola, c'erano 3 metodi comunemente usati per la selezione casuale di righe da una tabella.

1- Ordina per random()

A scopo di test, dobbiamo creare una tabella e inserire alcuni dati al suo interno.

Creiamo la tabella ts_test e inseriamo 1M di righe al suo interno:

CREATE TABLE ts_test (
  id SERIAL PRIMARY KEY,
  title TEXT
);

INSERT INTO ts_test (title)
SELECT
    'Record #' || i
FROM
    generate_series(1, 1000000) i;

Considerando la seguente istruzione SQL per selezionare 10 righe casuali:

SELECT * FROM ts_test ORDER BY random() LIMIT 10;

Fa sì che PostgreSQL esegua una scansione completa della tabella e anche l'ordinamento. Pertanto questo metodo non è preferito per le tabelle con un numero elevato di righe per motivi di prestazioni.

Esaminiamo EXPLAIN ANALYZE output di questa query sopra:

random=# EXPLAIN ANALYZE SELECT * FROM ts_test ORDER BY random() LIMIT 10;
 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Limit (cost=33959.03..33959.05 rows=10 width=36) (actual time=1956.786..1956.807 rows=10 loops=1)
 -> Sort (cost=33959.03..35981.18 rows=808863 width=36) (actual time=1956.780..1956.789 rows=10 loops=1)
 Sort Key: (random())
 Sort Method: top-N heapsort Memory: 25kB
 -> Seq Scan on ts_test (cost=0.00..16479.79 rows=808863 width=36) (actual time=0.276..1001.109 rows=1000000 loops=1)
 Planning time: 1.434 ms
 Execution time: 1956.900 ms
(7 rows)

Come EXPLAIN ANALYZE sottolinea, la selezione di 10 righe su 1 milione ha richiesto quasi 2 secondi. La query utilizzava anche l'output di random() come chiave di ordinamento per ordinare i risultati. L'ordinamento sembra essere l'attività che richiede più tempo qui. Eseguiamolo con lo scenario usando TABLESAMPLE .

In PostgreSQL 9.5, per ottenere il numero esatto di righe in modo casuale, possiamo usare il metodo di campionamento SYSTEM_ROWS. Fornito da tsm_system_rows modulo contrib, ci permette di specificare quante righe devono essere restituite dal campionamento. Normalmente solo la percentuale richiesta può essere specificata con TABLESAMPLE SYSTEMBERNOULLI metodi.

Per prima cosa, dobbiamo creare tsm_system_rows estensione per l'utilizzo di questo metodo poiché entrambi TABLESAMPLE SYSTEM e TABLESAMPLE BERNOULLI i metodi non garantiscono che la percentuale fornita risulterà nel numero previsto di righe. Si prega di controllare il precedente TABLESAMPLE p ost per ricordare perché funzionano così.

Iniziamo creando tsm_system_rows estensione:

random=# CREATE EXTENSION tsm_system_rows;
CREATE EXTENSION

Ora confrontiamo "ORDER BY random()EXPLAIN ANALYZE output con EXPLAIN ANALYZE output di tsm_system_rows query che restituisce 10 righe casuali da una tabella di 1 milione di righe.

random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE SYSTEM_ROWS(10);
 QUERY PLAN
-------------------------------------------------------------------------------------------------------
 Sample Scan on ts_test (cost=0.00..4.10 rows=10 width=18) (actual time=0.069..0.083 rows=10 loops=1)
 Sampling: system_rows ('10'::bigint)
 Planning time: 0.646 ms
 Execution time: 0.159 ms
(4 rows)

L'intera query ha richiesto 0,159 ms. Questo metodo di campionamento è estremamente veloce rispetto a "ORDER BY random() ” metodo che ha richiesto 1956,9 ms.

2- Confronta con random()

Il seguente SQL ci consente di recuperare righe casuali con una probabilità del 10%

SELECT * FROM ts_test WHERE random() <= 0.1;

Questo metodo è più veloce dell'ordinamento casuale perché non ordina le righe selezionate. Restituirà una percentuale approssimativa di righe dalla tabella proprio come BERNOULLI o SYSTEM TABLESAMPLE metodi.

Controlliamo il EXPLAIN ANALYZE output di random() query sopra:

random=# EXPLAIN ANALYZE SELECT * FROM ts_test WHERE random() <= 0.1;
 QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Seq Scan on ts_test (cost=0.00..21369.00 rows=333333 width=18) (actual time=0.089..280.483 rows=100014 loops=1)
 Filter: (random() <= '0.1'::double precision)
 Rows Removed by Filter: 899986
 Planning time: 0.704 ms
 Execution time: 367.527 ms
(5 rows)

La query ha richiesto 367,5 ms. Molto meglio di ORDER BY random() . Ma è più difficile controllare il conteggio esatto delle righe. Confrontiamo "random()EXPLAIN ANALYZE output con EXPLAIN ANALYZE output di TABLESAMPLE BERNOULLI query che restituisce circa il 10% di righe casuali dalla tabella di 1 milione di righe.

random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE BERNOULLI (10);
 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
 Sample Scan on ts_test  (cost=0.00..7369.00 rows=100000 width=18) (actual time=0.015..147.002 rows=100155 loops=1)
   Sampling: bernoulli ('10'::real)
 Planning time: 0.076 ms
 Execution time: 239.289 ms
(4 rows)

Abbiamo dato 10 come parametro a BERNOULLI perché abbiamo bisogno del 10% di tutti i record.

Qui possiamo vedere che il BERNOULLI il metodo ha richiesto 239,289 ms per l'esecuzione. Questi due metodi sono abbastanza simili nel modo in cui funzionano, BERNOULLI è leggermente più veloce in quanto la selezione casuale viene eseguita a un livello inferiore. Un vantaggio nell'usare BERNOULLI rispetto a WHERE random() <= 0.1 è il REPEATABLE clausola che abbiamo descritto nel precedente TABLESAMPLE posta.

3- Colonna aggiuntiva con un valore casuale

Questo metodo suggerisce di aggiungere una nuova colonna con valori assegnati casualmente, aggiungere un indice ad essa, eseguire l'ordinamento in base a quella colonna e, facoltativamente, aggiornare periodicamente i loro valori per randomizzare la distribuzione.

Questa strategia consente un campionamento casuale per lo più ripetibile. Funziona molto più velocemente del primo metodo, ma richiede uno sforzo per la configurazione per la prima volta e comporta un costo in termini di prestazioni nelle operazioni di inserimento, aggiornamento ed eliminazione.

Applichiamo questo metodo sul nostro ts_test tabella.

Per prima cosa, aggiungeremo una nuova colonna chiamata randomcolumn con il tipo a doppia precisione perché random() di PostgreSQL la funzione restituisce un numero con precisione doppia.

random=# ALTER TABLE ts_test ADD COLUMN randomcolumn DOUBLE PRECISION;
ALTER TABLE

Quindi aggiorneremo la nuova colonna utilizzando random() funzione.

random=# \timing
Timing is on.
random=# BEGIN;
BEGIN
Time: 2.071 ms
random=# UPDATE ts_test SET randomcolumn = RANDOM();
UPDATE 1000000
Time: 8483.741 ms
random=# COMMIT;
COMMIT
Time: 2.615 ms

Questo metodo ha un costo iniziale per la creazione di una nuova colonna e il popolamento di quella nuova colonna con valori casuali (0,0-1,0), ma è un costo una tantum. In questo esempio, per 1 milione di righe, ci sono voluti quasi 8,5 secondi.

Proviamo ad osservare se i nostri risultati sono riproducibili interrogando 100 righe con il nostro nuovo metodo:

random=# SELECT * FROM ts_test ORDER BY randomcolumn LIMIT 100;
 -------+---------------+----------------------
 13522  | Record #13522  | 6.4261257648468e-08
 671584 | Record #671584 | 6.4261257648468e-07
 714012 | Record #714012 | 1.95764005184174e-06
 162016 | Record #162016 | 3.44449654221535e-06
 106867 | Record #106867 | 3.66196036338806e-06
 865669 | Record #865669 | 3.96883115172386e-06
 927    | Record #927    | 4.65428456664085e-06
 526017 | Record #526017 | 4.65987250208855e-06
 98338  | Record #98338  | 4.91179525852203e-06
 769625 | Record #769625 | 4.91319224238396e-06
 ...
 462484 | Record #462484 | 9.83504578471184e-05
 (100 rows)

Quando eseguiamo la query sopra, otteniamo principalmente lo stesso set di risultati, ma questo non è garantito perché abbiamo utilizzato random() funzione per popolare randomcolumn valori e in questo caso più di una colonna potrebbe avere lo stesso valore. Per essere sicuri di ottenere gli stessi risultati ogni volta che viene eseguito, dovremmo migliorare la nostra query aggiungendo la colonna ID a ORDER BY clausola, in questo modo assicuriamo che ORDER BY La clausola specifica un insieme univoco di righe, perché la colonna id contiene l'indice della chiave primaria.

Ora eseguiamo la query migliorata di seguito:

random=# SELECT * FROM ts_test ORDER BY randomcolumn, id LIMIT 100;
 id | title | randomcolumn
--------+----------------+----------------------
 13522  | Record #13522  | 6.4261257648468e-08
 671584 | Record #671584 | 6.4261257648468e-07
 714012 | Record #714012 | 1.95764005184174e-06
 162016 | Record #162016 | 3.44449654221535e-06
 106867 | Record #106867 | 3.66196036338806e-06
 865669 | Record #865669 | 3.96883115172386e-06
 927    | Record #927    | 4.65428456664085e-06
 526017 | Record #526017 | 4.65987250208855e-06
 98338  | Record #98338  | 4.91179525852203e-06
 769625 | Record #769625 | 4.91319224238396e-06 
 ...
 462484 | Record #462484 | 9.83504578471184e-05
 (100 rows)

Ora siamo sicuri di poter ottenere campioni casuali riproducibili utilizzando questo metodo.

È ora di vedere EXPLAIN ANALYZE risultati della nostra domanda finale:

random=# EXPLAIN ANALYZE SELECT * FROM ts_test ORDER BY randomcolumn, id LIMIT 100;
 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Limit (cost=55571.28..55571.53 rows=100 width=26) (actual time=1951.811..1952.039 rows=100 loops=1)
 -> Sort (cost=55571.28..58071.28 rows=1000000 width=26) (actual time=1951.804..1951.891 rows=100 loops=1)
 Sort Key: randomcolumn, id
 Sort Method: top-N heapsort Memory: 32kB
 -> Seq Scan on ts_test (cost=0.00..17352.00 rows=1000000 width=26) (actual time=0.058..995.011 rows=1000000 loops=1)
 Planning time: 0.481 ms
 Execution time: 1952.215 ms
(7 rows)

Per confrontare questo metodo con TABLESAMPLE metodi, scegliamo SYSTEM metodo con REPEATABLE opzione, poiché questo metodo ci dà risultati riproducibili.

TABLESAMPLE SYSTEM il metodo fondamentalmente fa il campionamento a livello di blocco/pagina; legge e restituisce pagine casuali dal disco. Pertanto fornisce casualità a livello di pagina anziché a livello di tupla. Ecco perché è difficile controllare esattamente il conteggio delle righe recuperate. Se non ci sono pagine selezionate potremmo non ottenere alcun risultato. Anche il campionamento a livello di pagina tende all'effetto di raggruppamento.

TABLESAMPLE LA SINTASSI è la stessa per BERNOULLI e SYSTEM metodi, specificheremo la percentuale come abbiamo fatto in BERNOULLI metodo.

Promemoria rapido: SINTASSI

SELECT select_expression
FROM table_name
TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ]
...

Per recuperare circa 100 righe, dobbiamo richiedere 100 * 100 / 1 000 000 =0,01% delle righe della tabella. Quindi la nostra percentuale sarà 0,01.

Prima di iniziare a interrogare, ricordiamo come REPEATABLE lavori. Fondamentalmente sceglieremo un parametro seed e otterremo gli stessi risultati ogni volta che utilizzeremo lo stesso seme con la query precedente. Se forniamo un seme diverso, la query produrrà un set di risultati abbastanza diverso.

Proviamo a vedere i risultati con le query.

random=# Select * from ts_test tablesample system (0.01) repeatable (60);
 id | title | randomcolumn
--------+----------------+---------------------
 659598 | Record #659598 | 0.964113113470376
 659599 | Record #659599 | 0.531714483164251
 659600 | Record #659600 | 0.477636905387044
 659601 | Record #659601 | 0.861940925940871
 659602 | Record #659602 | 0.545977566856891
 659603 | Record #659603 | 0.326795583125204
 659604 | Record #659604 | 0.469275736715645
 659605 | Record #659605 | 0.734953186474741
 659606 | Record #659606 | 0.41613544523716
 ...
 659732 | Record #659732 | 0.893704727292061
 659733 | Record #659733 | 0.847225237637758
 (136 rows)

Otteniamo 136 righe, poiché puoi considerare che questo numero dipende da quante righe sono memorizzate in una singola pagina.

Ora proviamo a eseguire la stessa query con lo stesso numero di seed:

random=# Select * from ts_test tablesample system (0.01) repeatable (60);
 id | title | randomcolumn
--------+----------------+---------------------
 659598 | Record #659598 | 0.964113113470376
 659599 | Record #659599 | 0.531714483164251
 659600 | Record #659600 | 0.477636905387044
 659601 | Record #659601 | 0.861940925940871
 659602 | Record #659602 | 0.545977566856891
 659603 | Record #659603 | 0.326795583125204
 659604 | Record #659604 | 0.469275736715645
 659605 | Record #659605 | 0.734953186474741
 659606 | Record #659606 | 0.41613544523716 
 ...
 659732 | Record #659732 | 0.893704727292061
 659733 | Record #659733 | 0.847225237637758
 (136 rows)

Otteniamo lo stesso set di risultati grazie a REPEATABLE opzione. Ora possiamo confrontare TABLESAMPLE SYSTEM metodo con metodo a colonna casuale guardando EXPLAIN ANALYZE uscita.

random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE SYSTEM (0.01) REPEATABLE (60);
 QUERY PLAN
---------------------------------------------------------------------------------------------------------
 Sample Scan on ts_test (cost=0.00..5.00 rows=100 width=26) (actual time=0.091..0.230 rows=136 loops=1)
 Sampling: system ('0.01'::real) REPEATABLE ('60'::double precision)
 Planning time: 0.323 ms
 Execution time: 0.398 ms
(4 rows)

SYSTEM il metodo utilizza la scansione del campione ed è estremamente veloce. L'unico aspetto negativo di questo metodo è che non possiamo garantire che la percentuale fornita comporterà il numero previsto di righe.

Conclusione

In questo post del blog abbiamo confrontato TABLESAMPLE standard (SYSTEM , BERNOULLI ) e il tsm_system_rows modulo con i metodi casuali precedenti.

Qui puoi rivedere rapidamente i risultati:

  • ORDER BY random() è lento a causa dell'ordinamento
  • random() <= 0.1 è simile a BERNOULLI metodo
  • L'aggiunta di una colonna con un valore casuale richiede un lavoro iniziale e potrebbe causare problemi di prestazioni
  • SYSTEM è veloce ma è difficile controllare il numero di righe
  • tsm_system_rows è veloce e può controllare il numero delle righe

Di conseguenza tsm_system_rows batte qualsiasi altro metodo per selezionare solo poche righe casuali.

Ma il vero vincitore è sicuramente TABLESAMPLE si. Grazie alla sua estensibilità, estensioni personalizzate (es. tsm_system_rows , tsm_system_time ) può essere sviluppato utilizzando TABLESAMPLE funzioni del metodo.

Nota per gli sviluppatori: Maggiori informazioni su come scrivere metodi di campionamento personalizzati sono disponibili nel capitolo Scrittura di un metodo di campionamento su tabelle della documentazione di PostgreSQL.

Nota per il futuro: Discuteremo del progetto AXLE e dell'estensione tsm_system_time nel nostro prossimo TABLESAMPLE post del blog.