帮你快速理解、总结文档立即下载

plpgsql_check

最近更新时间:2026-07-21 09:53:00

我的收藏

概述

plpgsql_check 是 PostgreSQL 的 PL/pgSQL 代码静态分析与检查插件。它利用 PostgreSQL 内部的解析器和评估器,能够在函数创建或调用前发现 SQL 语义错误、未使用的变量、类型不匹配、性能问题等潜在缺陷,是 PL/pgSQL 开发者的必备质量保障工具。

前置条件

v17.10_r1.19、v18.4_r1.10及以上版本。
需要 plpgsql 扩展(默认已安装)。
如需使用 Profiler 的跨会话共享功能,需在 shared_preload_libraries 中加载。

创建插件

步骤1:检查前置参数

如需使用 Profiler 功能,请确认 shared_preload_libraries 包含 plpgsql_check:
postgres=# SHOW shared_preload_libraries;
shared_preload_libraries
---------------------------
plpgsql,plpgsql_check
(1 row)
说明:
如果仅使用代码检查功能(Active 模式),无需配置 shared_preload_libraries,直接创建扩展即可。

步骤2:创建并验证插件

postgres=# CREATE EXTENSION IF NOT EXISTS plpgsql_check;
CREATE EXTENSION

postgres=# SELECT extname, extversion FROM pg_extension WHERE extname = 'plpgsql_check';
extname | extversion
---------------+------------
plpgsql_check | 2.9
(1 row)

Active 模式:主动代码检查

基本用法

通过调用 plpgsql_check_function 主动检查指定函数:
-- 创建测试表
CREATE TABLE t1(a int, b int);

-- 创建一个有 bug 的函数(引用了不存在的列 c)
CREATE OR REPLACE FUNCTION f1()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE r record;
BEGIN
FOR r IN SELECT * FROM t1
LOOP
RAISE NOTICE '%', r.c; -- bug: t1 没有 c 列
END LOOP;
END;
$function$;
普通执行不会发现错误(因为 t1 表是空的):
postgres=# SELECT f1();
f1
----
(1 row)
使用 plpgsql_check 检查,立即发现问题:
postgres=# SELECT * FROM plpgsql_check_function('f1()');
plpgsql_check_function
---------------------------------------------------
error:42703:6:RAISE:record "r" has no field "c"
(1 row)

核心检查函数

plpgsql_check_function( funcoid, ... )

主要的代码检查函数,支持 text json xml 三种输出格式。
函数签名:
plpgsql_check_function(
funcoid regprocedure, -- 必填:函数标识
relid regclass DEFAULT 0, -- 触发器关联表
format text DEFAULT 'text', -- 输出格式:text/json/xml
fatal_errors boolean DEFAULT true, -- 是否在首个错误后停止
other_warnings boolean DEFAULT true, -- 常规警告
extra_warnings boolean DEFAULT true, -- 额外警告
performance_warnings boolean DEFAULT false, -- 性能警告
security_warnings boolean DEFAULT false, -- 安全警告
compatibility_warnings boolean DEFAULT false, -- 兼容性警告
anyelementtype regtype DEFAULT 'int',
anyenumtype regtype DEFAULT '-',
anyrangetype regtype DEFAULT 'int4range',
anycompatibletype regtype DEFAULT 'int',
anycompatiblerangetype regtype DEFAULT 'int4range',
without_warnings boolean DEFAULT false, -- 禁用所有警告
all_warnings boolean DEFAULT false, -- 启用所有警告
newtable text DEFAULT NULL, -- NEW 转换表名
oldtable text DEFAULT NULL, -- OLD 转换表名
use_incomment_options boolean DEFAULT true,
incomment_options_usage_warning boolean DEFAULT false
);
参数说明:
funcoid:必填,函数标识。支持三种形式:
OID:如 16799::regprocedure。
签名:如 'f1()'::regprocedure。
函数名(唯一时):如 'f1'。
relid:可选,触发器函数关联的表 OID。检查触发器函数时必填。
format:可选,输出格式。text(默认)、json、xml。
fatal_errors:可选,遇到首个错误是否停止。默认 true。
other_warnings:可选,显示常规警告(变量未使用、赋值左右属性数量不同等)。默认 true。
extra_warnings:可选,显示额外警告(缺失 RETURN、变量遮蔽、死代码等)。默认 true。
performance_warnings:可选,显示性能相关警告(隐式类型转换等)。默认 false。
security_warnings:可选,显示安全相关警告(SQL 注入检测)。默认 false。
all_warnings:可选,启用所有类型的警告。默认 false。
调用示例:
-- 基本检查
postgres=# SELECT * FROM plpgsql_check_function('f1()');
plpgsql_check_function
---------------------------------------------------
error:42703:6:RAISE:record "r" has no field "c"
(1 row)

