【问题标题】:How does one interpret the following PostgreSQL query plan如何解释以下 PostgreSQL 查询计划
【发布时间】:2013-12-06 12:34:41
【问题描述】:

请注意:

(忘记添加订单了,计划更新了)

查询:

EXPLAIN ANALYZE
SELECT DISTINCT(id), special, customer, business_no, bill_to_name, bill_to_address1, bill_to_address2, bill_to_postal_code, ship_to_name, ship_to_address1, ship_to_address2, ship_to_postal_code, 
purchase_order_no, ship_date::text, calc_discount_text(o) AS discount, discount_absolute, delivery, hst_percents, sub_total, total_before_hst, hst, total, total_discount, terms, rep, ship_via, 
item_count, version, to_char(modified, 'YYYY-MM-DD HH24:MI:SS') AS "modified", to_char(created, 'YYYY-MM-DD HH24:MI:SS') AS "created"
FROM invoices o
LEFT JOIN reps ON reps.rep_id = o.rep_id
LEFT JOIN terms ON terms.terms_id = o.terms_id
LEFT JOIN shipVia ON shipVia.ship_via_id = o.ship_via_id
JOIN invoiceItems items ON items.invoice_id = o.id 
WHERE items.qty < 5
ORDER BY modified
LIMIT 100

结果:

Limit  (cost=2931740.10..2931747.85 rows=100 width=635) (actual time=414307.004..414387.899 rows=100 loops=1)
  ->  Unique  (cost=2931740.10..3076319.37 rows=1865539 width=635) (actual time=414307.001..414387.690 rows=100 loops=1)
        ->  Sort  (cost=2931740.10..2936403.95 rows=1865539 width=635) (actual time=414307.000..414325.058 rows=2956 loops=1)
              Sort Key: (to_char(o.modified, 'YYYY-MM-DD HH24:MI:SS'::text)), o.id, o.special, o.customer, o.business_no, o.bill_to_name, o.bill_to_address1, o.bill_to_address2, o.bill_to_postal_code, o.ship_to_name, o.ship_to_address1, o.ship_to_address2, (...)
              Sort Method: external merge  Disk: 537240kB
              ->  Hash Join  (cost=11579.63..620479.38 rows=1865539 width=635) (actual time=1535.805..131378.864 rows=1872673 loops=1)
                    Hash Cond: (items.invoice_id = o.id)
                    ->  Seq Scan on invoiceitems items  (cost=0.00..78363.45 rows=1865539 width=4) (actual time=0.110..4591.117 rows=1872673 loops=1)
                          Filter: (qty < 5)
                          Rows Removed by Filter: 1405763
                    ->  Hash  (cost=5498.18..5498.18 rows=64996 width=635) (actual time=1530.786..1530.786 rows=64996 loops=1)
                          Buckets: 1024  Batches: 64  Memory Usage: 598kB
                          ->  Hash Left Join  (cost=113.02..5498.18 rows=64996 width=635) (actual time=0.214..1043.207 rows=64996 loops=1)
                                Hash Cond: (o.ship_via_id = shipvia.ship_via_id)
                                ->  Hash Left Join  (cost=75.35..4566.81 rows=64996 width=607) (actual time=0.154..754.957 rows=64996 loops=1)
                                      Hash Cond: (o.terms_id = terms.terms_id)
                                      ->  Hash Left Join  (cost=37.67..3800.33 rows=64996 width=579) (actual time=0.071..506.145 rows=64996 loops=1)
                                            Hash Cond: (o.rep_id = reps.rep_id)
                                            ->  Seq Scan on invoices o  (cost=0.00..2868.96 rows=64996 width=551) (actual time=0.010..235.977 rows=64996 loops=1)
                                            ->  Hash  (cost=22.30..22.30 rows=1230 width=36) (actual time=0.044..0.044 rows=4 loops=1)
                                                  Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                                  ->  Seq Scan on reps  (cost=0.00..22.30 rows=1230 width=36) (actual time=0.027..0.032 rows=4 loops=1)
                                      ->  Hash  (cost=22.30..22.30 rows=1230 width=36) (actual time=0.067..0.067 rows=3 loops=1)
                                            Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                            ->  Seq Scan on terms  (cost=0.00..22.30 rows=1230 width=36) (actual time=0.001..0.007 rows=3 loops=1)
                                ->  Hash  (cost=22.30..22.30 rows=1230 width=36) (actual time=0.043..0.043 rows=4 loops=1)
                                      Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                      ->  Seq Scan on shipvia  (cost=0.00..22.30 rows=1230 width=36) (actual time=0.027..0.032 rows=4 loops=1)
