创建存储过程和函数
本章将详细介绍如何在PostgreSQL中创建存储过程和函数,包括语法、参数、返回值等关键概念。
存储过程与函数的区别
基本概念
PostgreSQL中存储过程和函数的主要区别:
特性 | 函数 (FUNCTION) | 存储过程 (PROCEDURE) |
---|---|---|
返回值 | 必须返回值 | 可选返回值 |
调用方式 | SELECT function_name() | CALL procedure_name() |
事务控制 | 不能包含事务控制语句 | 可以包含事务控制语句 |
SQL语句中使用 | 可以在SQL语句中调用 | 不能在SQL语句中直接调用 |
版本支持 | 所有版本支持 | PostgreSQL 11+支持 |
创建函数
基本语法
创建函数的基本语法结构:
CREATE [OR REPLACE] FUNCTION function_name (
[parameter_name parameter_type [DEFAULT default_value]] [, ...]
)
RETURNS return_type AS $$
DECLARE
-- 变量声明
BEGIN
-- 函数体
RETURN return_value;
EXCEPTION
-- 异常处理
END;
$$ LANGUAGE plpgsql;
简单函数示例
-- 创建简单的加法函数
CREATE OR REPLACE FUNCTION add_numbers(a INTEGER, b INTEGER)
RETURNS INTEGER AS $$
BEGIN
RETURN a + b;
END;
$$ LANGUAGE plpgsql;
-- 调用函数
SELECT add_numbers(5, 3); -- 返回 8
带默认参数的函数
-- 创建带默认参数的函数
CREATE OR REPLACE FUNCTION calculate_discount(
price NUMERIC(10,2),
discount_percent NUMERIC(5,2) DEFAULT 10.00
)
RETURNS NUMERIC(10,2) AS $$
BEGIN
RETURN price * (1 - discount_percent / 100);
END;
$$ LANGUAGE plpgsql;
-- 调用函数
SELECT calculate_discount(100.00); -- 使用默认折扣10%,返回 90.00
SELECT calculate_discount(100.00, 20.00); -- 指定折扣20%,返回 80.00
返回表的函数
-- 创建返回表的函数
CREATE OR REPLACE FUNCTION get_active_users()
RETURNS TABLE(
user_id INTEGER,
username VARCHAR(50),
email VARCHAR(255)
) AS $$
BEGIN
RETURN QUERY
SELECT id, username, email
FROM users
WHERE is_active = true;
END;
$$ LANGUAGE plpgsql;
-- 调用函数
SELECT * FROM get_active_users();
返回集合的函数
-- 创建返回集合的函数
CREATE OR REPLACE FUNCTION get_user_orders(user_id INTEGER)
RETURNS SETOF orders AS $$
BEGIN
RETURN QUERY
SELECT * FROM orders WHERE orders.user_id = get_user_orders.user_id;
END;
$$ LANGUAGE plpgsql;
-- 调用函数
SELECT * FROM get_user_orders(1);
创建存储过程
基本语法
创建存储过程的基本语法(PostgreSQL 11+):
CREATE [OR REPLACE] PROCEDURE procedure_name (
[parameter_name parameter_mode parameter_type [DEFAULT default_value]] [, ...]
)
LANGUAGE plpgsql
AS $$
DECLARE
-- 变量声明
BEGIN
-- 存储过程体
END;
$$;
简单存储过程示例
-- 创建简单的存储过程
CREATE OR REPLACE PROCEDURE create_user(
p_username VARCHAR(50),
p_email VARCHAR(255),
p_first_name VARCHAR(50),
p_last_name VARCHAR(50)
)
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO users (username, email, first_name, last_name, created_at)
VALUES (p_username, p_email, p_first_name, p_last_name, CURRENT_TIMESTAMP);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
$$;
-- 调用存储过程
CALL create_user('newuser', 'newuser@example.com', 'New', 'User');
带INOUT参数的存储过程
-- 创建带INOUT参数的存储过程
CREATE OR REPLACE PROCEDURE update_user_stats(
p_user_id INTEGER,
INOUT p_order_count INTEGER DEFAULT 0,
INOUT p_total_spent NUMERIC(12,2) DEFAULT 0.00
)
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*), COALESCE(SUM(total_amount), 0)
INTO p_order_count, p_total_spent
FROM orders
WHERE user_id = p_user_id;
END;
$$;
-- 调用存储过程
CALL update_user_stats(1, 0, 0.00);
参数模式
参数类型
PostgreSQL支持的参数模式:
-- IN参数(默认)- 输入参数
CREATE OR REPLACE FUNCTION greet_user(name VARCHAR)
RETURNS VARCHAR AS $$
BEGIN
RETURN 'Hello, ' || name;
END;
$$ LANGUAGE plpgsql;
-- OUT参数 - 输出参数
CREATE OR REPLACE FUNCTION get_user_info(user_id INTEGER, OUT username VARCHAR, OUT email VARCHAR)
AS $$
BEGIN
SELECT u.username, u.email INTO get_user_info.username, get_user_info.email
FROM users u
WHERE u.id = user_id;
END;
$$ LANGUAGE plpgsql;
-- INOUT参数 - 输入输出参数
CREATE OR REPLACE FUNCTION swap_values(INOUT a INTEGER, INOUT b INTEGER)
AS $$
DECLARE
temp INTEGER;
BEGIN
temp := a;
a := b;
b := temp;
END;
$$ LANGUAGE plpgsql;
可变参数函数
-- 创建可变参数函数
CREATE OR REPLACE FUNCTION sum_numbers(VARIADIC numbers INTEGER[])
RETURNS INTEGER AS $$
DECLARE
total INTEGER := 0;
num INTEGER;
BEGIN
FOREACH num IN ARRAY numbers LOOP
total := total + num;
END LOOP;
RETURN total;
END;
$$ LANGUAGE plpgsql;
-- 调用可变参数函数
SELECT sum_numbers(1, 2, 3, 4, 5); -- 返回 15
SELECT sum_numbers(10, 20, 30); -- 返回 60
复杂示例
业务逻辑函数
-- 创建订单处理函数
CREATE OR REPLACE FUNCTION process_order(
p_user_id INTEGER,
p_products JSONB
)
RETURNS INTEGER AS $$
DECLARE
order_id INTEGER;
product_item JSONB;
product_id INTEGER;
quantity INTEGER;
price NUMERIC(10,2);
total_amount NUMERIC(10,2) := 0;
BEGIN
-- 开始事务
BEGIN
-- 创建订单
INSERT INTO orders (user_id, total_amount, status, created_at)
VALUES (p_user_id, 0, 'pending', CURRENT_TIMESTAMP)
RETURNING id INTO order_id;
-- 处理每个产品
FOR product_item IN SELECT jsonb_array_elements(p_products)
LOOP
product_id := (product_item->>'product_id')::INTEGER;
quantity := (product_item->>'quantity')::INTEGER;
-- 获取产品价格
SELECT price INTO price FROM products WHERE id = product_id;
-- 检查库存
IF (SELECT stock_quantity FROM products WHERE id = product_id) < quantity THEN
RAISE EXCEPTION 'Insufficient stock for product %', product_id;
END IF;
-- 添加订单项
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (order_id, product_id, quantity, price);
-- 更新库存
UPDATE products
SET stock_quantity = stock_quantity - quantity
WHERE id = product_id;
-- 计算总金额
total_amount := total_amount + (price * quantity);
END LOOP;
-- 更新订单总金额
UPDATE orders
SET total_amount = total_amount
WHERE id = order_id;
-- 记录日志
INSERT INTO order_logs (order_id, action, details, created_at)
VALUES (order_id, 'created', 'Order processed successfully', CURRENT_TIMESTAMP);
-- 提交事务
RETURN order_id;
EXCEPTION
WHEN OTHERS THEN
-- 回滚事务
RAISE NOTICE 'Order processing failed: %', SQLERRM;
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
-- 调用函数
SELECT process_order(1, '[{"product_id": 1, "quantity": 2}, {"product_id": 2, "quantity": 1}]');
数据清洗存储过程
-- 创建数据清洗存储过程
CREATE OR REPLACE PROCEDURE clean_user_data()
LANGUAGE plpgsql
AS $$
DECLARE
cleaned_count INTEGER := 0;
BEGIN
-- 清洗空用户名
UPDATE users
SET username = 'anonymous_' || id
WHERE username IS NULL OR TRIM(username) = '';
GET DIAGNOSTICS cleaned_count = ROW_COUNT;
RAISE NOTICE 'Cleaned % users with empty usernames', cleaned_count;
-- 清洗无效邮箱
UPDATE users
SET email = NULL
WHERE email !~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';
GET DIAGNOSTICS cleaned_count = ROW_COUNT;
RAISE NOTICE 'Cleaned % users with invalid emails', cleaned_count;
-- 删除重复用户
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at) as rn
FROM users
WHERE email IS NOT NULL
) t WHERE rn > 1
);
GET DIAGNOSTICS cleaned_count = ROW_COUNT;
RAISE NOTICE 'Removed % duplicate users', cleaned_count;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE NOTICE 'Data cleaning failed: %', SQLERRM;
RAISE;
END;
$$;
-- 调用存储过程
CALL clean_user_data();
最佳实践
命名规范
- 函数命名:使用动词开头,如get_user_info、calculate_total
- 存储过程命名:使用动词开头,如create_user、update_order
- 参数命名:使用p_前缀,如p_user_id、p_email
- 局部变量命名:使用有意义的名称,避免单字母变量
错误处理
-- 良好的错误处理示例
CREATE OR REPLACE FUNCTION safe_divide(dividend NUMERIC, divisor NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
result NUMERIC;
BEGIN
-- 输入验证
IF divisor = 0 THEN
RAISE EXCEPTION 'Division by zero error' USING ERRCODE = '22012';
END IF;
-- 执行计算
result := dividend / divisor;
-- 返回结果
RETURN result;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Cannot divide by zero';
RETURN NULL;
WHEN OTHERS THEN
RAISE LOG 'Error in safe_divide: %', SQLERRM;
RAISE EXCEPTION 'Calculation failed: %', SQLERRM;
END;
$$ LANGUAGE plpgsql;
性能优化
- 避免在循环中执行SQL:尽量使用集合操作
- 合理使用游标:只在必要时使用
- 索引优化:确保函数中使用的查询有适当索引
- 事务管理:在存储过程中合理使用事务控制
综合示例
-- 创建完整的用户管理系统函数
CREATE OR REPLACE FUNCTION manage_user(
p_action VARCHAR(20), -- 'CREATE', 'UPDATE', 'DELETE'
p_user_id INTEGER DEFAULT NULL,
p_username VARCHAR(50) DEFAULT NULL,
p_email VARCHAR(255) DEFAULT NULL,
p_first_name VARCHAR(50) DEFAULT NULL,
p_last_name VARCHAR(50) DEFAULT NULL
)
RETURNS TABLE(
result_code INTEGER,
result_message VARCHAR(255),
user_id INTEGER
) AS $$
DECLARE
v_user_id INTEGER;
v_action_count INTEGER;
BEGIN
CASE UPPER(p_action)
WHEN 'CREATE' THEN
-- 验证输入
IF p_username IS NULL OR p_email IS NULL THEN
RETURN QUERY SELECT -1, 'Username and email are required for creation'::VARCHAR(255), NULL::INTEGER;
RETURN;
END IF;
-- 检查用户是否已存在
SELECT COUNT(*) INTO v_action_count
FROM users
WHERE username = p_username OR email = p_email;
IF v_action_count > 0 THEN
RETURN QUERY SELECT -2, 'User already exists'::VARCHAR(255), NULL::INTEGER;
RETURN;
END IF;
-- 创建用户
INSERT INTO users (username, email, first_name, last_name, created_at)
VALUES (p_username, p_email, p_first_name, p_last_name, CURRENT_TIMESTAMP)
RETURNING id INTO v_user_id;
RETURN QUERY SELECT 0, 'User created successfully'::VARCHAR(255), v_user_id;
WHEN 'UPDATE' THEN
-- 验证用户ID
IF p_user_id IS NULL THEN
RETURN QUERY SELECT -3, 'User ID is required for update'::VARCHAR(255), NULL::INTEGER;
RETURN;
END IF;
-- 检查用户是否存在
SELECT COUNT(*) INTO v_action_count
FROM users
WHERE id = p_user_id;
IF v_action_count = 0 THEN
RETURN QUERY SELECT -4, 'User not found'::VARCHAR(255), NULL::INTEGER;
RETURN;
END IF;
-- 更新用户
UPDATE users
SET
username = COALESCE(p_username, username),
email = COALESCE(p_email, email),
first_name = COALESCE(p_first_name, first_name),
last_name = COALESCE(p_last_name, last_name),
updated_at = CURRENT_TIMESTAMP
WHERE id = p_user_id;
RETURN QUERY SELECT 0, 'User updated successfully'::VARCHAR(255), p_user_id;
WHEN 'DELETE' THEN
-- 验证用户ID
IF p_user_id IS NULL THEN
RETURN QUERY SELECT -3, 'User ID is required for deletion'::VARCHAR(255), NULL::INTEGER;
RETURN;
END IF;
-- 检查用户是否存在
SELECT COUNT(*) INTO v_action_count
FROM users
WHERE id = p_user_id;
IF v_action_count = 0 THEN
RETURN QUERY SELECT -4, 'User not found'::VARCHAR(255), NULL::INTEGER;
RETURN;
END IF;
-- 删除用户
DELETE FROM users WHERE id = p_user_id;
RETURN QUERY SELECT 0, 'User deleted successfully'::VARCHAR(255), p_user_id;
ELSE
RETURN QUERY SELECT -5, 'Invalid action. Use CREATE, UPDATE, or DELETE'::VARCHAR(255), NULL::INTEGER;
END CASE;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT -99, ('Error: ' || SQLERRM)::VARCHAR(255), NULL::INTEGER;
END;
$$ LANGUAGE plpgsql;
-- 使用示例
SELECT * FROM manage_user('CREATE', NULL, 'john_doe', 'john@example.com', 'John', 'Doe');
SELECT * FROM manage_user('UPDATE', 1, NULL, 'john_new@example.com');
SELECT * FROM manage_user('DELETE', 1);
提示:在创建存储过程和函数时,要充分考虑错误处理、输入验证和性能优化。使用CREATE OR REPLACE可以方便地修改现有函数,但要注意这会丢失原有的权限设置。