使用 PL/pgSQL 开发 UDF

1. 功能概述

PL/pgSQL 是数据库内置的一个可加载的过程化编程语言。它专为 SQL 而设计,在其基础上添加了过程化编程的核心功能,如变量声明、流程控制和错误处理。

重要限制说明: 当前版本不支持任何形式的数据库表访问操作(包括但不限于 SELECT, INSERT, UPDATE, DELETE, SELECT...INTO 等)。PL/pgSQL 目前仅可用于进行纯粹的计算逻辑处理、流程控制和参数操作。

核心价值:

  • 逻辑封装: 将复杂的计算逻辑封装为可重用的代码单元。
  • 流程控制: 提供条件判断、循环等结构化编程能力。
  • 数据转换: 对输入参数进行计算和转换后返回结果。
  • 代码模块化: 提高代码的可维护性和重用性。

注:使用 PL/pgSQL 需要首先绑定 UDF Server,并保持 UDF Server 处于运行状态。

2. 核心特性与语法要点

2.1 变量声明 (DECLARE)

DECLARE
    -- 基本类型
    counter INTEGER := 0;
    total_amount NUMERIC(10, 2);
    user_name VARCHAR(50) := 'guest';
    is_valid BOOLEAN DEFAULT false;
    current_time TIMESTAMP := NOW();
 
    -- 数组类型
    scores INTEGER[] := ARRAY[90, 85, 78, 92];
    names TEXT[] := ARRAY['Alice', 'Bob', 'Charlie'];
 
    -- 记录类型
    user_record RECORD;
    complex_data JSONB;

2.2 丰富的流程控制示例

  • 复杂条件判断

    -- 多条件复合判断
    IF age >= 18 AND has_id = true AND (membership_level > 1 OR is_vip = true) THEN
        access_granted := true;
        welcome_message := '尊贵的会员,欢迎光临!';
    ELSIF age >= 18 AND has_id = true THEN
        access_granted := true;
        welcome_message := '欢迎光临!';
    ELSE
        access_granted := false;
        welcome_message := '抱歉,您不符合入场条件';
    END IF;
  • 数组循环处理

    -- 遍历数组并处理
    DECLARE
        numbers INTEGER[] := ARRAY[1, 2, 3, 4, 5];
        squared_numbers INTEGER[];
        total INTEGER := 0;
    BEGIN
        FOR i IN 1..array_length(numbers, 1) LOOP
            squared_numbers[i] := numbers[i] * numbers[i];
            total := total + numbers[i];
        END LOOP;
     
        -- 计算平均值
        avg_value := total / array_length(numbers, 1);
    END;
  • 嵌套循环示例

    -- 生成乘法表
    FOR i IN 1..9 LOOP
        FOR j IN 1..9 LOOP
            result := i * j;
            -- 可以存储到数组或直接处理
        END LOOP;
    END LOOP;

2.3 高级计算与数据处理

  • 数学计算函数

    CREATE OR REPLACE FUNCTION calculate_compound_interest(
        principal NUMERIC,
        rate NUMERIC,
        years INTEGER
    ) RETURNS NUMERIC AS $$
    DECLARE
        amount NUMERIC;
    BEGIN
        amount := principal * POWER(1 + rate/100, years);
        RETURN ROUND(amount, 2);
    END;
    $$ LANGUAGE plpgsql;
  • 字符串处理函数

    CREATE OR REPLACE FUNCTION format_phone_number(
        raw_phone TEXT
    ) RETURNS TEXT AS $$
    BEGIN
        -- 移除所有非数字字符
        raw_phone := REGEXP_REPLACE(raw_phone, '[^0-9]', '', 'g');
     
        -- 格式化为标准手机号格式
        IF LENGTH(raw_phone) = 11 THEN
            RETURN SUBSTRING(raw_phone FROM 1 FOR 3) || '-' ||
                   SUBSTRING(raw_phone FROM 4 FOR 4) || '-' ||
                   SUBSTRING(raw_phone FROM 8 FOR 4);
        ELSE
            RETURN raw_phone;
        END IF;
    END;
    $$ LANGUAGE plpgsql;
  • 日期时间处理

    CREATE OR REPLACE FUNCTION get_business_days(
        start_date DATE,
        end_date DATE
    ) RETURNS INTEGER AS $$
    DECLARE
        curr_date DATE;
        business_days INTEGER := 0;
    BEGIN
        curr_date := start_date;
     
        WHILE curr_date <= end_date LOOP
            -- 排除周末 (0=周日, 6=周六)
            IF EXTRACT(DOW FROM curr_date) NOT IN (0, 6) THEN
                business_days := business_days + 1;
            END IF;
            curr_date := curr_date + 1;
        END LOOP;
     
        RETURN business_days;
    END;
    $$ LANGUAGE plpgsql;

