为什么PostgreSQL选择较昂贵的联接顺序?


13

PostgreSQL使用默认值,加上

default_statistics_target=1000
random_page_cost=1.5

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

我已经吸尘并进行了分析。该查询非常简单:

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

id列都是主键,连接的所有内容都是外键关系,并且每个外键都有索引。加上的索引account_payer.account_id

返回3.6k行需要3.93s。

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

如果我设置join_collapse_limit=1,则需要0.16s,即25倍的加速。

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

这些输出对我来说毫无意义。对于计划表和费率索引,嵌套循环联接的第一个成本(很高)为280,500。为什么PostgreSQL首先故意选择非常昂贵的联接?

通过评论请求其他信息

rate_schedule_id_code_modifier_facility_idx复合指数吗?

它是schedule_id第一列。我已经将其设为专用索引,并且由查询计划者选择,但是它不会影响性能或以其他方式影响计划。


您可以更改设置default_statistics_targetrandom_page_cost恢复为默认设置吗?当您default_statistics_target进一步加注时会发生什么?您可以制作一个DB Fiddle(位于dbfiddle.uk)并尝试在那里重现该问题吗?
Colin't Hart,

3
您可以检查实际的统计信息以查看您的数据是否存在偏斜/怪异吗?postgresql.org/docs/10/static/planner-stats.html
Colin't Hart

参数的当前值是work_mem多少?更改它会给出不同的时间?
eppesuig

Answers:


1

似乎您的统计信息不准确(运行真空分析以刷新它们)或者您的模型中具有关联的列(因此您需要执行此操作create statistics以告知计划者该事实)。

join_collapse参数允许计划者重新安排联接,因此它首先执行获取较少数据的任务。但是,为了提高性能,我们不能让计划者对具有大量联接的查询执行此操作。默认情况下,它的最大连接数为8。通过将其设置为1,您只需禁用该功能。

那么postgres如何预见该查询应该获取多少行呢?它使用统计信息来估计行数。

我们可以在您的解释计划中看到的是,行估计数不准确(第一个值是估计值,第二个是实际值)。

例如,在这里:

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

该计划人员估计实际获得74697时可获得55行。

我要做的(如果我在你的鞋子里)是:

  • analyze 用于刷新统计信息的五个表
  • 重播 explain analyze
  • 查看估算行数与实际行数之间的差异
  • 如果估算的行数正确,则可能是计划有所更改并且效率更高。如果一切正常,则可以考虑更改自动真空设置,以便更频繁地进行分析(和清理)
  • 如果估算的行号仍然错误,则表明您已在表中关联了数据(违反了第三种规范形式)。您可以考虑使用CREATE STATISTICS此处的文档)进行声明

如果您需要有关行估计及其计算的更多信息,请在Tomas Vondra的conf演讲“创建统计信息-它的作用是什么?”中找到所需的一切。(在此处滑动)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.