Come velocizzare le prestazioni di inserimento in PostgreSQL


216

Sto testando le prestazioni di inserimento di Postgres. Ho una tabella con una colonna con il numero come tipo di dati. C'è anche un indice su di esso. Ho riempito il database usando questa query:

insert into aNumber (id) values (564),(43536),(34560) ...

Ho inserito 4 milioni di righe molto rapidamente 10.000 alla volta con la query sopra. Dopo che il database ha raggiunto 6 milioni di righe, le prestazioni sono diminuite drasticamente a 1 milione di righe ogni 15 minuti. C'è qualche trucco per aumentare le prestazioni di inserimento? Ho bisogno di prestazioni di inserimento ottimali su questo progetto.

Utilizzo di Windows 7 Pro su un computer con 5 GB di RAM.


5
Vale la pena menzionare la tua versione Pg anche nelle domande. In questo caso non fa molta differenza, ma lo fa per molte domande.
Craig Ringer,

1
rilasciare gli indici sulla tabella e si innesca se presente ed eseguire lo script di inserimento. Una volta completato il caricamento di massa, è possibile ricreare gli indici.
Sandeep,

Risposte:


481

Vedi popolare un database nel manuale di PostgreSQL, l' articolo eccellente come al solito di Depz sull'argomento e questa domanda SO .

(Si noti che questa risposta riguarda il caricamento di massa di dati in un DB esistente o per crearne uno nuovo. Se sei interessato a ripristinare le prestazioni pg_restoreo l' psqlesecuzione pg_dumpdell'output del DB , gran parte di questo non si applica da quando pg_dumpe pg_restoregià fa cose come la creazione innesca e indicizza al termine di uno schema + ripristino dei dati) .

C'è molto da fare. La soluzione ideale sarebbe quella di importare in una UNLOGGEDtabella senza indici, quindi cambiarla in registrata e aggiungere gli indici. Sfortunatamente in PostgreSQL 9.4 non è supportato il passaggio da tabelle UNLOGGEDa registrate. 9.5 aggiunge ALTER TABLE ... SET LOGGEDper consentirti di farlo.

Se è possibile portare offline il database per l'importazione in blocco, utilizzare pg_bulkload.

Altrimenti:

  • Disabilita eventuali trigger sul tavolo

  • Rilasciare gli indici prima di iniziare l'importazione, ricrearli successivamente. (Ci vuole molto meno tempo per costruire un indice in un passaggio rispetto ad aggiungere gli stessi dati progressivamente ad esso, e l'indice risultante è molto più compatto).

  • Se si esegue l'importazione all'interno di una singola transazione, è possibile eliminare vincoli di chiave esterna, eseguire l'importazione e ricreare i vincoli prima di eseguire il commit. Non farlo se l'importazione è suddivisa su più transazioni poiché potresti introdurre dati non validi.

  • Se possibile, utilizzare al COPYposto di INSERTs

  • Se non è possibile utilizzare, COPYconsiderare l'utilizzo di messaggi multivalore INSERT, se pratico. Sembra che tu lo stia già facendo. Non provare a elencare troppi valori in un singolo VALUESperò; quei valori devono rientrare nella memoria un paio di volte, quindi tienilo a poche centinaia per istruzione.

  • Inserisci i tuoi inserti in transazioni esplicite, eseguendo centinaia di migliaia o milioni di inserti per transazione. Non esiste un limite pratico AFAIK, ma il batch ti consente di recuperare da un errore contrassegnando l'inizio di ogni batch nei dati di input. Ancora una volta, sembra che tu lo stia già facendo.

  • Utilizzare synchronous_commit=offe un enorme commit_delayper ridurre i costi di fsync (). Questo non sarà di grande aiuto se hai messo il tuo lavoro in grandi transazioni, però.

  • INSERTo COPYin parallelo da più connessioni. Quanti dipendono dal sottosistema del disco dell'hardware; come regola generale, si desidera una connessione per disco rigido fisico se si utilizza l'archiviazione collegata diretta.

  • Impostare un checkpoint_segmentsvalore elevato e abilitare log_checkpoints. Guarda i log di PostgreSQL e assicurati che non si lamentino dei checkpoint che si verificano troppo frequentemente.

  • Se e solo se non ti dispiace perdere l'intero cluster PostgreSQL (il tuo database e tutti gli altri sullo stesso cluster) a corruzione catastrofica se il sistema si arresta in modo anomalo durante l'importazione, puoi interrompere Pg, impostare fsync=off, avviare Pg, eseguire l'importazione, quindi (vitale) fermare Pg e reimpostare fsync=on. Vedi configurazione WAL . Non farlo se ci sono già dati che ti interessano in qualsiasi database sulla tua installazione PostgreSQL. Se si imposta fsync=offè anche possibile impostare full_page_writes=off; di nuovo, ricordati di riattivarlo dopo l'importazione per prevenire il danneggiamento del database e la perdita di dati. Vedere le impostazioni non durevoli nel manuale Pg.

