PostgreSQL 的 check_function_bodies:函数创建时的语法校验开关

2026-05-21 23 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:7 分钟

PostgreSQL 在执行 CREATE FUNCTION 时,默认会立刻检查函数体的语法有效性——拼写错误、引用了不存在的对象,都会在建函数的那一刻报错,而不是等到第一次调用时才炸。这个行为由 GUC 参数 check_function_bodies 控制,默认值为 on。看起来很合理,但实际场景里你会遇到需要把它关掉的时刻。

默认行为:创建即校验

先看默认情况下 PostgreSQL 做了什么。当你写下:

CREATE OR REPLACE FUNCTION get_user_name(user_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN selct name FROM users WHERE id = user_id;  -- 拼写错误:selct
END;
$$;

PostgreSQL 立刻抛出错误:

ERROR:  syntax error at or near "selct"
LINE 3:     RETURN selct name FROM users WHERE id = user_id;

函数根本不会被创建。这比"建成功了、调用时才报错"要好得多——你不会在上线后才发现一个低级拼写问题。

同样,如果函数体引用了一个尚不存在的表:

CREATE OR REPLACE FUNCTION count_orders()
RETURNS bigint
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN COUNT(*) FROM orders_not_yet_created;
END;
$$;

默认情况下也会报错:

ERROR:  relation "orders_not_yet_created" does not exist

这种"提前拦截"是 check_function_bodies = on 的核心价值。

什么时候需要关掉它

问题出在第二个例子上。在真实项目中,函数引用尚未创建的对象是常见需求,典型场景有三类:

迁移脚本中的创建顺序依赖。 你的迁移文件里,函数 A 引用表 B,但表 B 在同一批迁移中排在后面才创建。默认校验会让整个迁移脚本跑不过去。

pg_dump 的恢复过程。 pg_dump 生成的恢复脚本开头就显式设置 SET check_function_bodies = off;,正是因为导出顺序无法保证所有被引用对象都已存在——函数可能引用稍后才恢复的表、视图或其他函数。

跨 schema 的延迟依赖。 函数引用其他 schema 的对象,而那些 schema 在当前会话中尚不可访问(权限或搜索路径问题),但运行时是可用的。

实操:关闭校验完成批量迁移

下面是一个完整的迁移脚本示例,演示如何在创建顺序有依赖时关闭校验:

-- migration_001.sql
-- 场景:函数引用了同批迁移中稍后才创建的表

BEGIN;

-- 关闭函数体校验,允许引用尚未创建的对象
SET LOCAL check_function_bodies = off;

-- 先建函数(引用了 orders 表,但 orders 还不存在)
CREATE OR REPLACE FUNCTION total_revenue(p_customer_id integer)
RETURNS numeric
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN SUM(amount)
        FROM orders
        WHERE customer_id = p_customer_id;
END;
$$;

-- 再建被引用的表
CREATE TABLE orders (
    id          serial PRIMARY KEY,
    customer_id integer NOT NULL,
    amount      numeric(10,2) NOT NULL
);

-- 恢复默认校验行为,后续语句回到安全状态
RESET check_function_bodies;

COMMIT;

几点说明:

  • SET LOCAL 让设置只在当前事务内生效,COMMIT 后自动恢复默认值。这是最安全的做法——不要用 SET(不带 LOCAL),那会让整个会话都跳过校验。
  • 关闭校验只跳过语法和对象引用的检查,函数的语言(LANGUAGE plpgsql)和参数类型声明仍然会被验证。
  • 如果函数体本身有真正的语法错误(比如 SELCT 而不是 SELECT),关闭校验后函数能建成功,但调用时会报错。风险自担。

验证两种模式的差异

你可以用下面这段脚本直接对比两种行为:

# 用 psql 运行对比测试
psql -d yourdb -f test_check_function_bodies.sql
-- test_check_function_bodies.sql

-- 测试 1:默认模式,引用不存在的表 → 报错
\echo '>>> Test 1: default check_function_bodies = on'

CREATE OR REPLACE FUNCTION broken_ref()
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN (SELECT name FROM nonexistent_table LIMIT 1);
END;
$$;
-- 预期:ERROR: relation "nonexistent_table" does not exist

-- 测试 2:关闭校验,同样的函数 → 创建成功
\echo '>>> Test 2: check_function_bodies = off'

SET check_function_bodies = off;

CREATE OR REPLACE FUNCTION broken_ref()
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN (SELECT name FROM nonexistent_table LIMIT 1);
END;
$$;
-- 预期:CREATE FUNCTION(成功,但调用时会报错)

RESET check_function_bodies;

-- 清理
DROP FUNCTION IF EXISTS broken_ref();

决策清单

场景 建议
日常开发、手动建函数 保持 on,让错误尽早暴露
执行 pg_dump 恢复 保持 off(脚本已自动设置)
批量迁移脚本、对象间有创建顺序依赖 SET LOCAL check_function_bodies = off,事务结束后自动恢复
只有一个函数引用尚未创建的对象 考虑调整迁移顺序,把表建在函数前面,而不是关校验
函数体引用运行时才确定存在的临时表 关闭校验是合理选择,plpgsql 本身对临时表就是延迟解析的

核心判断原则:能用默认校验就用默认校验。关掉它意味着你把语法错误和引用错误的发现时机从"创建时"推迟到了"调用时",这增加了上线后出问题的概率。只在创建顺序确实无法调整时才关闭,并且务必用 SET LOCAL 把影响范围限制在单个事务内。


相关推荐