Total runtime: 414488.582 ms

显然,这很糟糕。我对解释查询计划非常陌生,想知道如何从这样的计划中提取有用的性能改进提示。

编辑 1

  • 此查询涉及两种实体 - 发票和具有一对多关系的发票项目。
  • 发票项目指定其在父发票中的数量。
  • 给定的查询返回 100 张发票,其中至少有一项数量小于 5。

这应该可以解释为什么我需要DISTINCT - 一张发票可能有多个满足过滤条件的项目,但我不希望同一张发票多次返回。因此使用DISTINCT。但是,我完全清楚,可能有比使用 DISTINCT 更好的方法来实现相同的语义 - 我非常愿意了解它们。

编辑 2

请在查询时在 invoiceItems 表的索引下方找到:

CREATE INDEX invoiceitems_invoice_id_idx ON invoiceitems (invoice_id);
CREATE INDEX invoiceitems_invoice_id_name_index ON invoiceitems (invoice_id, name varchar_pattern_ops);
CREATE INDEX invoiceitems_name_index ON invoiceitems (name varchar_pattern_ops);
CREATE INDEX invoiceitems_qty_index ON invoiceitems (qty);

编辑 3

https://stackoverflow.com/users/808806/yieldsfalsehood 给出的关于如何消除 DISTINCT(以及为什么)的建议被证明是一个非常好的建议。这是新的查询:

EXPLAIN ANALYZE
SELECT id, special, customer, business_no, bill_to_name, bill_to_address1, bill_to_address2, bill_to_postal_code, ship_to_name, ship_to_address1, ship_to_address2, ship_to_postal_code, 
purchase_order_no, ship_date::text, calc_discount_text(o) AS discount, discount_absolute, delivery, hst_percents, sub_total, total_before_hst, hst, total, total_discount, terms, rep, ship_via, 
item_count, version, to_char(modified, 'YYYY-MM-DD HH24:MI:SS') AS "modified", to_char(created, 'YYYY-MM-DD HH24:MI:SS') AS "created"
FROM invoices o
LEFT JOIN reps ON reps.rep_id = o.rep_id
LEFT JOIN terms ON terms.terms_id = o.terms_id
LEFT JOIN shipVia ON shipVia.ship_via_id = o.ship_via_id
WHERE EXISTS (SELECT 1 FROM invoiceItems items WHERE items.invoice_id = id AND items.qty < 5)
ORDER BY modified DESC
LIMIT 100

这是新计划:

