一、openGauss存储过程开发基础

存储过程是数据库开发中的瑞士军刀,它能将复杂的业务逻辑封装在数据库内部。在openGauss中,存储过程使用PL/pgSQL语法,这个和PostgreSQL非常相似,但又有一些华为特色的增强。

先来看个最简单的例子,我们创建一个计算员工奖金的存储过程:

-- 创建计算奖金的存储过程
CREATE OR REPLACE PROCEDURE calculate_bonus(
    IN employee_id INT,          -- 输入参数:员工ID
    INOUT bonus_amount NUMERIC   -- 输入输出参数:奖金金额
)
AS $$
DECLARE
    base_salary NUMERIC;         -- 声明局部变量:基本工资
    performance_rating NUMERIC; -- 绩效评分
BEGIN
    -- 获取员工基本工资
    SELECT salary INTO base_salary 
    FROM employees 
    WHERE id = employee_id;
    
    -- 获取员工绩效评分
    SELECT rating INTO performance_rating
    FROM performance_reviews
    WHERE employee_id = employee_id;
    
    -- 计算奖金(基本工资 × 绩效系数)
    bonus_amount := base_salary * performance_rating * 0.1;
    
    -- 记录奖金发放日志
    INSERT INTO bonus_log(employee_id, amount, calc_date)
    VALUES (employee_id, bonus_amount, CURRENT_DATE);
    
    -- 异常处理
    EXCEPTION WHEN NO_DATA_FOUND THEN
        RAISE NOTICE '员工ID % 不存在或数据不完整', employee_id;
        bonus_amount := 0;
END;
$$ LANGUAGE plpgsql;

这个例子展示了存储过程的基本结构:参数声明、变量定义、业务逻辑和异常处理。openGauss的存储过程支持事务控制,这意味着你可以在过程中使用COMMIT和ROLLBACK。

二、存储过程高级特性实战

openGauss提供了一些很酷的高级特性,能让你的存储过程更强大。比如动态SQL、游标处理和自定义异常。

2.1 动态SQL应用

动态SQL特别适合需要根据不同条件构建查询的场景:

-- 创建动态查询员工信息的存储过程
CREATE OR REPLACE PROCEDURE query_employees(
    IN department_id INT DEFAULT NULL,
    IN min_salary NUMERIC DEFAULT NULL,
    OUT result_cursor REFCURSOR  -- 返回游标用于客户端获取结果
)
AS $$
DECLARE
    query_text TEXT;  -- 动态SQL语句
BEGIN
    -- 基础查询
    query_text := 'SELECT id, name, salary FROM employees WHERE 1=1';
    
    -- 根据参数动态添加条件
    IF department_id IS NOT NULL THEN
        query_text := query_text || ' AND department_id = ' || department_id;
    END IF;
    
    IF min_salary IS NOT NULL THEN
        query_text := query_text || ' AND salary >= ' || min_salary;
    END IF;
    
    -- 打开游标返回结果
    OPEN result_cursor FOR EXECUTE query_text;
    
    -- 记录查询日志
    INSERT INTO query_log(sql_text, exec_time)
    VALUES (query_text, CURRENT_TIMESTAMP);
END;
$$ LANGUAGE plpgsql;

2.2 游标批量处理

处理大量数据时,游标能帮你节省内存:

-- 批量更新员工工资的存储过程
CREATE OR REPLACE PROCEDURE batch_update_salary(
    IN increase_percent NUMERIC
)
AS $$
DECLARE
    emp_rec RECORD;  -- 定义记录类型变量
    emp_cursor CURSOR FOR 
        SELECT id, salary FROM employees 
        WHERE status = 'active';
BEGIN
    -- 开始事务
    BEGIN
        -- 打开游标
        OPEN emp_cursor;
        
        LOOP
            -- 获取下一条记录
            FETCH emp_cursor INTO emp_rec;
            EXIT WHEN NOT FOUND;
            
            -- 更新工资
            UPDATE employees 
            SET salary = emp_rec.salary * (1 + increase_percent/100)
            WHERE id = emp_rec.id;
            
            -- 每处理100条记录提交一次
            IF MOD(emp_rec.id, 100) = 0 THEN
                COMMIT;
                BEGIN;  -- 开始新事务
            END IF;
        END LOOP;
        
        -- 关闭游标
        CLOSE emp_cursor;
        COMMIT;
    EXCEPTION
        WHEN OTHERS THEN
            ROLLBACK;
            RAISE EXCEPTION '批量更新失败: %', SQLERRM;
    END;
END;
$$ LANGUAGE plpgsql;

三、性能优化关键技巧

存储过程写出来容易,但要写得高效就需要一些技巧了。以下是几个openGauss特有的优化建议。

3.1 参数传递优化

openGauss对IN、OUT、INOUT参数的处理方式不同,会影响性能:

-- 不推荐的写法:频繁修改INOUT参数
CREATE OR REPLACE PROCEDURE slow_calculation(
    INOUT counter INT
)
AS $$
BEGIN
    FOR i IN 1..10000 LOOP
        counter := counter + 1;  -- 每次修改都会产生副本
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 推荐的优化写法
CREATE OR REPLACE PROCEDURE fast_calculation(
    IN initial_value INT,
    OUT result INT
)
AS $$
DECLARE
    temp_counter INT := initial_value;
BEGIN
    FOR i IN 1..10000 LOOP
        temp_counter := temp_counter + 1;  -- 操作局部变量
    END LOOP;
    result := temp_counter;  -- 最后一次性赋值
END;
$$ LANGUAGE plpgsql;

