Warum wählt PostgreSQL die teurere Join-Bestellung?


13

PostgreSQL mit Standardeinstellungen, plus

default_statistics_target=1000
random_page_cost=1.5

Ausführung

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

Ich habe gesaugt und analysiert. Die Abfrage ist sehr einfach:

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

Jede idSpalte ist der Primärschlüssel, und alles, was verbunden wird, ist eine Fremdschlüsselbeziehung, und jeder Fremdschlüssel hat einen Index. Plus ein Index füraccount_payer.account_id .

Es dauert 3,93 Sekunden, um 76.000 Zeilen zurückzugeben.

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

Wenn ich es einstelle join_collapse_limit=1, dauert es 0,16s, eine 25-fache Beschleunigung.

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

Diese Ausgaben machen für mich wenig Sinn. Der erste hat einen (sehr hohen) Preis von 280.500 für den Nested-Loop-Join für die Zeitplan- und Ratenindizes. Warum wählt PostgreSQL diesen sehr teuren Join absichtlich zuerst aus?

Zusätzliche Informationen über Kommentare angefordert

Ist rate_schedule_id_code_modifier_facility_idxein zusammengesetzter Index?

Es ist mit schedule_idder ersten Spalte. Ich habe es zu einem dedizierten Index gemacht und er wird vom Abfrageplaner ausgewählt, hat jedoch keine Auswirkungen auf die Leistung oder auf andere Weise auf den Plan.


Können Sie die Einstellungen ändern default_statistics_targetund random_page_costauf die Standardeinstellungen zurücksetzen ? Was passiert, wenn Sie default_statistics_targetnoch weiter erhöhen ? Können Sie eine DB Fiddle (bei dbfiddle.uk) erstellen und versuchen, das Problem dort zu reproduzieren?
Colin 't Hart

3
Können Sie die tatsächlichen Statistiken überprüfen, um festzustellen, ob Ihre Daten verfälscht sind? postgresql.org/docs/10/static/planner-stats.html
Colin 't Hart

Was ist der aktuelle Wert für den Parameter work_mem? Ändern gibt es unterschiedliche Timings?
Eppesuig

Antworten:


1

Entweder sind Ihre Statistiken nicht genau (führen Sie eine Vakuumanalyse durch, um sie zu aktualisieren), oder Sie haben Spalten in Ihrem Modell korreliert (und daher müssen Sie eine Aktion ausführen create statistics, um den Planer über diese Tatsache zu informieren).

Mit diesem join_collapseParameter kann der Planer Verknüpfungen neu anordnen, sodass zuerst diejenige ausgeführt wird, die weniger Daten abruft. Aus Gründen der Leistung kann der Planer dies jedoch nicht für eine Abfrage mit vielen Verknüpfungen zulassen. Standardmäßig sind 8 Joins eingestellt. Indem Sie den Wert auf 1 setzen, deaktivieren Sie diese Fähigkeit einfach.

Wie sieht postgres nun vor, wie viele Zeilen diese Abfrage abrufen soll? Mithilfe von Statistiken wird die Anzahl der Zeilen geschätzt.

Was wir in Ihren Erklärungsplänen sehen können, ist, dass es mehrere ungenaue Zeilennummernschätzungen gibt (der erste Wert ist die Schätzung, der zweite ist die tatsächliche).

Zum Beispiel hier:

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

Der Planer schätzte die Anzahl der Zeilen auf 55, als er tatsächlich 74697 bekam.

Was ich tun würde (wenn ich in deinen Schuhen stecke) ist:

  • analyze die fünf beteiligten Tabellen, um Statistiken zu aktualisieren
  • Wiederholung explain analyze
  • Betrachten Sie den Unterschied zwischen geschätzten Zeilennummern und tatsächlichen Zeilennummern
  • Wenn die geschätzten Zeilennummern stimmen, hat sich der Plan möglicherweise geändert und ist effizienter. Wenn alles in Ordnung ist, können Sie die Einstellungen für das automatische Vakuum ändern, damit das Analysieren (und Vakuumieren) häufiger durchgeführt wird
  • Wenn die geschätzten Zeilennummern immer noch falsch sind, scheinen Sie Daten in Ihrer Tabelle korreliert zu haben (Verstoß gegen die dritte normale Form). Sie könnten in Betracht ziehen, sie mit zu deklarieren CREATE STATISTICS (Dokumentation hier )

Wenn Sie weitere Informationen zu Zeilenschätzungen und deren Berechnungen benötigen, finden Sie in Tomas Vondras Konferenz "Statistiken erstellen - Wozu dient das?" (Folien hier )

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.