Query PostgreSQL molto lenta quando viene aggiunta una subquery


10

Ho una query relativamente semplice su una tabella con 1,5 milioni di righe:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE produzione:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

Fin qui tutto bene, veloce e utilizza gli indici disponibili.
Ora, se modifico un po 'una query, il risultato sarà:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

L' EXPLAIN ANALYZEoutput è:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

Non così veloce e usando seq scan ...

Naturalmente, la query originale eseguita dall'applicazione è un po 'più complessa e persino più lenta, e ovviamente l'originale generato dall'ibernazione non lo è (SELECT 9762715), ma la lentezza è presente anche per questo (SELECT 9762715)! La query viene generata da ibernazione, quindi è piuttosto una sfida modificarli e alcune funzionalità non sono disponibili (ad es. UNIONNon è disponibile, il che sarebbe veloce).

Le domande

  1. Perché non è possibile utilizzare l'indice nel secondo caso? Come potrebbero essere usati?
  2. Posso migliorare le prestazioni della query in qualche altro modo?

Pensieri aggiuntivi

Sembra che potremmo usare il primo caso facendo manualmente un SELECT, quindi inserendo l'elenco risultante nella query. Anche con 5000 numeri nell'elenco IN () è quattro volte più veloce della seconda soluzione. Tuttavia, sembra proprio SBAGLIATO (inoltre, potrebbe essere 100 volte più veloce :)). È del tutto incomprensibile il motivo per cui il pianificatore di query utilizza un metodo completamente diverso per queste due query, quindi vorrei trovare una soluzione migliore a questo problema.


Puoi in qualche modo riscrivere il tuo codice in modo che l'ibernazione generi un JOINinvece del IN ()? Inoltre, è publicationstato analizzato di recente?
dezso

Sì, ho eseguito sia VACUUM ANALYZE sia VACUUM FULL. Non ci sono stati cambiamenti nelle prestazioni. Per quanto riguarda il secondo, AFAIR ci ha provato e ciò non ha influito in modo significativo sulle prestazioni della query.
P.Péter,

1
Se Hibernate non riesce a generare una query corretta, perché non usi semplicemente SQL non elaborato? È come insistere su Google Translate mentre sai già meglio come esprimerlo in inglese. Per quanto riguarda la tua domanda: dipende davvero dalla query effettiva nascosta dietro (SELECT 9762715).
Erwin Brandstetter,

Come ho detto in seguito, è lenta, anche se l'interrogazione interna è (SELECT 9762715) . Alla domanda di ibernazione: potrebbe essere fatto, ma richiede una seria riscrittura del codice, poiché abbiamo query di criteri di ibernazione definite dall'utente che vengono tradotte al volo. Quindi essenzialmente dovremmo modificare l'ibernazione che è un'impresa enorme con molti possibili effetti collaterali.
P.Péter,

Risposte:


6

Il nocciolo del problema diventa evidente qui:

Scansione Seq alla pubblicazione (costo = 0,01..349652,84 righe = 744661 larghezza = 8) (tempo effettivo = 2735,888..2841,393 righe = 1 loop = 1)

Postgres stima di restituire 744661 righe mentre, in effetti, risulta essere una riga singola. Se Postgres non sa cosa aspettarsi dalla query, non può pianificare meglio. Dovremmo vedere la query effettiva nascosta dietro (SELECT 9762715)- e probabilmente conoscere anche la definizione della tabella, i vincoli, le cardinalità e la distribuzione dei dati. Ovviamente, Postgres non è in grado di prevedere quante poche righe verranno restituite da esso. Ci possono essere modi per riscrivere la query, a seconda di cosa si tratta .

Se sai che la sottoquery non può mai restituire più di nrighe, puoi semplicemente dire a Postgres usando:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Se n è abbastanza piccolo, Postgres passerà alle scansioni dell'indice (bitmap). Tuttavia , funziona solo per il caso semplice. Smette di funzionare quando si aggiunge una ORcondizione: il pianificatore di query non può attualmente farcela.

Raramente lo uso IN (SELECT ...)per cominciare. In genere esiste un modo migliore per implementare lo stesso, spesso con un EXISTSsemi-join. A volte con un ( LEFT) JOIN( LATERAL) ...

La soluzione ovvia sarebbe da usare UNION, ma tu l'hai escluso. Non posso aggiungere altro senza conoscere la subquery effettiva e altri dettagli pertinenti.


2
Non c'è nessuna query nascosta dietro (SELECT 9762715) ! Se eseguo quella query esatta che vedi sopra. Ovviamente, la query di ibernazione originale è un po 'più complicata, ma io (penso di essere riuscito) a individuare dove si trova il pianificatore di query, quindi ho presentato quella parte della query. Tuttavia, le spiegazioni e le domande di cui sopra sono testualmente ctrl-cv.
P.Péter,

Per quanto riguarda la seconda parte, il limite interno non funziona: EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;esegue anche una scansione sequenziale e funziona anche per circa 3 secondi ...
P.Péter

@ P.Péter: Funziona per me nel mio test locale con una vera subquery su Postgres 9.4. Se quello che mostri è la tua vera query, allora hai già la tua soluzione: usa la prima query nella tua domanda con una costante anziché una sottoquery.
Erwin Brandstetter,

Beh, ho anche provato una subquery su una nuova tabella di prova: CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. E l'effetto era ancora lì per le stesse query su test: qualsiasi subquery ha provocato una scansione seq ... Ho provato sia 9.1 che 9.4. L'effetto è lo stesso.
P.Péter,

1
@ P.Péter: ho eseguito di nuovo il test e mi sono reso conto di aver provato senza la ORcondizione. Il trucco LIMITfunziona solo per il caso più semplice.
Erwin Brandstetter,

6

Il mio collega ha trovato un modo per modificare la query in modo che abbia bisogno di una semplice riscrittura e faccia quello che deve fare, cioè facendo la sottoselezione in un solo passaggio e quindi facendo le ulteriori operazioni sul risultato:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

La spiegazione analizza ora è:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Sembra che possiamo creare un semplice parser che trova e riscrive tutte le sottoselezioni in questo modo e aggiungerlo a un hook ibernato per manipolare la query nativa.


Questo sembra divertente. Non è più semplice rimuovere tutte le SELECTs, come hai fatto nella tua prima query nella domanda?
dezso

Ovviamente, potrei seguire un approccio in due fasi: fare SELECTseparatamente, quindi fare la selezione esterna con un elenco statico dopo il IN. Tuttavia, questo è significativamente più lento (5-10 volte se la subquery ha più di alcuni risultati), poiché hai round-trip di rete aggiuntivi e hai formati postgres molti risultati e quindi java analizzando quei risultati (e quindi facendo lo stesso di nuovo all'indietro). La soluzione sopra fa lo stesso semanticamente, lasciando il processo all'interno di Postgres. Tutto sommato, attualmente questo sembra essere il modo più veloce con la più piccola modifica nel nostro caso.
P.Péter,

Ah, capisco. Quello che non sapevo è che puoi ottenere molti ID alla volta.
dezso

1

Rispondi a una seconda domanda: Sì, puoi aggiungere ORDER BY alla tua sottoquery, che avrà un impatto positivo. Ma è simile alla soluzione "EXISTS (subquery)" nelle prestazioni. Esiste una differenza significativa anche con la subquery che risulta in due righe.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
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.