Dovresti anche cercare di ottimizzare il tuo sistema:

  • Utilizzare SSD di buona qualità per l'archiviazione il più possibile. I buoni SSD con cache di riscrittura affidabili e con protezione dell'alimentazione rendono i tassi di commit incredibilmente più veloci. Sono meno utili quando segui i consigli sopra - che riducono i risciacqui del disco / numero di fsync()s - ma possono comunque essere di grande aiuto. Non utilizzare SSD economici senza un'adeguata protezione da mancanza di corrente a meno che non ti interessi conservare i tuoi dati.

  • Se stai utilizzando RAID 5 o RAID 6 per l'archiviazione collegata diretta, fermati ora. Eseguire il backup dei dati, ristrutturare l'array RAID su RAID 10 e riprovare. RAID 5/6 è senza speranza per le prestazioni di scrittura in blocco, anche se un buon controller RAID con una cache di grandi dimensioni può aiutare.

  • Se hai la possibilità di utilizzare un controller RAID hardware con una grande cache di write-back con batteria, questo può davvero migliorare le prestazioni di scrittura per carichi di lavoro con molti commit. Non aiuta tanto se si utilizza il commit asincrono con un commit_delay o se si eseguono meno grandi transazioni durante il caricamento di massa.

  • Se possibile, archiviare WAL ( pg_xlog) su un disco / array di dischi separato. Non ha senso usare un filesystem separato sullo stesso disco. Le persone spesso scelgono di utilizzare una coppia RAID1 per WAL. Ancora una volta, questo ha un effetto maggiore sui sistemi con alte percentuali di commit e ha scarso effetto se si utilizza una tabella non bloccata come destinazione del caricamento dei dati.

Potrebbe interessarti anche ottimizzare PostgreSQL per test rapidi .


1
Concorderesti che la penalità di scrittura da RAID 5/6 è in qualche modo mitigata se si utilizzano SSD di buona qualità? Ovviamente c'è ancora una penalità, ma penso che la differenza sia molto meno dolorosa rispetto agli HDD.

1
Non l'ho provato. Direi che probabilmente è meno male - i cattivi effetti di amplificazione della scrittura e (per le piccole scritture) hanno ancora bisogno di un ciclo di lettura-modifica-scrittura, ma la severa penalità per la ricerca eccessiva dovrebbe essere un problema.
Craig Ringer,

Possiamo semplicemente disabilitare gli indici invece di lasciarli cadere, ad esempio, impostando indisvalid( postgresql.org/docs/8.3/static/catalog-pg-index.html ) su false, quindi caricare i dati e quindi portare gli indici online di REINDEX?
Vladislav Rastrusny il

1
@CraigRinger Ho testato RAID-5 vs RAID-10 con SSD su Perc H730. RAID-5 è in realtà più veloce. Inoltre, vale la pena notare che inserire / transazioni in combinazione con bytea di grandi dimensioni sembra essere più veloce della copia. Nel complesso un buon consiglio però.
atlaste

2
Qualcuno sta vedendo importanti miglioramenti della velocità con UNLOGGED? Un test rapido mostra un miglioramento del 10-20%.
serg

15

L'uso COPY table TO ... WITH BINARYche è secondo la documentazione è " leggermente più veloce dei formati di testo e CSV ". Fallo solo se hai milioni di righe da inserire e se sei a tuo agio con i dati binari.

