7.0版PL/pgSQL函数
PL/pgSQL(Procedural Language/PostgreSQL)是AnalyticDB PostgreSQL版默认支持的过程语言。AnalyticDB PostgreSQL版的PL/pgSQL基于PostgreSQL 12实现。您可以使用PL/pgSQL创建用户自定义函数(UDF),实现复杂的数据处理和业务逻辑。
概述
PL/pgSQL是一种块结构的命令式语言,专为数据库编程设计。您可以使用PL/pgSQL实现以下功能:
创建函数和存储过程。
实现复杂的业务规则和数据校验逻辑。
封装数据操作,简化应用代码。
使用变量、条件判断、循环和异常处理等编程能力扩展SQL。
设计目标
目标 | 说明 |
易用性 | 语法类似于Oracle PL/SQL,便于学习和迁移。 |
安全性 | 在数据库服务器内执行,支持事务和权限控制。 |
高效性 | 直接访问数据库数据,避免网络往返开销。 |
可扩展性 | 支持所有PostgreSQL数据类型和操作符。 |
与SQL的关系
PL/pgSQL是对SQL的扩展,而非替代品。AnalyticDB PostgreSQL版的PL/pgSQL基于PostgreSQL 12的PL/pgSQL实现:
SQL语句可直接嵌入PL/pgSQL代码。
PL/pgSQL提供变量、控制结构、异常处理等编程能力。
最佳实践:尽量使用SQL的集合操作,PL/pgSQL用于逻辑控制。
适用场景
场景 | 说明 |
数据验证 | 实现复杂的业务规则校验。 |
数据转换 | 在ETL过程中自定义数据转换逻辑。 |
业务封装 | 将多步操作封装为函数,简化应用层调用。 |
游标处理 | 逐行处理大数据集。 |
使用限制
不支持触发器(Triggers)。
游标仅支持前向移动,不可滚动。
不支持可更新游标(
UPDATE...WHERE CURRENT OF和DELETE...WHERE CURRENT OF)。不支持并行检索游标(
DECLARE...PARALLEL RETRIEVE)。EXECUTE语句中不支持SELECT INTO。
语言结构
块结构
PL/pgSQL代码由块(Block)组成。每个块包含声明部分、语句部分和可选的异常处理部分。语法如下:
[ <<label>> ]
[ DECLARE
-- 声明部分
declarations ]
BEGIN
-- 语句部分
statements
[ EXCEPTION
-- 异常处理部分
WHEN condition THEN
handler_statements ]
END [ label ];示例:基本块结构
CREATE FUNCTION simple_example() RETURNS integer AS $$
DECLARE
counter integer := 0;
BEGIN
counter := counter + 1;
RETURN counter;
END;
$$ LANGUAGE plpgsql;示例:嵌套块
块可以嵌套,内层块可以访问外层块的变量。
CREATE FUNCTION nested_blocks() RETURNS void AS $$
<<outer_block>>
DECLARE
outer_var integer := 1;
BEGIN
RAISE NOTICE 'outer_var = %', outer_var;
<<inner_block>>
DECLARE
inner_var integer := 2;
BEGIN
RAISE NOTICE 'inner_var = %', inner_var;
-- 可以访问外层变量
RAISE NOTICE 'outer_var from inner = %', outer_var;
END inner_block;
END outer_block;
$$ LANGUAGE plpgsql;标签(<<label>>)的用途:
在
EXIT和CONTINUE语句中指定退出的目标块或循环。区分同名变量,通过
label.variable访问外层变量。
注释
PL/pgSQL支持两种注释风格:
-- 单行注释
x := 1; -- 行末注释
/*
* 多行注释
* 可以跨越多行
*/
x := 1;声明与数据类型
变量声明
在DECLARE部分声明变量,语法如下:
name [ CONSTANT ] type [ NOT NULL ] [ { DEFAULT | := } expression ];示例
DECLARE
-- 基本声明
user_id integer;
-- 带默认值
quantity integer := 10;
-- 使用 DEFAULT 关键字
tax_rate numeric(5,2) DEFAULT 0.08;
-- 常量声明
PI CONSTANT numeric := 3.14159265;
-- NOT NULL 约束(必须提供默认值)
counter integer NOT NULL := 0;
-- 字符串变量
greeting text := 'Hello, World!';数据类型
PL/pgSQL支持所有PostgreSQL基础数据类型,同时提供以下专用类型:
%TYPE:引用列类型。使用
%TYPE引用表列的数据类型,当表结构变更时函数自动适配。-- 引用表列的类型,保持类型同步 DECLARE emp_name employees.name%TYPE; emp_salary employees.salary%TYPE;%ROWTYPE:引用行类型。使用
%ROWTYPE声明与表结构相同的记录变量。-- 声明与表结构相同的记录变量 DECLARE emp_row employees%ROWTYPE; BEGIN SELECT * INTO emp_row FROM employees WHERE id = 1; RAISE NOTICE 'Employee: %, Salary: %', emp_row.name, emp_row.salary; END;RECORD:动态记录类型。
RECORD类型的变量在运行时确定结构,适用于动态查询场景。-- 动态结构的记录,在运行时确定结构 DECLARE rec RECORD; BEGIN FOR rec IN SELECT id, name FROM employees LOOP RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name; END LOOP; END;数组类型:支持一维和多维数组。
DECLARE -- 一维数组 int_array integer[]; str_array text[] := ARRAY['a', 'b', 'c']; -- 数组初始化 numbers integer[] := '{1, 2, 3, 4, 5}'; BEGIN -- 数组访问(索引从 1 开始) RAISE NOTICE 'First element: %', str_array[1]; -- 数组追加 int_array := int_array || 10; -- 数组长度 RAISE NOTICE 'Array length: %', array_length(str_array, 1); END;复合类型:支持自定义结构体类型。
-- 先创建类型 CREATE TYPE person_type AS ( name text, age integer, email text ); -- 在函数中使用 DECLARE person person_type; BEGIN person.name := 'John'; person.age := 30; person.email := 'john@example.com'; END;
函数参数
函数参数支持以下引用方式:
-- 方式1:使用 $n 位置参数
CREATE FUNCTION add_nums(integer, integer) RETURNS integer AS $$
BEGIN
RETURN $1 + $2;
END;
$$ LANGUAGE plpgsql;
-- 方式2:使用命名参数(推荐)
CREATE FUNCTION add_nums(a integer, b integer) RETURNS integer AS $$
BEGIN
RETURN a + b;
END;
$$ LANGUAGE plpgsql;
-- 方式3:使用 ALIAS FOR
CREATE FUNCTION add_nums(integer, integer) RETURNS integer AS $$
DECLARE
first_num ALIAS FOR $1;
second_num ALIAS FOR $2;
BEGIN
RETURN first_num + second_num;
END;
$$ LANGUAGE plpgsql;表达式与赋值
赋值
PL/pgSQL支持以下赋值方式:
:= 赋值操作符:直接为变量赋值。
DECLARE x integer; y integer; total integer; BEGIN x := 10; y := 20; total := x + y; END;SELECT INTO赋值:通过
SELECT INTO将查询结果赋值给变量。DECLARE emp_name text; emp_salary numeric; emp_count integer; BEGIN -- 单值赋值 SELECT name INTO emp_name FROM employees WHERE id = 1; -- 多值赋值 SELECT name, salary INTO emp_name, emp_salary FROM employees WHERE id = 1; -- 聚合结果赋值 SELECT count(*) INTO emp_count FROM employees; -- 严格模式:要求恰好返回一行,否则抛出异常 SELECT name INTO STRICT emp_name FROM employees WHERE id = 1; END;FOUND变量:
FOUND是一个布尔变量,用于判断上一条SQL语句是否影响了数据行。DECLARE emp_name text; BEGIN SELECT name INTO emp_name FROM employees WHERE id = 999; IF NOT FOUND THEN RAISE NOTICE 'Employee not found'; ELSE RAISE NOTICE 'Found: %', emp_name; END IF; END;
表达式求值
PL/pgSQL中的表达式使用标准SQL表达式语法,支持数学运算、字符串操作、条件判断和SQL函数调用。
DECLARE
result numeric;
condition boolean;
BEGIN
-- 数学表达式
result := 10 + 20 * 3;
-- 字符串表达式
result := length('Hello') + length('World');
-- 条件表达式
condition := (result > 100) AND (result < 200);
-- 使用SQL函数
result := abs(-50) + round(3.7);
END;表达式缓存(Plan Caching)
PL/pgSQL会自动缓存SQL语句的执行计划,提高重复执行的效率。但在以下情况需要注意:
-- 缓存的计划可能不适合参数值变化大的场景
CREATE FUNCTION get_employee(emp_id integer) RETURNS text AS $$
DECLARE
result text;
BEGIN
-- 此查询计划会被缓存(如果emp_id变化比较大则不太适合)
SELECT name INTO result FROM employees WHERE id = emp_id;
RETURN result;
END;
$$ LANGUAGE plpgsql;函数的波动性分类(IMMUTABLE/STABLE/VOLATILE)影响数据库缓存引用该函数的查询计划的方式。
IMMUTABLE:纯函数,相同输入永远返回相同输出,函数的结果可在规划时常量折叠。不依赖数据库内容、配置或时间。优化器可在规划时直接计算结果(常量折叠)。适用场景:数学计算、字符串格式化、固定规则转换。
CREATE FUNCTION celsius_to_fahrenheit(c numeric) RETURNS numeric AS $$ BEGIN RETURN c * 9.0 / 5.0 + 32; END; $$ LANGUAGE plpgsql IMMUTABLE;STABLE:函数在单次表扫描中返回相同结果。可以读取数据库,但在同一条SQL执行期间结果不变。优化器可以在单次扫描中缓存结果,避免重复调用。可用于索引条件(Index Scan的WHERE子句)。适用场景:读取配置表、基于当前事务快照的查询、时区转换。
CREATE FUNCTION get_config_value(key text) RETURNS text AS $$ DECLARE val text; BEGIN SELECT value INTO val FROM config_table WHERE config_key = key; RETURN val; END; $$ LANGUAGE plpgsql STABLE;VOLATILE:函数每次调用可能返回不同结果,可以修改数据库状态(INSERT/UPDATE/DELETE)。优化器不会缓存或优化掉任何调用。不能用于索引条件。适用场景:修改数据、依赖随机数、依赖当前时间、有副作用。
CREATE FUNCTION log_access(user_id integer) RETURNS void AS $$ BEGIN INSERT INTO access_log VALUES (user_id, now()); END; $$ LANGUAGE plpgsql VOLATILE;
类型转换
DECLARE
str_val text := '123';
int_val integer;
float_val numeric;
BEGIN
-- 显式类型转换
int_val := str_val::integer;
float_val := CAST(str_val AS numeric);
-- 使用转换函数
int_val := to_number(str_val, '999');
END;基本语句
PERFORM
PERFORM用于执行查询但不关心返回结果,语法与SELECT相同,但使用PERFORM关键字替代SELECT。
BEGIN
-- 调用函数
PERFORM my_procedure(10);
-- 执行有副作用的操作
PERFORM pg_sleep(1);
-- 检查数据是否存在
PERFORM 1 FROM employees WHERE id = 100;
IF FOUND THEN
RAISE NOTICE 'Employee exists';
END IF;
END;在PL/pgSQL中,直接使用SELECT语句(不带INTO子句)会导致语法错误。如果不需要查询结果,请使用PERFORM替代。
EXECUTE
EXECUTE用于执行动态构建的SQL语句。当表名、列名或查询条件需要在运行时确定时,使用EXECUTE。
使用USING传递参数:通过
USING子句安全地传递参数值。DECLARE min_salary numeric := 50000; dept_id integer := 10; emp_count integer; BEGIN EXECUTE 'SELECT count(*) FROM employees WHERE salary > $1 AND department_id = $2' INTO emp_count USING min_salary, dept_id; RAISE NOTICE 'Count: %', emp_count; END;
AnalyticDB PostgreSQL版中,EXECUTE语句不支持SELECT INTO语法。请使用EXECUTE ... INTO ...语法替代。
防止SQL注入:动态拼接SQL时,必须对用户输入进行转义处理。
-- 错误示例:直接拼接用户输入,存在SQL注入风险 EXECUTE 'SELECT * FROM ' || user_input; -- 正确示例:使用quote_ident处理标识符,quote_literal处理值 EXECUTE 'SELECT * FROM ' || quote_ident(table_name) || ' WHERE name = ' || quote_literal(user_name); -- 推荐:使用format函数(%I=标识符,%L=字面值) EXECUTE format('SELECT * FROM %I WHERE name = %L', table_name, user_name); -- 推荐:使用USING传递参数值 EXECUTE 'SELECT * FROM employees WHERE name = $1' USING user_name;
RETURN
PL/pgSQL函数支持以下返回方式:
返回单个值:使用
RETURN返回标量值。CREATE FUNCTION get_sum(a integer, b integer) RETURNS integer AS $$ BEGIN RETURN a + b; END; $$ LANGUAGE plpgsql;RETURN NEXT和RETURN QUERY:返回结果集。
-- RETURN NEXT:逐行返回 CREATE FUNCTION get_numbers(n integer) RETURNS SETOF integer AS $$ DECLARE i integer; BEGIN FOR i IN 1..n LOOP RETURN NEXT i; END LOOP; RETURN; END; $$ LANGUAGE plpgsql; -- RETURN QUERY:返回查询结果集 CREATE FUNCTION get_employees(dept_id integer) RETURNS SETOF employees AS $$ BEGIN RETURN QUERY SELECT * FROM employees WHERE department_id = dept_id; RETURN; END; $$ LANGUAGE plpgsql; -- RETURN QUERY EXECUTE:返回动态查询结果集 CREATE FUNCTION get_by_table(tbl text) RETURNS SETOF RECORD AS $$ BEGIN RETURN QUERY EXECUTE 'SELECT * FROM ' || quote_ident(tbl); END; $$ LANGUAGE plpgsql;
RETURN NEXT和RETURN QUERY会将结果缓冲在内存中(受work_mem限制,超出后写入磁盘)。处理大结果集时请关注内存消耗。
RAISE
RAISE用于输出调试信息或抛出异常。
消息级别:
RAISE支持以下消息级别。级别
说明
DEBUG
调试信息,默认不显示。
LOG
日志信息。
INFO
普通信息。
NOTICE
提示信息,默认显示。
WARNING
警告信息。
EXCEPTION
抛出异常,中断执行并回滚当前事务。
格式化消息:使用
%作为占位符,按顺序替换后续参数。DECLARE user_name text := 'John'; user_age integer := 30; BEGIN RAISE NOTICE 'User % is % years old', user_name, user_age; -- 输出:NOTICE: User John is 30 years old RAISE NOTICE 'Completion: %%100'; -- 使用%%输出百分号 END;抛出自定义异常:使用
USING子句指定错误详情。BEGIN RAISE EXCEPTION 'Invalid input' USING ERRCODE = 'invalid_parameter_value', DETAIL = 'The value must be positive', HINT = 'Provide a positive integer'; END;
控制结构
条件语句
PL/pgSQL支持以下条件语句:
IF语句:支持
IF...THEN...ELSIF...ELSE...END IF结构。DECLARE grade integer := 85; result text; BEGIN IF grade >= 90 THEN result := 'A'; ELSIF grade >= 80 THEN result := 'B'; ELSIF grade >= 70 THEN result := 'C'; ELSIF grade >= 60 THEN result := 'D'; ELSE result := 'F'; END IF; RAISE NOTICE 'Grade: %', result; END;CASE表达式:支持简单CASE和搜索CASE两种形式。
-- 简单CASE DECLARE status integer := 1; status_text text; BEGIN status_text := CASE status WHEN 0 THEN 'Inactive' WHEN 1 THEN 'Active' WHEN 2 THEN 'Pending' ELSE 'Unknown' END; END; -- 搜索CASE DECLARE age integer := 25; category text; BEGIN category := CASE WHEN age < 13 THEN 'Child' WHEN age < 20 THEN 'Teenager' WHEN age < 60 THEN 'Adult' ELSE 'Senior' END; END;
循环语句
PL/pgSQL支持以下循环语句:
LOOP:无限循环,需配合
EXIT退出。DECLARE counter integer := 0; BEGIN LOOP counter := counter + 1; RAISE NOTICE 'Counter: %', counter; EXIT WHEN counter >= 5; END LOOP; END;WHILE循环:满足条件时持续循环。
DECLARE counter integer := 1; BEGIN WHILE counter <= 5 LOOP RAISE NOTICE 'Counter: %', counter; counter := counter + 1; END LOOP; END;FOR循环(整数范围):遍历整数区间,支持正向、反向和自定义步长。
BEGIN -- 正向遍历 FOR i IN 1..5 LOOP RAISE NOTICE 'i = %', i; END LOOP; -- 反向遍历 FOR i IN REVERSE 5..1 LOOP RAISE NOTICE 'i = %', i; END LOOP; -- 指定步长 FOR i IN 1..10 BY 2 LOOP RAISE NOTICE 'i = %', i; -- 输出 1, 3, 5, 7, 9 END LOOP; END;FOR循环(查询结果):遍历查询返回的每一行。
DECLARE rec RECORD; BEGIN FOR rec IN SELECT id, name FROM employees ORDER BY id LOOP RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name; END LOOP; -- 遍历动态查询结果 FOR rec IN EXECUTE 'SELECT * FROM employees WHERE salary > $1' USING 50000 LOOP RAISE NOTICE 'High earner: %', rec.name; END LOOP; END;FOREACH(遍历数组):遍历数组中的每个元素。
DECLARE arr integer[] := ARRAY[1, 2, 3, 4, 5]; elem integer; BEGIN FOREACH elem IN ARRAY arr LOOP RAISE NOTICE 'Element: %', elem; END LOOP; END;EXIT和CONTINUE:
EXIT退出当前循环或指定标签的循环,CONTINUE跳过当前迭代继续下一次循环。BEGIN <<outer_loop>> FOR i IN 1..10 LOOP FOR j IN 1..10 LOOP IF j = 5 THEN CONTINUE; -- 跳过当前内层迭代 END IF; IF i * j > 30 THEN EXIT outer_loop; -- 退出外层循环 END IF; RAISE NOTICE '% * % = %', i, j, i * j; END LOOP; END LOOP outer_loop; END;
游标
游标用于逐行处理查询结果,适用于需要分步处理大数据集的场景。
声明游标
DECLARE
-- 绑定游标:编译时绑定查询
emp_cursor CURSOR FOR
SELECT id, name, salary FROM employees ORDER BY id;
-- 未绑定游标
ref_cursor refcursor;
-- 参数化游标
emp_cursor2 CURSOR (min_sal numeric) FOR
SELECT * FROM employees WHERE salary >= min_sal;操作游标
游标支持以下操作:
打开游标:根据游标类型选择不同的打开方式。
DECLARE emp_cursor CURSOR FOR SELECT * FROM employees; ref_cursor refcursor; BEGIN -- 打开绑定游标 OPEN emp_cursor; -- 打开未绑定游标 OPEN ref_cursor FOR SELECT * FROM departments; -- 打开参数化游标 OPEN emp_cursor2(50000); -- 动态游标 OPEN ref_cursor FOR EXECUTE 'SELECT * FROM ' || quote_ident(table_name); END;FETCH:获取数据:从游标中逐行读取数据。
DECLARE emp_cursor CURSOR FOR SELECT id, name FROM employees; emp_id integer; emp_name text; BEGIN OPEN emp_cursor; LOOP FETCH emp_cursor INTO emp_id, emp_name; EXIT WHEN NOT FOUND; RAISE NOTICE 'ID: %, Name: %', emp_id, emp_name; END LOOP; CLOSE emp_cursor; END;FOR IN游标:自动管理游标生命周期,无需手动打开和关闭。
DECLARE emp_cursor CURSOR FOR SELECT * FROM employees; rec RECORD; BEGIN FOR rec IN emp_cursor LOOP RAISE NOTICE 'Employee: %', rec.name; END LOOP; -- 游标自动打开和关闭,无需手动管理 END;
游标与性能
游标按需逐行获取数据,不会一次性加载全部结果到内存。
事务期间游标保持打开状态会占用数据库资源。
长时间运行的事务中,未关闭的游标可能阻止VACUUM清理过期数据。
错误处理
EXCEPTION块
在BEGIN...END块中使用EXCEPTION子句捕获和处理异常。
CREATE FUNCTION safe_divide(a numeric, b numeric) RETURNS numeric AS $$
DECLARE
result numeric;
BEGIN
BEGIN
result := a / b;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Division by zero, returning NULL';
result := NULL;
WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'Result out of range';
result := NULL;
END;
RETURN result;
END;
$$ LANGUAGE plpgsql;常用错误条件
条件名称 | SQLSTATE | 说明 |
division_by_zero | 22012 | 除以零。 |
null_value_not_allowed | 22004 | 空值不允许。 |
numeric_value_out_of_range | 22003 | 数值超出范围。 |
string_data_right_truncation | 22001 | 字符串截断。 |
unique_violation | 23505 | 唯一约束冲突。 |
foreign_key_violation | 23503 | 外键约束冲突。 |
check_violation | 23514 | CHECK约束冲突。 |
not_null_violation | 23502 | NOT NULL约束冲突。 |
no_data_found | P0002 | SELECT INTO无数据。 |
too_many_rows | P0003 | SELECT INTO返回多行。 |
OTHERS | - | 匹配所有未明确捕获的异常。 |
获取错误信息
使用SQLERRM和SQLSTATE变量获取错误详情。
BEGIN
INSERT INTO unique_table VALUES (duplicate_key);
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Error: % (SQLSTATE: %)', SQLERRM, SQLSTATE;
IF SQLSTATE = '23505' THEN
RAISE NOTICE 'Duplicate key error';
END IF;
END;多异常捕获
BEGIN
-- 业务操作
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'Record already exists';
WHEN foreign_key_violation THEN
RAISE NOTICE 'Referenced record not found';
WHEN check_violation OR not_null_violation THEN
RAISE NOTICE 'Data validation failed';
WHEN OTHERS THEN
RAISE NOTICE 'Unexpected error: %', SQLERRM;
RAISE; -- 重新抛出异常
END;EXCEPTION块的子事务机制
每次进入包含EXCEPTION子句的BEGIN块时,PL/pgSQL都会创建一个子事务(Subtransaction)。
子事务的创建与函数的波动性分类(IMMUTABLE/STABLE/VOLATILE)无关。即使将函数声明为IMMUTABLE,EXCEPTION块仍然会创建子事务。在高频调用场景中,子事务会消耗大量vmem资源并显著降低执行性能。
性能优化
计划缓存
PL/pgSQL会自动缓存SQL语句的执行计划。首次执行时生成计划并缓存,后续调用直接复用缓存的计划。
CREATE FUNCTION get_employee(emp_id integer) RETURNS text AS $$
DECLARE
emp_name text;
BEGIN
-- 此计划被缓存,后续调用直接使用
SELECT name INTO emp_name FROM employees WHERE id = emp_id;
RETURN emp_name;
END;
$$ LANGUAGE plpgsql;当参数值分布不均匀时,缓存的通用计划可能不是最优的。此时可使用EXECUTE强制每次重新生成计划。
CREATE FUNCTION search_employees(status text) RETURNS SETOF employees AS $$
BEGIN
-- status='active' 占 99%,status='inactive' 占 1%
-- 使用 EXECUTE 让每次都重新规划
RETURN QUERY EXECUTE
'SELECT * FROM employees WHERE status = $1'
USING status;
END;
$$ LANGUAGE plpgsql;方式 | 是否缓存计划 | 适用场景 |
静态SQL | 是 | 参数值分布均匀的场景。 |
EXECUTE + USING | 否 | 参数值分布不均匀的场景。 |
EXECUTE拼接 | 否 | 动态表名或列名的场景。 |
最小化包含EXCEPTION函数调用次数
谓词中包含EXCEPTION块的PL/pgSQL函数不建议放在大量行运行场景:每个子事务的内存上下文都计入vmem使用量。高频调用EXCEPTION块函数会快速消耗vmem配额,而且会极大的拖慢执行性能。
ORCA优化器可能不会重排WHERE子句中的谓词,因此书写时注意对应顺序:
show optimizer;
-- explain 查看对应过滤谓词的执行顺序
explain SELECT * FROM large_table
WHERE cheap_condition
AND expensive_plpgsql_function(column);-- 错误示例:对所有行调用
SELECT * FROM million_rows WHERE plpgsql_func(col);内存管理
RETURN NEXT和RETURN QUERY的结果集缓冲在内存中,超出work_mem限制后写入磁盘。对于大结果集,优先使用RETURN QUERY。
-- 推荐:使用RETURN QUERY返回大结果集
CREATE FUNCTION generate_rows() RETURNS SETOF integer AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 1000000);
END;
$$ LANGUAGE plpgsql;大结果集处理建议:使用游标分批处理。
CREATE FUNCTION process_large_result() RETURNS void AS $$
DECLARE
cur CURSOR FOR SELECT * FROM large_table;
rec RECORD;
batch_size integer := 1000;
counter integer := 0;
BEGIN
FOR rec IN cur LOOP
-- 处理单行
PERFORM process_row(rec.id);
counter := counter + 1;
IF counter % batch_size = 0 THEN
-- 定期释放资源
RAISE NOTICE 'Processed % rows', counter;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;批量操作
减少PL/pgSQL循环,优先使用SQL集合操作。
-- 错误示例:逐行更新(慢)
CREATE FUNCTION update_salaries_slow() RETURNS void AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT id, salary FROM employees LOOP
UPDATE employees SET salary = rec.salary * 1.1 WHERE id = rec.id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 正确示例:集合操作(快)
CREATE FUNCTION update_salaries_fast() RETURNS void AS $$
BEGIN
UPDATE employees SET salary = salary * 1.1;
END;
$$ LANGUAGE plpgsql;
-- 或者直接使用SQL,无需PL/pgSQL
UPDATE employees SET salary = salary * 1.1;使用PERFORM执行批量DML:
BEGIN
-- 批量删除
PERFORM 1 FROM delete_old_records();
-- 批量插入
PERFORM insert_batch_data(source_table, target_table);
END;避免逐行处理的替代方案:
-- 错误示例:逐行插入
FOR i IN 1..1000 LOOP
INSERT INTO target VALUES (i, 'value' || i);
END LOOP;
-- 正确示例:批量插入
INSERT INTO target
SELECT i, 'value' || i FROM generate_series(1, 1000) i;
-- 或使用COPY
COPY target FROM '/path/to/data.csv';调试与诊断
使用RAISE NOTICE调试
CREATE FUNCTION debug_example(input integer) RETURNS integer AS $$
DECLARE
step1_result integer;
step2_result integer;
BEGIN
RAISE NOTICE 'Function called with input: %', input;
step1_result := input * 2;
RAISE NOTICE 'After step 1: %', step1_result;
step2_result := step1_result + 10;
RAISE NOTICE 'After step 2: %', step2_result;
RETURN step2_result;
END;
$$ LANGUAGE plpgsql;变量名冲突处理
当PL/pgSQL变量名与SQL列名相同时,可通过plpgsql.variable_conflict参数控制优先级。
SET plpgsql.variable_conflict = 'error'; -- 报错(默认)
SET plpgsql.variable_conflict = 'use_variable'; -- 优先使用变量
SET plpgsql.variable_conflict = 'use_column'; -- 优先使用列名日志级别配置
SET log_min_messages = 'debug1'; -- 设置服务端日志级别
SET client_min_messages = 'notice'; -- 设置客户端消息级别常见问题
函数执行效率低的常见原因
原因
解决方案
EXCEPTION块子事务累积
减少包含EXCEPTION块的函数在高频场景中的调用次数。
RETURN NEXT结果集过大
改用RETURN QUERY,或使用游标分批处理。
游标未关闭
长时间持有大量游标。
逐行处理数据
改用SQL集合操作(UPDATE、INSERT...SELECT)。
变量持有大数据
避免在变量中存储大型JSON或TEXT数据。
IMMUTABLE/STABLE能否避免EXCEPTION块的子事务?
不能。函数的波动性分类仅影响优化器行为,不影响EXCEPTION块的子事务创建。以下函数仍然会为每次调用创建子事务:
CREATE FUNCTION my_func(text) RETURNS boolean AS $$ BEGIN PERFORM $1::json; RETURN true; EXCEPTION WHEN OTHERS THEN RETURN false; END; $$ LANGUAGE plpgsql IMMUTABLE; -- IMMUTABLE不能避免子事务PL/pgSQL、PL/Python和C函数如何选择?
场景
推荐
原因
简单数据处理
PL/pgSQL
语法简单,与SQL集成好。
复杂字符串或JSON处理
PL/Python
Python库丰富。
高频调用(百万次级)
C函数
无解释器开销。
需要错误处理且高频调用
C函数
无子事务开销。
需要网络或文件IO
PL/Python
支持系统库调用。
数学或科学计算
PL/Python + NumPy
专业计算库支持。
性能极致要求
C函数
最高性能。