Come gestire un piano di query errato causato dalla parità esatta sul tipo di intervallo?


28

Sto eseguendo un aggiornamento in cui ho bisogno di una parità esatta su una tstzrangevariabile. Vengono modificate ~ 1 milione di righe e la query richiede ~ 13 minuti. Il risultato di EXPLAIN ANALYZEpuò essere visto qui e i risultati effettivi sono estremamente diversi da quelli stimati dal pianificatore di query. Il problema è che la scansione dell'indice su si t_rangeaspetta che venga restituita una singola riga.

Ciò sembra essere correlato al fatto che le statistiche sui tipi di intervallo sono memorizzate in modo diverso da quelle di altri tipi. Guardando la pg_statsvista per la colonna, n_distinctè -1 e altri campi (ad esempio most_common_vals, most_common_freqs) sono vuoti.

Tuttavia, ci devono essere statistiche archiviate da t_rangequalche parte. Un aggiornamento estremamente simile in cui utilizzo un 'entro' su t_range anziché una parità esatta richiede circa 4 minuti per essere eseguito e utilizza un piano di query sostanzialmente diverso (vedi qui ). Il secondo piano di query ha senso per me perché verranno utilizzate tutte le righe nella tabella temporanea e una parte sostanziale della tabella cronologica. Ancora più importante, il pianificatore di query prevede un numero approssimativamente corretto di righe per il filtro attivo t_range.

La distribuzione di t_rangeè un po 'insolita. Sto usando questa tabella per memorizzare lo stato storico di un'altra tabella e le modifiche all'altra tabella si verificano tutte contemporaneamente in grandi discariche, quindi non ci sono molti valori distinti di t_range. Ecco i conteggi corrispondenti a ciascuno dei valori univoci di t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

I conteggi per i distinti t_rangeprecedenti sono completi, quindi la cardinalità è ~ 3M (di cui ~ 1M sarà influenzato da entrambe le query di aggiornamento).

Perché la query 1 ha prestazioni molto più scarse rispetto alla query 2? Nel mio caso, la query 2 è un buon sostituto, ma se fosse veramente necessaria un'eguaglianza di intervallo esatta, come potrei fare in modo che Postgres utilizzi un piano di query più intelligente?

