7.0版PL/pgSQL函数

更新时间:
复制 MD 格式

PL/pgSQL(Procedural Language/PostgreSQL)是AnalyticDB PostgreSQL默认支持的过程语言。AnalyticDB PostgreSQLPL/pgSQL基于PostgreSQL 12实现。您可以使用PL/pgSQL创建用户自定义函数(UDF),实现复杂的数据处理和业务逻辑。

概述

PL/pgSQL是一种块结构的命令式语言,专为数据库编程设计。您可以使用PL/pgSQL实现以下功能:

  • 创建函数和存储过程。

  • 实现复杂的业务规则和数据校验逻辑。

  • 封装数据操作,简化应用代码。

  • 使用变量、条件判断、循环和异常处理等编程能力扩展SQL。

设计目标

目标

说明

易用性

语法类似于Oracle PL/SQL,便于学习和迁移。

安全性

在数据库服务器内执行,支持事务和权限控制。

高效性

直接访问数据库数据,避免网络往返开销。

可扩展性

支持所有PostgreSQL数据类型和操作符。

SQL的关系

PL/pgSQL是对SQL的扩展,而非替代品。AnalyticDB PostgreSQLPL/pgSQL基于PostgreSQL 12PL/pgSQL实现:

  • SQL语句可直接嵌入PL/pgSQL代码。

  • PL/pgSQL提供变量、控制结构、异常处理等编程能力。

  • 最佳实践:尽量使用SQL的集合操作,PL/pgSQL用于逻辑控制。

适用场景

场景

说明

数据验证

实现复杂的业务规则校验。

数据转换

ETL过程中自定义数据转换逻辑。

业务封装

将多步操作封装为函数,简化应用层调用。

游标处理

逐行处理大数据集。

使用限制

  • 不支持触发器(Triggers)。

  • 游标仅支持前向移动,不可滚动。

  • 不支持可更新游标(UPDATE...WHERE CURRENT OFDELETE...WHERE CURRENT OF)。

  • 不支持并行检索游标(DECLARE...PARALLEL RETRIEVE)。

  • EXECUTE语句中不支持SELECT INTO

语言结构

块结构

PL/pgSQL代码由块(Block)组成。每个块包含声明部分、语句部分和可选的异常处理部分。语法如下:

[ <<label>> ]
[ DECLARE
    -- 声明部分
    declarations ]
BEGIN
    -- 语句部分
    statements
[ EXCEPTION
    -- 异常处理部分
    WHEN condition THEN
        handler_statements ]
END [ label ];

示例:基本块结构

CREATE FUNCTION simple_example() RETURNS integer AS $$
DECLARE
    counter integer := 0;
BEGIN
    counter := counter + 1;
    RETURN counter;
END;
$$ LANGUAGE plpgsql;

示例:嵌套块

块可以嵌套,内层块可以访问外层块的变量。

CREATE FUNCTION nested_blocks() RETURNS void AS $$
<<outer_block>>
DECLARE
    outer_var integer := 1;
BEGIN
    RAISE NOTICE 'outer_var = %', outer_var;
    
    <<inner_block>>
    DECLARE
        inner_var integer := 2;
    BEGIN
        RAISE NOTICE 'inner_var = %', inner_var;
        -- 可以访问外层变量
        RAISE NOTICE 'outer_var from inner = %', outer_var;
    END inner_block;
    
END outer_block;
$$ LANGUAGE plpgsql;

标签(&lt;&lt;label&gt;&gt;)的用途:

  • EXITCONTINUE语句中指定退出的目标块或循环。

  • 区分同名变量,通过label.variable访问外层变量。

注释

PL/pgSQL支持两种注释风格:

-- 单行注释
x := 1;  -- 行末注释

/*
 * 多行注释
 * 可以跨越多行
 */
x := 1;

声明与数据类型

变量声明

DECLARE部分声明变量,语法如下:

name [ CONSTANT ] type [ NOT NULL ] [ { DEFAULT | := } expression ];

示例

