云数据库 PostgreSQL plpgsql_check

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

相关产品
云数据库 PostgreSQL 产品页,含版本规格、价格与购买入口。

二、前置条件

·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)

七、基本用法

通过调用 plpgsql_check_function 主动检查指定函数:

— 创建测试表CREATE TABLE t1(a int, b int);– 创建一个有 bug 的函数(引用了不存在的列 c)CREATE OR REPLACE FUNCTION f1()RETURNS voidLANGUAGE plpgsqlAS $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 | 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$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 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 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).contextFROM( 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) 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 函数性能分析器,可精确到每行代码的执行时间。

20、前提条件

·建议通过 shared_preload_libraries 加载 plpgsql_check 以支持跨会话共享统计数据。

·plpgsql 必须在 plpgsql_check 之前加载。

— postgresql.confshared_preload_libraries = 'plpgsql,plpgsql_check'

21、启用 Profiler

— 方式一:通过 GUC 参数SET plpgsql_check.profiler = on;– 方式二:通过函数调用SELECT plpgsql_check_profiler(true);

23、按行查看

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 源码文本

24、按语句查看

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 语句类型名称

25、查看所有已 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)

26、清除 Profile 数据

— 清除所有函数的 ProfileSELECT plpgsql_profiler_reset_all();– 清除指定函数的 ProfileSELECT plpgsql_profiler_reset('fx(int)');

27、代码覆盖率

plpgsql_check 提供覆盖率统计函数:

— 语句覆盖率SELECT * FROM plpgsql_coverage_statements('my_function');– 分支覆盖率SELECT * FROM plpgsql_coverage_branches('my_function');

28、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 延迟更新共享统计(高并发场景)

29、Pragma 指令

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

30、基本语法

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

31、常用 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 创建临时序列定义

32、使用 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;

33、使用限制

·动态 SQL:plpgsql_check 无法检查运行时拼接的动态 SQL(EXECUTE 语句),因为查询内容在检查时不可知。可使用 type pragma 辅助。

·refcursor:plpgsql_check 无法分析游标引用指向的结果结构。建议使用具体行类型代替 record 类型。

·临时表:plpgsql_check 无法验证函数运行时创建的临时表上的查询。可使用 table pragma 定义临时表结构。

·安全审计:SQL 注入检测功能仅能检测部分漏洞,不可作为安全审计的唯一工具。

·Passive 模式性能:启用 every_start 模式会在每次函数调用前触发检查,有额外性能开销,仅建议开发/预生产环境使用。

34、完整使用示例

以下示例展示了 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 $$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 textLANGUAGE 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)');

35、与 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;

赞 (0)
上一篇 4天前
下一篇 4天前