本文档解读 Amit Langote 提交的外键检查快速路径优化系列,该优化通过绕过 SPI(Server Programming Interface)直接探测索引,显著提升外键约束检查性能。
Commit | 描述 |
|---|---|
2da86c1ef9b5 | Add fast path for foreign key constraint checks |
b7b27eb41a5cc | Optimize fast-path FK checks with batched index probes |
edee5634564d | Make FastPathMeta self-contained by copying FmgrInfo structs |
e484b0eea614 | Fix two issues in fast-path FK check |
68a8601ee9ec | Fix use-after-free in ri_LoadConstraintInfo |
外键约束检查传统上使用 SPI 执行 SELECT ... FOR KEY SHARE 来验证引用完整性。这种方式存在以下开销:
// src/backend/utils/adt/ri_triggers.c (约 line 430-460)
// SPI 路径伪代码
SPI_connect(); // 每行一次连接开销
SetUserIdAndSecContext(); // 每行一次安全上下文切换
SPI_execute_snapshot(); // 执行查询
SPI_finish(); // 每行一次断开连接
每行都有一次完整的 CCI(Command Counter Increment)和安全上下文切换,这是外键检查的主要开销来源。
快速路径的核心函数位于 src/backend/utils/adt/ri_triggers.c:
if (ri_fastpath_is_applicable(riinfo))
{
if (AfterTriggerIsActive())
ri_FastPathBatchAdd(...); // 普通 DML:批量路径
else
ri_FastPathCheck(...); // ALTER TABLE VALIDATION:单行路径
return NULL;
}
// 回退到 SPI 路径...
static bool
ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo)
{
/* 分区 PK 表跳过 - 需要通过 PartitionDirectory 路由探查 */
if (riinfo->pk_is_partitioned)
return false;
/* 时态 FK 使用范围重叠语义 - 仍使用 SPI */
if (riinfo->hasperiod)
return false;
return true;
}
快速路径禁用于:
WITHOUT OVERLAPS):需要 &&、<@、范围聚合等语义ri_FastPathCheck(...)
{
// 1. 获取/创建缓存条目
entry = ri_FastPathGetEntry(fcinfo, riinfo);
// 2. 构造索引扫描键
// ...
// 3. 执行索引探查
index_beginscan(...)
index_getnext_slot(...) // 获取匹配行并加锁
index_endscan(...)
// 4. 验证是否存在匹配行
}
// 批量添加 FK 行到缓冲区
ri_FastPathBatchAdd(RI_FastPathArgData *arg)
{
FastPathArgData *ent = &entry->batch[entry->batch_count++];
ent->fk attnum = arg->fk attnum;
// 复制要检查的列值
// ...
// 缓冲区满时刷新
if (entry->batch_count >= RI_FASTPATH_BATCH_SIZE)
ri_FastPathBatchFlush(entry);
}
批次大小:RI_FASTPATH_BATCH_SIZE = 64 (line 213)
ri_FastPathFlushArray(RI_FastPathEntry *entry)
{
// 使用 SK_SEARCHARRAY 进行数组扫描
ScanKey sk = ...;
sk->sk_argument = entry->batch_values; // 包含 64 个键值的数组
index_beginscan(...)
while (index_getnext_slot(...))
{
// 对每个匹配行加锁
table_tuple_lock(...);
}
index_endscan(...);
}
ri_FastPathFlushLoop(RI_FastPathEntry *entry)
{
for (i = 0; i < entry->batch_count; i++)
{
// 每行单独探查一次
// 构造复合索引键...
index_beginscan(...)
index_getnext_slot(...)
// 处理匹配...
index_endscan(...);
}
}
typedef struct RI_FastPathEntry
{
Oid conoid; // 哈希键: pg_constraint OID
Oid fk_relid; // 用于 ri_FastPathEndBatch()
Relation pk_rel; // PK 表 Relation
Relation idx_rel; // 索引 Relation
TupleTableSlot *pk_slot; // PK 元组槽
TupleTableSlot *fk_slot; // FK 元组槽
MemoryContext flush_cxt; // 每次刷新的工作内存上下文
int batch_count; // 当前批次行数
HeapTuple batch[RI_FASTPATH_BATCH_SIZE]; // 批次缓冲区
} RI_FastPathEntry;
typedef struct FastPathMeta
{
FmgrInfo eq_opr_finfo[RI_MAX_NUMKEYS]; // 等值操作符函数信息
FmgrInfo cast_func_finfo[RI_MAX_NUMKEYS]; // 类型转换函数信息
RegProcedure regops[RI_MAX_NUMKEYS]; // 注册的操作符
int strats[RI_MAX_NUMKEYS]; // 策略编号
Oid subtypes[RI_MAX_NUMKEYS]; // 子类型
} FastPathMeta;
方面 | 快速路径 | SPI 路径 |
|---|---|---|
机制 | 直接索引扫描 (index_beginscan + index_getnext_slot) | SPI_execute_snapshot() + SELECT ... FOR KEY SHARE |
每行开销 | 批次(最多64行)共享一次 CCI 和安全上下文切换 | 每行一次 SPI_connect()/SPI_finish() 和 SetUserIdAndSecContext() |
计划缓存 | RI_FastPathEntry 哈希表缓存 | SPI 计划缓存 (ri_FetchPreparedPlan()) |
元数据查找 | ri_populate_fastpath_metadata() 缓存操作符/转换函数一次 | 每行通过 ri_HashCompareOp() 查找 |
锁定 | 直接 table_tuple_lock() 调用 | 通过 SPI 执行 SELECT ... FOR KEY SHARE |
多列处理 | 单列:使用 SK_SEARCHARRAY;多列:每行一次探查 | 始终每行执行一次 SELECT |
核心优势说明(lines 2901-2909 注释):
"CCI 和安全上下文切换对整个批次只执行一次。每行 CCI 是不必要的,因为...单个 CCI 会推进所有效果。每行安全上下文切换是不必要的,因为每行的探查完全以 PK 表所有者身份运行...唯一区别是 SPI 路径每行设置和恢复上下文,而我们围绕整个批次只做一次。"
由于快速路径是自动启用的,DBA 可以通过性能视图观察:
-- 监控外键检查性能
SELECT * FROM pg_stat_user_tables WHERE relname = 'your_fk_table';
-- 检查查询计划中的触发器执行时间
EXPLAIN ANALYZE INSERT INTO child_table (fk_col) VALUES (1);
以下情况不使用快速路径:
-- 分区 PK 表 - 不使用快速路径
CREATETABLE pk_parent (idINT PRIMARY KEY) PARTITIONBYRANGE (id);
CREATETABLEchild (fk_id INTREFERENCES pk_parent(id));
-- 时态 FK - 不使用快速路径
CREATETABLE t1 (r intrange);
CREATETABLE t2 (r intrange REFERENCES t1 NOT VALID);
-- 测试脚本
DROPTABLEIFEXISTS pk_table CASCADE;
DROPTABLEIFEXISTS fk_table CASCADE;
CREATETABLE pk_table (idINT PRIMARY KEY);
CREATETABLE fk_table (idINTREFERENCES pk_table(id));
-- 批量插入 100 万行
INSERTINTO pk_table SELECT generate_series(1, 1000000);
INSERTINTO fk_table SELECT generate_series(1, 1000000);
-- 性能对比(快速路径应比之前快约 1.8 倍)
EXPLAINANALYZEINSERTINTO fk_table SELECT generate_series(1, 1000000);
-- 查看触发器相关信息
SELECT
tgname,
tgtype,
tgconstrrelid::regclass AS referencing_table,
tgconstrid::regclass AS constraint_name
FROM pg_trigger
WHERE tgparentid != 0;
如果外键检查变慢,检查:
-- 确保索引存在(快速路径依赖唯一索引)
SELECT
conname,
conrelid::regclass,
confrelid::regclass,
pg_get_constraintdef(oid)
FROM pg_constraint
WHERE contype = 'r';
-- 检查是否有缺失索引
SELECT
c.relname AS foreign_table,
j.attname AS foreign_column
FROM pg_constraint k
JOIN pg_class c ON c.oid = k.conrelid
JOIN pg_class c2 ON c2.oid = k.confrelid
JOIN pg_attribute j ON j.attrelid = c.oid AND j.attnum = k.confkey[1]
LEFTJOIN pg_index i ON i.indrelid = c.oid AND i.indkey[1] = j.attnum
WHERE k.contype = 'r'AND i.indkey ISNULL;
外键检查快速路径是 2026 年最重要的性能改进之一。通过绕过 SPI 直接使用索引探查,批量插入场景下获得约 1.8 倍的性能提升。该优化对 DBA 透明,自动生效,但需注意:
pg_stat_user_tables 观察性能变化