DECLARE
    -- 基本声明
    user_id integer;
    -- 带默认值
    quantity integer := 10;
    
    -- 使用 DEFAULT 关键字
    tax_rate numeric(5,2) DEFAULT 0.08;
    
    -- 常量声明
    PI CONSTANT numeric := 3.14159265;
    
    -- NOT NULL 约束(必须提供默认值)
    counter integer NOT NULL := 0;
    
    -- 字符串变量
    greeting text := 'Hello, World!';

数据类型

PL/pgSQL支持所有PostgreSQL基础数据类型,同时提供以下专用类型:

  • %TYPE:引用列类型。使用%TYPE引用表列的数据类型,当表结构变更时函数自动适配。

    -- 引用表列的类型,保持类型同步
    DECLARE
        emp_name employees.name%TYPE;
        emp_salary employees.salary%TYPE;
  • %ROWTYPE:引用行类型。使用%ROWTYPE声明与表结构相同的记录变量。

    -- 声明与表结构相同的记录变量
    DECLARE
        emp_row employees%ROWTYPE;
    BEGIN
        SELECT * INTO emp_row FROM employees WHERE id = 1;
        RAISE NOTICE 'Employee: %, Salary: %', emp_row.name, emp_row.salary;
    END;
  • RECORD:动态记录类型。RECORD类型的变量在运行时确定结构,适用于动态查询场景。

    -- 动态结构的记录,在运行时确定结构
    DECLARE
        rec RECORD;
    BEGIN
        FOR rec IN SELECT id, name FROM employees LOOP
            RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name;
        END LOOP;
    END;
  • 数组类型:支持一维和多维数组。

    DECLARE
        -- 一维数组
        int_array integer[];
        str_array text[] := ARRAY['a', 'b', 'c'];
        -- 数组初始化
        numbers integer[] := '{1, 2, 3, 4, 5}';
    BEGIN
        -- 数组访问(索引从 1 开始)
        RAISE NOTICE 'First element: %', str_array[1];
        -- 数组追加
        int_array := int_array || 10;
        -- 数组长度
        RAISE NOTICE 'Array length: %', array_length(str_array, 1);
    END;
  • 复合类型:支持自定义结构体类型。

    -- 先创建类型
    CREATE TYPE person_type AS (
        name text,
        age integer,
        email text
    );
    
    -- 在函数中使用
    DECLARE
        person person_type;
    BEGIN
        person.name := 'John';
        person.age := 30;
        person.email := 'john@example.com';
    END;

函数参数

函数参数支持以下引用方式:

-- 方式1:使用 $n 位置参数
CREATE FUNCTION add_nums(integer, integer) RETURNS integer AS $$
BEGIN
    RETURN $1 + $2;
END;
$$ LANGUAGE plpgsql;

-- 方式2:使用命名参数(推荐)
CREATE FUNCTION add_nums(a integer, b integer) RETURNS integer AS $$
BEGIN
    RETURN a + b;
END;
$$ LANGUAGE plpgsql;

-- 方式3:使用 ALIAS FOR
CREATE FUNCTION add_nums(integer, integer) RETURNS integer AS $$
DECLARE
    first_num ALIAS FOR $1;
    second_num ALIAS FOR $2;
BEGIN
    RETURN first_num + second_num;
END;
$$ LANGUAGE plpgsql;

表达式与赋值

赋值

PL/pgSQL支持以下赋值方式:

  • := 赋值操作符:直接为变量赋值。

    DECLARE
        x integer;
        y integer;
        total integer;
    BEGIN
        x := 10;
        y := 20;
        total := x + y;
    END;
  • SELECT INTO赋值:通过SELECT INTO将查询结果赋值给变量。

    DECLARE
        emp_name text;
        emp_salary numeric;
        emp_count integer;
    BEGIN
        -- 单值赋值
        SELECT name INTO emp_name FROM employees WHERE id = 1;
        
        -- 多值赋值
        SELECT name, salary INTO emp_name, emp_salary 
        FROM employees WHERE id = 1;
        
        -- 聚合结果赋值
        SELECT count(*) INTO emp_count FROM employees;
        
        -- 严格模式:要求恰好返回一行,否则抛出异常
        SELECT name INTO STRICT emp_name FROM employees WHERE id = 1;
    END;
  • FOUND变量FOUND是一个布尔变量,用于判断上一条SQL语句是否影响了数据行。

    DECLARE
        emp_name text;
    BEGIN
        SELECT name INTO emp_name FROM employees WHERE id = 999;
        
        IF NOT FOUND THEN
            RAISE NOTICE 'Employee not found';
        ELSE
            RAISE NOTICE 'Found: %', emp_name;
        END IF;
    END;

