Migliorare le prestazioni di COUNT / GROUP-BY nella tabella PostgresSQL di grandi dimensioni?


24

Sto eseguendo PostgresSQL 9.2 e ho una relazione di 12 colonne con circa 6.700.000 righe. Contiene nodi in uno spazio 3D, ognuno dei quali fa riferimento a un utente (che l'ha creato). Per interrogare l'utente che ha creato il numero di nodi che eseguo quanto segue (aggiunto explain analyzeper ulteriori informazioni):

EXPLAIN ANALYZE SELECT user_id, count(user_id) FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                    QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253668.70..253669.07 rows=37 width=8) (actual time=1747.620..1747.623 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220278.79 rows=6677983 width=8) (actual time=0.019..886.803 rows=6677983 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1747.653 ms

Come puoi vedere, ci vogliono circa 1,7 secondi. Ciò non è male considerando la quantità di dati, ma mi chiedo se questo possa essere migliorato. Ho provato ad aggiungere un indice BTree nella colonna utente, ma questo non ha aiutato in alcun modo.

Hai suggerimenti alternativi?


Per completezza, questa è la definizione di tabella completa con tutti i suoi indici (senza vincoli di chiave esterna, riferimenti e trigger):

    Column     |           Type           |                      Modifiers                    
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 location      | double3d                 | not null
 reviewer_id   | integer                  | not null default (-1)
 review_time   | timestamp with time zone |
 editor_id     | integer                  |
 parent_id     | bigint                   |
 radius        | double precision         | not null default 0
 confidence    | integer                  | not null default 5
 skeleton_id   | bigint                   |
Indexes:
    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)
    "skeleton_id_treenode_index" btree (skeleton_id)
    "treenode_editor_index" btree (editor_id)
    "treenode_location_x_index" btree (((location).x))
    "treenode_location_y_index" btree (((location).y))
    "treenode_location_z_index" btree (((location).z))
    "treenode_parent_id" btree (parent_id)
    "treenode_user_index" btree (user_id)

Modifica: questo è il risultato, quando uso la query (e l'indice) proposta da @ypercube (la query impiega circa 5,3 secondi senza EXPLAIN ANALYZE):

EXPLAIN ANALYZE SELECT u.id, ( SELECT COUNT(*) FROM treenode AS t WHERE t.project_id=1 AND t.user_id = u.id ) AS number_of_nodes FROM auth_user As u;
                                                                        QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on auth_user u  (cost=0.00..6987937.85 rows=46 width=4) (actual time=29.934..5556.147 rows=46 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=151911.65..151911.66 rows=1 width=0) (actual time=120.780..120.780 rows=1 loops=46)
           ->  Bitmap Heap Scan on treenode t  (cost=4634.41..151460.44 rows=180486 width=0) (actual time=13.785..114.021 rows=145174 loops=46)
                 Recheck Cond: ((project_id = 1) AND (user_id = u.id))
                 Rows Removed by Index Recheck: 461076
                 ->  Bitmap Index Scan on treenode_user_index  (cost=0.00..4589.29 rows=180486 width=0) (actual time=13.082..13.082 rows=145174 loops=46)
                       Index Cond: ((project_id = 1) AND (user_id = u.id))
 Total runtime: 5556.190 ms
(9 rows)

Time: 5556.804 ms

Modifica 2: Questo è il risultato, quando uso un indexon project_id, user_id(ma ancora nessuna ottimizzazione dello schema) come suggerito da @ erwin-brandstetter (la query viene eseguita con 1,5 secondi alla stessa velocità della mia query originale):

EXPLAIN ANALYZE SELECT user_id, count(user_id) as ct FROM treenode WHERE project_id=1 GROUP BY user_id;
                                                        QUERY PLAN                                                      
---------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=253670.88..253671.24 rows=37 width=8) (actual time=1807.334..1807.339 rows=38 loops=1)
   ->  Seq Scan on treenode  (cost=0.00..220280.62 rows=6678050 width=8) (actual time=0.183..893.491 rows=6678050 loops=1)
         Filter: (project_id = 1)
 Total runtime: 1807.368 ms
(4 rows)

Hai anche una tabella Userscon user_idcome chiave primaria?
ypercubeᵀᴹ

Ho appena visto che c'è un addon columnstore di terze parti per Postgres. Inoltre, volevo solo postare dalla nuova app ios
swasheck il

2
Grazie per la buona, chiara, completa domanda - versioni, definizioni di tabella, ecc.
Craig Ringer

@ypercube Sì, ho una tabella Users.
tomka,

Quanti diversi project_ide user_id? La tabella viene aggiornata continuamente o potresti lavorare con una vista materializzata (per qualche tempo)?
Erwin Brandstetter,

Risposte:


25

Il problema principale è l'indice mancante. Ma c'è di più.