Ecco una ricetta di esempio in Python, usando psycopg2 con input binario .


1
La modalità binaria può far risparmiare molto tempo su alcuni input, come i timestamp, dove l'analisi non è banale. Per molti tipi di dati non offre molti vantaggi o può anche essere leggermente più lento a causa della maggiore larghezza di banda (ad esempio numeri interi piccoli). Buon punto sollevandolo.
Craig Ringer,

11

Oltre all'eccellente post di Craig Ringer e al post sul blog di Depesz, se desideri velocizzare i tuoi inserti tramite l' interfaccia ODBC ( psqlodbc ) utilizzando inserti con istruzioni preparate all'interno di una transazione, ci sono alcune cose extra che devi fare per farlo lavorare velocemente:

  1. Impostare il livello di rollback sugli errori su "Transazione" specificando Protocol=-1la stringa di connessione. Di default psqlodbc utilizza il livello "Statement", che crea un SAVEPOINT per ogni istruzione anziché un'intera transazione, rendendo gli inserimenti più lenti.
  2. Utilizzare le istruzioni preparate sul lato server specificando UseServerSidePrepare=1nella stringa di connessione. Senza questa opzione il client invia l'intera istruzione insert insieme a ciascuna riga inserita.
  3. Disabilita il commit automatico su ogni istruzione usando SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);
  4. Dopo aver inserito tutte le righe, eseguire il commit della transazione utilizzando SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT);. Non è necessario aprire esplicitamente una transazione.

Sfortunatamente, psqlodbc "implementa" SQLBulkOperationsemettendo una serie di istruzioni insert non preparate, in modo che per ottenere l'inserimento più veloce sia necessario codificare manualmente i passaggi precedenti.


A8=30000000Per accelerare gli inserti, è necessario utilizzare anche dimensioni di buffer socket di grandi dimensioni, nella stringa di connessione.
Andrus,

10

Oggi ho trascorso circa 6 ore sullo stesso problema. Gli inserti vanno a una velocità 'normale' (meno di 3 secondi per 100K) fino a 5MI (su un totale di 30MI) e quindi le prestazioni diminuiscono drasticamente (fino a 1 minuto per 100K).

Non elencherò tutte le cose che non hanno funzionato e tagliato direttamente sulla carne.

Ho lasciato cadere una chiave primaria sulla tabella di destinazione (che era un GUID) e il mio 30MI o le mie righe scorrevano felicemente verso la loro destinazione a una velocità costante di meno di 3 secondi per 100K.


7

Se sei felice di inserire colonne con UUID (che non è esattamente il tuo caso) e di aggiungere alla risposta @Dennis (non posso ancora commentare), tieni presente che usare gen_random_uuid () (richiede PG 9.4 e modulo pgcrypto) è (a lotto) più veloce di uuid_generate_v4 ()

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)

vs


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

Inoltre, è il modo ufficiale suggerito per farlo

Nota

Se hai solo bisogno di UUID generati casualmente (versione 4), considera invece di utilizzare la funzione gen_random_uuid () dal modulo pgcrypto.

Questo tempo di inserimento è sceso da ~ 2 ore a ~ 10 minuti per 3,7 milioni di righe.


1

Per prestazioni di inserimento ottimali, disabilitare l'indice se questa è un'opzione per te. Oltre a ciò, anche un hardware migliore (disco, memoria) è utile


-1

Ho riscontrato anche questo problema di prestazioni di inserimento. La mia soluzione è generare alcune routine per completare il lavoro di inserimento. Nel frattempo, SetMaxOpenConnsdovrebbe essere assegnato un numero adeguato, altrimenti verrebbero avvisati troppi errori di connessione aperta.

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries {
    wg.Add(1)
    go func(msg string) {
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil {
            fmt.Println(err)
        }
    }(query)
}
wg.Wait()

La velocità di caricamento è molto più veloce per il mio progetto. Questo frammento di codice ha appena dato un'idea di come funziona. I lettori dovrebbero essere in grado di modificarlo facilmente.


Bene, puoi dirlo. Ma riduce il tempo di esecuzione da poche ore a diversi minuti per milioni di righe per il mio caso. :)
Patrick
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.