报告的索引大小和执行计划中的缓冲区数之间存在巨大的不匹配


10

问题

我们有一个类似的查询

SELECT COUNT(1) 
  FROM article
  JOIN reservation ON a_id = r_article_id 
 WHERE r_last_modified < now() - '8 weeks'::interval 
   AND r_group_id = 1 
   AND r_status = 'OPEN';

由于超时(通常在10分钟后)更多,因此我决定调查此问题。

EXPLAIN (ANALYZE, BUFFERS)输出如下所示:

 Aggregate  (cost=264775.48..264775.49 rows=1 width=0) (actual time=238960.290..238960.291 rows=1 loops=1)
   Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
   I/O Timings: read=169806.955 write=0.154
   ->  Hash Join  (cost=52413.67..264647.65 rows=51130 width=0) (actual time=1845.483..238957.588 rows=21644 loops=1)
         Hash Cond: (reservation.r_article_id = article.a_id)
         Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
         I/O Timings: read=169806.955 write=0.154
         ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..205458.72 rows=51130 width=4) (actual time=34.035..237000.197 rows=21644 loops=1)
               Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
               Rows Removed by Filter: 151549
               Buffers: shared hit=200193 read=48853 dirtied=450 written=8
               I/O Timings: read=168614.105 write=0.154
         ->  Hash  (cost=29662.22..29662.22 rows=1386722 width=4) (actual time=1749.392..1749.392 rows=1386814 loops=1)
               Buckets: 32768  Batches: 8  Memory Usage: 6109kB
               Buffers: shared hit=287 read=15508 dirtied=216, temp written=3551
               I/O Timings: read=1192.850
               ->  Seq Scan on article  (cost=0.00..29662.22 rows=1386722 width=4) (actual time=23.822..1439.310 rows=1386814 loops=1)
                     Buffers: shared hit=287 read=15508 dirtied=216
                     I/O Timings: read=1192.850
 Total runtime: 238961.812 ms

瓶颈节点显然是索引扫描。因此,让我们看一下索引定义:

CREATE INDEX reservation_r_article_id_idx1 
    ON reservation USING btree (r_article_id)
 WHERE (r_status <> ALL (ARRAY['FULFILLED', 'CLOSED', 'CANCELED']));

大小和行号

它的大小(通过\di+或通过访问物理文件报告)为36 MB。由于预订通常只在较短的时间内用在上面未列出的所有状态上,因此发生了很多更新,因此索引相当膨胀(此处浪费了大约24 MB)-仍然,其大小相对较小。

reservation表的大小约为3.8 GB,包含约4000万行。尚未关闭的保留数约为170,000(确切的数目在上面的索引扫描节点中报告)。

现在令人惊讶的是:索引扫描报告报告正在获取大量缓冲区(即8 kb页):

Buffers: shared hit=200193 read=48853 dirtied=450 written=8

从缓存和磁盘(或OS缓存)读取的数字总计为1.9 GB!

最坏的情况

另一方面,在最坏的情况下,当每个元组位于表的不同页面上时,将占去访问(21644 + 151549)+ 4608页(从表中获取的总行数加上从物理表中获取的索引页数)尺寸)。这仍然只有不到18万,远低于观察到的近25万。

有趣(也许很重要)的是,磁盘读取速度约为2.2 MB / s,这很正常,我猜。

所以呢?

有谁知道这种差异可能来自何处?

注意:明确地说,我们在这里有什么要改进/更改的想法,但是我真的很想了解我得到的数字-这就是问题所在。

更新:检查缓存或微真空的效果

根据jjanes的回答,我检查了立即重新运行完全相同的查询时会发生什么。受影响的缓冲区的数量并没有真正改变。(为此,我将查询简化到最低限度,仍然显示了该问题。)这是我从第一次运行中看到的结果:

 Aggregate  (cost=240541.52..240541.53 rows=1 width=0) (actual time=97703.589..97703.590 rows=1 loops=1)
   Buffers: shared hit=413981 read=46977 dirtied=56
   I/O Timings: read=96807.444
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240380.54 rows=64392 width=0) (actual time=13.757..97698.461 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232481
         Buffers: shared hit=413981 read=46977 dirtied=56
         I/O Timings: read=96807.444
 Total runtime: 97703.694 ms

在第二个之后:

 Aggregate  (cost=240543.26..240543.27 rows=1 width=0) (actual time=388.123..388.124 rows=1 loops=1)
   Buffers: shared hit=460990
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240382.28 rows=64392 width=0) (actual time=0.032..385.900 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232584
         Buffers: shared hit=460990
 Total runtime: 388.187 ms

1
可能无关紧要,但您需要加入article吗?似乎所有涉及的列都来自reservation表,并且(假设)有一个FK,结果应该是相同的。
ypercubeᵀᴹ

这是一个很好的问题。没错,这是不需要的-这是另一个团队在监视中使用的查询。不过,至少从查询计划来看,所有其他一切都只是对讨厌的索引扫描的一种修饰:)
dezso 2015年

1
让我补充说,删除联接并没有太大的不同-过分的索引扫描将保留在那里。
dezso 2015年

烤面包表访问?尽管我怀疑您显示的任何专栏都会被敬酒。如果您有一个空闲的数据库克隆用于测试目的,则可以pg_stat_reset()在该数据库上运行该数据库,然后运行查询,然后查看该数据库pg_statio_user_tables在何处将这些块归为属性。
jjanes

Answers:


4

我认为这里的关键是大量的更新,以及索引的膨胀。

索引包含指向表中不再“活动”的行的指针。这些是更新行的旧版本。将旧的行版本保留一会儿,以使用旧快照满足查询,然后保留一会儿,因为没有人愿意做比必要更多的删除工作。

扫描索引时,它必须访问这些行,然后注意到它们不再可见,因此将其忽略。该explain (analyze,buffers)语句没有明确报告此活动,除非通过检查这些行的过程中对读取/命中的缓冲区进行计数。

btree有一些“ microvacuum”代码,这样当扫描再次返回索引时,它会记住被追逐的指针不再有效,并在索引中将其标记为无效。这样,下一个运行的类似查询就无需再次追逐它。因此,如果再次运行完全相同的查询,则可能会看到缓冲区访问次数接近您的预期。

您还可以VACUUM更频繁地使用表,这将从表本身中清除死元组,而不仅仅是从部分索引中清除。通常,与默认级别相比,具有高周转部分索引的表可能会从更具侵略性的真空中受益。


请看我的编辑-对我来说,它看起来像是缓存,而不是微真空。
dezso

您的新数字与旧数字有很大的不同(大约两倍),因此,如果不查看实际行的新数字和为索引扫描过滤的行,就很难解释它们的含义。
jjanes 2015年

添加了今天的完整计划。自上周五以来,受影响的缓冲区数量增加了很多,行数也是如此。
dezso 2015年

您是否有长期交易?如果是这样,则索引扫描可能仍在跟踪对其不可见的行(这会导致额外的缓冲区命中),但仍无法对它们进行微真空处理,因为其他年龄较大的其他人可能会看到它们快照。
jjanes 2015年

我没有-典型的交易时间不到一秒钟。偶尔几秒钟,但不会更长。
dezso
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.