另一个问题来自我发现EXPLAIN
PostgreSQL 中很棒的新选项。这一个侧重于BUFFERS
选项。
这是解释:
EXPLAIN (ANALYZE, BUFFERS) SELECT event_time FROM ui_events_v2 WHERE page ~ 'foo' LIMIT 1;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
Limit (cost=0.00..1539.68 rows=1 width=8) (actual time=0.858..0.858 rows=1 loops=1)
Buffers: shared read=10
I/O Timings: read=0.735
-> Seq Scan on ui_events_v2 (cost=0.00..3313394.58 rows=2152 width=8) (actual time=0.857..0.857 rows=1 loops=1)
Filter: (page ~ 'foo'::text)
Rows Removed by Filter: 112
Buffers: shared read=10
I/O Timings: read=0.735
Planning Time: 6.455 ms
Execution Time: 0.877 ms
它非常快 - 而且这个查询在冷启动时非常慢。这是一个 30M 行的表,七行page
包含 substring foo
。与此查询匹配的第一行似乎显示了 604k 页面,这将是中提到的 ~3M 页面的 ~20% pg_class
:
SELECT min(ctid) FROM ui_events_v2 WHERE page ~ 'foo';
min
-------------
(604435,10)
我的假设是,即使该表的每个页面都在 PostgreSQL 或操作系统缓存中,它仍然需要遍历一些线性页面列表以进行顺序扫描。sectionBuffers: shared read=10
和0.877 ms
执行时间向我表明,它以某种方式“从中断的地方开始”并从远离开始的页面开始。我认为它通过页面 ID 移动并在移动时尝试查看每个页面的缓存,但它是否可能从缓存本身开始?如果是这样,它如何发现非缓存页面?
我知道不能保证以任何特定顺序找到行,但我认为在相同的查询策略中,遵循的路径会相对相似。
如果多个进程尝试对同一个表进行 seq 扫描,它将尝试将它们全部排成一行,以便所有进程几乎同时读取相同的页面,以最大限度地提高缓存命中率。它通过让后来者从中间开始(现有的那些恰好在的地方)开始,然后环绕结束以在开始之前的页面完成表格。这样做的副作用是,如果一个 seq 扫描提前停止,下一个将在最后一个停止的同一页面上启动。因此,如果整个 LIMIT 可以通过一页的行来满足,那么像您这样的扫描只会一遍又一遍地扫描同一页。
您可以使用 关闭此优化
set synchronize_seqscans=off
,然后它们都从表中的第一个块开始。