-- 启用所有警告
postgres=# SELECT * FROM plpgsql_check_function('f1()', all_warnings := true);

-- 不在首个错误时停止
postgres=# SELECT * FROM plpgsql_check_function('f1()', fatal_errors := false);

-- XML 格式输出
postgres=# SELECT * FROM plpgsql_check_function('f1()', format := 'xml');

plpgsql_check_function_tb( funcoid, ... )

表格式输出的检查函数,返回结构化结果,便于程序处理。
postgres=# SELECT * FROM plpgsql_check_function_tb('f1()');
-[ RECORD 1 ]-------------------
functionid | f1
lineno | 6
statement | RAISE
sqlstate | 42703
message | record "r" has no field "c"
detail |
hint |
level | error
position | 0
query |
返回字段说明:
functionid:函数名。
lineno:错误所在行号。
statement:错误所在语句类型。
sqlstate:SQL 状态码。
message:错误消息。
detail:详细信息。
hint:修复提示。
level:严重级别(error warning note)。
position:位置信息。
query:相关查询。

检查触发器函数

检查触发器函数时,必须指定关联的表:
CREATE TABLE bar(a int, b int);

CREATE OR REPLACE FUNCTION foo_trg()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.c := NEW.a + NEW.b; -- bug: bar 表没有 c 列
RETURN NEW;
END;
$function$;

-- 不指定关联表会报错
postgres=# SELECT * FROM plpgsql_check_function('foo_trg()');
ERROR: missing trigger relation
HINT: Trigger relation oid must be valid

-- 正确方式:指定关联表
postgres=# SELECT * FROM plpgsql_check_function('foo_trg()', 'bar');
plpgsql_check_function
--------------------------------------------------------
error:42703:3:assignment:record "new" has no field "c"
(1 row)
对于使用转换表(transition tables)的触发器,需额外指定 newtable 和 oldtable 参数:
SELECT * FROM plpgsql_check_function('footab_trig_func', 'footab', newtable := 'newtab');

批量检查所有函数

检查所有非触发器 PL/pgSQL 函数

SELECT p.oid, p.proname, plpgsql_check_function(p.oid)
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p ON pronamespace = n.oid
JOIN pg_catalog.pg_language l ON p.prolang = l.oid
WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279;

检查所有触发器 PL/pgSQL 函数

SELECT p.proname, tgrelid::regclass, cf.*
FROM pg_proc p
JOIN pg_trigger t ON t.tgfoid = p.oid
JOIN pg_language l ON p.prolang = l.oid
JOIN pg_namespace n ON p.pronamespace = n.oid,
LATERAL plpgsql_check_function(p.oid, t.tgrelid) cf
WHERE n.nspname = 'public' AND l.lanname = 'plpgsql';

全量检查(函数 + 已定义触发器的触发器函数)

SELECT
(pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement,
(pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level,
(pcf)."position", (pcf).query, (pcf).context
FROM
(
SELECT
plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0),
oldtable=>pg_trigger.tgoldtable,
newtable=>pg_trigger.tgnewtable) AS pcf
FROM pg_proc
LEFT JOIN pg_trigger
ON (pg_trigger.tgfoid = pg_proc.oid)
WHERE
prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND
pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND
(pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR
pg_trigger.tgfoid IS NOT NULL)
OFFSET 0
) ss
ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno;

Passive 模式:被动代码检查

说明:
Passive 模式会在每次函数执行前进行检查,有一定的性能开销,仅建议在开发或预生产环境使用。

配置方式

在 postgresql.conf 中设置或通过 SQL 动态配置:
-- 加载模块(如果未通过 shared_preload_libraries 加载)
LOAD 'plpgsql_check';

-- 设置被动检查模式
SET plpgsql_check.mode = 'every_start'; -- 每次执行前检查
模式选项:
参数值
说明
disabled
禁用被动检查
by_function
默认值,仅在主动调用 plpgsql_check_function 时检查
fresh_start
函数首次被调用时检查(冷启动)
every_start
每次函数执行前都检查
其他配置参数:
-- 是否在发现错误时立即终止
SET plpgsql_check.fatal_errors = yes;

-- 是否显示非性能类警告
SET plpgsql_check.show_nonperformance_warnings = false;

-- 是否显示性能类警告
SET plpgsql_check.show_performance_warnings = false;

依赖分析

plpgsql_show_dependency_tb 函数可以显示函数内部使用的所有函数、操作符和关系:
postgres=# SELECT * FROM plpgsql_show_dependency_tb('testfunc(int,float)');
type | oid | schema | name | params
----------+-------+--------+---------+----------------------------
FUNCTION | 36008 | public | myfunc1 | (integer,double precision)
FUNCTION | 35999 | public | myfunc2 | (integer,double precision)
OPERATOR | 36007 | public | ** | (integer,integer)
RELATION | 36005 | public | myview |
RELATION | 36002 | public | mytable |
(4 rows)
该功能可用于:
分析函数的依赖关系,辅助重构决策。
评估修改某张表后受影响的函数范围。
生成函数依赖关系图。

