Pourquoi PostgreSQL choisit-il l'ordre de jointure le plus cher?


13

PostgreSQL utilisant les valeurs par défaut, plus

default_statistics_target=1000
random_page_cost=1.5

Version

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

J'ai aspiré et analysé. La requête est très simple:

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

Chaque id colonne est la clé primaire, et tout ce qui est joint est une relation de clé étrangère, et chaque clé étrangère a un index. Plus un index pour account_payer.account_id.

Il faut 3,93 secondes pour renvoyer 76 000 lignes.

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

Si je mets join_collapse_limit=1, cela prend 0,16 s, une accélération de 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

Ces sorties ont peu de sens pour moi. Le premier a un coût (très élevé) de 280 500 pour la jointure de boucle imbriquée pour les indices de calendrier et de taux. Pourquoi PostgreSQL choisit-il intentionnellement cette jointure très coûteuse en premier?

Informations supplémentaires demandées via les commentaires

Est rate_schedule_id_code_modifier_facility_idxun indice composé?

C'est, avec schedule_idêtre la première colonne. J'en ai fait un index dédié, et il est choisi par le planificateur de requêtes, mais cela n'affecte pas les performances ou autrement affecte le plan.


Pouvez-vous modifier les paramètres default_statistics_targetet random_page_costrevenir à leurs valeurs par défaut? Que se passe-t-il lorsque vous relancez default_statistics_targetencore plus? Pouvez-vous créer un DB Fiddle (sur dbfiddle.uk) et tenter de reproduire le problème là-bas?
Colin 't Hart

3
Pouvez-vous inspecter les statistiques réelles pour voir s'il y a quelque chose de biais / bizarre dans vos données? postgresql.org/docs/10/static/planner-stats.html
Colin 't Hart

Quelle est la valeur actuelle du paramètre work_mem? Le changer donne des horaires différents?
eppesuig

Réponses:


1

Il semble que vos statistiques ne soient pas exactes (exécutez une analyse de vide pour les actualiser) ou que vous ayez des colonnes corrélées dans votre modèle (et vous devrez donc effectuer create statisticspour informer le raboteur de ce fait).

Le join_collapseparamètre permet au planificateur de réorganiser les jointures pour qu'il exécute d'abord celle qui récupère le moins de données. Mais, pour les performances, nous ne pouvons pas laisser le planificateur faire cela sur une requête avec beaucoup de jointures. Par défaut, il est défini sur 8 jointures max. En le définissant sur 1, vous désactivez simplement cette capacité.

Alors, comment postgres prévoit-il combien de lignes cette requête devrait récupérer? Il utilise des statistiques pour estimer le nombre de lignes.

Ce que nous pouvons voir dans vos plans d'explication, c'est qu'il y a plusieurs estimations de nombre de lignes inexactes (la première valeur est une estimation, la seconde est réelle).

Par exemple, ici:

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

Le planificateur a estimé qu'il obtiendrait 55 lignes alors qu'il en avait obtenu 74697.

Ce que je ferais (si j'étais à votre place) c'est:

  • analyze les cinq tables impliquées pour actualiser les statistiques
  • Rejouer explain analyze
  • Regardez la différence entre les numéros de ligne estimés et les numéros de ligne réels
  • Si les numéros de ligne estimés sont corrects, le plan a peut-être changé et est plus efficace. Si tout va bien, vous pourriez envisager de modifier vos paramètres de vide automatique afin que l'analyse (et le vide) fonctionne plus souvent
  • Si les numéros de ligne estimés sont toujours erronés, il semble que vous ayez corrélé les données dans votre table (troisième violation de formulaire normal). Vous pourriez envisager de le déclarer avec CREATE STATISTICS(documentation ici )

Si vous avez besoin de plus d'informations sur les estimations de ligne et ses calculs, vous trouverez tout ce dont vous avez besoin dans la conférence de Tomas Vondra "Créer des statistiques - À quoi ça sert?" (diapositives ici )

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.