别再踩坑了!KingbaseES 存储过程与触发器开发避坑实录

别再踩坑了!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 过程,数据校验和审计用触发器。同时做好异常处理和日志记录,确保生产环境的稳定性。

相关推荐
元宝骑士1 小时前
MySQL 实战:跨表排序 + 指定类型置顶四种写法
后端·mysql
ConardLi2 小时前
啊?我刚开源的 Skills 已经 7K Star 了?!
前端·人工智能·后端
道友可好2 小时前
Git Worktree:一个仓库,多个分身
前端·后端·程序员
鱼鳞_2 小时前
苍穹外卖-Day10(Spring task)
java·后端·spring
我是一颗柠檬2 小时前
【Redis】事务与Lua脚本Day7(2026年)
数据库·redis·后端·lua·database
farerboy3 小时前
15-Java while 和 do...while循环
java·后端
i220818 Faiz Ul3 小时前
民谣网站|基于Springboot的民谣网站管理系统(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·民谣网站
Oneslide3 小时前
windows cmd输入输出都很卡
后端
ihuyigui3 小时前
国际企业办公短信接口
前端·后端·架构