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

dez*_*zso 10 postgresql index execution-plan postgresql-9.3

问题

我们有一个类似的查询

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';
Run Code Online (Sandbox Code Playgroud)

由于它经常遇到超时(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
Run Code Online (Sandbox Code Playgroud)

瓶颈节点显然是索引扫描。那么让我们看看索引定义:

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

大小和行号

它的大小(由\di+或通过访问物理文件报告)为 36 MB。由于保留在上面未列出的所有状态中通常只花费相对较短的时间,因此发生了很多更新,因此索引非常臃肿(这里浪费了大约 24 MB) - 尽管如此,大小还是相对较小。

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

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

Buffers: shared hit=200193 read=48853 dirtied=450 written=8
Run Code Online (Sandbox Code Playgroud)

从缓存和磁盘(或操作系统缓存)读取的数字加起来为 1.9 GB!

最坏的情况

另一方面,最坏的情况是,当每个元组都位于表的不同页上时,将考虑访问 (21644 + 151549) + 4608 页(从表中获取的总行数加上物理页中的索引页数)。尺寸)。这仍然只有不到 180,000 - 远低于观察到的近 250,000。

有趣(也许很重要)的是,磁盘读取速度约为 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
Run Code Online (Sandbox Code Playgroud)

在第二个之后:

 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
Run Code Online (Sandbox Code Playgroud)

jja*_*nes 4

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

该索引包含指向表中不再“活动”的行的指针。这些是更新行的旧版本。旧的行版本会保留一段时间,以满足使用旧快照的查询,然后再保留一段时间,因为没有人愿意比必要时更频繁地删除它们。

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

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

您还可以VACUUM更频繁地清理表,这将从表本身中清除死元组,而不仅仅是从部分索引中清除。一般来说,具有高周转率部分索引的表可能会受益于比默认水平更激进的真空。