Configurazione di PostgreSQL per prestazioni di lettura


39

Il nostro sistema scrive molti dati (tipo di sistema Big Data). Le prestazioni di scrittura sono abbastanza buone per le nostre esigenze, ma le prestazioni di lettura sono davvero troppo lente.

La struttura della chiave primaria (vincolo) è simile per tutte le nostre tabelle:

timestamp(Timestamp) ; index(smallint) ; key(integer).

Una tabella può avere milioni di righe, anche miliardi di righe, e una richiesta di lettura è in genere per un periodo specifico (data / ora / indice) e tag. È comune avere una query che restituisce circa 200k righe. Attualmente, possiamo leggere circa 15k righe al secondo, ma dobbiamo essere 10 volte più veloci. È possibile e, se sì, come?

Nota: PostgreSQL è fornito con il nostro software, quindi l'hardware è diverso da un client all'altro.

È una macchina virtuale utilizzata per i test. L'host della VM è Windows Server 2008 R2 x64 con 24,0 GB di RAM.

Specifiche server (Virtual Machine VMWare)

Server 2008 R2 x64
2.00 GB of memory
Intel Xeon W3520 @ 2.67GHz (2 cores)

postgresql.conf ottimizzazioni

shared_buffers = 512MB (default: 32MB)
effective_cache_size = 1024MB (default: 128MB)
checkpoint_segment = 32 (default: 3)
checkpoint_completion_target = 0.9 (default: 0.5)
default_statistics_target = 1000 (default: 100)
work_mem = 100MB (default: 1MB)
maintainance_work_mem = 256MB (default: 16MB)

Definizione della tabella

CREATE TABLE "AnalogTransition"
(
  "KeyTag" integer NOT NULL,
  "Timestamp" timestamp with time zone NOT NULL,
  "TimestampQuality" smallint,
  "TimestampIndex" smallint NOT NULL,
  "Value" numeric,
  "Quality" boolean,
  "QualityFlags" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag" ),
  CONSTRAINT "FK_AnalogTransition_Tag" FOREIGN KEY ("KeyTag")
      REFERENCES "Tag" ("Key") MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE,
  autovacuum_enabled=true
);

domanda

L'esecuzione della query richiede circa 30 secondi in pgAdmin3, ma vorremmo avere lo stesso risultato in meno di 5 secondi, se possibile.

SELECT 
    "AnalogTransition"."KeyTag", 
    "AnalogTransition"."Timestamp" AT TIME ZONE 'UTC', 
    "AnalogTransition"."TimestampQuality", 
    "AnalogTransition"."TimestampIndex", 
    "AnalogTransition"."Value", 
    "AnalogTransition"."Quality", 
    "AnalogTransition"."QualityFlags", 
    "AnalogTransition"."UpdateTimestamp"
FROM "AnalogTransition"
WHERE "AnalogTransition"."Timestamp" >= '2013-05-16 00:00:00.000' AND "AnalogTransition"."Timestamp" <= '2013-05-17 00:00:00.00' AND ("AnalogTransition"."KeyTag" = 56 OR "AnalogTransition"."KeyTag" = 57 OR "AnalogTransition"."KeyTag" = 58 OR "AnalogTransition"."KeyTag" = 59 OR "AnalogTransition"."KeyTag" = 60)
ORDER BY "AnalogTransition"."Timestamp" DESC, "AnalogTransition"."TimestampIndex" DESC
LIMIT 500000;

Spiega 1

"Limit  (cost=0.00..125668.31 rows=500000 width=33) (actual time=2.193..3241.319 rows=500000 loops=1)"
"  Buffers: shared hit=190147"
"  ->  Index Scan Backward using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..389244.53 rows=1548698 width=33) (actual time=2.187..1893.283 rows=500000 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-16 01:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-16 15:00:00-04'::timestamp with time zone))"
"        Filter: (("KeyTag" = 56) OR ("KeyTag" = 57) OR ("KeyTag" = 58) OR ("KeyTag" = 59) OR ("KeyTag" = 60))"
"        Buffers: shared hit=190147"
"Total runtime: 3863.028 ms"

Spiega 2

Nel mio ultimo test, ci sono voluti 7 minuti per selezionare i miei dati! Vedi sotto:

"Limit  (cost=0.00..313554.08 rows=250001 width=35) (actual time=0.040..410721.033 rows=250001 loops=1)"
"  ->  Index Scan using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..971400.46 rows=774511 width=35) (actual time=0.037..410088.960 rows=250001 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-22 20:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-24 20:00:00-04'::timestamp with time zone) AND ("KeyTag" = 16))"
"Total runtime: 411044.175 ms"

Risposte:


52

Allineamento dei dati e dimensioni della memoria

In realtà, l'overhead per tupla è di 24 byte per l'intestazione della tupla più 4 byte per il puntatore dell'elemento.
Maggiori dettagli nel calcolo in questa risposta correlata:

Nozioni di base sull'allineamento e il riempimento dei dati in questa risposta correlata su SO:

Abbiamo tre colonne per la chiave primaria:

PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag")

"Timestamp"      timestamp (8 bytes)
"TimestampIndex" smallint  (2 bytes)
"KeyTag"         integer   (4 bytes)

Risultati in:

 Puntatore elemento da 4 byte nell'intestazione della pagina (senza contare per multipli di 8 byte)
---
23 byte per l'intestazione tupla
 Padding di 1 byte per allineamento dei dati (o bitmap NULL)
 8 byte "Timestamp"
 2 byte "TimestampIndex"
 Riempimento di 2 byte per l'allineamento dei dati
 4 byte "KeyTag" 
 0 padding al multiplo più vicino di 8 byte