表达式求值

PL/pgSQL中的表达式使用标准SQL表达式语法,支持数学运算、字符串操作、条件判断和SQL函数调用。

DECLARE
    result numeric;
    condition boolean;
BEGIN
    -- 数学表达式
    result := 10 + 20 * 3;
    
    -- 字符串表达式
    result := length('Hello') + length('World');
    
    -- 条件表达式
    condition := (result > 100) AND (result < 200);
    
    -- 使用SQL函数
    result := abs(-50) + round(3.7);
END;

表达式缓存(Plan Caching)

PL/pgSQL会自动缓存SQL语句的执行计划,提高重复执行的效率。但在以下情况需要注意:

-- 缓存的计划可能不适合参数值变化大的场景
CREATE FUNCTION get_employee(emp_id integer) RETURNS text AS $$
DECLARE
    result text;
BEGIN
    -- 此查询计划会被缓存(如果emp_id变化比较大则不太适合)
    SELECT name INTO result FROM employees WHERE id = emp_id;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

函数的波动性分类(IMMUTABLE/STABLE/VOLATILE)影响数据库缓存引用该函数的查询计划的方式。

  • IMMUTABLE:纯函数,相同输入永远返回相同输出,函数的结果可在规划时常量折叠。不依赖数据库内容、配置或时间。优化器可在规划时直接计算结果(常量折叠)。适用场景:数学计算、字符串格式化、固定规则转换。

    CREATE FUNCTION celsius_to_fahrenheit(c numeric) RETURNS numeric AS $$
    BEGIN
        RETURN c * 9.0 / 5.0 + 32;
    END;
    $$ LANGUAGE plpgsql IMMUTABLE;
  • STABLE:函数在单次表扫描中返回相同结果。可以读取数据库,但在同一条SQL执行期间结果不变。优化器可以在单次扫描中缓存结果,避免重复调用。可用于索引条件(Index ScanWHERE子句)。适用场景:读取配置表、基于当前事务快照的查询、时区转换。

    CREATE FUNCTION get_config_value(key text) RETURNS text AS $$
    DECLARE
        val text;
    BEGIN
        SELECT value INTO val FROM config_table WHERE config_key = key;
        RETURN val;
    END;
    $$ LANGUAGE plpgsql STABLE;
  • VOLATILE:函数每次调用可能返回不同结果,可以修改数据库状态(INSERT/UPDATE/DELETE)。优化器不会缓存或优化掉任何调用。不能用于索引条件。适用场景:修改数据、依赖随机数、依赖当前时间、有副作用。

    CREATE FUNCTION log_access(user_id integer) RETURNS void AS $$
    BEGIN
        INSERT INTO access_log VALUES (user_id, now());
    END;
    $$ LANGUAGE plpgsql VOLATILE;

类型转换

DECLARE
    str_val text := '123';
    int_val integer;
    float_val numeric;
BEGIN
    -- 显式类型转换
    int_val := str_val::integer;
    float_val := CAST(str_val AS numeric);
    
    -- 使用转换函数
    int_val := to_number(str_val, '999');
END;

基本语句

PERFORM

PERFORM用于执行查询但不关心返回结果,语法与SELECT相同,但使用PERFORM关键字替代SELECT

BEGIN
    -- 调用函数
    PERFORM my_procedure(10);
    
    -- 执行有副作用的操作
    PERFORM pg_sleep(1);
    
    -- 检查数据是否存在
    PERFORM 1 FROM employees WHERE id = 100;
    IF FOUND THEN
        RAISE NOTICE 'Employee exists';
    END IF;
