Interrogazione lenta su tabella di grandi dimensioni con GROUP BY e ORDER BY


14

Ho un tavolo con 7,2 milioni di tuple che assomiglia a questo:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Ora voglio selezionare alcuni valori ma la query è incredibilmente lenta:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

La hashcolonna è l'hash md5 di stringe ha un indice. Quindi penso che il mio problema sia che l'intera tabella sia ordinata per id e non per hash, quindi ci vuole un po 'per ordinarla prima e poi raggrupparla?

La tabella nostringcontiene solo un elenco di hash che non voglio avere. Ma ho bisogno che entrambe le tabelle abbiano tutti i valori. Quindi non è un'opzione per eliminarli.

informazioni aggiuntive: nessuna delle colonne può essere nulla (risolta quella nella definizione della tabella) e sto usando Postgresql 9.2.


1
Fornisci sempre la versione di PostgreSQL che usi. Qual è la percentuale di NULLvalori nella colonna method? Ci sono duplicati su string?
Erwin Brandstetter,

Risposte:


18

La risposta diLEFT JOIN in @ dezso dovrebbe essere buona. Un indice, tuttavia, difficilmente sarà utile (di per sé), poiché la query deve comunque leggere l'intera tabella, ad eccezione delle scansioni di solo indice in Postgres 9.2+ e delle condizioni favorevoli, vedi sotto.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Esegui EXPLAIN ANALYZEsulla query. Più volte per escludere effetti di incasso e rumore. Confronta i risultati migliori.

Crea un indice multi-colonna che corrisponda alla tua query:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

Aspettare? Dopo che ho detto che un indice non avrebbe aiutato? Bene, ne abbiamo bisogno al CLUSTERtavolo:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Rieseguire EXPLAIN ANALYZE. Più veloce? Dovrebbe essere.

CLUSTERè un'operazione una tantum per riscrivere l'intera tabella nell'ordine dell'indice utilizzato. È anche efficacemente a VACUUM FULL. Se vuoi essere sicuro, eseguiresti un pre-test da VACUUM FULLsolo per vedere cosa si può attribuire a questo.

Se la tua tabella vede molte operazioni di scrittura, l'effetto si degraderà nel tempo. Pianificare CLUSTERal di fuori delle ore per ripristinare l'effetto. La messa a punto dipende dal caso d'uso esatto. Il manuale su CLUSTER.

CLUSTERè uno strumento piuttosto grezzo, necessita di un blocco esclusivo sul tavolo. Se non puoi permetterlo, considera pg_repackquale può fare lo stesso senza blocco esclusivo. Altro in questa risposta successiva:


Se la percentuale di NULLvalori nella colonna methodè alta (più del 20% circa, a seconda delle dimensioni delle righe effettive), un indice parziale dovrebbe aiutare:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(Il tuo aggiornamento successivo mostra che le tue colonne sono NOT NULL, quindi non applicabile.)

Se stai eseguendo PostgreSQL 9.2 o versioni successive (come ha commentato @deszo ), gli indici presentati potrebbero essere utili senza CLUSTERche il pianificatore possa utilizzare scansioni solo indice . Applicabile solo a condizioni favorevoli: nessuna operazione di scrittura che influirebbe sulla mappa di visibilità dall'ultima VACUUMe tutte le colonne nella query devono essere coperte dall'indice. Fondamentalmente le tabelle di sola lettura possono usarlo in qualsiasi momento, mentre le tabelle fortemente scritte sono limitate. Maggiori dettagli nel Wiki di Postgres.

L'indice parziale sopra menzionato potrebbe essere ancora più utile in quel caso.

Se , d'altra parte, ci sono non NULL valori nella colonna method, si dovrebbe
1.) definirlo NOT NULLe
2.) l'uso count(*)al posto di count(method), che è leggermente più veloce e fa lo stesso in assenza di NULLvalori.

Se devi chiamare questa query spesso e la tabella è di sola lettura, crea un MATERIALIZED VIEW.