-----
44 byte per tupla

Maggiori informazioni sulla misurazione della dimensione dell'oggetto in questa risposta correlata:

Ordine delle colonne in un indice a più colonne

Leggi queste due domande e risposte per capire:

Il modo in cui hai il tuo indice (chiave primaria), puoi recuperare le righe senza un passaggio di ordinamento, è interessante, soprattutto con LIMIT. Ma recuperare le righe sembra estremamente costoso.

Generalmente, in un indice multi-colonna, le colonne "uguaglianza" dovrebbero andare per prime e le colonne "intervallo" per ultimo:

Pertanto, prova un indice aggiuntivo con ordine di colonna invertito :

CREATE INDEX analogransition_mult_idx1
   ON "AnalogTransition" ("KeyTag", "TimestampIndex", "Timestamp");

Dipende dalla distribuzione dei dati. Ma con millions of row, even billion of rowsquesto potrebbe essere sostanzialmente più veloce.

La dimensione della tupla è maggiore di 8 byte, a causa dell'allineamento e del riempimento dei dati. Se lo stai utilizzando come indice semplice, potresti provare a eliminare la terza colonna "Timestamp". Potrebbe essere un po 'più veloce o meno (poiché potrebbe aiutare con l'ordinamento).

Potresti voler mantenere entrambi gli indici. A seconda di una serie di fattori, l'indice originale potrebbe essere preferibile, in particolare con un piccolo LIMIT.

autovacuum e statistiche della tabella

Le statistiche della tabella devono essere aggiornate. Sono sicuro che hai l' autovacuum in esecuzione.

Poiché la tua tabella sembra essere enorme e le statistiche importanti per il giusto piano di query, aumenterei sostanzialmente l' obiettivo statistico per le colonne pertinenti:

ALTER TABLE "AnalogTransition" ALTER "Timestamp" SET STATISTICS 1000;

... o anche più in alto con miliardi di file. Il massimo è 10000, il valore predefinito è 100.

Fallo per tutte le colonne coinvolte in WHEREo ORDER BYclausole. Quindi corri ANALYZE.

Layout della tabella

Tuttavia, applicando quanto appreso sull'allineamento e il riempimento dei dati, questo layout di tabella ottimizzato dovrebbe risparmiare un po 'di spazio su disco e aiutare un po' le prestazioni (ignorando pk & fk):

CREATE TABLE "AnalogTransition"(
  "Timestamp" timestamp with time zone NOT NULL,
  "KeyTag" integer NOT NULL,
  "TimestampIndex" smallint NOT NULL,
  "TimestampQuality" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  "QualityFlags" smallint,
  "Quality" boolean,
  "Value" numeric
);

CLUSTER / pg_repack

Per ottimizzare le prestazioni di lettura per le query che utilizzano un determinato indice (sia esso originale o la mia alternativa suggerita), è possibile riscrivere la tabella nell'ordine fisico dell'indice. CLUSTERlo fa, ma è piuttosto invasivo e richiede un blocco esclusivo per la durata dell'operazione. pg_repackè un'alternativa più sofisticata che può fare lo stesso senza un blocco esclusivo sul tavolo.
Questo può aiutare sostanzialmente con tabelle enormi, poiché è necessario leggere molti meno blocchi della tabella.

RAM

In genere, 2 GB di RAM fisica non sono sufficienti per gestire rapidamente miliardi di righe. Più RAM potrebbe fare molto, accompagnata da un'impostazione adattata: ovviamente una più grande effective_cache_sizeper cominciare.


2
Ho aggiunto un semplice indice solo su KeyTag e ora sembra essere abbastanza veloce. Applicherò anche i tuoi consigli sull'allineamento dei dati. Molte grazie!
JPelletier,

9

Quindi, dai piani vedo una cosa: l'indice è o gonfio (quindi accanto alla tabella sottostante) o semplicemente non è davvero buono per questo tipo di query (ho cercato di affrontare questo nel mio ultimo commento sopra).

Una riga dell'indice contiene 14 byte di dati (e alcuni per l'intestazione). Ora, calcolando dai numeri indicati nel piano: hai ottenuto 500.000 righe da 190147 pagine - ciò significa, in media, meno di 3 righe utili per pagina, cioè circa 37 byte per una pagina da 8 kb. Questo è un pessimo rapporto, non è vero? Poiché la prima colonna dell'indice è il Timestampcampo e viene utilizzata nella query come intervallo, il pianificatore può - e lo fa - scegliere l'indice per trovare le righe corrispondenti. Ma non vi è alcuna TimestampIndexmenzione nelle WHEREcondizioni, quindi il filtro su KeyTagnon è molto efficace poiché quei valori apparentemente appaiono casualmente nelle pagine dell'indice.

Quindi, una possibilità è cambiare la definizione dell'indice in

CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp", "KeyTag", "TimestampIndex")

(o, dato il carico del tuo sistema, crea questo indice come nuovo:

CREATE INDEX CONCURRENTLY "idx_AnalogTransition" 
    ON "AnalogTransition" ("Timestamp", "KeyTag", "TimestampIndex");
  • questo richiederà sicuramente un po 'di tempo, ma puoi comunque lavorare nel frattempo.)

L'altra possibilità che una grande parte delle pagine dell'indice sia occupata da file morte, che potrebbero essere rimosse aspirando. Hai creato la tabella con impostazione autovacuum_enabled=true- ma hai mai iniziato a aspirare automaticamente? O eseguire VACUUMmanualmente?

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.