PostgreSQL 的 enable_tidscan:一个几乎不需要调整的查询规划开关

2026-07-13 31 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:6 分钟

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 通常会创建新的行版本,因此同一条逻辑记录可能获得新的 ctidVACUUM 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 的价值主要在于提供规划器实验和故障诊断能力,而不是充当常规性能旋钮。知道它为什么很少需要调整,比尝试寻找一个“最佳值”更重要。


相关推荐