PostgreSQL 的 enable_tidscan 控制查询规划器是否考虑 TID Scan,也就是根据元组标识符直接定位表中的物理行。它听起来像一个值得调优的扫描策略,但实际触发条件非常窄:查询通常必须显式使用 ctid。因此,对绝大多数应用来说,这个参数保持默认值即可。
TID Scan 到底扫描什么
PostgreSQL 表中的每个行版本都有一个系统列 ctid。它包含两个坐标:数据页编号和该页中的行位置。例如,(3,7) 表示某个数据页上的第 7 个元组位置。
当查询使用等值条件指定 ctid 时,PostgreSQL 可以直接访问对应位置,不必顺序检查全表,也不需要经过普通索引:
SELECT *
FROM accounts
WHERE ctid = '(3,7)';
这种访问方式在执行计划中通常显示为 Tid Scan。关键点在于,它不会因为某张表缺少索引就自动出现,也不是顺序扫描的通用替代方案。应用必须先拿到 ctid,再明确按它查询。
用一个可运行的实验观察执行计划
下面的示例可直接在 PostgreSQL 的 psql 中运行。它创建测试表,取出一行的 ctid,然后比较启用和关闭 enable_tidscan 时的计划。具体成本数字和备用计划可能随 PostgreSQL 版本、统计信息及数据量变化。
DROP TABLE IF EXISTS tidscan_demo;
CREATE TABLE tidscan_demo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
payload text NOT NULL
);
INSERT INTO tidscan_demo (payload)
SELECT 'row-' || n
FROM generate_series(1, 10000) AS n;
ANALYZE tidscan_demo;
-- 先查看目标行当前的物理位置。
SELECT id, ctid, payload
FROM tidscan_demo
WHERE id = 5000;
-- 将下一条语句中的 CTID 替换为上一条查询返回的值。
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM tidscan_demo
WHERE ctid = '(0,1)';
SET enable_tidscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM tidscan_demo
WHERE ctid = '(0,1)';
RESET enable_tidscan;
为了避免手工替换 ctid,也可以在 psql 中使用变量:
SELECT ctid AS target_ctid
FROM tidscan_demo
WHERE id = 5000
\gset
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM tidscan_demo
WHERE ctid = :'target_ctid'::tid;
这个实验展示的是规划器行为,不是推荐的业务查询模式。关闭参数后,规划器不能采用正常的 TID Scan,往往只能选择代价更高的路径来求值 ctid 条件。
为什么业务代码不该把 ctid 当主键
ctid 描述的是行版本当前所在的物理位置,不是稳定的业务标识。一次 UPDATE 通常会创建新的行版本,因此同一条逻辑记录可能获得新的 ctid。VACUUM FULL 等重写表的操作也会改变物理布局。
下面的实验可以直观看到这种变化:
SELECT id, ctid, payload
FROM tidscan_demo
WHERE id = 5000;
UPDATE tidscan_demo
SET payload = payload || '-updated'
WHERE id = 5000;
SELECT id, ctid, payload
FROM tidscan_demo
WHERE id = 5000;
因此,不能把 ctid 持久化到缓存、消息或其他表中,并假设它长期指向同一条记录。跨事务执行“先读取 ctid,稍后再更新”的流程,还需要考虑并发更新、行版本可见性和目标位置失效。
ctid 更适合短生命周期的维护任务。例如,在同一个受控操作中分批处理重复数据,或者诊断某个行版本的存储位置。即使在这些场景里,也应先验证并发模型和事务边界。
什么时候才值得碰 enable_tidscan
实践中的判断很简单:
- 普通 CRUD 使用主键或业务索引时,保持
enable_tidscan = on。 - 查询中完全没有
ctid条件时,这个开关通常与性能问题无关。 - 只有在复现规划器行为、比较执行计划或排查极特殊的
ctid查询时,才临时修改它。 - 测试结束后执行
RESET enable_tidscan,不要把会话级实验直接变成全局配置。 - 若业务依赖
ctid获得性能,应重新检查数据模型;稳定主键和合适的索引通常更可靠。
enable_tidscan 的价值主要在于提供规划器实验和故障诊断能力,而不是充当常规性能旋钮。知道它为什么很少需要调整,比尝试寻找一个“最佳值”更重要。