Ottimizzazione delle query su un intervallo di timestamp (due colonne)


96

Uso PostgreSQL 9.1 su Ubuntu 12.04.

Devo selezionare i record in un intervallo di tempo: la mia tabella time_limitsha due timestampcampi e una integerproprietà. Ci sono ulteriori colonne nella mia tabella reale che non sono coinvolte con questa query.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Questa tabella contiene circa 2 milioni di record.

Query come le seguenti hanno richiesto enormi quantità di tempo:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Quindi ho provato ad aggiungere un altro indice - l'inverso del PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

Ho avuto l'impressione che le prestazioni siano migliorate: il tempo per accedere ai record al centro della tabella sembra essere più ragionevole: tra 40 e 90 secondi.

Ma sono ancora diverse decine di secondi per i valori nell'intervallo di tempo. E altre due volte quando si prende di mira la fine del tavolo (in ordine cronologico).

Ho provato explain analyzeper la prima volta a ottenere questo piano di query:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Vedi i risultati su depesz.com.

Cosa potrei fare per ottimizzare la ricerca? Puoi vedere tutto il tempo trascorso scansionando le due colonne timestamp una volta id_phiimpostato su 0. E non capisco la grande scansione (60K righe!) Sui timestamp. Non sono indicizzati dalla chiave primaria e idx_inversedho aggiunto?

Dovrei passare dai tipi di data e ora a qualcos'altro?

Ho letto qualcosa sugli indici GIST e GIN. Capisco che possono essere più efficienti a determinate condizioni per tipi personalizzati. È un'opzione praticabile per il mio caso d'uso?


1
beh, è ​​45s. Non so perché si dice 45ms. Non inizierei nemmeno a lamentarmi se fosse veloce come 45ms ... :-) Forse un errore nell'output di spiega analizzare. O forse è il momento dell'analisi da eseguire. Boh. Ma 40/50 secondi è ciò che misuro.
Stephane Rolland,

2
Il tempo riportato explain analyzenell'output è il tempo richiesto dalla query sul server . Se la query impiega 45 secondi, il tempo aggiuntivo viene impiegato per il trasferimento dei dati dal database al programma che esegue la query. Dopo tutto sono 62682 righe e se ogni riga è grande (ad esempio ha lunghe varcharo textcolonne), ciò può influire sul tempo di trasferimento drasticamente.
a_horse_with_no_name

@a_horse_with_no_name: rows=62682 rowsè la stima del pianificatore . La query restituisce 0 righe. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter,

@ErwinBrandstetter: ah, giusto. L'ho trascurato. Ma non ho mai visto l'output di spiegarsi analizzare mentire sui tempi di esecuzione.
a_horse_with_no_name il

Risposte:


162

Per Postgres 9.1 o successivo:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

Nella maggior parte dei casi, l'ordinamento di un indice è poco rilevante. Postgres può eseguire la scansione all'indietro praticamente alla stessa velocità. Ma per le query di intervallo su più colonne può fare una differenza enorme . Strettamente correlato:

Considera la tua richiesta:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

L'ordinamento della prima colonna id_phinell'indice è irrilevante. Dal momento che è verificato per l' uguaglianza ( =), dovrebbe venire prima. Hai ragione. Altro in questa risposta correlata:

Postgres può saltare id_phi = 0in pochissimo tempo e considerare le seguenti due colonne dell'indice corrispondente. Questi vengono interrogati con condizioni di intervallo di ordinamento invertito ( <=, >=). Nel mio indice, le righe qualificanti vengono prima di tutto. Dovrebbe essere il modo più veloce possibile con un indice B-Tree 1 :

  • Volete start_date_time <= something: index ha prima il primo timestamp.
    • Se si qualifica, controlla anche la colonna 3.
      Recurse fino a quando la prima riga non riesce a qualificarsi (super veloce).
  • Volete end_date_time >= something: index ha prima l'ultimo timestamp.
    • Se si qualifica, continua a recuperare le righe fino a quando il primo non lo fa (super veloce).
      Continua con il valore successivo per la colonna 2 ..

Postgres può eseguire la scansione in avanti o indietro. Il modo in cui hai avuto l'indice, deve leggere tutte le righe corrispondenti sulle prime due colonne e quindi filtrare sulla terza. Assicurati di leggere il capitolo Indici eORDER BY nel manuale. Si adatta abbastanza bene alla tua domanda.

Quante righe corrispondono sulle prime due colonne?
Solo pochi con un start_date_timevicino all'inizio dell'intervallo di tempo della tabella. Ma quasi tutte le file con id_phi = 0alla fine cronologica della tabella! Quindi le prestazioni peggiorano con orari di inizio successivi.

Stime del pianificatore

Il pianificatore stima rows=62682per la tua query di esempio. Di questi, nessuno si qualifica ( rows=0). Potresti ottenere stime migliori se aumenti l'obiettivo statistico per la tabella. Per 2.000.000 di righe ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... potrebbe pagare. O anche più in alto. Altro in questa risposta correlata:

Immagino che non sia necessario per id_phi(solo pochi valori distinti, distribuiti uniformemente), ma per i timestamp (molti valori distinti, distribuiti in modo non uniforme).
Inoltre, non penso che importi molto con l'indice migliorato.

CLUSTER / pg_repack

Se lo desideri più velocemente, puoi semplificare l'ordine fisico delle righe nella tabella. Se puoi permetterti di bloccare il tuo tavolo esclusivamente per un breve periodo di tempo (ad esempio nelle ore di riposo) per riscrivere il tuo tavolo e ordinare le righe in base all'indice:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

