简单联接中未使用的主键索引


16

我有以下表和索引定义:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

为什么在以下查询中不使用munkalap_id上的索引?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

即使添加过滤器也是如此:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms

Answers:


22

许多人都听过有关“顺序扫描是不好的”的指导,并试图将其从计划中删除,但这并不是那么简单。如果查询将覆盖表中的每一行,则顺序扫描是获取这些行的最快方法。这就是为什么您的原始联接查询使用seq扫描的原因,因为两个表中的所有行都是必需的。

在计划查询时,Postgres的计划者会估计在不同可能方案下的各种操作(计算,顺序和随机IO)的成本,并选择估计成本最低的计划。从旋转存储(磁盘)进行IO时,随机IO通常比顺序IO慢得多,random_page_cost和seq_page_cost的默认pg配置估计成本差异为4:1。

在考虑使用索引或顺序扫描表的连接或筛选方法时,这些考虑因素会起作用。使用索引时,计划可能会通过索引快速找到行,然后必须考虑读取的随机块来解析行数据。在第二个查询中添加了过滤谓词的情况下WHERE NOT lezarva,您可以在EXPLAIN ANALYZE结果中看到这对计划估算的影响。计划者估计联接产生的1006行(与实际结果集964非常接近)。鉴于较大的表munkalap_lepes包含约38K行,因此计划者认为联接将不得不访问表中的约1006/38046或1/38行。它还知道平均行宽度为214字节,一个块为8K,所以每个块大约38行。

借助这些统计信息,计划者认为联接可能必须读取表的所有或大部分数据块。由于索引查找也不是免费的,并且相对于IO而言,扫描块以评估过滤条件的计算非常便宜,因此计划程序选择顺序扫描表并避免索引开销和随机读取,因为它会计算seq扫描会更快。

在现实世界中,数据通常可以通过OS页面缓存在内存中使用,因此并非每个读取的块都需要IO。很难预测缓存对给定查询的有效性,但是Pg计划程序确实使用了一些简单的试探法。配置值effective_cache_size通知计划者估计发生实际IO成本的可能性。较大的值将导致其估计随机IO的成本较低,因此可能会使其在顺序扫描上偏向于索引驱动的方法。


谢谢,这是迄今为止我所阅读的最好的(也是最简洁的)描述。阐明了几个关键点。
dezso 2012年

1
很好的解释。但是,行/数据页的计算有些偏离。您必须考虑页面标题(24字节)+每个行项目指针的4个字节+行标题HeapTupleHeader(每行23个字节)+ NULL位掩码+根据MAXALIGN的对齐方式。最后,取决于列的数据类型及其顺序,由于数据对齐而导致的填充量未知。在这种情况下,在8 kb的页面上总共不超过33行。(不考虑TOAST。)
Erwin Brandstetter,2012年

1
@ErwinBrandstetter感谢您填写更严格的行大小计算。我一直认为,explain输出的行宽估计将包括每行注意事项,例如标头和NULL位掩码,但不包括页级开销。
dbenhur

1
@dbenhur:您可以EXPLAIN ANALYZE SELECT foo from bar使用基本的虚拟表快速进行验证。此外,实际的磁盘空间取决于数据对齐方式,当仅检索某些行时,很难考虑这一点。中的行宽EXPLAIN表示检索到的列集的基本空间要求。
Erwin Brandstetter'4

5

您正在从两个表中检索所有行,因此使用索引扫描并没有真正的好处。仅当您从表中仅选择几行(通常少于10%-15%)时,索引扫描才有意义


是的,您是对的:)我试图通过更具体的情况来澄清这种情况,请参阅最后一个查询。
dezso 2012年

@dezso:一样。如果索引已打开(lezarva, munkalap_id)并且具有足够的选择性,则可以使用它。这NOT使得可能性较小。
ypercubeᵀᴹ

我根据您的建议添加了部分索引并使用了该索引,因此解决了一半问题。但我不希望外键是没用的索引因为我想加入只针对87值相比原来的3252
·德热

1
@dezso行平均214字节宽,因此每个8K数据块的行数不到40。索引的选择性也约为1/40(1006/38046)。因此,Pg认为顺序读取所有块比使用索引随机读取大约相同数量的块便宜。这些估计的交易量可能会受到Effective_cache_size和random_page_cost配置值的影响。
dbenhur

@dbenhur:您能否将您的评论作为一个正确的答案?
dezso 2012年
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.