首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PG 19 预览 - 外键检查快速通道:绕过 SPI 的性能飞跃

PG 19 预览 - 外键检查快速通道:绕过 SPI 的性能飞跃

作者头像
用户4035096
发布2026-07-10 09:27:18
发布2026-07-10 09:27:18
50
举报

PG 19 预览 - 外键检查快速通道:绕过 SPI 的性能飞跃

本文档解读 Amit Langote 提交的外键检查快速路径优化系列,该优化通过绕过 SPI(Server Programming Interface)直接探测索引,显著提升外键约束检查性能。

相关 Commit

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

核心价值

  • 性能提升:批量 FK 插入(1M 行)约有 1.8 倍加速
  • 适用场景:非分区 PK 表、无时态语义的外键约束
  • 技术原理:绕过 SPI,直接使用索引探查

原理详解

1. 传统 SPI 路径的问题

外键约束检查传统上使用 SPI 执行 SELECT ... FOR KEY SHARE 来验证引用完整性。这种方式存在以下开销:

代码语言:javascript
复制
// src/backend/utils/adt/ri_triggers.c (约 line 430-460)
// SPI 路径伪代码
SPI_connect();           // 每行一次连接开销
SetUserIdAndSecContext(); // 每行一次安全上下文切换
SPI_execute_snapshot();  // 执行查询
SPI_finish();            // 每行一次断开连接

每行都有一次完整的 CCI(Command Counter Increment)和安全上下文切换,这是外键检查的主要开销来源。

2. 快速路径核心实现

快速路径的核心函数位于 src/backend/utils/adt/ri_triggers.c

2.1 决策入口 (line 463)
代码语言:javascript
复制
if (ri_fastpath_is_applicable(riinfo))
{
    if (AfterTriggerIsActive())
        ri_FastPathBatchAdd(...);   // 普通 DML:批量路径
    else
        ri_FastPathCheck(...);     // ALTER TABLE VALIDATION:单行路径
    return NULL;
}
// 回退到 SPI 路径...
2.2 适用性判断 (line 3285-3304)
代码语言:javascript
复制
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;
}

快速路径禁用于

  • 分区 PK 表:分区表需要特殊的路由逻辑
  • 时态外键(如 WITHOUT OVERLAPS):需要 &&<@、范围聚合等语义
2.3 单行检查 (line 2779)
代码语言:javascript
复制
ri_FastPathCheck(...)
{
    // 1. 获取/创建缓存条目
    entry = ri_FastPathGetEntry(fcinfo, riinfo);

    // 2. 构造索引扫描键
    // ...

    // 3. 执行索引探查
    index_beginscan(...)
    index_getnext_slot(...)  // 获取匹配行并加锁
    index_endscan(...)

    // 4. 验证是否存在匹配行
}
2.4 批量处理 (line 2859-2890)
代码语言:javascript
复制
// 批量添加 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)

2.5 批量刷新 - 单列 FK (line 3031)
代码语言:javascript
复制
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(...);
}
2.6 批量刷新 - 多列 FK (line 2987)
代码语言:javascript
复制
ri_FastPathFlushLoop(RI_FastPathEntry *entry)
{
    for (i = 0; i < entry->batch_count; i++)
    {
        // 每行单独探查一次
        // 构造复合索引键...

        index_beginscan(...)
        index_getnext_slot(...)
        // 处理匹配...
        index_endscan(...);
    }
}

3. 关键数据结构

3.1 每约束缓存条目 (line 233-251)
代码语言:javascript
复制
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;
3.2 FastPathMeta (line 152-159)
代码语言:javascript
复制
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;

4. SPI vs 快速路径对比

方面

快速路径

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

5. 为什么快速路径更快

核心优势说明(lines 2901-2909 注释):

"CCI 和安全上下文切换对整个批次只执行一次。每行 CCI 是不必要的,因为...单个 CCI 会推进所有效果。每行安全上下文切换是不必要的,因为每行的探查完全以 PK 表所有者身份运行...唯一区别是 SPI 路径每行设置和恢复上下文,而我们围绕整个批次只做一次。"


使用实践

1. 查看外键约束是否使用快速路径

由于快速路径是自动启用的,DBA 可以通过性能视图观察:

代码语言:javascript
复制
-- 监控外键检查性能
SELECT * FROM pg_stat_user_tables WHERE relname = 'your_fk_table';

-- 检查查询计划中的触发器执行时间
EXPLAIN ANALYZE INSERT INTO child_table (fk_col) VALUES (1);

2. 确认快速路径可用

以下情况使用快速路径:

代码语言:javascript
复制
-- 分区 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);

3. 批量插入性能测试

代码语言:javascript
复制
-- 测试脚本
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);

4. 监控外键触发器调用

代码语言:javascript
复制
-- 查看触发器相关信息
SELECT
    tgname,
    tgtype,
    tgconstrrelid::regclass AS referencing_table,
    tgconstrid::regclass AS constraint_name
FROM pg_trigger
WHERE tgparentid != 0;

5. 故障排除

如果外键检查变慢,检查:

代码语言:javascript
复制
-- 确保索引存在(快速路径依赖唯一索引)
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 透明,自动生效,但需注意:

  1. 适用条件:非分区 PK 表、无时态语义的外键
  2. 依赖:PK 表上存在唯一索引
  3. 监控:通过 pg_stat_user_tables 观察性能变化
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 digoal德哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PG 19 预览 - 外键检查快速通道:绕过 SPI 的性能飞跃
    • 相关 Commit
    • 核心价值
  • 原理详解
    • 1. 传统 SPI 路径的问题
    • 2. 快速路径核心实现
      • 2.1 决策入口 (line 463)
      • 2.2 适用性判断 (line 3285-3304)
      • 2.3 单行检查 (line 2779)
      • 2.4 批量处理 (line 2859-2890)
      • 2.5 批量刷新 - 单列 FK (line 3031)
      • 2.6 批量刷新 - 多列 FK (line 2987)
    • 3. 关键数据结构
      • 3.1 每约束缓存条目 (line 233-251)
      • 3.2 FastPathMeta (line 152-159)
    • 4. SPI vs 快速路径对比
    • 5. 为什么快速路径更快
  • 使用实践
    • 1. 查看外键约束是否使用快速路径
    • 2. 确认快速路径可用
    • 3. 批量插入性能测试
    • 4. 监控外键触发器调用
    • 5. 故障排除
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档