Perché PostgreSQL sceglie l'ordine di join più costoso?


13

PostgreSQL utilizzando le impostazioni predefinite, oltre a

default_statistics_target=1000
random_page_cost=1.5

Versione

PostgreSQL 10.4 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit

Ho passato l'aspirapolvere e analizzato. La query è molto semplice:

SELECT r.price
FROM account_payer ap
  JOIN account_contract ac ON ap.id = ac.account_payer_id
  JOIN account_schedule "as" ON ac.id = "as".account_contract_id
  JOIN schedule s ON "as".id = s.account_schedule_id
  JOIN rate r ON s.id = r.schedule_id
WHERE ap.account_id = 8

Ogni idcolonna è la chiave primaria e tutto ciò su cui si unisce è una relazione di chiave esterna e ogni chiave esterna ha un indice. Più un indice per account_payer.account_id.

Sono necessari 3,93 secondi per restituire 76.000 righe.

Merge Join  (cost=8.06..83114.08 rows=3458267 width=6) (actual time=0.228..3920.472 rows=75548 loops=1)
  Merge Cond: (s.account_schedule_id = "as".id)
  ->  Nested Loop  (cost=0.57..280520.54 rows=6602146 width=14) (actual time=0.163..3756.082 rows=448173 loops=1)
        ->  Index Scan using schedule_account_schedule_id_idx on schedule s  (cost=0.14..10.67 rows=441 width=16) (actual time=0.035..0.211 rows=89 loops=1)
        ->  Index Scan using rate_schedule_id_code_modifier_facility_idx on rate r  (cost=0.43..486.03 rows=15005 width=10) (actual time=0.025..39.903 rows=5036 loops=89)
              Index Cond: (schedule_id = s.id)
  ->  Materialize  (cost=0.43..49.46 rows=55 width=8) (actual time=0.060..12.984 rows=74697 loops=1)
        ->  Nested Loop  (cost=0.43..49.32 rows=55 width=8) (actual time=0.048..1.110 rows=66 loops=1)
              ->  Nested Loop  (cost=0.29..27.46 rows=105 width=16) (actual time=0.030..0.616 rows=105 loops=1)
                    ->  Index Scan using account_schedule_pkey on account_schedule "as"  (cost=0.14..6.22 rows=105 width=16) (actual time=0.014..0.098 rows=105 loops=1)
                    ->  Index Scan using account_contract_pkey on account_contract ac  (cost=0.14..0.20 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=105)
                          Index Cond: (id = "as".account_contract_id)
              ->  Index Scan using account_payer_pkey on account_payer ap  (cost=0.14..0.21 rows=1 width=8) (actual time=0.003..0.003 rows=1 loops=105)
                    Index Cond: (id = ac.account_payer_id)
                    Filter: (account_id = 8)
                    Rows Removed by Filter: 0
Planning time: 5.843 ms
Execution time: 3929.317 ms

Se ho impostato join_collapse_limit=1, ci vogliono 0,16 secondi, una velocità di 25x.

