在PostgreSQL中,PL/pgSQL是一种可加载的过程式语言,它建立在标准SQL之上,增加了过程化编程结构。这种语言的设计目标是让开发者能够在数据库服务器端执行复杂的计算逻辑,减少客户端与服务器之间的数据传输。PL/pgSQL代码块以特定的结构组织,包含声明部分、执行部分和异常处理部分,每个部分都有明确的职责划分。

PL/pgSQL基础语法结构与块组织
PL/pgSQL代码的基本单元是块结构。一个完整的块由声明部分、执行部分和异常处理部分组成。声明部分使用DECLARE关键字开头,用于定义变量、常量和类型;执行部分以BEGIN开头,包含具体的业务逻辑语句;异常处理部分以EXCEPTION开头,用于捕获和处理运行时错误。这种结构支持嵌套,内部块可以访问外部块的变量,但外部块不能访问内部块声明的变量。
变量声明是PL/pgSQL编程的基础。变量可以引用数据库表中的列类型,也可以使用基本数据类型。使用%TYPE属性可以引用某个表列的数据类型,这样当表结构变化时,变量类型会自动同步更新。此外,%ROWTYPE属性允许声明一个能够接收整行数据的记录变量,这在处理查询结果集时非常实用。
-- 基本块结构示例
DO $$
DECLARE
user_count INTEGER := 0;
user_name VARCHAR(50) := 'default_user';
-- 引用表列类型
user_id users.id%TYPE;
-- 引用整行记录类型
user_record users%ROWTYPE;
BEGIN
-- 执行部分
SELECT COUNT(*) INTO user_count FROM users;
RAISE NOTICE '当前用户总数: %', user_count;
-- 异常处理部分
EXCEPTION
WHEN others THEN
RAISE NOTICE '捕获到异常: %', SQLERRM;
END;
$$ LANGUAGE plpgsql;
上述代码展示了完整的块结构。$$符号用作字符串界定符,避免单引号转义问题。INTO子句是PL/pgSQL特有的语法,用于将查询结果赋值给变量。需要注意的是,当查询返回多行时,只有第一行数据会被赋值给变量,其余行会被忽略。如果查询没有返回任何行,变量的值将保持不变,这可能导致逻辑错误,因此在实际开发中应当配合FOUND变量检查查询是否成功。
流程控制:条件判断与多分支选择
流程控制是过程式语言的核心能力。PL/pgSQL提供了IF语句和CASE语句两种条件判断结构。IF语句支持三种形式:IF-THEN、IF-THEN-ELSE和IF-THEN-ELSIF-THEN-ELSE。条件表达式可以包含SQL运算符和函数调用,判断逻辑与标准SQL的WHERE子句类似,但支持更复杂的布尔逻辑组合。
CASE语句分为简单形式和搜索形式。简单CASE将一个表达式与多个常量值比较,搜索CASE则评估多个独立的布尔条件。在多分支场景下,CASE语句通常比嵌套的IF-ELSIF结构更清晰易读。当条件分支超过三个时,推荐使用CASE语句以提升代码可维护性。两种结构在性能上没有显著差异,选择依据主要是代码可读性。
-- 条件判断综合示例
CREATE OR REPLACE FUNCTION calculate_discount(
customer_level VARCHAR,
purchase_amount NUMERIC
) RETURNS NUMERIC AS $$
DECLARE
discount_rate NUMERIC := 0;
final_price NUMERIC := 0;
BEGIN
-- 使用IF语句进行基础判断
IF purchase_amount <= 0 THEN
RAISE EXCEPTION '购买金额必须大于零';
END IF;
-- 使用CASE语句处理多分支逻辑
CASE customer_level
WHEN '普通会员' THEN
IF purchase_amount > 1000 THEN
discount_rate := 0.05;
ELSE
discount_rate := 0.02;
END IF;
WHEN '银卡会员' THEN
discount_rate := CASE
WHEN purchase_amount > 2000 THEN 0.10
WHEN purchase_amount > 1000 THEN 0.08
ELSE 0.05
END;
WHEN '金卡会员' THEN
discount_rate := 0.15;
ELSE
-- 未知会员等级走默认逻辑
discount_rate := 0.01;
END CASE;
final_price := purchase_amount * (1 - discount_rate);
RETURN final_price;
END;
$$ LANGUAGE plpgsql;
上述函数展示了条件判断的综合应用。在CASE语句内部嵌套IF语句是常见模式,用于处理二级判断逻辑。当CASE没有匹配项且没有ELSE子句时,会抛出CASE_NOT_FOUND异常。因此,除非确定所有情况都已覆盖,否则应当始终提供ELSE分支作为兜底处理。另外,RAISE EXCEPTION语句用于主动抛出异常,可以指定错误消息和错误级别,这在参数校验场景中非常实用。
循环控制与游标遍历机制
循环结构允许重复执行一组语句,PL/pgSQL支持多种循环形式。LOOP语句定义无条件循环,必须配合EXIT或RETURN语句跳出,否则会形成死循环。WHILE语句在每次迭代前评估条件表达式,条件为真时继续执行。FOR语句用于遍历整数范围或查询结果集,是最常用的循环结构之一。此外,FOREACH语句专门用于遍历数组元素。
游标是处理查询结果集的重要机制。显式游标允许逐行处理数据,适用于大数据集场景,避免一次性加载所有数据到内存。游标声明后需要先打开,然后通过FETCH语句提取数据,最后关闭释放资源。隐式游标通过FOR循环自动管理,代码更简洁但灵活性较低。在处理百万级数据时,应当优先考虑使用批量操作而非逐行游标处理。
-- 循环与游标综合示例
CREATE OR REPLACE FUNCTION batch_update_user_status() RETURNS INTEGER AS $$
DECLARE
affected_rows INTEGER := 0;
-- 声明游标变量
user_cursor CURSOR FOR
SELECT id, last_login FROM users WHERE status = 'active';
-- 声明记录变量接收游标数据
user_rec RECORD;
BEGIN
-- 使用FOR循环遍历查询结果(隐式游标)
FOR user_rec IN SELECT id, last_login FROM users WHERE status = 'pending' LOOP
IF user_rec.last_login < CURRENT_DATE - INTERVAL '90 days' THEN
UPDATE users SET status = 'inactive' WHERE id = user_rec.id;
affected_rows := affected_rows + 1;
END IF;
END LOOP;
-- 使用显式游标处理复杂逻辑
OPEN user_cursor;
LOOP
FETCH user_cursor INTO user_rec;
EXIT WHEN NOT FOUND;
-- 检查登录时间并更新状态
IF user_rec.last_login < CURRENT_DATE - INTERVAL '180 days' THEN
UPDATE users SET status = 'dormant' WHERE id = user_rec.id;
affected_rows := affected_rows + 1;
END IF;
-- 每处理1000行提交一次事务
IF affected_rows % 1000 = 0 THEN
COMMIT;
END IF;
END LOOP;
CLOSE user_cursor;
RETURN affected_rows;
END;
$$ LANGUAGE plpgsql;
上述代码同时展示了隐式游标和显式游标的使用方式。隐式游标通过FOR record IN query LOOP语法自动管理生命周期,代码简洁但无法中途控制游标位置。显式游标需要手动OPEN、FETCH和CLOSE,但提供了更精细的控制能力。FOUND是一个特殊的布尔变量,在FETCH后指示是否成功获取到数据,在SELECT INTO后指示是否找到匹配行。对于大批量更新操作,在循环中定期提交事务可以防止事务日志过大,但需要注意这会牺牲事务的原子性。
除了基本循环结构,CONTINUE语句可以跳过当前迭代进入下一次循环,EXIT语句可以带条件表达式实现条件退出。在嵌套循环中,可以通过给循环添加标签实现跨层跳转。这些控制语句的组合使用能够应对各种复杂的业务逻辑场景。需要特别注意的是,在循环中执行DML语句时,应当关注锁竞争和性能影响,长时间运行的循环可能阻塞其他事务。
PL/pgSQLPostgreSQL存储过程修改时间:2026-08-26 19:18:33