Indici per query SQL con condizione WHERE e GROUP BY


15

Sto cercando di determinare quali indici utilizzare per una query SQL con una WHEREcondizione e una GROUP BYche è attualmente in esecuzione molto lenta.

La mia domanda:

SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id

La tabella ha attualmente 32.000.000 di righe. Il tempo di esecuzione della query aumenta molto quando aumento il periodo di tempo.

La tabella in questione è simile alla seguente:

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id bigint NOT NULL
);

Al momento ho i seguenti indici, ma le prestazioni sono ancora lente:

CREATE INDEX ts_index
  ON counter
  USING btree
  (ts);

CREATE INDEX group_id_index
  ON counter
  USING btree
  (group_id);

CREATE INDEX comp_1_index
  ON counter
  USING btree
  (ts, group_id);

CREATE INDEX comp_2_index
  ON counter
  USING btree
  (group_id, ts);

L'esecuzione di EXPLAIN sulla query fornisce il seguente risultato:

"QUERY PLAN"
"HashAggregate  (cost=467958.16..467958.17 rows=1 width=4)"
"  ->  Index Scan using ts_index on counter  (cost=0.56..467470.93 rows=194892 width=4)"
"        Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"

SQL Fiddle con dati di esempio: http://sqlfiddle.com/#!15/7492b/1

La domanda

È possibile migliorare le prestazioni di questa query aggiungendo indici migliori o è necessario aumentare la potenza di elaborazione?

Modifica 1

Viene utilizzata la versione 9.3.2 di PostgreSQL.

Modifica 2

Ho provato la proposta di @Erwin con EXISTS:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

Ma sfortunatamente questo non sembra aumentare le prestazioni. Il piano di query:

"QUERY PLAN"
"Nested Loop Semi Join  (cost=1607.18..371680.60 rows=113 width=4)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Bitmap Heap Scan on counter c  (cost=1607.18..158895.53 rows=60641 width=4)"
"        Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        ->  Bitmap Index Scan on comp_2_index  (cost=0.00..1592.02 rows=60641 width=0)"
"              Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

Modifica 3

Il piano di query per la query LATERAL da ypercube:

"QUERY PLAN"
"Nested Loop  (cost=8.98..1200.42 rows=133 width=20)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Result  (cost=8.98..8.99 rows=1 width=0)"
"        One-Time Filter: ($1 IS NOT NULL)"
"        InitPlan 1 (returns $1)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan using comp_2_index on counter c  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        InitPlan 2 (returns $2)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan Backward using comp_2_index on counter c_1  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

Quanti group_idvalori diversi ci sono sul tavolo?
ypercubeᵀᴹ

Esistono 133 differenti group_id.

I timestamp vanno dal 2011 al 2014. Sono in uso sia secondi che millisecondi.

Ti interessa solo group_ide non in alcun modo?
Erwin Brandstetter,

@Erwin Siamo interessati anche a max () e (min) su una quarta colonna non mostrata nell'esempio.
uldall,

Risposte:


6

Un'altra idea, che utilizza anche il groupstavolo e una costruzione chiamataLATERAL join (per i fan di SQL Server, questo è quasi identico a OUTER APPLY). Ha il vantaggio che gli aggregati possono essere calcolati nella sottoquery:

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

Prova a SQL-Fiddle mostra che la query esegue scansioni dell'indice(group_id, ts)sull'indice.

Piani simili vengono prodotti utilizzando 2 join laterali, uno per min e uno per max e anche con 2 subquery correlate in linea. Potrebbero anche essere utilizzati se è necessario mostrare tutte le counterrighe oltre alle date min e max:

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;

@ypercube Ho aggiunto il piano di query per la tua query alla domanda originale. La query viene eseguita in meno di 50 ms anche su lunghi intervalli di tempo.
uldall

5

Poiché non è presente alcun aggregato nell'elenco di selezione, il group by è praticamente lo stesso di inserire un distinctnell'elenco di selezione, giusto?

Se è quello che desideri, potresti essere in grado di ottenere una rapida ricerca dell'indice su comp_2_index riscrivendolo per utilizzare una query ricorsiva, come descritto nel wiki PostgreSQL .

Esegui una vista per restituire in modo efficiente i group_ids distinti:

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

