前言
今天在某个项目里进行迁移时,遇到一个比较 tricky 的问题 —— 迁移某个函数时报错:
ERROR: schema “xxx” does not exist
检查了一下,目标库中确实不存在此模式,我去源库看了一下,这个函数定义中又确实引用了此模式下的某个类型,那这是为何?又是怎么创建的呢?让我们简单分析一下。
复现
简单复现一下:
1 | mydb=# CREATE OR REPLACE FUNCTION myfunc() |
由于还没有创建 myschema,因此创建会报错:
1 | mydb=# create schema myschema; |
创建好相应的表及类型了之后,即可正常创建。
但是尴尬的是,当你删除了模式之后,并不会级联删除这个函数:
1 | mydb=# drop schema myschema cascade ; |
因此,这个函数就像一个”雷”一样,悄无声息地躺在那里,当你在真正迁移的时候,才会让你栽到坑里。那么为何会这样?在官网上有这样一段解释
As each expression and SQL command is first executed in the function, the PL/pgSQL interpreter parses and analyzes the command to create a prepared statement, using the SPI manager’s
SPI_prepare
function. Subsequent visits to that expression or command reuse the prepared statement. Thus, a function with conditional code paths that are seldom visited will never incur the overhead of analyzing those commands that are never executed within the current session. A disadvantage is that errors in a specific expression or command cannot be detected until that part of the function is reached in execution. (Trivial syntax errors will be detected during the initial parsing pass, but anything deeper will not be detected until execution.)
简而言之,这段话说明了:
- PL/pgSQL 在函数执行过程中对 SQL 命令采取延迟解析 + 预编译 + 复用的机制,有效减少了重复解析的性能开销。
- 但也因此,无法在函数创建或首次整体解析时就捕获所有潜在错误,需要等到真正执行到相应分支后才会引发错误。
如果在“深层次”有错误(比如语义、类型不匹配等,而不是简单的 SQL 语法错误)并且只会出现在某些分支内,只有当函数运行时真正进入到该分支执行该命令时,才会爆出错误。也就是说,语法层面的问题会在最初解析时被发现,但更深层的检查(如对象是否存在、数据类型是否匹配等)只有在该命令真正执行时才会进行,从而导致错误的出现时间被推迟。
对于这种机制,就需要使用到 plpgsql_check 了:
- checks fields of referenced database objects and types inside embedded SQL:检查函数中嵌入 SQL 所引用到的数据库对象及类型
- validates you are using the correct types for function parameters:校验函数参数使用的类型是否正确
- identifies unused variables and function arguments, unmodified OUT arguments:识别未使用的变量和函数参数,以及未被修改的 OUT 参数
- partial detection of dead code (code after an RETURN command):部分检测函数中可能存在的”死”代码 (比如位于
RETURN
语句之后的代码) - detection of missing RETURN command in function (common after exception handlers, complex logic):检测函数里可能缺失的
RETURN
语句 (在异常处理或复杂逻辑中常见) - tries to identify unwanted hidden casts, which can be a performance issue like unused indexes:尝试识别潜在的隐式类型转换,避免对性能 (如索引利用) 造成影响
- ability to collect relations and functions used by function:可以收集函数所使用的表 (关系) 及函数信息
- ability to check EXECUTE statements against SQL injection vulnerability:可对
EXECUTE
动态 SQL 语句进行检测,防范 SQL 注入漏洞
装好之后,让我们再检查一下:
1 | mydb=# select sqlstate,message,level,position,context from plpgsql_check_function_tb('myfunc()'); |
这次就很明了了,提示相应 schema 不存在。
限制
以下只列举部分,更多信息可以参照其官网:https://github.com/okbob/plpgsql_check
静态检查 vs. 动态 SQL
plpgsql_check
主要做的是编译期静态检查,即在函数定义或编译时对函数代码进行分析。- 如果函数里存在动态 SQL(如
EXECUTE 'some sql string'
),那么只有在 SQL 字符串文字(literal)是确定并可被解析的情况下,才能进行较为完整的检查。 - 对于需要运行时拼接才能构建出的 SQL(尤其是外部参数、变量拼接),
plpgsql_check
无法完全模拟真实执行效果,可能无法准确识别潜在问题,也会有一定概率出现漏报或误报。
部分检查的启发式
plpgsql_check
的某些检查(如死代码检测、类型推断)依赖内部的启发式分析,可能无法覆盖所有情况。- 对于高度动态化、通过条件分支或异常捕获等复杂结构的函数,工具可能产生误报(false positive)或漏报(false negative)。
运行时依赖对象的可用性
- 工具会检查函数内引用的表、视图、列、类型等对象,如果在检查时找不到对应对象(或权限不足)就会报错或跳过某些检查。
- 如果数据库环境在检查后发生变化(例如表结构变更、对象被删除),则需要重新对函数进行检查,否则可能报告过时信息。
死代码与 RETURN 分支检测的局限性
- 对于
RETURN
后面紧随的语句,plpgsql_check
能较好地识别为死代码。 - 但如果函数里有更复杂的流程控制,如循环、条件分支、嵌套异常处理等,死代码检测并不能保证 100% 覆盖。
只能检查 PL/pgSQL 语言函数
- 该扩展仅针对
plpgsql
语言的函数进行检查。其他语言(如 SQL, C, Python 等)写的函数无法使用此扩展进行检查。
更进一步
你以为到这里就完了吗?使用 plpgsql_check 检查一下就万事大吉了吗?非也。
让我们看一个更加常见但是较为费解的现象 (为了方便起见,仅有一个函数的空库):
1 | mydb=# \dn |
这样的话,转储文件中仅包含一个 myfunc 的 DDL。如前文所述,手动在 psql 中创建是没问题的,但是如果你进行恢复的话,却又很神奇地创建成功了!难道是 BUG?
1 | [postgres@mypg ~]$ createdb mydb2 |
其实这是因为 check_function_bodies 参数搞的鬼,默认为 on,也就是说
- 在创建函数时,PostgreSQL 会对函数内部引用的对象(如表、视图、类型等)做更深入的检查。
- 如果引用的对象不存在,就会直接报错,导致函数无法创建。
为 false 的话
- 在创建函数时,PostgreSQL 不会执行对函数体内部引用对象的存在性、合法性检查。
- 即使你函数里写了一个根本不存在的模式、表或类型,也不会在这一步报错。
- 直到真正执行该函数时,才会进行实际检查,如果那时依赖对象仍不存在,就会报错。
而 pg_dump 导出的转储文件中,将其设为了 false,所以你可以恢复成功。
1 | [postgres@mypg ~]$ cat mydb.sql |
不用语言,检测的行为也有可能不同,取决于具体的函数:
1 | mydb=# CREATE OR REPLACE FUNCTION myfunc() |
小结
让我们小结一下,对于 PL/pgSQL:
- 执行过程中对 SQL 命令采取延迟解析 + 预编译 + 复用的机制,有效减少了重复解析的性能开销。
- 但也因此,无法在函数创建或首次整体解析时就捕获所有潜在错误,需要等到真正执行到相应分支后才会引发错误。
基于此,plpgsql_check 的作用便出来了,预检查,以发现潜在的问题,还可以分析性能,检查 SQL 注入等等。
还有一些类似的插件,各位读者可以自行探索:
- pldbgapi:调试函数
- pldebugger:调试函数 + pgAdmin
- piggly:PL/pgSQL 代码覆盖测试工具
- pg_linegazer:Transparent code coverage for PL/pgSQL,针对 PLPGSQL 的 TDE
- pgora-osql:Oracle PLSQL 代码兼容插件, 类似 PL/pgSQL 一样, 作为一种新的存储过程语言.
- plprofiler:类 Perf 火焰图
推荐阅读
https://www.postgresql.org/docs/current/plpgsql-implementation.html
https://www.percona.com/blog/using-plpgsql_check-to-find-compilation-errors-and-profile-functions/