SELEZIONA DISTINCT ON La sottoquery utilizza un piano inefficiente


8

Ho una tabella progresses(contiene attualmente nell'ordine di centinaia di migliaia di record):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

e una vista v_latest_progressesche richiede le più recenti progressdi user_ide lesson_id:

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

Un utente può avere molti progressi per una determinata lezione, ma spesso desideriamo eseguire una query per un set degli ultimi progressi creati per un determinato set di utenti o lezioni (o una combinazione dei due).

La vista lo v_latest_progressesfa bene ed è anche performante quando specifico un set di user_ids:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

Tuttavia, se provo a fare la stessa query sostituendo il set di user_ids con una subquery, diventa molto inefficiente:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

Quello che sto cercando di capire è il motivo per cui PostgreSQL vuole eseguire la DISTINCTquery sull'intera progressestabella prima che venga filtrata dalla subquery nel secondo esempio.

Qualcuno avrebbe qualche consiglio su come migliorare questa query?

Risposte:


11

Aronne,

Nel mio recente lavoro, ho esaminato alcune domande simili con PostgreSQL. PostgreSQL è quasi sempre abbastanza bravo a generare il giusto piano di query, ma non è sempre perfetto.

Alcuni semplici suggerimenti potrebbero essere quello di assicurarsi di eseguirne uno ANALYZEsul tuo progressestavolo per assicurarti di avere statistiche aggiornate, ma questo non è garantito per risolvere i tuoi problemi!

Per motivi che probabilmente sono troppo lunghi per questo post, ho trovato alcuni comportamenti strani nella raccolta delle statistiche ANALYZEe nel pianificatore di query che potrebbe essere necessario risolvere a lungo termine. A breve termine, il trucco è riscrivere la query per provare a eliminare il piano di query desiderato.

Senza avere accesso ai tuoi dati per i test, ti darò i seguenti due possibili suggerimenti.

1) Usa ARRAY()

PostgreSQL tratta array e set di record in modo diverso nel suo planner di query. A volte finirai con un piano di query identico. In questo caso, come in molti dei miei casi, non lo fai.

Nella tua query originale avevi:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

Come primo passaggio per provare a risolverlo, prova

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

Nota la modifica della sottoquery da INa =ANY(ARRAY()).

2) Utilizzare CTE

Un altro trucco è forzare ottimizzazioni separate, se il mio primo suggerimento non funziona. So che molte persone usano questo trucco, perché le query all'interno di un CTE sono ottimizzate e materializzate separatamente dalla query principale.

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

In sostanza, creando il CTE user_selectionusando la WITHclausola, stai chiedendo a PostgreSQL di eseguire un'ottimizzazione separata sulla sottoquery

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

e poi materializzando quei risultati. Quindi, ancora una volta uso l' =ANY(ARRAY())espressione per provare a manipolare manualmente il piano.

In questi casi, probabilmente non ti puoi fidare solo dei risultati EXPLAIN, perché pensava già di aver trovato la soluzione meno costosa. Assicurati di eseguirlo EXPLAIN (ANALYZE,BUFFERS)...per scoprire quanto costa veramente in termini di tempo e letture della pagina.


A quanto pare, il tuo primo suggerimento fa miracoli. Il costo per quella query è 144.07..144.6, IN MODO inferiore ai 70.000 che ho ricevuto! Grazie mille.
Aaron,

1
Ha! Sono contento di poterti aiutare. Faccio molto fatica a superare questi problemi di "hacking del piano di query"; è un po 'di arte in cima alla scienza.
Chris,

Nel corso degli anni ho imparato trucchi per far sì che i database facessero quello che volevo e devo dire che questa è stata una delle situazioni più strane che ho affrontato. È veramente un'arte. Apprezzo molto la tua spiegazione ben ponderata!
Aaron,
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.