END;
说明

PL/pgSQL中,直接使用SELECT语句(不带INTO子句)会导致语法错误。如果不需要查询结果,请使用PERFORM替代。

EXECUTE

EXECUTE用于执行动态构建的SQL语句。当表名、列名或查询条件需要在运行时确定时,使用EXECUTE

  • 使用USING传递参数:通过USING子句安全地传递参数值。

    DECLARE
        min_salary numeric := 50000;
        dept_id integer := 10;
        emp_count integer;
    BEGIN
        EXECUTE 'SELECT count(*) FROM employees 
                 WHERE salary > $1 AND department_id = $2'
            INTO emp_count
            USING min_salary, dept_id;
        
        RAISE NOTICE 'Count: %', emp_count;
    END;
重要

AnalyticDB PostgreSQL中,EXECUTE语句不支持SELECT INTO语法。请使用EXECUTE ... INTO ...语法替代。

  • 防止SQL注入:动态拼接SQL时,必须对用户输入进行转义处理。

    -- 错误示例:直接拼接用户输入,存在SQL注入风险
    EXECUTE 'SELECT * FROM ' || user_input;
    
    -- 正确示例:使用quote_ident处理标识符,quote_literal处理值
    EXECUTE 'SELECT * FROM ' || quote_ident(table_name) 
        || ' WHERE name = ' || quote_literal(user_name);
    
    -- 推荐:使用format函数(%I=标识符,%L=字面值)
    EXECUTE format('SELECT * FROM %I WHERE name = %L', 
                   table_name, user_name);
    
    -- 推荐:使用USING传递参数值
    EXECUTE 'SELECT * FROM employees WHERE name = $1'
        USING user_name;

RETURN

PL/pgSQL函数支持以下返回方式:

  • 返回单个值:使用RETURN返回标量值。

    CREATE FUNCTION get_sum(a integer, b integer) RETURNS integer AS $$
    BEGIN
        RETURN a + b;
    END;
    $$ LANGUAGE plpgsql;
  • RETURN NEXTRETURN QUERY:返回结果集。

    -- RETURN NEXT:逐行返回
    CREATE FUNCTION get_numbers(n integer) RETURNS SETOF integer AS $$
    DECLARE
        i integer;
    BEGIN
        FOR i IN 1..n LOOP
            RETURN NEXT i;
        END LOOP;
        RETURN;
    END;
    $$ LANGUAGE plpgsql;
    
    -- RETURN QUERY:返回查询结果集
    CREATE FUNCTION get_employees(dept_id integer) 
    RETURNS SETOF employees AS $$
    BEGIN
        RETURN QUERY SELECT * FROM employees WHERE department_id = dept_id;
        RETURN;
    END;
    $$ LANGUAGE plpgsql;
    
    -- RETURN QUERY EXECUTE:返回动态查询结果集
    CREATE FUNCTION get_by_table(tbl text) RETURNS SETOF RECORD AS $$
    BEGIN
        RETURN QUERY EXECUTE 'SELECT * FROM ' || quote_ident(tbl);
    END;
    $$ LANGUAGE plpgsql;
重要

RETURN NEXTRETURN QUERY会将结果缓冲在内存中(受work_mem限制,超出后写入磁盘)。处理大结果集时请关注内存消耗。

RAISE

RAISE用于输出调试信息或抛出异常。

  • 消息级别RAISE支持以下消息级别。

    级别

    说明

    DEBUG

    调试信息,默认不显示。

    LOG

    日志信息。

    INFO

    普通信息。

    NOTICE

    提示信息,默认显示。

    WARNING

    警告信息。

    EXCEPTION

    抛出异常,中断执行并回滚当前事务。

  • 格式化消息:使用%作为占位符,按顺序替换后续参数。

    DECLARE
        user_name text := 'John';
        user_age integer := 30;
    BEGIN
        RAISE NOTICE 'User % is % years old', user_name, user_age;
        -- 输出:NOTICE:  User John is 30 years old
        
        RAISE NOTICE 'Completion: %%100';  -- 使用%%输出百分号
    END;
  • 抛出自定义异常:使用USING子句指定错误详情。

    BEGIN
        RAISE EXCEPTION 'Invalid input'
            USING ERRCODE = 'invalid_parameter_value',
                  DETAIL = 'The value must be positive',
                  HINT = 'Provide a positive integer';
    END;

