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)
我认为这里的关键是大量更新和索引的膨胀。
该索引包含指向表中不再“活动”的行的指针。这些是更新行的旧版本。旧的行版本会保留一段时间,以满足使用旧快照的查询,然后再保留一段时间,因为没有人愿意比必要时更频繁地删除它们。
当扫描索引时,它必须去访问这些行,然后注意到它们不再可见,因此忽略它们。该explain (analyze,buffers)语句不会明确报告此活动,除非在检查这些行的过程中对缓冲区读取/命中进行计数。
btree 有一些“microvacuum”代码,这样当扫描再次返回索引时,它会记住它所追踪的指针不再有效,并在索引中将其标记为死亡。这样,下一个运行的类似查询就不需要再次追踪它。因此,如果您再次运行完全相同的查询,您可能会看到缓冲区访问量下降到更接近您的预测。
您还可以VACUUM更频繁地清理表,这将从表本身中清除死元组,而不仅仅是从部分索引中清除。一般来说,具有高周转率部分索引的表可能会受益于比默认水平更激进的真空。