外观
PostgreSQL 存储过程性能优化
存储过程性能优化基础
1. 什么是存储过程
存储过程(Stored Procedure)是 PostgreSQL 11+ 引入的数据库对象,用于封装一组 SQL 语句,支持事务控制和复杂逻辑:
sql
-- 创建一个简单的存储过程
CREATE OR REPLACE PROCEDURE 简单存储过程(输入参数 INTEGER)
LANGUAGE plpgsql
AS $$
BEGIN
-- 执行 SQL 语句
INSERT INTO 日志表(消息, 创建时间)
VALUES ('调用了简单存储过程', CURRENT_TIMESTAMP);
-- 提交事务
COMMIT;
END;
$$;
-- 调用存储过程
CALL 简单存储过程(123);2. 存储过程与函数的区别
| 特性 | 函数 | 存储过程 |
|---|---|---|
| 调用方式 | SELECT function() | CALL procedure() |
| 事务控制 | 不支持 COMMIT/ROLLBACK | 支持完整的事务控制 |
| 返回值 | 必须有返回值 | 可以没有返回值 |
| 用途 | 主要用于查询和计算 | 主要用于数据修改和批量操作 |
3. 性能优化的重要性
存储过程性能优化可以带来以下好处:
- 减少网络传输开销
- 提高数据库资源利用率
- 减少锁定和等待时间
- 提高系统整体吞吐量
- 改善用户体验
执行计划优化
1. 使用 EXPLAIN ANALYZE 分析执行计划
sql
-- 创建一个示例存储过程
CREATE OR REPLACE PROCEDURE 示例存储过程(部门ID INTEGER)
LANGUAGE plpgsql
AS $$
BEGIN
-- 执行复杂查询
UPDATE 员工表
SET 工资 = 工资 * 1.1
WHERE 部门ID = 部门ID;
COMMIT;
END;
$$;
-- 使用 EXPLAIN ANALYZE 分析执行计划
EXPLAIN ANALYZE CALL 示例存储过程(10);2. 避免硬解析
PostgreSQL 会为每个存储过程调用生成执行计划,但可以通过以下方式优化:
sql
-- 使用参数化查询
CREATE OR REPLACE PROCEDURE 参数化存储过程(部门ID INTEGER)
LANGUAGE plpgsql
AS $$
DECLARE
v_sql TEXT;
BEGIN
-- 使用 format 函数生成参数化查询
v_sql := format('UPDATE 员工表 SET 工资 = 工资 * 1.1 WHERE 部门ID = $1');
EXECUTE v_sql USING 部门ID;
COMMIT;
END;
$$;3. 优化存储过程内部查询
sql
-- 优化前:使用低效查询
CREATE OR REPLACE PROCEDURE 优化前存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 低效查询:没有使用索引
UPDATE 员工表
SET 状态 = '离职'
WHERE 最后活跃日期 < current_date - interval '90 days';
COMMIT;
END;
$$;
-- 优化后:确保使用索引
CREATE OR REPLACE PROCEDURE 优化后存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 优化查询:确保最后活跃日期列有索引
UPDATE 员工表
SET 状态 = '离职'
WHERE 最后活跃日期 < current_date - interval '90 days';
COMMIT;
END;
$$;
-- 为最后活跃日期列创建索引
CREATE INDEX idx_员工表_最后活跃日期 ON 员工表(最后活跃日期);批量操作优化
1. 使用批量插入替代循环插入
sql
-- 优化前:使用循环插入
CREATE OR REPLACE PROCEDURE 循环插入存储过程()
LANGUAGE plpgsql
AS $$
DECLARE
v_counter INTEGER := 1;
BEGIN
-- 低效:逐行插入
WHILE v_counter <= 1000 LOOP
INSERT INTO 测试表(ID, 名称)
VALUES (v_counter, '测试数据 ' || v_counter);
v_counter := v_counter + 1;
END LOOP;
COMMIT;
END;
$$;
-- 优化后:使用批量插入
CREATE OR REPLACE PROCEDURE 批量插入存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 高效:使用 generate_series 批量插入
INSERT INTO 测试表(ID, 名称)
SELECT
g.id,
'测试数据 ' || g.id
FROM generate_series(1, 1000) AS g(id);
COMMIT;
END;
$$;2. 使用 RETURN QUERY 替代 RETURN NEXT
sql
-- 优化前:使用 RETURN NEXT 循环
CREATE OR REPLACE FUNCTION 循环返回函数()
RETURNS TABLE(ID INTEGER, 名称 TEXT)
LANGUAGE plpgsql
AS $$
DECLARE
v_rec RECORD;
BEGIN
FOR v_rec IN SELECT ID, 名称 FROM 员工表 WHERE 部门ID = 10 LOOP
ID := v_rec.ID;
名称 := v_rec.名称;
RETURN NEXT;
END LOOP;
END;
$$;
-- 优化后:使用 RETURN QUERY
CREATE OR REPLACE FUNCTION 批量返回函数()
RETURNS TABLE(ID INTEGER, 名称 TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT ID, 名称 FROM 员工表 WHERE 部门ID = 10;
END;
$$;3. 合理使用事务
sql
-- 优化前:频繁提交事务
CREATE OR REPLACE PROCEDURE 频繁提交存储过程()
LANGUAGE plpgsql
AS $$
DECLARE
v_counter INTEGER := 1;
BEGIN
WHILE v_counter <= 1000 LOOP
INSERT INTO 测试表(ID, 名称)
VALUES (v_counter, '测试数据 ' || v_counter);
-- 低效:每插入一行就提交一次
COMMIT;
v_counter := v_counter + 1;
END LOOP;
END;
$$;
-- 优化后:批量提交事务
CREATE OR REPLACE PROCEDURE 批量提交存储过程()
LANGUAGE plpgsql
AS $$
DECLARE
v_counter INTEGER := 1;
BEGIN
WHILE v_counter <= 1000 LOOP
INSERT INTO 测试表(ID, 名称)
VALUES (v_counter, '测试数据 ' || v_counter);
v_counter := v_counter + 1;
END LOOP;
-- 高效:所有插入完成后一次性提交
COMMIT;
END;
$$;索引使用优化
1. 为存储过程的查询条件创建索引
sql
-- 创建存储过程,查询条件为部门ID和状态
CREATE OR REPLACE PROCEDURE 索引优化存储过程(部门ID INTEGER, 员工状态 TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
-- 查询将使用部门ID和状态的组合索引
UPDATE 员工表
SET 工资 = 工资 * 1.05
WHERE 部门ID = 部门ID AND 状态 = 员工状态;
COMMIT;
END;
$$;
-- 为查询条件创建组合索引
CREATE INDEX idx_员工表_部门状态 ON 员工表(部门ID, 状态);2. 避免索引失效
sql
-- 优化前:可能导致索引失效的查询
CREATE OR REPLACE PROCEDURE 索引失效存储过程(员工姓名 TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
-- 索引失效:使用了函数包装列
UPDATE 员工表
SET 状态 = '活跃'
WHERE UPPER(姓名) = UPPER(员工姓名);
COMMIT;
END;
$$;
-- 优化后:使用索引友好的查询
CREATE OR REPLACE PROCEDURE 索引友好存储过程(员工姓名 TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
-- 索引友好:直接使用列名比较
UPDATE 员工表
SET 状态 = '活跃'
WHERE 姓名 = 员工姓名;
COMMIT;
END;
$$;
-- 或者创建函数索引
CREATE INDEX idx_员工表_姓名大写 ON 员工表(UPPER(姓名));内存和资源优化
1. 调整 work_mem 参数
sql
-- 在存储过程中调整 work_mem
CREATE OR REPLACE PROCEDURE 内存优化存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 临时增加 work_mem 用于复杂排序或哈希操作
SET LOCAL work_mem = '256MB';
-- 执行需要大量内存的操作
CREATE TEMP TABLE 临时表 AS
SELECT * FROM 大表 ORDER BY 复杂列;
-- 处理临时表数据
-- ...
COMMIT;
END;
$$;2. 合理使用临时表
sql
-- 优化前:频繁查询大表
CREATE OR REPLACE PROCEDURE 频繁查询存储过程()
LANGUAGE plpgsql
AS $$
DECLARE
v_rec RECORD;
BEGIN
-- 低效:多次查询大表
FOR v_rec IN SELECT ID FROM 大表 WHERE 条件1 LOOP
-- 再次查询大表
UPDATE 大表 SET 列1 = 值1 WHERE ID = v_rec.ID;
END LOOP;
COMMIT;
END;
$$;
-- 优化后:使用临时表缓存数据
CREATE OR REPLACE PROCEDURE 临时表存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 高效:使用临时表缓存需要的数据
CREATE TEMP TABLE 临时ID表 AS
SELECT ID FROM 大表 WHERE 条件1;
-- 对临时表进行操作
UPDATE 大表 t1
SET 列1 = 值1
FROM 临时ID表 t2
WHERE t1.ID = t2.ID;
COMMIT;
END;
$$;并行执行优化
1. 启用并行查询
sql
-- 配置并行查询参数
ALTER SYSTEM SET max_parallel_workers = 4;
ALTER SYSTEM SET max_parallel_workers_per_gather = 2;
-- 重新加载配置
SELECT pg_reload_conf();
-- 创建支持并行执行的存储过程
CREATE OR REPLACE PROCEDURE 并行存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 执行可以并行的查询
CREATE TEMP TABLE 并行结果表 AS
SELECT
部门ID,
SUM(工资) AS 总工资,
COUNT(*) AS 员工数量
FROM 员工表
GROUP BY 部门ID
ORDER BY 总工资 DESC;
COMMIT;
END;
$$;2. 优化并行度
sql
-- 根据硬件配置调整并行度
CREATE OR REPLACE PROCEDURE 并行度优化存储过程()
LANGUAGE plpgsql
AS $$
BEGIN
-- 临时调整并行度
SET LOCAL max_parallel_workers_per_gather = 4;
-- 执行大规模并行查询
CREATE TEMP TABLE 大规模结果表 AS
SELECT * FROM 超大表 WHERE 日期 >= '2023-01-01' ORDER BY 日期;
COMMIT;
END;
$$;性能监控与分析
1. 使用 pg_stat_statements 监控存储过程
sql
-- 安装 pg_stat_statements 扩展
CREATE EXTENSION pg_stat_statements;
-- 监控存储过程执行情况
SELECT
calls AS 调用次数,
total_time AS 总时间,
mean_time AS 平均时间,
max_time AS 最大时间,
query AS 查询语句
FROM pg_stat_statements
WHERE query LIKE '%PROCEDURE%' OR query LIKE '%CALL%'
ORDER BY total_time DESC;2. 使用 auto_explain 记录执行计划
sql
-- 启用 auto_explain 扩展
ALTER SYSTEM SET shared_preload_libraries = 'auto_explain';
ALTER SYSTEM SET auto_explain.log_min_duration = '100ms';
ALTER SYSTEM SET auto_explain.log_analyze = true;
ALTER SYSTEM SET auto_explain.log_buffers = true;
ALTER SYSTEM SET auto_explain.log_nested_statements = true;
-- 重新加载配置
SELECT pg_reload_conf();3. 监控锁定和等待
sql
-- 监控锁定情况
SELECT
pid,
usename,
query,
wait_event_type,
wait_event,
state
FROM pg_stat_activity
WHERE state = 'active' AND wait_event_type IS NOT NULL;存储过程最佳实践
1. 命名和代码规范
- 使用描述性的存储过程名,如
更新员工工资()而不是proc1() - 参数名使用
p_前缀,如p_部门ID - 变量名使用
v_前缀,如v_计数器 - 编写清晰的注释,解释存储过程的用途和逻辑
- 保持存储过程简短,每个存储过程只做一件事
2. 错误处理
sql
-- 包含错误处理的存储过程
CREATE OR REPLACE PROCEDURE 错误处理存储过程(部门ID INTEGER)
LANGUAGE plpgsql
AS $$
BEGIN
-- 开始事务
BEGIN
UPDATE 员工表
SET 工资 = 工资 * 1.1
WHERE 部门ID = 部门ID;
-- 提交事务
COMMIT;
RAISE NOTICE '成功更新了部门 % 的员工工资', 部门ID;
EXCEPTION
WHEN division_by_zero THEN
ROLLBACK;
RAISE NOTICE '发生除零错误';
WHEN unique_violation THEN
ROLLBACK;
RAISE NOTICE '发生唯一约束冲突';
WHEN OTHERS THEN
ROLLBACK;
RAISE NOTICE '发生未知错误: %', SQLERRM;
END;
END;
$$;3. 测试和验证
- 为存储过程编写单元测试
- 使用不同规模的数据集测试性能
- 定期进行性能基准测试
- 记录存储过程的执行时间和资源消耗
常见问题(FAQ)
Q1:如何识别慢存储过程?
A1:识别慢存储过程的方法:
- 使用
pg_stat_statements查看执行时间较长的存储过程 - 启用
auto_explain记录慢查询的执行计划 - 监控数据库日志中的慢查询
- 使用
EXPLAIN ANALYZE分析存储过程的执行计划 - 监控存储过程执行时的系统资源使用情况
Q2:存储过程和函数哪个性能更好?
A2:存储过程和函数的性能比较:
- 对于查询操作,函数通常性能更好,因为可以被内联优化
- 对于数据修改操作,存储过程更适合,因为支持完整的事务控制
- 简单逻辑使用函数,复杂逻辑使用存储过程
- 频繁调用的操作使用函数,批量操作使用存储过程
Q3:如何优化存储过程中的循环?
A3:优化存储过程中循环的方法:
- 避免在循环中执行 DML 操作,改为批量操作
- 使用
RETURN QUERY替代RETURN NEXT循环 - 使用
generate_series生成序列数据 - 使用
UNNEST处理数组数据 - 考虑使用
LANGUAGE SQL替代LANGUAGE plpgsql
Q4:如何优化存储过程中的动态 SQL?
A4:优化动态 SQL 的方法:
- 使用
EXECUTE USING进行参数化查询 - 避免在循环中生成动态 SQL
- 使用
format()函数安全地生成 SQL - 预编译常用的动态 SQL 语句
- 监控动态 SQL 的执行计划
Q5:存储过程性能优化的常见误区?
A5:常见的性能优化误区:
- 过早优化:没有分析就进行优化
- 过度优化:优化了影响不大的部分
- 忽略执行计划:没有分析实际的执行计划
- 不考虑数据规模:小数据量测试通过但大数据量性能差
- 忽略锁定和并发:没有考虑多用户并发访问
Q6:如何监控存储过程的执行情况?
A6:监控存储过程执行情况的方法:
- 使用
pg_stat_statements查看执行统计 - 启用
auto_explain记录执行计划 - 在存储过程中添加日志记录
- 使用
pg_stat_activity查看当前执行的存储过程 - 监控系统资源使用情况
Q7:如何优化存储过程的启动开销?
A7:优化存储过程启动开销的方法:
- 减少存储过程的复杂度
- 避免在存储过程中加载大量数据
- 使用
LANGUAGE SQL替代LANGUAGE plpgsql - 避免在存储过程中进行大量初始化操作
- 考虑使用连接池减少连接开销
Q8:如何优化存储过程中的事务?
A8:优化存储过程中事务的方法:
- 保持事务简短:减少锁定持有时间
- 批量提交:避免频繁提交小事务
- 使用适当的隔离级别:根据需求选择隔离级别
- 避免长事务:及时提交或回滚事务
- 使用
SAVEPOINT进行部分回滚
性能优化案例
案例1:批量数据导入优化
sql
-- 优化前:逐行导入数据
CREATE OR REPLACE PROCEDURE 逐行导入存储过程()
LANGUAGE plpgsql
AS $$
DECLARE
v_data TEXT;
BEGIN
-- 假设从外部源读取数据
FOR v_data IN 外部数据源 LOOP
-- 逐行插入
INSERT INTO 目标表(列1, 列2) VALUES (值1, 值2);
END LOOP;
COMMIT;
END;
$$;
-- 优化后:批量导入数据
CREATE OR REPLACE PROCEDURE 批量导入存储过程()
LANGUAGE plpgsql
AS $$
DECLARE
v_data_array TEXT[];
BEGIN
-- 假设从外部源读取数据到数组
v_data_array := 获取外部数据();
-- 批量插入数据
INSERT INTO 目标表(列1, 列2)
SELECT
(v ->> '列1')::INT,
(v ->> '列2')::TEXT
FROM UNNEST(v_data_array) AS d,
LATERAL jsonb_populate_record(NULL::目标表, d::jsonb) AS v;
COMMIT;
END;
$$;案例2:复杂查询优化
sql
-- 优化前:复杂嵌套查询
CREATE OR REPLACE PROCEDURE 复杂查询存储过程(开始日期 DATE, 结束日期 DATE)
LANGUAGE plpgsql
AS $$
BEGIN
-- 复杂嵌套查询
CREATE TEMP TABLE 结果表 AS
SELECT
(SELECT 名称 FROM 部门表 WHERE ID = e.部门ID) AS 部门名称,
COUNT(*) AS 员工数量,
SUM(e.工资) AS 总工资
FROM 员工表 e
WHERE e.入职日期 BETWEEN 开始日期 AND 结束日期
GROUP BY e.部门ID;
COMMIT;
END;
$$;
-- 优化后:使用 JOIN 替代子查询
CREATE OR REPLACE PROCEDURE 优化查询存储过程(开始日期 DATE, 结束日期 DATE)
LANGUAGE plpgsql
AS $$
BEGIN
-- 使用 JOIN 优化查询
CREATE TEMP TABLE 结果表 AS
SELECT
d.名称 AS 部门名称,
COUNT(*) AS 员工数量,
SUM(e.工资) AS 总工资
FROM 员工表 e
JOIN 部门表 d ON e.部门ID = d.ID
WHERE e.入职日期 BETWEEN 开始日期 AND 结束日期
GROUP BY d.名称;
COMMIT;
END;
$$;