Limit  (cost=64717.14..64717.39 rows=100 width=635) (actual time=7830.347..7830.869 rows=100 loops=1)
  ->  Sort  (cost=64717.14..64827.01 rows=43949 width=635) (actual time=7830.334..7830.568 rows=100 loops=1)
        Sort Key: (to_char(o.modified, 'YYYY-MM-DD HH24:MI:SS'::text))
        Sort Method: top-N heapsort  Memory: 76kB
        ->  Hash Left Join  (cost=113.46..63037.44 rows=43949 width=635) (actual time=2.322..6972.679 rows=64467 loops=1)
              Hash Cond: (o.ship_via_id = shipvia.ship_via_id)
              ->  Hash Left Join  (cost=75.78..50968.72 rows=43949 width=607) (actual time=0.650..3809.276 rows=64467 loops=1)
                    Hash Cond: (o.terms_id = terms.terms_id)
                    ->  Hash Left Join  (cost=38.11..50438.25 rows=43949 width=579) (actual time=0.550..3527.558 rows=64467 loops=1)
                          Hash Cond: (o.rep_id = reps.rep_id)
                          ->  Nested Loop Semi Join  (cost=0.43..49796.28 rows=43949 width=551) (actual time=0.015..3200.735 rows=64467 loops=1)
                                ->  Seq Scan on invoices o  (cost=0.00..2868.96 rows=64996 width=551) (actual time=0.002..317.954 rows=64996 loops=1)
                                ->  Index Scan using invoiceitems_invoice_id_idx on invoiceitems items  (cost=0.43..7.61 rows=42 width=4) (actual time=0.030..0.030 rows=1 loops=64996)
                                      Index Cond: (invoice_id = o.id)
                                      Filter: (qty < 5)
                                      Rows Removed by Filter: 1
                          ->  Hash  (cost=22.30..22.30 rows=1230 width=36) (actual time=0.213..0.213 rows=4 loops=1)
                                Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                ->  Seq Scan on reps  (cost=0.00..22.30 rows=1230 width=36) (actual time=0.183..0.192 rows=4 loops=1)
                    ->  Hash  (cost=22.30..22.30 rows=1230 width=36) (actual time=0.063..0.063 rows=3 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 1kB
                          ->  Seq Scan on terms  (cost=0.00..22.30 rows=1230 width=36) (actual time=0.044..0.050 rows=3 loops=1)
              ->  Hash  (cost=22.30..22.30 rows=1230 width=36) (actual time=0.096..0.096 rows=4 loops=1)
                    Buckets: 1024  Batches: 1  Memory Usage: 1kB
                    ->  Seq Scan on shipvia  (cost=0.00..22.30 rows=1230 width=36) (actual time=0.071..0.079 rows=4 loops=1)
Total runtime: 7832.750 ms

这是我能指望的最好的吗?我已经重新启动了服务器(以清理数据库缓存)并在没有EXPLAIN ANALYZE 的情况下重新运行查询。大约需要 5 秒。可以进一步改进吗?我有 65,000 张发票和 3,278,436 个发票项目。

编辑 4

找到了。我是按计算结果订购的,modified = to_char(modified, 'YYYY-MM-DD HH24:MI:SS')。在修改后的发票字段上添加索引并按字段本身排序使结果低于 100 毫秒!

最终方案是:

Limit  (cost=1.18..1741.92 rows=100 width=635) (actual time=3.002..27.065 rows=100 loops=1)
  ->  Nested Loop Left Join  (cost=1.18..765042.09 rows=43949 width=635) (actual time=2.989..25.989 rows=100 loops=1)
        ->  Nested Loop Left Join  (cost=1.02..569900.41 rows=43949 width=607) (actual time=0.413..16.863 rows=100 loops=1)
              ->  Nested Loop Left Join  (cost=0.87..386185.48 rows=43949 width=579) (actual time=0.333..15.694 rows=100 loops=1)
                    ->  Nested Loop Semi Join  (cost=0.72..202470.54 rows=43949 width=551) (actual time=0.017..13.965 rows=100 loops=1)
                          ->  Index Scan Backward using invoices_modified_index on invoices o  (cost=0.29..155543.23 rows=64996 width=551) (actual time=0.003..4.543 rows=100 loops=1)
                          ->  Index Scan using invoiceitems_invoice_id_idx on invoiceitems items  (cost=0.43..7.61 rows=42 width=4) (actual time=0.079..0.079 rows=1 loops=100)
                                Index Cond: (invoice_id = o.id)
                                Filter: (qty < 5)
                                Rows Removed by Filter: 1
                    ->  Index Scan using reps_pkey on reps  (cost=0.15..4.17 rows=1 width=36) (actual time=0.007..0.008 rows=1 loops=100)
                          Index Cond: (rep_id = o.rep_id)
              ->  Index Scan using terms_pkey on terms  (cost=0.15..4.17 rows=1 width=36) (actual time=0.003..0.004 rows=1 loops=100)
                    Index Cond: (terms_id = o.terms_id)
        ->  Index Scan using shipvia_pkey on shipvia  (cost=0.15..4.17 rows=1 width=36) (actual time=0.006..0.008 rows=1 loops=100)
              Index Cond: (ship_via_id = o.ship_via_id)
Total runtime: 27.572 ms

太棒了!谢谢大家的帮助。

【问题讨论】:

  • 首先:对列引用进行完全别名限定,从 ORDER BY o.modified DESCBINGO 开始!

标签: postgresql sql-execution-plan


【解决方案1】:

对于初学者来说,将解释计划发布到 http://explain.depesz.com 是非常标准的 - 这将为其添加一些漂亮的格式,为您提供一种分发计划的好方法,并让您匿名化可能包含敏感数据的计划。即使您不分发计划,也可以更轻松地了解正在发生的事情,并且有时可以准确说明瓶颈所在。

有无数资源可以解释 postgres 解释计划的细节(请参阅https://wiki.postgresql.org/wiki/Using_EXPLAIN)。数据库选择计划时需要考虑很多小细节,但有一些通用概念可以使其更容易。首先,掌握数据和索引的基于页面的布局(你不需要知道页面格式的细节,只需要知道数据和索引是如何分成页面的)。从那里开始,感受一下两种基本的数据访问方法——全表扫描和索引扫描——稍微思考一下,应该开始清楚其中一种比另一种更受欢迎的不同情况(也请记住,索引扫描甚至不总是可能的)。此时,您可以开始研究影响计划选择的一些不同配置项,了解它们可能会如何倾斜规模以支持表扫描或索引扫描。

一旦你把它搞定了,继续执行计划并阅读你找到的不同节点的详细信息 - 在这个计划中你有很多哈希连接,所以请阅读它以开始.然后,要将苹果与苹果进行比较,请完全禁用哈希连接(“set enable_hashjoin = false;”)并再次运行您的解释分析。现在你看到了什么加入方法?仔细阅读。将该方法的估计成本与散列连接的估计成本进行比较。为什么它们可能不同?第二个计划的估计成本将高于第一个计划(否则它本来会是首选)但是运行第二个计划所需的实际时间呢?是低还是高?

最后,具体来说一下这个计划。关于需要很长时间的那种: distinct 不是函数。 “DISTINCT(id)”并不是说“给我所有在列 id 上不同的行”,而是对行进行排序并根据输出中的所有列获取唯一值(即相当于写“不同的 id ...”)。你可能应该重新考虑你是否真的需要那个独特的地方。规范化将倾向于设计消除对不同的需求,虽然偶尔会需要它们,但它们是否真的超级真正需要并不总是正确的。

【讨论】:

  • +1 链接到explain.depesz.com。我将在帖子正文中添加DISTINCT 的原因 - 有一个很好的原因。但是,如果您知道一种在保留语义的同时消除它的方法 - 我非常愿意知道它。
  • 您可能会考虑类似“从存在的发票中选择 (从 o.id = items.invoice_id 和 items.qty
  • 如果您要保留一个模式列表(即使是在心理上),这是一个很好的选择:当您只想要一个实体列表时满意,尝试“exists”或“in”,避免“distinct”。 Exists/IN 通常会构建您想要的内容 - “distinct”获取所有内容,然后修剪为您想要的内容。
【解决方案2】:

您首先追踪耗时最长的节点,然后开始在那里进行优化。在你的情况下,这似乎是

Seq Scan on invoiceitems items

你应该在那里添加一个索引,其他表也有问题。

您也可以尝试增加work_mem 以摆脱外部排序。

当你这样做后,新计划可能看起来会完全不同,所以重新开始吧。

【讨论】:

  • 您的意思是索引 invoiceItems 表的 qty 字段吗?奇怪的是我确实尝试过,但计划完全一样。看起来数据库认为执行索引不值得。但我会重试。
  • 不,invoice_id 列。
  • invoice_id 列已编入索引。就像数量列一样。然而,它们似乎没有得到锻炼。我会按照stackoverflow.com/users/808806/yieldsfalsehood 的建议尝试消除DISTINCT
猜你喜欢
  • 2010-09-09
  • 2020-09-01
  • 2023-03-19
  • 2020-11-29
  • 2011-05-21
  • 2021-08-23
  • 1970-01-01
  • 2011-02-16
  • 1970-01-01
相关资源
最近更新 更多