Definizione della tabella con indici (eliminazione di colonne non pertinenti):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Query 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Query 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Aggiornamenti Q1 999753 righe e aggiornamenti Q2 999753 + 36791 = 1036544 (ovvero, la tabella temporanea è tale che ogni riga corrispondente alla condizione dell'intervallo di tempo viene aggiornata).

Ho provato questa query in risposta al commento di @ ypercube :

Query 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Il piano di query e i risultati (vedi qui ) erano intermedi tra i due casi precedenti (~ 6 minuti).

05/02/2016 EDIT

Non avendo più accesso ai dati dopo 1,5 anni, ho creato una tabella di test con la stessa struttura (senza indici) e cardinalità simile. La risposta di jjanes ha proposto che la causa potrebbe essere l'ordinamento della tabella temporanea utilizzata per l'aggiornamento. Non sono stato in grado di verificare direttamente l'ipotesi perché non ho accesso a track_io_timing(utilizzando Amazon RDS).

  1. I risultati complessivi sono stati molto più rapidi (di un fattore di diversi). Immagino che ciò sia dovuto alla rimozione degli indici, in linea con la risposta di Erwin .

  2. In questo caso di test, le query 1 e 2 hanno impiegato sostanzialmente lo stesso tempo, poiché entrambi hanno utilizzato l'unione di tipo merge. Cioè, non sono stato in grado di innescare ciò che stava causando a Postgres la scelta dell'hash join, quindi non ho chiarezza sul motivo per cui Postgres ha scelto l'hash join con prestazioni scarse in primo luogo.


1
Che cosa succede se hai convertito la condizione uguaglianza (a = b)a due "contiene" condizioni: (a @> b AND b @> a)? Il piano cambia?
ypercubeᵀᴹ

@ypercube: il piano cambia sostanzialmente, anche se non è ancora del tutto ottimale - vedi la mia modifica n. 2.
abeboparebop,

1
Un'altra idea sarebbe quella di aggiungere un normale indice btree (lower(t_range),upper(t_range))dal momento che si controlla l'uguaglianza.
ypercubeᵀᴹ

Risposte:


9

La più grande differenza nel tempo nei tuoi piani di esecuzione è nel nodo in alto, lo stesso UPDATE. Questo suggerisce che la maggior parte del tuo tempo andrà in IO durante l'aggiornamento. È possibile verificarlo accendendo track_io_timinged eseguendo le query conEXPLAIN (ANALYZE, BUFFERS)

I diversi piani presentano righe da aggiornare in diversi ordini. Uno è in trip_idordine e l'altro è in qualunque ordine siano fisicamente presenti nella tabella temporanea.

La tabella in fase di aggiornamento sembra avere il suo ordine fisico correlato alla colonna trip_id e l'aggiornamento delle righe in questo ordine porta a schemi IO efficienti con letture avanti / sequenziali. Mentre l'ordine fisico della tabella temporanea sembra portare a molte letture casuali.

Se è possibile aggiungere un'istruzione order by trip_idall'istruzione che ha creato la tabella temporanea, ciò potrebbe risolvere il problema.

PostgreSQL non tiene conto degli effetti dell'ordinamento IO durante la pianificazione dell'operazione di AGGIORNAMENTO. (A differenza delle operazioni SELECT, in cui vengono prese in considerazione). Se PostgreSQL fosse più intelligente, realizzerebbe o che un piano produce un ordine più efficiente, oppure interpellerebbe un nodo di ordinamento esplicito tra l'aggiornamento e il suo nodo figlio in modo che l'aggiornamento venga alimentato in ordine ctid.

Hai ragione a dire che PostgreSQL fa un cattivo lavoro stimando la selettività dei join di uguaglianza sugli intervalli. Tuttavia, questo è solo tangenzialmente correlato al tuo problema fondamentale. Una query più efficiente sulla parte selezionata dell'aggiornamento potrebbe capitare accidentalmente di alimentare le righe nell'aggiornamento corretto in un ordine migliore, ma in tal caso ciò è per lo più sfortunato.


Purtroppo non sono in grado di modificare track_io_timinge (dato che è passato un anno e mezzo!) Non ho più accesso ai dati originali. Tuttavia, ho testato la tua teoria creando tabelle con lo stesso schema e dimensioni simili (milioni di righe) ed eseguendo due aggiornamenti diversi: uno in cui la tabella di aggiornamento temporanea è stata ordinata come la tabella originale e un'altra in cui è stata ordinata quasi-casuale. Sfortunatamente, i due aggiornamenti richiedono all'incirca la stessa quantità di tempo, il che implica che l'ordinamento della tabella di aggiornamento non influisce su questa query.
abeboparebop,

7

Non sono esattamente sicuro del perché la selettività di un predicato di uguaglianza sia così radicalmente sopravvalutata dall'indice GiST sulla tstzrangecolonna. Sebbene ciò rimanga interessante di per sé, sembra irrilevante per il tuo caso particolare.

Poiché il tuo UPDATEmodifica un terzo (!) Di tutte le righe 3M esistenti, un indice non ti aiuterà affatto . Al contrario, l'aggiornamento incrementale dell'indice oltre alla tabella comporterà un sostanziale costo per il tuo UPDATE.

Mantieni la tua semplice query 1 . La soluzione semplice e radicale è di eliminare l'indice prima del UPDATE. Se è necessario per altri scopi, ricrearlo dopo il UPDATE. Questo sarebbe ancora più veloce del mantenimento dell'indice durante l'ampia UPDATE.

Per un UPDATEterzo di tutte le righe, probabilmente pagherà eliminare anche tutti gli altri indici e ricrearli dopo il UPDATE. L'unico aspetto negativo: sono necessari privilegi aggiuntivi e un blocco esclusivo sul tavolo (solo per un breve momento se si utilizza CREATE INDEX CONCURRENTLY).

L'idea di @ ypercube di usare un btree invece dell'indice GiST sembra buona in linea di principio. Ma non per un terzo di tutte le righe (dove nessun indice è utile per cominciare), e non solo (lower(t_range),upper(t_range)), dal momento che tstzrangenon è un tipo di intervallo discreto.

La maggior parte dei tipi di intervallo discreti ha una forma canonica, il che rende più semplice il concetto di "uguaglianza": lo definisce il limite inferiore e superiore del valore in forma canonica. La documentazione:

Un tipo di intervallo discreto dovrebbe avere una funzione di canonicalizzazione consapevole della dimensione del passo desiderata per il tipo di elemento. La funzione di canonicalizzazione ha il compito di convertire valori equivalenti del tipo di intervallo per avere rappresentazioni identiche, in particolare coerentemente inclusivi o limiti esclusivi. Se non viene specificata una funzione di canonicalizzazione, gli intervalli con una formattazione diversa verranno sempre considerati disuguali, anche se potrebbero rappresentare lo stesso insieme di valori nella realtà.

Il built-in tipi di intervallo int4range, int8rangee daterangetutto l'uso una forma canonica che comprende il limite inferiore ed esclude il limite superiore; cioè [). I tipi di intervallo definiti dall'utente possono tuttavia utilizzare altre convenzioni.

Questo non è il caso tstzrange, in cui l'inclusione del limite superiore e inferiore deve essere considerata per l'uguaglianza. Un possibile indice btree dovrebbe essere su:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

E le query dovrebbero usare le stesse espressioni nella WHEREclausola.

Si potrebbe essere tentati di indicizzare l'intero valore cast su text: (cast(t_range AS text))- ma questa espressione non è IMMUTABLEpoiché la rappresentazione testuale dei timestamptzvalori dipende timezonedall'impostazione corrente . Dovresti mettere ulteriori passaggi in una IMMUTABLEfunzione wrapper che produce una forma canonica e creare un indice funzionale su quella ...

Misure aggiuntive / idee alternative

Se shape_dist_traveledpuoi già avere lo stesso valore tt.shape_dist_traveleddi più di alcune delle tue righe aggiornate (e non fai affidamento su alcun effetto collaterale dei tuoi UPDATEtrigger simili ...), puoi rendere la tua query più veloce escludendo gli aggiornamenti vuoti:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Naturalmente, si applicano tutti i consigli generali per l'ottimizzazione delle prestazioni. Il Wiki di Postgres è un buon punto di partenza.

VACUUM FULLsarebbe veleno per te, dal momento che alcune tuple morte (o spazio riservato da FILLFACTOR) sono benefiche per le UPDATEprestazioni.

Con così tante righe aggiornate e se te lo puoi permettere (nessun accesso simultaneo o altre dipendenze), potrebbe essere ancora più veloce scrivere una tabella completamente nuova invece di aggiornarla sul posto. Istruzioni in questa risposta correlata:

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.