控制结构

条件语句

PL/pgSQL支持以下条件语句:

  • IF语句:支持IF...THEN...ELSIF...ELSE...END IF结构。

    DECLARE
        grade integer := 85;
        result text;
    BEGIN
        IF grade >= 90 THEN
            result := 'A';
        ELSIF grade >= 80 THEN
            result := 'B';
        ELSIF grade >= 70 THEN
            result := 'C';
        ELSIF grade >= 60 THEN
            result := 'D';
        ELSE
            result := 'F';
        END IF;
        
        RAISE NOTICE 'Grade: %', result;
    END;
  • CASE表达式:支持简单CASE和搜索CASE两种形式。

    -- 简单CASE
    DECLARE
        status integer := 1;
        status_text text;
    BEGIN
        status_text := CASE status
            WHEN 0 THEN 'Inactive'
            WHEN 1 THEN 'Active'
            WHEN 2 THEN 'Pending'
            ELSE 'Unknown'
        END;
    END;
    
    -- 搜索CASE
    DECLARE
        age integer := 25;
        category text;
    BEGIN
        category := CASE
            WHEN age < 13 THEN 'Child'
            WHEN age < 20 THEN 'Teenager'
            WHEN age < 60 THEN 'Adult'
            ELSE 'Senior'
        END;
    END;

循环语句

PL/pgSQL支持以下循环语句:

  • LOOP:无限循环,需配合EXIT退出。

    DECLARE
        counter integer := 0;
    BEGIN
        LOOP
            counter := counter + 1;
            RAISE NOTICE 'Counter: %', counter;
            EXIT WHEN counter >= 5;
        END LOOP;
    END;
  • WHILE循环:满足条件时持续循环。

    DECLARE
        counter integer := 1;
    BEGIN
        WHILE counter <= 5 LOOP
            RAISE NOTICE 'Counter: %', counter;
            counter := counter + 1;
        END LOOP;
    END;
  • FOR循环(整数范围):遍历整数区间,支持正向、反向和自定义步长。

    BEGIN
        -- 正向遍历
        FOR i IN 1..5 LOOP
            RAISE NOTICE 'i = %', i;
        END LOOP;
        
        -- 反向遍历
        FOR i IN REVERSE 5..1 LOOP
            RAISE NOTICE 'i = %', i;
        END LOOP;
        
        -- 指定步长
        FOR i IN 1..10 BY 2 LOOP
            RAISE NOTICE 'i = %', i;  -- 输出 1, 3, 5, 7, 9
        END LOOP;
    END;
  • FOR循环(查询结果):遍历查询返回的每一行。

    DECLARE
        rec RECORD;
    BEGIN
        FOR rec IN SELECT id, name FROM employees ORDER BY id LOOP
            RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name;
        END LOOP;
        
        -- 遍历动态查询结果
        FOR rec IN EXECUTE 'SELECT * FROM employees WHERE salary > $1'
                   USING 50000
        LOOP
            RAISE NOTICE 'High earner: %', rec.name;
        END LOOP;
    END;
  • FOREACH(遍历数组):遍历数组中的每个元素。

    DECLARE
        arr integer[] := ARRAY[1, 2, 3, 4, 5];
        elem integer;
    BEGIN
        FOREACH elem IN ARRAY arr LOOP
            RAISE NOTICE 'Element: %', elem;
        END LOOP;
    END;
  • EXITCONTINUEEXIT退出当前循环或指定标签的循环,CONTINUE跳过当前迭代继续下一次循环。

    BEGIN
        <<outer_loop>>
        FOR i IN 1..10 LOOP
            FOR j IN 1..10 LOOP
                IF j = 5 THEN
                    CONTINUE;           -- 跳过当前内层迭代
                END IF;
                IF i * j > 30 THEN
                    EXIT outer_loop;    -- 退出外层循环
                END IF;
                RAISE NOTICE '% * % = %', i, j, i * j;
            END LOOP;
        END LOOP outer_loop;
    END;