Nested Loop  (cost=6.32..147323.97 rows=3458267 width=6) (actual time=8.908..151.860 rows=75548 loops=1)
  ->  Nested Loop  (cost=5.89..390.23 rows=231 width=8) (actual time=8.730..11.655 rows=66 loops=1)
        Join Filter: ("as".id = s.account_schedule_id)
        Rows Removed by Join Filter: 29040
        ->  Index Scan using schedule_pkey on schedule s  (cost=0.27..17.65 rows=441 width=16) (actual time=0.014..0.314 rows=441 loops=1)
        ->  Materialize  (cost=5.62..8.88 rows=55 width=8) (actual time=0.001..0.011 rows=66 loops=441)
              ->  Hash Join  (cost=5.62..8.61 rows=55 width=8) (actual time=0.240..0.309 rows=66 loops=1)
                    Hash Cond: ("as".account_contract_id = ac.id)
                    ->  Seq Scan on account_schedule "as"  (cost=0.00..2.05 rows=105 width=16) (actual time=0.010..0.028 rows=105 loops=1)
                    ->  Hash  (cost=5.02..5.02 rows=48 width=8) (actual time=0.178..0.178 rows=61 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 11kB
                          ->  Hash Join  (cost=1.98..5.02 rows=48 width=8) (actual time=0.082..0.143 rows=61 loops=1)
                                Hash Cond: (ac.account_payer_id = ap.id)
                                ->  Seq Scan on account_contract ac  (cost=0.00..1.91 rows=91 width=16) (actual time=0.007..0.023 rows=91 loops=1)
                                ->  Hash  (cost=1.64..1.64 rows=27 width=8) (actual time=0.048..0.048 rows=27 loops=1)
                                      Buckets: 1024  Batches: 1  Memory Usage: 10kB
                                      ->  Seq Scan on account_payer ap  (cost=0.00..1.64 rows=27 width=8) (actual time=0.009..0.023 rows=27 loops=1)
                                            Filter: (account_id = 8)
                                            Rows Removed by Filter: 24
  ->  Index Scan using rate_schedule_id_code_modifier_facility_idx on rate r  (cost=0.43..486.03 rows=15005 width=10) (actual time=0.018..1.685 rows=1145 loops=66)
        Index Cond: (schedule_id = s.id)
Planning time: 4.692 ms
Execution time: 160.585 ms

Queste uscite hanno poco senso per me. Il primo ha un costo (molto elevato) di 280.500 per il join loop annidato per gli indici di pianificazione e tariffa. Perché PostgreSQL ha scelto intenzionalmente per primo quella costosa join?

Ulteriori informazioni richieste tramite commenti

È rate_schedule_id_code_modifier_facility_idxun indice composto?

Lo è, schedule_idessendo la prima colonna. L'ho reso un indice dedicato ed è stato scelto dal pianificatore di query, ma non influisce sulle prestazioni o altrimenti sul piano.


Puoi modificare le impostazioni default_statistics_targete random_page_costtornare ai loro valori predefiniti? Cosa succede quando rilanci default_statistics_targetancora di più? Riesci a creare un DB Fiddle (su dbfiddle.uk) e provare a riprodurre il problema lì?
Colin 't Hart,

3
Puoi controllare le statistiche effettive per vedere se c'è qualcosa di distorto / strano nei tuoi dati? postgresql.org/docs/10/static/planner-stats.html
Colin 't Hart

Qual è il valore corrente per il parametro work_mem? Cambiarlo dà tempi diversi?
eppesuig,

Risposte:


1

Sembra che o le tue statistiche non siano accurate (esegui l'analisi del vuoto per aggiornarle) o hai colonne correlate nel tuo modello (e quindi dovrai eseguire create statisticsper informare il pianificatore di quel fatto).

Il join_collapseparametro consente al planner di riorganizzare i join in modo da eseguire prima quello che recupera meno dati. Ma, per quanto riguarda le prestazioni, non possiamo lasciare che il planner lo faccia su una query con molti join. Per impostazione predefinita, è impostato su 8 join max. Impostandolo su 1, semplicemente disabiliti quell'abilità.

In che modo Postgres prevede quante righe deve recuperare questa query? Usa le statistiche per stimare il numero di righe.

Quello che possiamo vedere nei tuoi piani di spiegazione è che ci sono diverse stime imprecise sul numero di righe (il primo valore è la stima, il secondo è reale).

Ad esempio, qui:

Materialize  (cost=0.43..49.46 rows=55 width=8) (actual time=0.060..12.984 rows=74697 loops=1)

Il pianificatore ha stimato di ottenere 55 righe quando ha effettivamente ottenuto 74697.

Quello che farei (se fossi nei tuoi panni) è:

  • analyze le cinque tabelle coinvolte per aggiornare le statistiche
  • Replay explain analyze
  • Guarda la differenza tra numeri di riga stimati e numeri di riga effettivi
  • Se i numeri delle righe stimati sono corretti, forse il piano è cambiato ed è più efficiente. Se tutto è a posto, potresti prendere in considerazione la possibilità di modificare le impostazioni del vuoto automatico in modo che l'analisi (e il vuoto) eseguano più spesso
  • Se i numeri delle righe stimati sono ancora errati, sembra che nella tabella siano presenti dati correlati (terza violazione del modulo normale). Potresti considerare di dichiararli con CREATE STATISTICS(documentazione qui )

Se hai bisogno di maggiori informazioni sulle stime delle righe e sui suoi calcoli, troverai tutto ciò di cui hai bisogno nella conferenza di Tomas Vondra "Crea statistiche - A cosa serve?" (diapositive qui )

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.