概述
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 EXTENSIONpostgres=# 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 voidLANGUAGE plpgsqlAS $function$DECLARE r record;BEGINFOR r IN SELECT * FROM t1LOOPRAISE 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/xmlfatal_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 | f1lineno | 6statement | RAISEsqlstate | 42703message | record "r" has no field "c"detail |hint |level | errorposition | 0query |
返回字段说明:
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 triggerLANGUAGE plpgsqlAS $function$BEGINNEW.c := NEW.a + NEW.b; -- bug: bar 表没有 c 列RETURN NEW;END;$function$;-- 不指定关联表会报错postgres=# SELECT * FROM plpgsql_check_function('foo_trg()');ERROR: missing trigger relationHINT: 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 nJOIN pg_catalog.pg_proc p ON pronamespace = n.oidJOIN pg_catalog.pg_language l ON p.prolang = l.oidWHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279;
检查所有触发器 PL/pgSQL 函数
SELECT p.proname, tgrelid::regclass, cf.*FROM pg_proc pJOIN pg_trigger t ON t.tgfoid = p.oidJOIN pg_language l ON p.prolang = l.oidJOIN pg_namespace n ON p.pronamespace = n.oid,LATERAL plpgsql_check_function(p.oid, t.tgrelid) cfWHERE 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).contextFROM(SELECTplpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0),oldtable=>pg_trigger.tgoldtable,newtable=>pg_trigger.tgnewtable) AS pcfFROM pg_procLEFT JOIN pg_triggerON (pg_trigger.tgfoid = pg_proc.oid)WHEREprolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') ANDpronamespace <> (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') ORpg_trigger.tgfoid IS NOT NULL)OFFSET 0) ssORDER 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.confshared_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, sourceFROM plpgsql_profiler_function_tb('fx(int)');lineno | stmt_lineno | avg_time | source--------+-------------+-------------+---------------------------------------1 | | |2 | | | declare result int = 0;3 | 3 | {0.075} | begin4 | 4 | {0.202} | for i in 1..$1 loop5 | 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, stmtnameFROM plpgsql_profiler_function_statements_tb('fx1');stmtid | parent_stmtid | block_num | lineno | exec_stmts | stmtname--------+---------------+-----------+--------+------------+-----------------0 | | 0 | 2 | 0 | statement block1 | 0 | 0 | 3 | 0 | IF2 | 1 | 1 | 4 | 0 | RAISE3 | 1 | 1 | 5 | 0 | RETURN4 | 1 | 2 | 7 | 0 | RAISE5 | 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 数据
-- 清除所有函数的 ProfileSELECT plpgsql_profiler_reset_all();-- 清除指定函数的 ProfileSELECT 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;BEGINEXECUTE '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 textLANGUAGE plpgsql AS $$DECLAREv_name text;BEGINSELECT 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 textLANGUAGE plpgsql AS $$DECLAREv_name text;BEGINSELECT 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, levelFROM 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;