Profiler 功能

plpgsql_check 内置了 PL/pgSQL 函数性能分析器,可精确到每行代码的执行时间。

前提条件

建议通过 shared_preload_libraries 加载 plpgsql_check 以支持跨会话共享统计数据。
plpgsql 必须在 plpgsql_check 之前加载。
-- postgresql.conf
shared_preload_libraries = 'plpgsql,plpgsql_check'

启用 Profiler

-- 方式一:通过 GUC 参数
SET plpgsql_check.profiler = on;

-- 方式二:通过函数调用
SELECT plpgsql_check_profiler(true);

查看函数 Profile

按行查看

plpgsql_profiler_function_tb 函数返回每行代码的性能数据。
说明:
avg_time、total_time、max_time 等列的返回类型为 double precision[](数组),因为同一行上可能包含多条语句。数组中的每个元素对应该行上一条语句的时间。
postgres=# SELECT lineno, stmt_lineno, avg_time, source
FROM plpgsql_profiler_function_tb('fx(int)');
lineno | stmt_lineno | avg_time | source
--------+-------------+-------------+---------------------------------------
1 | | |
2 | | | declare result int = 0;
3 | 3 | {0.075} | begin
4 | 4 | {0.202} | for i in 1..$1 loop
5 | 5 | {0.005} | select result + i into result;
6 | | | end loop;
7 | 7 | {0} | return result;
8 | | | end;
(8 rows)
说明:
时间单位为毫秒。avg_time 返回的 {0.075} 表示该行唯一语句的平均执行时间为0.075毫秒。
返回列说明:
列名
类型
说明
lineno
int
源码行号
stmt_lineno
int
语句起始行号
queryids
int8[]
关联的查询 ID
cmds_on_row
int
该行包含的语句数
exec_stmts
int8[]
执行次数(数组)
exec_stmts_err
int8[]
执行出错次数(数组)
total_time
double precision[]
总执行时间(毫秒,数组)
avg_time
double precision[]
平均执行时间(毫秒,数组)
max_time
double precision[]
最大执行时间(毫秒,数组)
processed_rows
int8[]
处理的行数(数组)
source
text
源码文本

按语句查看

plpgsql_profiler_function_statements_tb 函数以语句为粒度返回性能数据,返回的时间和执行次数为标量值:
postgres=# SELECT stmtid, parent_stmtid, block_num, lineno, exec_stmts, stmtname
FROM plpgsql_profiler_function_statements_tb('fx1');
stmtid | parent_stmtid | block_num | lineno | exec_stmts | stmtname
--------+---------------+-----------+--------+------------+-----------------
0 | | 0 | 2 | 0 | statement block
1 | 0 | 0 | 3 | 0 | IF
2 | 1 | 1 | 4 | 0 | RAISE
3 | 1 | 1 | 5 | 0 | RETURN
4 | 1 | 2 | 7 | 0 | RAISE
5 | 1 | 2 | 8 | 0 | RETURN
(6 rows)
返回列说明:
列名
类型
说明
stmtid
int
语句 ID
parent_stmtid
int
父语句 ID
block_num
int
代码块编号(0=主体, 1=then, 2=else 等)
lineno
int
行号
queryid
int8
查询 ID
exec_stmts
int8
执行次数
exec_stmts_err
int8
执行出错次数
total_time
double precision
总执行时间(毫秒)
avg_time
double precision
平均执行时间(毫秒)
max_time
double precision
最大执行时间(毫秒)
processed_rows
int8
处理的行数
stmtname
text
语句类型名称

查看所有已 Profile 的函数

postgres=# SELECT * FROM plpgsql_profiler_functions_all();
funcoid | exec_count | total_time | avg_time | stddev_time | min_time | max_time
-----------------------+------------+------------+----------+-------------+----------+----------
fxx(double precision) | 1 | 0.01 | 0.01 | 0.00 | 0.01 | 0.01
(1 row)

清除 Profile 数据

-- 清除所有函数的 Profile
SELECT plpgsql_profiler_reset_all();

-- 清除指定函数的 Profile
SELECT plpgsql_profiler_reset('fx(int)');

代码覆盖率

plpgsql_check 提供覆盖率统计函数:
-- 语句覆盖率
SELECT * FROM plpgsql_coverage_statements('my_function');

-- 分支覆盖率
SELECT * FROM plpgsql_coverage_branches('my_function');

Profiler 相关配置参数

参数
默认值
说明
plpgsql_check.profiler
off
是否启用 Profiler
plpgsql_check.max_stats_size
20MB
统计数据最大内存(1MB-200MB)
plpgsql_check.use_shared_stats_when_it_possible
on
是否使用共享内存存储统计
plpgsql_check.use_lxcache
on
延迟更新共享统计(高并发场景)