游标

游标用于逐行处理查询结果,适用于需要分步处理大数据集的场景。

声明游标

DECLARE
    -- 绑定游标:编译时绑定查询
    emp_cursor CURSOR FOR 
        SELECT id, name, salary FROM employees ORDER BY id;
    
    -- 未绑定游标
    ref_cursor refcursor;
    
    -- 参数化游标
    emp_cursor2 CURSOR (min_sal numeric) FOR
        SELECT * FROM employees WHERE salary >= min_sal;

操作游标

游标支持以下操作:

  • 打开游标:根据游标类型选择不同的打开方式。

    DECLARE
        emp_cursor CURSOR FOR SELECT * FROM employees;
        ref_cursor refcursor;
    BEGIN
        -- 打开绑定游标
        OPEN emp_cursor;
        
        -- 打开未绑定游标
        OPEN ref_cursor FOR SELECT * FROM departments;
        
        -- 打开参数化游标
        OPEN emp_cursor2(50000);
        
        -- 动态游标
        OPEN ref_cursor FOR EXECUTE 
            'SELECT * FROM ' || quote_ident(table_name);
    END;
  • FETCH:获取数据:从游标中逐行读取数据。

    DECLARE
        emp_cursor CURSOR FOR SELECT id, name FROM employees;
        emp_id integer;
        emp_name text;
    BEGIN
        OPEN emp_cursor;
        
        LOOP
            FETCH emp_cursor INTO emp_id, emp_name;
            EXIT WHEN NOT FOUND;
            RAISE NOTICE 'ID: %, Name: %', emp_id, emp_name;
        END LOOP;
        
        CLOSE emp_cursor;
    END;
  • FOR IN游标:自动管理游标生命周期,无需手动打开和关闭。

    DECLARE
        emp_cursor CURSOR FOR SELECT * FROM employees;
        rec RECORD;
    BEGIN
        FOR rec IN emp_cursor LOOP
            RAISE NOTICE 'Employee: %', rec.name;
        END LOOP;
        -- 游标自动打开和关闭,无需手动管理
    END;

游标与性能

  • 游标按需逐行获取数据,不会一次性加载全部结果到内存。

  • 事务期间游标保持打开状态会占用数据库资源。

  • 长时间运行的事务中,未关闭的游标可能阻止VACUUM清理过期数据。

错误处理

EXCEPTION

BEGIN...END块中使用EXCEPTION子句捕获和处理异常。

CREATE FUNCTION safe_divide(a numeric, b numeric) RETURNS numeric AS $$
DECLARE
    result numeric;
BEGIN
    BEGIN
        result := a / b;
    EXCEPTION
        WHEN division_by_zero THEN
            RAISE NOTICE 'Division by zero, returning NULL';
            result := NULL;
        WHEN numeric_value_out_of_range THEN
            RAISE NOTICE 'Result out of range';
            result := NULL;
    END;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

常用错误条件

条件名称

SQLSTATE

说明

division_by_zero

22012

除以零。

null_value_not_allowed

22004

空值不允许。

numeric_value_out_of_range

22003

数值超出范围。

string_data_right_truncation

22001

字符串截断。

unique_violation

23505

唯一约束冲突。

foreign_key_violation

23503

外键约束冲突。

check_violation

23514

CHECK约束冲突。

not_null_violation

23502

NOT NULL约束冲突。

no_data_found

P0002

SELECT INTO无数据。

too_many_rows

P0003

SELECT INTO返回多行。

OTHERS

-

匹配所有未明确捕获的异常。

获取错误信息

使用SQLERRMSQLSTATE变量获取错误详情。

BEGIN
    INSERT INTO unique_table VALUES (duplicate_key);