2.4 复杂数据验证

  • 多规则数据验证

    CREATE OR REPLACE FUNCTION validate_user_input(
        username TEXT,
        email TEXT,
        age INTEGER
    ) RETURNS JSONB AS $$
    DECLARE
        validation_result JSONB := '{"valid": true, "errors": []}';
        error_message TEXT;
    BEGIN
        -- 用户名验证
        IF LENGTH(username) < 3 THEN
            error_message := '用户名至少需要3个字符';
            validation_result := jsonb_set(validation_result, '{valid}', 'false');
            validation_result := jsonb_set(validation_result, '{errors}',
                (validation_result->'errors') || to_jsonb(error_message));
        END IF;
     
        -- 邮箱验证
        IF email !~ '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$' THEN
            error_message := '邮箱格式不正确';
            validation_result := jsonb_set(validation_result, '{valid}', 'false');
            validation_result := jsonb_set(validation_result, '{errors}',
                (validation_result->'errors') || to_jsonb(error_message));
        END IF;
     
        -- 年龄验证
        IF age < 18 OR age > 120 THEN
            error_message := '年龄必须在18-120岁之间';
            validation_result := jsonb_set(validation_result, '{valid}', 'false');
            validation_result := jsonb_set(validation_result, '{errors}',
                (validation_result->'errors') || to_jsonb(error_message));
        END IF;
     
        RETURN validation_result;
    END;
    $$ LANGUAGE plpgsql;

3. 实用示例集合

示例1:密码强度验证

CREATE OR REPLACE FUNCTION check_password_strength(
    password TEXT
) RETURNS TEXT AS $$
DECLARE
    strength_score INTEGER := 0;
BEGIN
    -- 长度检查
    IF LENGTH(password) >= 8 THEN strength_score := strength_score + 1; END IF;
    IF LENGTH(password) >= 12 THEN strength_score := strength_score + 1; END IF;
 
    -- 复杂度检查
    IF password ~ '[A-Z]' THEN strength_score := strength_score + 1; END IF; -- 大写字母
    IF password ~ '[a-z]' THEN strength_score := strength_score + 1; END IF; -- 小写字母
    IF password ~ '[0-9]' THEN strength_score := strength_score + 1; END IF; -- 数字
    IF password ~ '[^A-Za-z0-9]' THEN strength_score := strength_score + 1; END IF; -- 特殊字符
 
    -- 返回强度等级
    RETURN CASE strength_score
        WHEN 0 THEN '极弱'
        WHEN 1 THEN '弱'
        WHEN 2 THEN '中'
        WHEN 3 THEN '强'
        ELSE '极强'
    END;
END;
$$ LANGUAGE plpgsql;

示例2:数据加密函数

CREATE OR REPLACE FUNCTION simple_encrypt(
    input_text TEXT,
    shift_key INTEGER
) RETURNS TEXT AS $$
DECLARE
    encrypted_text TEXT := '';
    i INTEGER;
    char_code INTEGER;
BEGIN
    FOR i IN 1..LENGTH(input_text) LOOP
        char_code := ASCII(SUBSTRING(input_text FROM i FOR 1));
        -- 简单的凯撒密码加密
        encrypted_text := encrypted_text || CHR((char_code + shift_key) % 256);
    END LOOP;
 
    RETURN encrypted_text;
END;
$$ LANGUAGE plpgsql;

示例3:配置解析器

CREATE OR REPLACE FUNCTION parse_config(
    config_string TEXT
) RETURNS JSONB AS $$
DECLARE
    config_json JSONB := '{}';
    key_value_pair TEXT[];
    lines TEXT[];
    line TEXT;
BEGIN
    -- 按行分割配置字符串
    lines := STRING_TO_ARRAY(config_string, E'\n');
 
    FOREACH line IN ARRAY lines LOOP
        -- 解析键值对(忽略注释和空行)
        IF line != '' AND line NOT LIKE '#%' AND POSITION('=' IN line) > 0 THEN
            key_value_pair := STRING_TO_ARRAY(line, '=');
            config_json := jsonb_set(config_json, ARRAY[TRIM(key_value_pair[1])],
                to_jsonb(TRIM(key_value_pair[2])));
        END IF;
    END LOOP;
 
    RETURN config_json;
END;
$$ LANGUAGE plpgsql;

示例4:数学统计函数

CREATE OR REPLACE FUNCTION calculate_statistics(
    numbers NUMERIC[]
) RETURNS JSONB AS $$
DECLARE
    stats JSONB;
    total NUMERIC := 0;
    min_val NUMERIC;
    max_val NUMERIC;
    i INTEGER;
BEGIN
    IF array_length(numbers, 1) IS NULL THEN
        RETURN '{"count": 0, "error": "空数组"}'::JSONB;
    END IF;
 
    -- 初始化
    min_val := numbers[1];
    max_val := numbers[1];
 
    -- 计算统计量
    FOR i IN 1..array_length(numbers, 1) LOOP
        total := total + numbers[i];
        IF numbers[i] < min_val THEN min_val := numbers[i]; END IF;
        IF numbers[i] > max_val THEN max_val := numbers[i]; END IF;
    END LOOP;
 
    stats := jsonb_build_object(
        'count', array_length(numbers, 1),
        'sum', total,
        'average', total / array_length(numbers, 1),
        'min', min_val,
        'max', max_val,
        'range', max_val - min_val
    );
 
    RETURN stats;
END;
$$ LANGUAGE plpgsql;