SELECT user_id, count(*) AS ct
FROM   treenode
WHERE  project_id = 1
GROUP  BY user_id;
  • Hai molte bigintcolonne. Probabilmente eccessivo. In genere, integerè più che sufficiente per colonne come project_ide user_id. Ciò aiuterebbe anche l'articolo successivo.
    Mentre ottimizzi la definizione della tabella, considera questa risposta correlata, con particolare attenzione all'allineamento e al riempimento dei dati . Ma la maggior parte del resto vale anche:

  • L' elefante nella stanza : non c'è indice accesoproject_id . Crearne uno. Questo è più importante del resto di questa risposta.
    Mentre ci sei, rendilo un indice a più colonne:

    CREATE INDEX treenode_project_id_user_id_index ON treenode (project_id, user_id);

    Se hai seguito il mio consiglio, integersarebbe perfetto qui:

  • user_idè definito NOT NULL, quindi count(user_id)equivale a count(*), ma quest'ultimo è un po 'più corto e più veloce. (In questa query specifica, questo si applicherebbe anche senza user_idessere definito NOT NULL.)

  • idè già la chiave primaria, il UNIQUEvincolo aggiuntivo è la zavorra inutile . Rilascialo:

    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE CONSTRAINT, btree (id)

    A parte: non userei idcome nome di colonna. Usa qualcosa di descrittivo come treenode_id.

Informazioni aggiunte

D: How many different project_id and user_id?
A: not more than five different project_id.

Ciò significa che Postgres deve leggere circa il 20% dell'intera tabella per soddisfare la tua richiesta. A meno che non sia in grado di utilizzare una scansione di soli indici , una scansione sequenziale sulla tabella sarà più rapida rispetto al coinvolgimento di qualsiasi indice. Non ci sono più prestazioni da ottenere qui, tranne ottimizzando le impostazioni della tabella e del server.

Per quanto riguarda la scansione solo indice : per vedere quanto può essere efficace, esegui VACUUM ANALYZEse puoi permetterlo (blocca esclusivamente la tabella). Quindi riprovare a eseguire la query. Ora dovrebbe essere moderatamente più veloce usando solo l'indice. Leggi prima questa risposta correlata:

Oltre alla pagina di manuale aggiunta con Postgres 9.6 e Postgres Wiki su scansioni solo indice .


1
Erwin, grazie per i tuoi suggerimenti. Hai ragione, perché user_ide project_id integerdovrebbe essere più che sufficiente. Utilizzando count(*)invece di count(user_id)risparmiare circa 70 ms qui, è bene sapere. Ho aggiunto la EXPLAIN ANALYZEquery dopo aver aggiunto il tuo suggerito indexal primo post. Tuttavia, non migliora le prestazioni (ma non fa male). Sembra che indexnon sia affatto usato. Proverò presto le ottimizzazioni dello schema.
tomka,

1
Se disabilito seqscan, viene utilizzato l'indice ( Index Only Scan using treenode_project_id_user_id_index on treenode), ma la query richiede circa 2,5 secondi (che è di circa 1 secondo in più rispetto a seqscan).
tomka,

1
Grazie per l'aggiornamento Questi bit mancanti avrebbero dovuto far parte della mia domanda, giusto. Non ero a conoscenza del loro impatto. Ottimizzerò il mio schema come mi hai suggerito --- vediamo cosa posso guadagnare da quello. Grazie per la tua spiegazione, ha senso per me e quindi segnerò la tua risposta come accettata.
tomka,

7

Aggiungerei prima un indice (project_id, user_id)e poi nella versione 9.3, proverei questa query:

SELECT u.user_id, c.number_of_nodes 
FROM users AS u
   , LATERAL
     ( SELECT COUNT(*) AS number_of_nodes 
       FROM treenode AS t
       WHERE t.project_id = 1 
         AND t.user_id = u.user_id
     ) c 
-- WHERE c.number_of_nodes > 0 ;   -- you probably want this as well
                                   -- to show only relevant users

In 9.2, prova questo:

SELECT u.user_id, 
       ( SELECT COUNT(*) 
         FROM treenode AS t
         WHERE t.project_id = 1 
           AND t.user_id = u.user_id
       ) AS number_of_nodes  
FROM users AS u ;

Presumo che tu abbia un userstavolo. In caso contrario, sostituire userscon:
(SELECT DISTINCT user_id FROM treenode)


Grazie mille per la tua risposta. Hai ragione, ho una tabella utenti. Tuttavia, utilizzando la query in 9.2 sono necessari circa 5 secondi per ottenere il risultato, indipendentemente dal fatto che l'indice sia stato creato o meno. Ho creato l'indice in questo modo:, CREATE INDEX treenode_user_index ON treenode USING btree (project_id, user_id);ma ho provato anche senza la USINGclausola. Mi manca qualcosa?
tomka,

Quante righe ci sono nella userstabella e quante righe restituisce la query (quindi quanti utenti ci sono project_id=1)? Puoi mostrare la spiegazione di questa query, dopo aver aggiunto l'indice?
ypercubeᵀᴹ

1
Innanzitutto, ho sbagliato nel mio primo commento. Senza l'indice suggerito sono necessari circa 40 secondi (!) Per recuperare il risultato. Ci vogliono circa 5 secondi con il indexposto. Dispiace per la confusione. Nella mia userstabella ho 46 voci. La query restituisce solo 9 righe. Sorprendentemente, SELECT DISTINCT user_id FROM treenode WHERE project_id=1;restituisce 38 righe. Ho aggiunto il explainal mio primo post. E per evitare confusione: il mio userstavolo viene effettivamente chiamato auth_user.
tomka,

Mi chiedo come può SELECT DISTINCT user_id FROM treenode WHERE project_id=1;restituire 38 righe mentre le query restituiscono solo 9. Buffled.
ypercubeᵀᴹ

Puoi provare questo ?:SET enable_seqscan = OFF; (Query); SET enable_seqscan = ON;
ypercubeᵀᴹ
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.