EXCEPTION
    WHEN OTHERS THEN
        RAISE NOTICE 'Error: % (SQLSTATE: %)', SQLERRM, SQLSTATE;
        IF SQLSTATE = '23505' THEN
            RAISE NOTICE 'Duplicate key error';
        END IF;
END;

多异常捕获

BEGIN
    -- 业务操作
EXCEPTION
    WHEN unique_violation THEN
        RAISE NOTICE 'Record already exists';
    WHEN foreign_key_violation THEN
        RAISE NOTICE 'Referenced record not found';
    WHEN check_violation OR not_null_violation THEN
        RAISE NOTICE 'Data validation failed';
    WHEN OTHERS THEN
        RAISE NOTICE 'Unexpected error: %', SQLERRM;
        RAISE;  -- 重新抛出异常
END;

EXCEPTION块的子事务机制

每次进入包含EXCEPTION子句的BEGIN块时,PL/pgSQL都会创建一个子事务(Subtransaction)。

重要

子事务的创建与函数的波动性分类(IMMUTABLE/STABLE/VOLATILE)无关。即使将函数声明为IMMUTABLEEXCEPTION块仍然会创建子事务。在高频调用场景中,子事务会消耗大量vmem资源并显著降低执行性能。

性能优化

计划缓存

PL/pgSQL会自动缓存SQL语句的执行计划。首次执行时生成计划并缓存,后续调用直接复用缓存的计划。

CREATE FUNCTION get_employee(emp_id integer) RETURNS text AS $$
DECLARE
    emp_name text;
BEGIN
    -- 此计划被缓存,后续调用直接使用
    SELECT name INTO emp_name FROM employees WHERE id = emp_id;
    RETURN emp_name;
END;
$$ LANGUAGE plpgsql;

当参数值分布不均匀时,缓存的通用计划可能不是最优的。此时可使用EXECUTE强制每次重新生成计划。

CREATE FUNCTION search_employees(status text) RETURNS SETOF employees AS $$
BEGIN
    -- status='active' 占 99%,status='inactive' 占 1%
    -- 使用 EXECUTE 让每次都重新规划
    RETURN QUERY EXECUTE 
        'SELECT * FROM employees WHERE status = $1'
        USING status;
END;
$$ LANGUAGE plpgsql;

方式

是否缓存计划

适用场景

静态SQL

参数值分布均匀的场景。

EXECUTE + USING

参数值分布不均匀的场景。

EXECUTE拼接

动态表名或列名的场景。

最小化包含EXCEPTION函数调用次数

谓词中包含EXCEPTION块的PL/pgSQL函数不建议放在大量行运行场景:每个子事务的内存上下文都计入vmem使用量。高频调用EXCEPTION块函数会快速消耗vmem配额,而且会极大的拖慢执行性能。

ORCA优化器可能不会重排WHERE子句中的谓词,因此书写时注意对应顺序:

show optimizer;
-- explain 查看对应过滤谓词的执行顺序
explain SELECT * FROM large_table
WHERE cheap_condition
  AND expensive_plpgsql_function(column);
-- 错误示例:对所有行调用
SELECT * FROM million_rows WHERE plpgsql_func(col);

内存管理

RETURN NEXTRETURN QUERY的结果集缓冲在内存中,超出work_mem限制后写入磁盘。对于大结果集,优先使用RETURN QUERY

-- 推荐:使用RETURN QUERY返回大结果集
CREATE FUNCTION generate_rows() RETURNS SETOF integer AS $$
BEGIN
    RETURN QUERY SELECT generate_series(1, 1000000);
END;
$$ LANGUAGE plpgsql;

大结果集处理建议:使用游标分批处理。

CREATE FUNCTION process_large_result() RETURNS void AS $$
DECLARE
    cur CURSOR FOR SELECT * FROM large_table;
    rec RECORD;
    batch_size integer := 1000;
    counter integer := 0;
BEGIN
    FOR rec IN cur LOOP
        -- 处理单行
        PERFORM process_row(rec.id);
        
        counter := counter + 1;
        IF counter % batch_size = 0 THEN
            -- 定期释放资源
            RAISE NOTICE 'Processed % rows', counter;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

