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 把影响范围限制在单个事务内。