Punto esotico: il tuo tavolo è chiamato nostring, ma sembra contenere hash. Escludendo gli hash anziché le stringhe, è possibile che vengano escluse più stringhe del previsto. Molto improbabile, ma possibile.


con il cluster è molto più veloce. ho ancora bisogno di circa 5 minuti per la query, ma è molto meglio che eseguirla tutta la notte: D
reox

@reox: da quando hai eseguito v9.2: hai provato solo con l'indice, prima del clustering? Sarebbe interessante se vedessi una differenza. (Non è possibile riprodurre la differenza dopo il clustering.) Inoltre (e questo sarebbe economico), EXPLAIN mostra ora una scansione dell'indice o una scansione della tabella completa?
Erwin Brandstetter,

5

Benvenuto in DBA.SE!

Puoi provare a riformulare la tua query in questo modo:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

o un'altra possibilità:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN è un tipico sink per le prestazioni poiché è difficile utilizzare un indice con esso.

Questo può essere ulteriormente migliorato con gli indici. Un indice su nostring.hashsembra utile. Ma prima: cosa ottieni adesso? (Sarebbe meglio vedere l'output di EXPLAIN ANALYZEpoiché i costi stessi non dicono il tempo impiegato per le operazioni.)


un indice è già stato creato su nostring.hash, ma penso che Postgres non lo usi a causa di troppe tuple ... quando esplico disabilito la scansione di sequenza, usa l'indice. se uso il join sinistro ottengo un costo di 32 milioni, quindi è meglio ... ma sto cercando di ottimizzarlo di più ...
reox

3
Il costo è solo per il pianificatore di essere in grado di scegliere un piano sufficientemente buono. I tempi reali di solito sono correlati ad esso, ma non necessariamente. Quindi, se vuoi essere sicuro, usa EXPLAIN ANALYZE.
dezso

1

Dato che l'hash è un md5, probabilmente potresti provare a convertirlo in un numero: puoi memorizzarlo come numero o semplicemente creare un indice funzionale che calcoli quel numero in una funzione immutabile.

Altre persone hanno già creato una funzione pl / pgsql che converte (parte di) un valore md5 da testo a stringa. Vedi /programming/9809381/hashing-a-string-to-a-numeric-value-in-postgressql per un esempio

Credo che tu stia davvero trascorrendo molto tempo nel confronto delle stringhe durante la scansione dell'indice. Se riesci a memorizzare quel valore come numero, dovrebbe essere molto più veloce.


1
Dubito che questa conversione accelererebbe le cose. Tutte le query qui usano l'uguaglianza per il confronto. Calcolare le rappresentazioni numeriche e quindi verificare l'uguaglianza non mi promette grandi guadagni.
dezso

2
Penso che memorizzerei md5 come bytea piuttosto che un numero per l'efficienza dello spazio: sqlfiddle.com/#!12/d41d8/252
Jack dice che prova topanswers.xyz il

Inoltre, benvenuto su dba.se!
Jack dice di provare topanswers.xyz il

@JackDouglas: commento interessante! 16 byte per md5 invece di 32 è abbastanza per i tavoli di grandi dimensioni.
Erwin Brandstetter,

0

Mi sono imbattuto molto in questo problema e ho scoperto un semplice trucco in 2 parti.

  1. Crea un indice di sottostringa sul valore di hash: (7 di solito è una buona lunghezza)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Chiedi alle tue ricerche / join di includere una corrispondenza di sottostringa, quindi il planner di query viene suggerito per utilizzare l'indice:

    vecchio: WHERE hash = :kwarg

    nuovo: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

Dovresti anche avere un indice anche sul raw hash.

il risultato (di solito) è che il planner consulterà prima l'indice di sottostringa e eliminerà la maggior parte delle righe. quindi abbina l'hash completo di 32 caratteri all'indice (o tabella) corrispondente. questo approccio ha ridotto di 800 ms le query a 4 per me.

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.