批量操作

减少PL/pgSQL循环,优先使用SQL集合操作。

-- 错误示例:逐行更新(慢)
CREATE FUNCTION update_salaries_slow() RETURNS void AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT id, salary FROM employees LOOP
        UPDATE employees SET salary = rec.salary * 1.1 WHERE id = rec.id;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 正确示例:集合操作(快)
CREATE FUNCTION update_salaries_fast() RETURNS void AS $$
BEGIN
    UPDATE employees SET salary = salary * 1.1;
END;
$$ LANGUAGE plpgsql;

-- 或者直接使用SQL,无需PL/pgSQL
UPDATE employees SET salary = salary * 1.1;

使用PERFORM执行批量DML:

BEGIN
    -- 批量删除
    PERFORM 1 FROM delete_old_records();
    
    -- 批量插入
    PERFORM insert_batch_data(source_table, target_table);
END;

避免逐行处理的替代方案:

-- 错误示例:逐行插入
FOR i IN 1..1000 LOOP
    INSERT INTO target VALUES (i, 'value' || i);
END LOOP;

-- 正确示例:批量插入
INSERT INTO target 
SELECT i, 'value' || i FROM generate_series(1, 1000) i;

-- 或使用COPY
COPY target FROM '/path/to/data.csv';

调试与诊断

使用RAISE NOTICE调试

CREATE FUNCTION debug_example(input integer) RETURNS integer AS $$
DECLARE
    step1_result integer;
    step2_result integer;
BEGIN
    RAISE NOTICE 'Function called with input: %', input;
    
    step1_result := input * 2;
    RAISE NOTICE 'After step 1: %', step1_result;
    
    step2_result := step1_result + 10;
    RAISE NOTICE 'After step 2: %', step2_result;
    
    RETURN step2_result;
END;
$$ LANGUAGE plpgsql;

变量名冲突处理

PL/pgSQL变量名与SQL列名相同时,可通过plpgsql.variable_conflict参数控制优先级。

SET plpgsql.variable_conflict = 'error';         -- 报错(默认)
SET plpgsql.variable_conflict = 'use_variable';  -- 优先使用变量
SET plpgsql.variable_conflict = 'use_column';    -- 优先使用列名

日志级别配置

SET log_min_messages = 'debug1';       -- 设置服务端日志级别
SET client_min_messages = 'notice';    -- 设置客户端消息级别

常见问题

  • 函数执行效率低的常见原因

    原因

    解决方案

    EXCEPTION块子事务累积

    减少包含EXCEPTION块的函数在高频场景中的调用次数。

    RETURN NEXT结果集过大

    改用RETURN QUERY,或使用游标分批处理。

    游标未关闭

    长时间持有大量游标。

    逐行处理数据

    改用SQL集合操作(UPDATE、INSERT...SELECT)。

    变量持有大数据

    避免在变量中存储大型JSONTEXT数据。

  • IMMUTABLE/STABLE能否避免EXCEPTION块的子事务?

    不能。函数的波动性分类仅影响优化器行为,不影响EXCEPTION块的子事务创建。以下函数仍然会为每次调用创建子事务:

    CREATE FUNCTION my_func(text) RETURNS boolean AS $$
    BEGIN
        PERFORM $1::json;
        RETURN true;
    EXCEPTION
        WHEN OTHERS THEN RETURN false;
    END;
    $$ LANGUAGE plpgsql IMMUTABLE;  -- IMMUTABLE不能避免子事务
  • PL/pgSQL、PL/PythonC函数如何选择?

    场景

    推荐

    原因

    简单数据处理

    PL/pgSQL

    语法简单,与SQL集成好。

    复杂字符串或JSON处理

    PL/Python

    Python库丰富。

    高频调用(百万次级)

    C函数

    无解释器开销。

    需要错误处理且高频调用

    C函数

    无子事务开销。

    需要网络或文件IO

    PL/Python

    支持系统库调用。

    数学或科学计算

    PL/Python + NumPy

    专业计算库支持。

    性能极致要求

    C函数

    最高性能。