3.2 批量操作替代循环

openGauss的批量DML操作比循环单条处理快得多:

-- 不推荐的写法:循环单条更新
CREATE OR REPLACE PROCEDURE slow_update_employees()
AS $$
DECLARE
    emp RECORD;
BEGIN
    FOR emp IN SELECT id FROM employees LOOP
        UPDATE employees 
        SET last_updated = NOW()
        WHERE id = emp.id;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 推荐的优化写法:批量更新
CREATE OR REPLACE PROCEDURE fast_update_employees()
AS $$
BEGIN
    UPDATE employees 
    SET last_updated = NOW()
    WHERE status = 'active';  -- 一次性更新所有符合条件的记录
END;
$$ LANGUAGE plpgsql;

四、最佳实践与避坑指南

在实际项目中,我总结了一些血泪教训,分享给大家。

4.1 错误处理规范

完善的错误处理能让你的存储过程更健壮:

-- 良好的错误处理示例
CREATE OR REPLACE PROCEDURE transfer_funds(
    IN from_account INT,
    IN to_account INT,
    IN amount NUMERIC,
    OUT status TEXT
)
AS $$
DECLARE
    current_balance NUMERIC;
BEGIN
    -- 验证参数
    IF amount <= 0 THEN
        RAISE EXCEPTION '转账金额必须大于0';
    END IF;
    
    -- 检查账户余额
    SELECT balance INTO current_balance
    FROM accounts
    WHERE account_id = from_account
    FOR UPDATE;  -- 加锁防止并发修改
    
    IF current_balance < amount THEN
        RAISE EXCEPTION '账户余额不足';
    END IF;
    
    -- 执行转账
    BEGIN
        -- 扣款
        UPDATE accounts
        SET balance = balance - amount
        WHERE account_id = from_account;
        
        -- 存款
        UPDATE accounts
        SET balance = balance + amount
        WHERE account_id = to_account;
        
        -- 记录交易
        INSERT INTO transactions(from_account, to_account, amount, trans_time)
        VALUES (from_account, to_account, amount, NOW());
        
        COMMIT;
        status := '转账成功';
    EXCEPTION
        WHEN OTHERS THEN
            ROLLBACK;
            status := '转账失败: ' || SQLERRM;
            -- 记录详细错误日志
            INSERT INTO error_logs(procedure_name, error_msg, error_time)
            VALUES ('transfer_funds', SQLERRM, NOW());
    END;
END;
$$ LANGUAGE plpgsql;

4.2 调试技巧

调试存储过程有时很头疼,试试这些方法:

-- 调试用的存储过程示例
CREATE OR REPLACE PROCEDURE complex_calculation(
    IN input_param INT,
    OUT result INT
)
AS $$
DECLARE
    debug_flag BOOLEAN := TRUE;  -- 调试开关
    temp_val1 INT;
    temp_val2 INT;
BEGIN
    -- 第一步计算
    SELECT some_value INTO temp_val1 FROM some_table WHERE id = input_param;
    IF debug_flag THEN
        RAISE NOTICE '第一步结果: %', temp_val1;
    END IF;
    
    -- 第二步计算
    temp_val2 := temp_val1 * 2 + 100;
    IF debug_flag THEN
        RAISE NOTICE '第二步结果: %', temp_val2;
    END IF;
    
    -- 复杂逻辑
    IF temp_val2 > 1000 THEN
        result := temp_val2 / 10;
    ELSE
        result := temp_val2 * 2;
    END IF;
    
    IF debug_flag THEN
        RAISE NOTICE '最终结果: %', result;
    END IF;
END;
$$ LANGUAGE plpgsql;

4.3 维护建议

长期维护的存储过程需要注意这些:

  1. 为每个存储过程添加清晰的注释头
  2. 使用一致的命名规范(如p_前缀表示参数,v_前缀表示变量)
  3. 避免在存储过程中写死业务逻辑数值
  4. 定期审查和重构复杂的存储过程
  5. 为关键存储过程编写单元测试
-- 良好注释的存储过程示例
/*
 * 功能: 计算员工年终奖金
 * 作者: 张三
 * 创建日期: 2023-01-15
 * 修改历史:
 *   2023-03-20 李四 - 增加绩效系数调整
 *   2023-06-10 王五 - 修复除零错误
 */
CREATE OR REPLACE PROCEDURE calculate_annual_bonus(
    IN p_emp_id INT,          -- 员工ID
    IN p_year INT,            -- 年度
    OUT p_bonus NUMERIC(10,2) -- 计算出的奖金
)
AS $$
DECLARE
    v_base_salary NUMERIC(10,2);  -- 基本工资
    v_performance_factor NUMERIC(5,2) := 1.0; -- 绩效系数
BEGIN
    -- 实现代码...
END;
$$ LANGUAGE plpgsql;

五、应用场景与技术选型

存储过程特别适合以下场景:

  1. 高频执行的复杂业务逻辑
  2. 需要保证数据完整性的多步操作
  3. 需要减少网络流量的批量操作
  4. 需要数据库层面封装的安全控制

openGauss存储过程的优势:

  1. 高性能:在数据库服务器端执行,减少网络开销
  2. 可维护性:业务逻辑集中管理
  3. 安全性:可以控制对基础表的直接访问
  4. 事务控制:复杂操作可以保证ACID特性

需要注意的局限性:

  1. 调试困难,不如应用代码方便
  2. 可能造成数据库服务器负载过高
  3. 版本控制和团队协作不如应用代码成熟
  4. 业务逻辑在数据库层可能导致应用层和数据库层耦合