使用 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;