E quindi usa quella vista al posto della tabella di ricerca nel existssemi-join di Erwin .


4

Dato che ce ne sono solo 133 different group_id's, potresti usare integer(o anche smallint) per il group_id. Non ti comprerà molto, però, perché il riempimento a 8 byte mangerà il resto nella tua tabella e possibili indici a più colonne. L'elaborazione di plain integerdovrebbe essere un po 'più veloce, però. Altro su intvsint2 .

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@Leo: i timestamp sono memorizzati come numeri interi a 8 byte nelle installazioni moderne e possono essere elaborati perfettamente velocemente. Dettagli.

@ypercube: l'indice on (group_id, ts)non può essere d'aiuto, poiché group_idnella query non è presente alcuna condizione .

Il tuo problema principale è l'enorme quantità di dati che devono essere elaborati:

Scansione indice utilizzando ts_index sul contatore (costo = 0,56..467470,93 righe = 194892 larghezza = 4)

Vedo che sei interessato solo all'esistenza di un group_id, e nessun conteggio effettivo. Inoltre, ci sono solo 133 differenti group_ids. Pertanto la tua query può essere soddisfatta con il primo hit per gorup_idnell'intervallo di tempo. Da qui questo suggerimento per una query alternativa con un EXISTSsemi-join :

Supponendo una tabella di ricerca per gruppi:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

Il tuo indice comp_2_indexsulla (group_id, ts)diventa strumentale ora.

SQL Fiddle (basandosi sul violino fornito da @ypercube nei commenti)

Qui, la query preferisce l'indice (ts, group_id), ma penso che sia a causa della configurazione del test con timestamp "cluster". Se si rimuovono gli indici con il comando principale ts( ulteriori informazioni al riguardo ), anche il pianificatore utilizzerà felicemente l'indice, in (group_id, ts)particolare in una scansione solo indice .

Se funziona, potresti non aver bisogno di questo altro possibile miglioramento: pre-aggregare i dati in una vista materializzata per ridurre drasticamente il numero di righe. Ciò avrebbe senso in particolare, se avessi bisogno anche di conteggi effettivi . Quindi hai il costo di elaborare più righe una volta durante l'aggiornamento di mv. È anche possibile combinare aggregati giornalieri e orari (due tabelle separate) e adattare la query a quella.

I tempi nelle tue query sono arbitrari? O principalmente a minuti / ore / giorni completi?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

Crea gli indici necessari su counter_mve adatta la tua query per lavorare con essa ...


1
Ho provato diverse cose simili in SQL-Fiddle , con 10k righe, ma tutte hanno mostrato una scansione sequenziale. L'uso della groupstabella fa la differenza?
ypercubeᵀᴹ

@ypercube: penso di si. Inoltre, ANALYZEfa la differenza. Ma anche gli indici su countersi abituano senza ANALYZEche io introduca la groupstabella. Il punto è che, senza quella tabella, è comunque necessario un seqscan per costruire l'insieme dei possibili group_id. Ho aggiunto altro alla mia risposta. E grazie per il tuo violino!
Erwin Brandstetter,

È strano. Stai dicendo che l'ottimizzatore di Postgres non utilizzerà l'indice group_idnemmeno per una SELECT DISTINCT group_id FROM t;query?
ypercubeᵀᴹ

1
@ErwinBrandstetter Questo è quello che ho pensato anch'io, ed è stato molto sorpreso di scoprire il contrario. Senza un LIMIT 1, può scegliere una scansione dell'indice bitmap, che non beneficia dell'arresto anticipato e richiede molto più tempo. (Ma se la tabella viene aspirata di recente, potrebbe preferire la scansione indexonly rispetto alla scansione bitmap, quindi il comportamento che vedi dipende dallo stato di vuoto della tabella).
jjanes,

1
@uldall: gli aggregati giornalieri ridurranno drasticamente il numero di righe. Questo dovrebbe fare il trucco. Ma assicurati di provare la query EXISTS. Potrebbe essere sorprendentemente veloce. Non funzionerà per min / max in aggiunta. Sarei interessato alla prestazione risultante, però, se tu fossi così gentile da lasciar perdere una linea qui.
Erwin Brandstetter,
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.