Con l'accesso simultaneo, considera pg_repack , che può fare lo stesso senza blocco esclusivo.

In entrambi i casi, l'effetto è che è necessario leggere meno blocchi dalla tabella e tutto è preordinato. È un effetto una tantum che si deteriora nel tempo con le scritture sul tavolo che frammentano l'ordinamento fisico.

Indice GiST in Postgres 9.2+

1 Con pg 9.2+ esiste un'altra opzione, forse più veloce: un indice GiST per una colonna di intervallo.

  • Esistono tipi di intervallo integrati per timestampe timestamp with time zone: tsrange,tstzrange . Un indice btree è in genere più veloce per una integercolonna aggiuntiva come id_phi. Anche più piccolo ed economico da mantenere. Ma la query sarà probabilmente ancora più veloce nel complesso con l'indice combinato.

  • Modifica la definizione della tabella o utilizza un indice di espressione .

  • Per l'indice GiST a più colonne a portata di mano è necessario btree_gistinstallare anche il modulo aggiuntivo (una volta per database) che fornisce alle classi di operatori l'inclusione di un integer.

La trifecta! Un indice GiST funzionale a più colonne :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Utilizzare ora l' operatore "contiene intervallo"@> nella query:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Indice SP-GiST in Postgres 9.3+

Un indice SP-GiST potrebbe essere ancora più veloce per questo tipo di query - a parte questo, citando il manuale :

Attualmente, solo i tipi di indice B-tree, GiST, GIN e BRIN supportano indici a più colonne.

Ancora vero in Postgres 12.
Dovresti combinare un spgistindice solo (tsrange(...))con un secondo btreeindice su (id_phi). Con l'overhead aggiunto, non sono sicuro che questo possa competere.
Risposta correlata con un benchmark per una sola tsrangecolonna:


78
Dovrei dirlo almeno una volta, che ognuna delle tue risposte su SO e DBA ha un valore aggiunto / competenza davvero elevati , e il più delle volte il più completo. Giusto per dirlo una volta: rispetto !.
Stephane Rolland,

1
Merci bien! :) Quindi hai ottenuto risultati più veloci?
Erwin Brandstetter,

Devo lasciare terminare la grande copia in blocco generata dalla mia query intensamente imbarazzante, quindi rendendo il processo molto lento, stava girando per ore prima di porre la domanda. Ma ho calcolato e ho deciso di lasciarlo girare fino a domani mattina, sarà finito, e il nuovo tavolo pronto per essere riempito domani. Ho provato a creare il tuo indice contemporaneamente durante il lavoro, ma a causa dell'eccessivo accesso (credo), la creazione dell'indice dovrebbe essere bloccata. Ripeterò lo stesso tempo di prova domani con la tua soluzione. Ho anche visto come l'aggiornamento a 9.2 ;-) per debian / ubuntu.
Stephane Rolland,

2
@StephaneRolland: sarebbe comunque interessante il motivo per cui l'output di spiegazione dell'analisi mostra 45 millisecondi mentre vedi che la query impiega più di 40 secondi.
a_horse_with_no_name il

1
@John: Postgres può attraversare un indice in avanti o indietro, ma non può cambiare direzione nella stessa scansione. Idealmente, hai tutte le righe qualificanti per nodo prima (o ultima), ma deve essere lo stesso allineamento (corrispondenti ai predicati della query) per tutte le colonne per ottenere i migliori risultati.
Erwin Brandstetter,

5

La risposta di Erwin è già completa, tuttavia:

I tipi di intervallo per i timestamp sono disponibili in PostgreSQL 9.1 con l'estensione Temporal di Jeff Davis: https://github.com/jeff-davis/PostgreSQL-Temporal

Nota: ha funzionalità limitate (usa Timestamptz e puoi solo sovrapporre lo stile '[)'). Inoltre, ci sono molte altre ottime ragioni per passare a PostgreSQL 9.2.


3

Potresti provare a creare l'indice a più colonne in un ordine diverso:

primary key(id_phi, start_date_time,end_date_time);

Una volta ho pubblicato una domanda simile anche correlata all'ordinamento degli indici su un indice a più colonne. La chiave sta cercando di utilizzare prima le condizioni più restrittive per ridurre lo spazio di ricerca.

Modifica : il mio errore. Ora vedo che hai già definito questo indice.


Ho già entrambi gli indici. Tranne che la chiave primaria è l'altra, ma l'indice che proponi esiste già ed è quello che viene usato se guardi la spiegazione:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland

1

Sono riuscito ad aumentare rapidamente (da 1 secondo a 70 ms)

Ho una tabella con aggregazioni di molte misure e molti livelli ( lcolonna) (30s, 1m, 1h, ecc.) Ci sono due colonne limitate: $sper inizio e $eper fine.

Ho creato due indici a più colonne: uno per l'inizio e uno per la fine.

Ho modificato select query: seleziona gli intervalli in cui il loro limite iniziale è compreso nell'intervallo dato. selezionare inoltre intervalli in cui il loro limite finale è compreso nell'intervallo specificato.

Spiega mostra due flussi di righe che utilizzano i nostri indici in modo efficiente.

indici:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Seleziona query:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Spiegare:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

Il trucco è che i nodi del piano contengono solo le righe desiderate. In precedenza abbiamo ottenuto migliaia di righe nel nodo del piano perché selezionato all points from some point in time to the very end, quindi il nodo successivo ha rimosso le righe non necessarie.

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.