别再踩坑了!KingbaseES 存储过程与触发器开发避坑实录
前言:存储过程和触发器是数据库开发中不可或缺的部分,能将业务逻辑下沉到数据库层,提升性能并保证数据一致性。本文基于 KingbaseES V9,通过丰富的代码示例,系统讲解函数、存储过程和触发器的开发方法与最佳实践。
一、自定义函数
1.1 PL/pgSQL 函数
KingbaseES V9 使用 PL/pgSQL 作为主要的过程语言,语法与 PostgreSQL 完全兼容:
sql
-- 计算员工年薪
CREATE OR REPLACE FUNCTION calc_annual_salary(base_salary NUMERIC, bonus_rate NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
annual_salary NUMERIC;
BEGIN
annual_salary := base_salary * 12 * (1 + bonus_rate);
RETURN ROUND(annual_salary, 2);
END;
$$;
-- 调用函数
SELECT calc_annual_salary(15000, 0.1);
1.2 返回结果集的函数
在需要返回多行数据的场景下,使用 RETURNS TABLE 或 RETURNS SETOF:
sql
-- 按部门统计薪资
CREATE OR REPLACE FUNCTION get_dept_salary_stats(p_dept_id INTEGER)
RETURNS TABLE(
dept_name VARCHAR,
emp_count BIGINT,
avg_salary NUMERIC,
max_salary NUMERIC,
min_salary NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT d.dept_name,
COUNT(*) AS emp_count,
ROUND(AVG(e.salary), 2),
MAX(e.salary),
MIN(e.salary)
FROM employee e
JOIN department d ON e.dept_id = d.id
WHERE e.dept_id = p_dept_id
GROUP BY d.dept_name;
END;
$$;
-- 调用
SELECT * FROM get_dept_salary_stats(1);
1.3 SQL 语言函数(高性能场景)
对于简单的查询逻辑,优先使用 SQL 语言函数,性能优于 PL/pgSQL:
sql
-- SQL 函数:获取员工完整信息
CREATE OR REPLACE FUNCTION get_employee_full_info(p_emp_id INTEGER)
RETURNS TABLE(emp_name VARCHAR, dept_name VARCHAR, salary NUMERIC)
LANGUAGE sql
AS $$
SELECT e.emp_name, d.dept_name, e.salary
FROM employee e
LEFT JOIN department d ON e.dept_id = d.id
WHERE e.emp_id = p_emp_id;
$$;
二、存储过程
2.1 基本存储过程
KingbaseES V9 支持 PROCEDURE 语法,与函数不同的是过程可以通过 CALL 调用,且支持事务控制:
sql
-- 批量调薪存储过程
CREATE OR REPLACE PROCEDURE adjust_salary(
p_dept_id INTEGER,
p_adjust_rate NUMERIC,
INOUT p_affected_count INTEGER DEFAULT 0
)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE employee
SET salary = salary * (1 + p_adjust_rate)
WHERE dept_id = p_dept_id;
GET DIAGNOSTICS p_affected_count = ROW_COUNT;
-- 记录操作日志
INSERT INTO salary_adjust_log (dept_id, adjust_rate, affected_count, operate_time)
VALUES (p_dept_id, p_adjust_rate, p_affected_count, CURRENT_TIMESTAMP);
COMMIT;
END;
$$;
-- 调用存储过程
CALL adjust_salary(1, 0.08, 0);
2.2 带异常处理的存储过程
生产环境的存储过程必须做好异常处理,避免数据不一致:
sql
-- 转账存储过程(含异常处理)
CREATE OR REPLACE PROCEDURE transfer_funds(
p_from_account INTEGER,
p_to_account INTEGER,
p_amount NUMERIC
)
LANGUAGE plpgsql
AS $$
DECLARE
v_balance NUMERIC;
BEGIN
-- 开启事务(过程中默认在事务内)
-- 检查转出账户余额
SELECT balance INTO v_balance FROM account WHERE id = p_from_account;
IF v_balance IS NULL THEN
RAISE EXCEPTION '转出账户 % 不存在', p_from_account;
END IF;
IF v_balance < p_amount THEN
RAISE EXCEPTION '余额不足,当前余额: %,转账金额: %', v_balance, p_amount;
END IF;
-- 扣款
UPDATE account SET balance = balance - p_amount WHERE id = p_from_account;
-- 入账
UPDATE account SET balance = balance + p_amount WHERE id = p_to_account;
IF NOT FOUND THEN
RAISE EXCEPTION '转入账户 % 不存在', p_to_account;
END IF;
-- 记录流水
INSERT INTO transfer_log (from_account, to_account, amount, status, create_time)
VALUES (p_from_account, p_to_account, p_amount, 'SUCCESS', CURRENT_TIMESTAMP);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
-- 记录失败日志
INSERT INTO transfer_log (from_account, to_account, amount, status, error_msg, create_time)
VALUES (p_from_account, p_to_account, p_amount, 'FAILED', SQLERRM, CURRENT_TIMESTAMP);
RAISE;
END;
$$;
-- 调用
CALL transfer_funds(1001, 1002, 500.00);
三、触发器
3.1 行级触发器
行级触发器针对每一行数据变更执行,常用于自动填充字段和审计:
sql
-- 自动更新 update_time 字段
CREATE OR REPLACE FUNCTION auto_update_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.update_time = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_employee_update_time
BEFORE UPDATE ON employee
FOR EACH ROW
EXECUTE FUNCTION auto_update_timestamp();
3.2 审计触发器
通过触发器自动记录数据变更,是保障数据安全的重要手段:
sql
-- 创建审计表
CREATE TABLE employee_audit (
audit_id SERIAL PRIMARY KEY,
emp_id INTEGER,
action VARCHAR(10),
old_salary NUMERIC,
new_salary NUMERIC,
operator VARCHAR(50),
operate_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 审计触发器函数
CREATE OR REPLACE FUNCTION fn_employee_audit()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'UPDATE' AND OLD.salary IS DISTINCT FROM NEW.salary THEN
INSERT INTO employee_audit (emp_id, action, old_salary, new_salary, operator)
VALUES (NEW.emp_id, 'UPDATE', OLD.salary, NEW.salary, CURRENT_USER);
END IF;
IF TG_OP = 'DELETE' THEN
INSERT INTO employee_audit (emp_id, action, old_salary, new_salary, operator)
VALUES (OLD.emp_id, 'DELETE', OLD.salary, NULL, CURRENT_USER);
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$;
-- 绑定触发器
CREATE TRIGGER trg_employee_audit
AFTER UPDATE OR DELETE ON employee
FOR EACH ROW
EXECUTE FUNCTION fn_employee_audit();
3.3 语句级触发器
语句级触发器在整条 SQL 执行前后触发一次,适合做整体校验:
sql
CREATE OR REPLACE FUNCTION fn_check_batch_update()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_count INTEGER;
BEGIN
-- 检查本次操作影响的行数(通过临时表或其他方式)
-- 这里演示触发器的创建方式
RAISE NOTICE '批量操作执行时间: %', CURRENT_TIMESTAMP;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_check_batch
AFTER UPDATE ON employee
FOR EACH STATEMENT
EXECUTE FUNCTION fn_check_batch_update();
四、管理与调试
4.1 查看与删除
sql
-- 查看当前 schema 下的所有函数
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'public';
-- 查看某个表的触发器
SELECT trigger_name, event_manipulation, action_timing
FROM information_schema.triggers
WHERE event_object_table = 'employee';
-- 删除函数
DROP FUNCTION IF EXISTS calc_annual_salary(NUMERIC, NUMERIC);
-- 删除触发器
DROP TRIGGER IF EXISTS trg_employee_audit ON employee;
4.2 开发调试建议
sql
-- 在函数中使用 RAISE 输出调试信息
RAISE NOTICE '当前变量值: v_count = %', v_count;
RAISE WARNING '警告: 数据异常 %', v_detail;
-- 开启函数执行时间统计
SET track_functions = 'all';
-- 查看函数执行统计
SELECT funcid::regprocedure, calls, total_time, mean_time
FROM sys_stat_user_functions
ORDER BY total_time DESC;
实践建议:存储过程的调试建议配合 RAISE NOTICE 和日志表结合使用。复杂业务逻辑优先考虑在应用层实现,存储过程更适合处理数据校验、审计等与数据强相关的逻辑。
五、Oracle 兼容模式下的差异
在 Oracle 兼容模式下,KingbaseES V9 支持部分 Oracle 的 PL/SQL 语法:
sql
-- Oracle 风格的包(Package)定义(兼容模式下)
CREATE OR REPLACE PACKAGE emp_pkg AS
FUNCTION get_salary(p_emp_id INTEGER) RETURN NUMBER;
PROCEDURE update_salary(p_emp_id INTEGER, p_new_salary NUMBER);
END emp_pkg;
/
CREATE OR REPLACE PACKAGE BODY emp_pkg AS
FUNCTION get_salary(p_emp_id INTEGER) RETURN NUMBER IS
v_salary NUMBER;
BEGIN
SELECT salary INTO v_salary FROM employee WHERE emp_id = p_emp_id;
RETURN v_salary;
END;
PROCEDURE update_salary(p_emp_id INTEGER, p_new_salary NUMBER) IS
BEGIN
UPDATE employee SET salary = p_new_salary WHERE emp_id = p_emp_id;
END;
END emp_pkg;
/
总结
KingbaseES V9 在函数、存储过程和触发器方面的能力与 PostgreSQL 高度一致,同时在 Oracle 兼容模式下还支持 PL/SQL 语法和包的概念。开发时建议遵循以下原则:简单查询逻辑用 SQL 函数,复杂业务用 PL/pgSQL 过程,数据校验和审计用触发器。同时做好异常处理和日志记录,确保生产环境的稳定性。