第四章 函数、触发器与过程语言

第一节 PL/pgSQL 编写存储过程详解

目标:深入掌握 PostgreSQL 中使用 PL/pgSQL 编写存储过程的方法,理解事务控制、异常处理、游标和循环等高级结构,并能够结合业务场景设计高性能的数据库逻辑层。

随着业务复杂度的提升,将部分逻辑从应用层下沉到数据库层变得越来越常见。PostgreSQL 支持丰富的存储过程功能,尤其在 v11 引入对存储过程(Procedure)的支持后,使得数据库具备了完整的事务控制能力。

本节将围绕以下核心内容展开:

  • 存储过程与函数的区别
  • PL/pgSQL 基础语法结构
  • 事务控制(COMMIT / ROLLBACK)
  • 游标(Cursor)与循环结构
  • 异常处理机制
  • 实战案例:批量订单状态更新的存储过程实现

📌 一、存储过程 vs 函数

特性函数(Function)存储过程(Procedure)
是否有返回值✅ 有❌ 无(可使用 OUT 参数)
可否调用 COMMIT/ROLLBACK❌ 不支持✅ 支持
调用方式SELECT 或 PERFORM(在 PL/pgSQL 中)CALL
是否可在 SQL 中直接调用

示例对比:

函数调用:

1
SELECT calculate_total_sales('2024-01-01', '2024-12-31');

存储过程调用:

1
CALL update_order_status_batch('shipped');

🧱 二、PL/pgSQL 基础语法结构

PL/pgSQL 是 PostgreSQL 内置的过程语言,其基本语法结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CREATE OR REPLACE PROCEDURE procedure_name(param1 type, param2 type, ...)
LANGUAGE plpgsql
AS $$
DECLARE
    -- 变量声明区
BEGIN
    -- 执行体
EXCEPTION
    -- 异常处理
END;
$$;

🔁 三、变量声明与赋值

1. 声明变量

1
2
3
4
DECLARE
    counter INT := 0;
    customer_name TEXT;
    total_amount NUMERIC(10,2) DEFAULT 0;

2. 赋值操作

1
2
3
4
5
counter := counter + 1;

SELECT name INTO customer_name FROM customers WHERE id = 1;

total_amount := calculate_total(customer_id);

💡 四、流程控制语句

1. IF 判断

1
2
3
4
5
6
7
IF total_amount > 1000 THEN
    RAISE NOTICE '大额订单';
ELSIF total_amount BETWEEN 500 AND 1000 THEN
    RAISE NOTICE '中等订单';
ELSE
    RAISE NOTICE '小额订单';
END IF;

2. LOOP 循环

1
2
3
4
LOOP
    counter := counter + 1;
    EXIT WHEN counter >= 10;
END LOOP;

3. FOR 循环(遍历结果集)

1
2
3
FOR rec IN SELECT * FROM orders WHERE status = 'pending' LOOP
    UPDATE orders SET status = 'processing' WHERE order_id = rec.order_id;
END LOOP;

🔄 五、事务控制(COMMIT / ROLLBACK)

存储过程支持显式事务控制,这在执行批量操作或关键数据变更时非常有用。

示例:插入用户并提交事务

1
2
3
4
5
6
7
8
CREATE OR REPLACE PROCEDURE create_user_and_commit(username TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO users (username) VALUES ($1);
    COMMIT;
END;
$$;

示例:出现错误时回滚事务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
CREATE OR REPLACE PROCEDURE transfer_funds(from_id INT, to_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;

    IF (SELECT balance < 0 FROM accounts WHERE id = from_id) THEN
        RAISE EXCEPTION '余额不足';
    END IF;

    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE NOTICE '转账失败:% %', SQLERRM, SQLSTATE;
END;
$$;

🧭 六、游标(Cursor)与大数据处理

当需要逐条处理大量数据时,可以使用游标来减少内存占用。

1. 显式游标示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
CREATE OR REPLACE PROCEDURE process_large_dataset()
LANGUAGE plpgsql
AS $$
DECLARE
    cur CURSOR FOR SELECT * FROM orders WHERE processed = FALSE;
    rec RECORD;
BEGIN
    OPEN cur;
    LOOP
        FETCH cur INTO rec;
        EXIT WHEN NOT FOUND;

        -- 处理单条记录
        UPDATE orders SET processed = TRUE WHERE order_id = rec.order_id;
    END LOOP;
    CLOSE cur;
END;
$$;

2. 使用 FOR 循环简化游标

1
2
3
FOR rec IN SELECT * FROM orders WHERE processed = FALSE LOOP
    UPDATE orders SET processed = TRUE WHERE order_id = rec.order_id;
END LOOP;

⚠️ 七、异常处理机制

PostgreSQL 支持强大的异常处理机制,可以在出错时优雅地进行日志记录或回滚操作。

示例:捕获特定异常类型

1
2
3
4
5
6
7
8
9
BEGIN
    -- 尝试删除一个被引用的记录
    DELETE FROM products WHERE product_id = 1;
EXCEPTION
    WHEN foreign_key_violation THEN
        RAISE NOTICE '该商品已被引用,无法删除';
    WHEN others THEN
        RAISE NOTICE '发生未知错误:% %', SQLERRM, SQLSTATE;
END;

🧪 八、实战演练:批量更新订单状态的存储过程

场景描述:

你需要编写一个存储过程,将所有“已付款”状态的订单统一改为“已发货”,并在过程中添加日志记录和异常处理。

步骤如下:

1. 创建订单表(如尚未存在)

1
2
3
4
5
6
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    user_id INT,
    status TEXT DEFAULT 'pending',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. 创建日志表

1
2
3
4
5
6
CREATE TABLE order_update_log (
    log_id SERIAL PRIMARY KEY,
    old_status TEXT,
    new_status TEXT,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3. 编写存储过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE OR REPLACE PROCEDURE batch_update_order_status(new_status TEXT)
LANGUAGE plpgsql
AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM orders WHERE status = 'paid' LOOP
        UPDATE orders SET status = new_status, updated_at = NOW()
        WHERE order_id = rec.order_id;

        INSERT INTO order_update_log (old_status, new_status)
        VALUES ('paid', new_status);
    END LOOP;

    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE NOTICE '更新失败:% %', SQLERRM, SQLSTATE;
END;
$$;

4. 调用存储过程

1
CALL batch_update_order_status('shipped');

📈 九、性能优化建议

技巧描述
避免逐行处理使用集合操作替代游标,提高效率
减少事务粒度对于大批量操作,适当分批提交
启用自动提交对只读操作可设置 SET LOCAL autocommit = on
使用临时表缓存中间结果减少重复计算
合理使用索引确保查询条件字段有合适的索引支持

📌 小结

技术功能推荐使用场景
存储过程支持事务控制批量数据处理、关键业务逻辑
PL/pgSQLPostgreSQL 原生语言通用业务封装
游标逐条处理大数据日志处理、ETL
异常处理捕获错误并恢复金融交易、支付系统
事务控制提交或回滚操作数据一致性要求高场景

通过本节的学习,你应该已经掌握了如何使用 PL/pgSQL 编写 PostgreSQL 存储过程,包括变量、流程控制、游标、事务管理和异常处理等高级特性,并能够在实际项目中设计高效的数据库逻辑模块。