Pragma 指令

plpgsql_check 支持在函数代码中通过 Pragma 指令控制检查行为,类似于 PL/SQL 的 PRAGMA 特性。

基本语法

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
BEGIN
-- 禁用后续代码的检查
PERFORM plpgsql_check_pragma('disable:check');
-- ... 不需要检查的代码 ...
-- 重新启用检查
PERFORM plpgsql_check_pragma('enable:check');
-- ... 继续检查 ...
END;
$$ LANGUAGE plpgsql;

常用 Pragma

Pragma
说明
enable:check / disable:check
启用/禁用代码检查
enable:extra_warnings / disable:extra_warnings
启用/禁用额外警告
enable:performance_warnings / disable:performance_warnings
启用/禁用性能警告
enable:security_warnings / disable:security_warnings
启用/禁用安全警告
type:varname typename
为 record 类型变量指定类型
table: name (col type, ...)
创建临时表定义(用于检查时)
sequence: name
创建临时序列定义

使用 Pragma 解决动态 SQL 检查

CREATE OR REPLACE FUNCTION dynamic_query()
RETURNS void AS $$
DECLARE r record;
BEGIN
EXECUTE 'SELECT * FROM some_table' INTO r;
-- 告诉 plpgsql_check r 的结构
PERFORM plpgsql_check_pragma('type: r (id int, name text)');
RAISE NOTICE 'id = %, name = %', r.id, r.name;
END;
$$ LANGUAGE plpgsql;

使用限制

动态 SQL:plpgsql_check 无法检查运行时拼接的动态 SQL(EXECUTE 语句),因为查询内容在检查时不可知。可使用 type pragma 辅助。
refcursor:plpgsql_check 无法分析游标引用指向的结果结构。建议使用具体行类型代替 record 类型。
临时表:plpgsql_check 无法验证函数运行时创建的临时表上的查询。可使用 table pragma 定义临时表结构。
安全审计:SQL 注入检测功能仅能检测部分漏洞,不可作为安全审计的唯一工具。
Passive 模式性能:启用 every_start 模式会在每次函数调用前触发检查,有额外性能开销,仅建议开发/预生产环境使用。

完整使用示例

以下示例展示了 plpgsql_check 的典型使用流程:
-- ========== 1. 安装扩展 ==========
CREATE EXTENSION IF NOT EXISTS plpgsql_check;

-- ========== 2. 创建测试对象 ==========
CREATE TABLE t_demo (
id int PRIMARY KEY,
name text NOT NULL
);
INSERT INTO t_demo VALUES (1, 'alice'), (2, 'bob');

-- 一个正确的函数
CREATE OR REPLACE FUNCTION fn_good(p_id int)
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_name text;
BEGIN
SELECT name INTO v_name FROM t_demo WHERE id = p_id;
RETURN coalesce(v_name, '<none>');
END;
$$;

-- 一个有 bug 的函数
CREATE OR REPLACE FUNCTION fn_bad(p_id int)
RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
v_name text;
BEGIN
SELECT no_such_col INTO v_name FROM t_demo WHERE id = p_id;
RETURN v_name;
END;
$$;

-- ========== 3. 执行检查 ==========
-- 正确的函数:无输出
SELECT * FROM plpgsql_check_function('fn_good(int)');
-- 预期:(0 rows)

-- 有 bug 的函数:报告错误
SELECT * FROM plpgsql_check_function('fn_bad(int)');
-- 预期输出:
-- plpgsql_check_function
-- --------------------------------------------------------
-- error:42703:5:SQL statement:column "no_such_col" of ...

-- ========== 4. 启用性能 + 安全检查 ==========
SELECT * FROM plpgsql_check_function('fn_bad(int)', all_warnings := true);

-- ========== 5. 结构化输出(便于程序处理) ==========
SELECT functionid, lineno, sqlstate, message, level
FROM plpgsql_check_function_tb('fn_bad(int)');

与 pgTAP 联合使用

plpgsql_check 可以与 pgTAP 结合,将代码质量检查纳入自动化测试:
BEGIN;
SELECT plan(3);

-- 验证扩展安装
SELECT has_extension('plpgsql_check', 'plpgsql_check 已安装');

-- 验证正确的函数无错误
SELECT is(
(SELECT count(*)::int FROM plpgsql_check_function('fn_good(int)')),
0,
'fn_good 应无 plpgsql_check 发现'
);

-- 验证有 bug 的函数能被检出
SELECT cmp_ok(
(SELECT count(*)::int FROM plpgsql_check_function('fn_bad(int)')),
'>=', 1,
'fn_bad 应至少有 1 条 plpgsql_check 发现'
);

SELECT * FROM finish();
ROLLBACK;