变量声明与赋值
DECLARE 段是 PL/pgSQL 的「数据准备区」:所有变量、常量、游标都在这里登记,进入块时按声明顺序初始化。本节讲透变量声明语法(含 CONSTANT、NOT NULL、COLLATE、ALIAS FOR)、内置类型与 %TYPE / %ROWTYPE 派生类型、三种赋值方式、作用域与遮蔽、保留字冲突的处理、SELECT INTO 的陷阱,以及 FOUND 与 GET DIAGNOSTICS 的用法。
前置知识
阅读本节前,建议先了解:函数结构(DECLARE / BEGIN / END)
基础概念
变量(Variable)是块内为某个数据起的名字,它有类型、可选默认值、可选约束(CONSTANT、NOT NULL),并在块的作用域内可见。
| 概念 | 术语 | 说明 |
|---|---|---|
| 变量 | Variable | DECLARE 中声明的具名存储位置 |
| 常量 | Constant | 带 CONSTANT 的声明,赋值一次后不可改 |
| 类型 | Type | 变量的数据类型,可以是内置类型、复合类型或派生类型 |
| 默认值 | Default / Initial Value | DEFAULT、:= 或 = 后跟的表达式 |
| 派生类型 | Derived Type | %TYPE、%ROWTYPE 从列或表推导出的类型 |
| 别名 | Alias | ALIAS FOR 为旧名字或 $n 参数建立新名字 |
| 作用域 | Scope | 变量可见的代码范围,块是作用域边界 |
| 遮蔽 | Shadowing | 内层同名声明覆盖外层声明 |
| 赋值 | Assignment | := 或 =,把表达式的值写入变量 |
| 结果诊断 | Diagnostics | GET DIAGNOSTICS 读取上条语句的统计信息 |
声明段的整体位置
CREATE FUNCTION f(p_x integer) RETURNS integer LANGUAGE plpgsql AS $$
DECLARE <- 声明段开始
v_a integer := 0; <- 普通变量
c_pi constant numeric := 3.14; <- 常量
v_t demo_t.col%TYPE; <- 派生类型
v_r demo_t%ROWTYPE; <- 行类型
cur CURSOR FOR SELECT ...; <- 游标
BEGIN <- 声明段结束,语句段开始
...
END;
$$;变量声明语法全景
名称 [ CONSTANT ] 类型 [ COLLATE 排序规则 ] [ NOT NULL ] [ { DEFAULT | := | = } 表达式 ];
名称 ALIAS FOR 源名称; -- 别名声明
名称 类型; -- 无默认值,初始为 NULL| 子句 | 是否必需 | 作用 | 注意事项 |
|---|---|---|---|
| 名称 | 必需 | 变量名 | 与列名冲突时默认以变量为准 |
CONSTANT | 可选 | 禁止后续赋值 | 必须有默认值,否则编译报错 |
| 类型 | 必需 | 数据类型 | 可用 %TYPE / %ROWTYPE 派生 |
COLLATE | 可选 | 指定排序规则 | 仅对可排序类型有意义 |
NOT NULL | 可选 | 禁止赋 NULL | 必须有非 NULL 默认值 |
DEFAULT / := / = | 可选 | 初始值 | 三者等价;每次进入块都重新求值 |
三种赋值方式
| 写法 | 说明 | 推荐度 |
|---|---|---|
v := 表达式; | PL/pgSQL 原生赋值 | 推荐 |
v = 表达式; | PostgreSQL 9.4+ 支持,与 := 等价 | 可用(在声明中 = 也合法) |
SELECT ... INTO v FROM ...; | 把查询结果赋给变量 | 推荐(赋值来自查询时) |
CREATE OR REPLACE FUNCTION demo_assign_forms()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_a integer := 1; -- 声明时用 :=
v_b integer = 2; -- 声明时用 =(等价)
v_c integer DEFAULT 3; -- 声明时用 DEFAULT(等价)
v_d integer;
BEGIN
v_d := v_a + v_b + v_c; -- 语句中用 :=
RETURN format('a=%s b=%s c=%s d=%s', v_a, v_b, v_c, v_d);
END;
$$;
SELECT demo_assign_forms() AS result; result
------------------
a=1 b=2 c=3 d=6
(1 row)语法与用法
变量声明的完整语法原型:
名称 [ CONSTANT ] 类型 [ COLLATE 排序规则 ] [ NOT NULL ] [ { DEFAULT | := | = } 表达式 ];派生类型与别名的语法:
变量 表名.列名%TYPE; -- 与某列同类型
变量 变量名%TYPE; -- 与另一变量同类型(含其类型修饰)
变量 表名%ROWTYPE; -- 表整行的复合类型
变量 复合类型名%ROWTYPE; -- 复合类型的整行类型
别名 ALIAS FOR 源名称; -- 为 $n 或旧名建立别名最小可运行示例——覆盖普通变量、常量、NOT NULL 与 %TYPE:
CREATE TABLE IF NOT EXISTS demo_stock(
id bigserial PRIMARY KEY,
sku varchar(32) NOT NULL,
qty integer NOT NULL DEFAULT 0
);
CREATE OR REPLACE FUNCTION demo_var_basic(p_sku varchar)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
c_tax_rate constant numeric(5,4) := 0.1300; -- 常量
v_sku demo_stock.sku%TYPE; -- 派生自列类型 varchar(32)
v_qty integer NOT NULL DEFAULT 0; -- 非空 + 默认值
v_comment text COLLATE "C" := 'ok'; -- 指定排序规则
BEGIN
v_sku := p_sku;
SELECT qty INTO v_qty FROM demo_stock WHERE sku = v_sku;
IF NOT FOUND THEN
v_qty := 0;
END IF;
RETURN format('%s qty=%s tax=%s comment=%s', v_sku, v_qty, c_tax_rate, v_comment);
END;
$$;
SELECT demo_var_basic('SKU-001') AS info; info
-----------------------------------------------------
SKU-001 qty=0 tax=0.1300 comment=ok
(1 row)详细说明
DECLARE 语法逐项拆解
DECLARE 关键字只出现一次,其后可以写任意多条声明,每条以分号结束。
| 声明形态 | 示例 | 说明 |
|---|---|---|
| 无默认值 | v_cnt integer; | 初始值为 NULL |
| 带默认值 | v_cnt integer := 0; | 进入块时求值 |
| 常量 | c_max constant integer := 100; | 必须带默认值 |
| 非空 | v_x integer NOT NULL := 0; | 必须带非 NULL 默认值 |
| 排序规则 | v_s text COLLATE "zh_CN" := ''; | 影响比较与排序 |
| 数组 | v_arr integer[] := '{}'; | 下标从 1 开始 |
| 复合类型 | v_r demo_stock%ROWTYPE; | 整行结构 |
| 派生类型 | v_sku demo_stock.sku%TYPE; | 跟随列类型变化 |
| 游标 | cur CURSOR FOR SELECT ...; | 需要显式 OPEN / FETCH |
| 别名 | v_old ALIAS FOR $1; | 给位置参数起名 |
每次进入块时,默认值表达式都会重新求值一次,而不是只在函数首次执行时求值一次。这一点在循环体内声明变量时影响很大:
DO $$
DECLARE
i integer;
BEGIN
FOR i IN 1 .. 3 LOOP
DECLARE
v_salt text := md5(random()::text); -- 每轮都是新值
BEGIN
RAISE NOTICE 'round %: %', i, left(v_salt, 8);
END;
END LOOP;
END;
$$;NOTICE: round 1: 3f2a91c7
NOTICE: round 2: a71bd0e4
NOTICE: round 3: 9c02f8b1
DO内置类型与 %TYPE
变量类型可以直接写内置类型,也可以用 %TYPE 从列或另一个变量推导。
| 写法 | 推导来源 | 好处 |
|---|---|---|
integer | 直接指定 | 明确、简单 |
demo_stock.sku%TYPE | 表列的类型 | 列类型变更时函数自动跟随 |
v_other%TYPE | 另一个变量的类型(含修饰) | 避免重复书写复杂类型 |
demo_stock.qty%TYPE | 列类型(含 NOT NULL?否) | 只取类型,不取约束 |
CREATE OR REPLACE FUNCTION demo_typeof(p_sku varchar)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_sku demo_stock.sku%TYPE; -- 等价于 varchar(32)
v_qty demo_stock.qty%TYPE; -- 等价于 integer
v_copy v_sku%TYPE; -- 派生自另一个变量
BEGIN
v_sku := p_sku;
v_copy := v_sku;
v_qty := 5;
RETURN format('v_sku=%s v_copy=%s v_qty=%s', v_sku, v_copy, v_qty);
END;
$$;
SELECT demo_typeof('SKU-9') AS result; result
----------------------------
v_sku=SKU-9 v_copy=SKU-9 v_qty=5
(1 row)%TYPE 只复制类型,不复制列的 NOT NULL 约束与默认值。若需要非空语义,得自己加 NOT NULL。
作用域与遮蔽
块是作用域边界。变量在声明它的块及其所有内层块可见;内层块退出后,内层变量立即失效。
| 规则 | 说明 |
|---|---|
| 内层可读外层 | 内层块能访问外层块与函数参数 |
| 内层可写外层 | 内层块能修改外层变量(除非是 CONSTANT) |
| 外层不可见内层 | 内层块结束后,其变量不可再引用 |
| 同名遮蔽 | 内层同名声明遮蔽外层,外层需用 外层标签.名字 访问 |
| 参数也在作用域内 | 函数参数相当于最外层块的变量 |
CREATE OR REPLACE FUNCTION demo_scope_write()
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_total integer := 0;
BEGIN
DECLARE
v_i integer;
BEGIN
FOR v_i IN 1 .. 5 LOOP
v_total := v_total + v_i; -- 内层块修改外层变量,合法
END LOOP;
END;
-- v_i 在这里已经不可见
RETURN v_total;
END;
$$;
SELECT demo_scope_write() AS sum_1_to_5; sum_1_to_5
------------
15
(1 row)保留字冲突与 variable_conflict
当变量名与列名同名时,PL/pgSQL 默认优先解释为变量,但这是一个容易出错的灰色地带。可选的处理方式:
| 方式 | 效果 | 推荐度 |
|---|---|---|
命名前缀(p_ / v_) | 从源头消除冲突 | 强烈推荐 |
限定列名(表别名.列名) | 明确指定为列 | 推荐 |
#variable_conflict use_column | 全局改为优先列名 | 不推荐(易失控) |
#variable_conflict use_variable | 全局改为优先变量 | 不推荐 |
#variable_conflict error | 冲突时报错 | 局部可用 |
CREATE OR REPLACE FUNCTION demo_fixed_conflict(p_name text)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_name text;
BEGIN
-- 推荐:参数加 p_ 前缀、变量加 v_ 前缀,再用表别名限定列,几乎不会冲突
SELECT e.name INTO v_name FROM demo_emp AS e WHERE e.name = p_name LIMIT 1;
RETURN coalesce(v_name, 'not found');
END;
$$;若确实需要在不改名的情况下让解析器明确按「列」处理,可在块内使用编译指令:
CREATE OR REPLACE FUNCTION demo_use_column(name text)
RETURNS text
LANGUAGE plpgsql
AS $$
#variable_conflict use_column
DECLARE
v_result text;
BEGIN
-- 此处的 name 被解释为表列 demo_emp.name,参数 name 在查询里无法引用
SELECT e.name INTO v_result
FROM demo_emp AS e
WHERE name = name -- 两侧都是列,条件恒真,仅用于演示名称解析
LIMIT 1;
RETURN v_result;
END;
$$;#variable_conflict 是 PL/pgSQL 的编译指令,必须独占一行,且只能放在函数体开头。
CONSTANT 与 NOT NULL
CONSTANT 让变量在初始化后不可再赋值;NOT NULL 让变量不可被赋为 NULL。两者都有「必须提供初值」的前置要求。
-- 错误!CONSTANT 变量必须有默认值
CREATE FUNCTION demo_bad_const() RETURNS void LANGUAGE plpgsql AS $$
DECLARE
c_max integer CONSTANT; -- 没有初值
BEGIN
RAISE NOTICE '%', c_max;
END;
$$;ERROR: constant "c_max" must have a default value-- 错误!NOT NULL 变量必须给非 NULL 默认值
CREATE FUNCTION demo_bad_notnull() RETURNS void LANGUAGE plpgsql AS $$
DECLARE
v_x integer NOT NULL; -- 无默认值等同于默认 NULL
BEGIN
v_x := 1;
END;
$$;ERROR: variable "v_x" must have a default value-- 推荐:两者都带合适的初值
CREATE OR REPLACE FUNCTION demo_const_notnull()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
c_start constant date := DATE '2026-01-01';
c_limit constant integer := 100;
v_count integer NOT NULL DEFAULT 0;
BEGIN
v_count := v_count + 1;
-- 错误!常量不可再次赋值(下面这行会抛错,示例中注释掉)
-- c_limit := 200; -- ERROR: assignment to constant variable
RETURN format('start=%s limit=%s count=%s', c_start, c_limit, v_count);
END;
$$;| 约束 | 报错时机 | 报错信息 |
|---|---|---|
CONSTANT 无默认值 | 编译期 | constant "x" must have a default value |
CONSTANT 后再次赋值 | 运行期 | assignment to constant variable |
NOT NULL 无(非空)默认值 | 编译期 | variable "x" must have a default value |
NOT NULL 变量被赋 NULL | 运行期 | null value cannot be assigned to variable "x" |
赋值语句与 SELECT INTO
| 赋值形式 | 语法 | 语义 |
|---|---|---|
| 简单赋值 | v := 表达式; | 计算表达式并写入 |
| 字段赋值 | v.字段 := 表达式; / v[i] := 表达式; | 修改复合类型字段或数组元素 |
| 查询赋值 | SELECT 列表 INTO 变量 FROM ...; | 取一行写入变量 |
| 严格查询赋值 | SELECT 列表 INTO STRICT 变量 FROM ...; | 必须恰好一行,否则报错 |
| 忽略结果 | PERFORM 查询; | 执行查询但丢弃返回值 |
SELECT INTO 的三种行为差异,是 plpgsql 最需要警惕的地方:
| 变体 | 返回 0 行 | 返回 1 行 | 返回多行 |
|---|---|---|---|
SELECT ... INTO v | 变量置为 NULL | 赋该行 | 取任意一行,其余丢弃 |
SELECT ... INTO STRICT v | 报 NO_DATA_FOUND | 赋该行 | 报 TOO_MANY_ROWS |
CREATE TABLE IF NOT EXISTS demo_emp(id int PRIMARY KEY, name text);
INSERT INTO demo_emp VALUES (1, 'Alice'), (2, 'Bob');
CREATE OR REPLACE FUNCTION demo_into_variants(p_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_name text;
v_rows integer;
BEGIN
-- 非 STRICT:0 行时 v_name 为 NULL,不报错
SELECT name INTO v_name FROM demo_emp WHERE id = p_id;
GET DIAGNOSTICS v_rows = ROW_COUNT; -- 取上条语句影响/返回的行数
IF v_name IS NULL THEN
RETURN format('id=%s not found (rows=%s, FOUND=%s)', p_id, v_rows, FOUND);
END IF;
RETURN format('id=%s name=%s (rows=%s, FOUND=%s)', p_id, v_name, v_rows, FOUND);
END;
$$;
SELECT demo_into_variants(1) AS r1, demo_into_variants(99) AS r2; r1 | r2
------------------------------+-----------------------------------
id=1 name=Alice (rows=1, FOUND=t) | id=99 not found (rows=0, FOUND=f)
(1 row)SELECT INTO STRICT 版本:
CREATE OR REPLACE FUNCTION demo_into_strict(p_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_name text;
BEGIN
SELECT name INTO STRICT v_name FROM demo_emp WHERE id = p_id;
RETURN v_name;
END;
$$;
-- 恰好一行:正常返回
SELECT demo_into_strict(2) AS ok; ok
------
Bob
(1 row)-- 0 行:抛 NO_DATA_FOUND
SELECT demo_into_strict(99);ERROR: query returned no rows
CONTEXT: PL/pgSQL function demo_into_strict(integer) line 5 at SELECT INTO variables-- 多行:抛 TOO_MANY_ROWS
SELECT demo_into_strict(NULL); -- 条件恒不成立,用于演示另一种写法使用 STRICT 时可以用 EXCEPTION 捕获:
CREATE OR REPLACE FUNCTION demo_into_safe(p_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_name text;
BEGIN
SELECT name INTO STRICT v_name FROM demo_emp WHERE id = p_id;
RETURN v_name;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 'no row for id=' || p_id;
WHEN TOO_MANY_ROWS THEN
RETURN 'too many rows for id=' || p_id;
END;
$$;
SELECT demo_into_safe(1) AS a, demo_into_safe(99) AS b; a | b
-------+-----------------
Alice | no row for id=99
(1 row)注意 NO_DATA_FOUND 与 TOO_MANY_ROWS 是 PL/pgSQL 的条件名(Condition Name),对应的 SQLSTATE 分别是 P0002 与 P0003。这两条只在 STRICT 或 INTO 场景下才会出现。
FOUND 与 GET DIAGNOSTICS
FOUND 是一个布尔型特殊变量,GET DIAGNOSTICS 则能取到更细的统计量。
| 手段 | 类型 | 含义 | 何时被设置 |
|---|---|---|---|
FOUND | boolean | 上条语句是否找到行 | SELECT INTO / PERFORM / UPDATE / INSERT / DELETE / FETCH / FOR 循环 |
ROW_COUNT | bigint | 上条语句影响的行数 | INSERT / UPDATE / DELETE / SELECT / FETCH / MOVE / COPY |
PG_ROUTINE_OID | oid | 当前函数自身的 OID | 任何时刻 |
RETURNED_SQLSTATE | text | 异常发生时的 SQLSTATE | 仅在 EXCEPTION 处理块内有效 |
FOUND 的行为按语句类型而异,这是最容易踩坑的一点:
| 语句 | 0 行时 | 有行时 |
|---|---|---|
SELECT INTO | FOUND = false | FOUND = true |
PERFORM | FOUND = false | FOUND = true |
UPDATE / DELETE | FOUND = false | FOUND = true |
INSERT | 恒为 true(除非 RETURNING 无行) | true |
FETCH | FOUND = false | FOUND = true |
FOR 循环 | 循环结束后 FOUND = false | 循环体内为 true |
EXECUTE | 不改变 FOUND | 不改变 FOUND |
CREATE OR REPLACE FUNCTION demo_diag_report(p_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_name text;
v_rows bigint;
BEGIN
UPDATE demo_emp SET name = name WHERE id = p_id; -- 只改动自身,行数仍计数
GET DIAGNOSTICS v_rows = ROW_COUNT;
RAISE NOTICE 'UPDATE 影响行数 = %,FOUND = %', v_rows, FOUND;
SELECT name INTO v_name FROM demo_emp WHERE id = p_id;
RAISE NOTICE 'SELECT INTO 后 FOUND = %', FOUND;
RETURN coalesce(v_name, '<null>');
END;
$$;
SELECT demo_diag_report(1) AS result;NOTICE: UPDATE 影响行数 = 1,FOUND = t
NOTICE: SELECT INTO 后 FOUND = t
result
--------
Alice
(1 row)GET DIAGNOSTICS 支持的诊断项(常用部分):
| 诊断项 | 类型 | 说明 |
|---|---|---|
ROW_COUNT | bigint | 上条 SQL 处理的行数 |
PG_ROUTINE_OID | oid | 当前函数 OID |
PG_CONTEXT | text | 当前调用栈文本,用于日志定位 |
RETURNED_SQLSTATE | text | 异常的 SQLSTATE,仅在异常处理块内 |
COLUMN_NAME / CONSTRAINT_NAME 等 | text | 异常的详细信息,需在异常处理块内 |
-- 用一个异常处理器时,可以用 GET DIAGNOSTICS 拿到精确的 SQLSTATE 与上下文
CREATE OR REPLACE FUNCTION demo_diag_context()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_state text;
v_message text;
BEGIN
BEGIN
PERFORM 1 / 0; -- 制造除零错误
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
v_state = RETURNED_SQLSTATE,
v_message = MESSAGE_TEXT;
RETURN format('SQLSTATE=%s MESSAGE=%s', v_state, v_message);
END;
END;
$$;
SELECT demo_diag_context() AS err; err
--------------------------------------------------
SQLSTATE=22012 MESSAGE=division by zero
(1 row)注意:在 EXCEPTION 处理块内取异常诊断项,必须用 GET STACKED DIAGNOSTICS;GET DIAGNOSTICS 只能取当前活跃语句的统计项。
默认值求值时机与常见陷阱
默认值在每次进入块时求值,这一行为带来两个实用结论:
| 结论 | 说明 |
|---|---|
| 循环体内声明会反复求值 | 想「只求一次」的值必须放在循环外 |
| 默认值可以引用前面的变量与参数 | 但不能引用后面声明的变量(尚未求值) |
-- 推荐:需要「每次调用求值一次」的值放在最外层块
CREATE OR REPLACE FUNCTION demo_eval_once()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_ts timestamptz := clock_timestamp(); -- 函数开始执行时求值一次
i integer;
v_out text := '';
BEGIN
FOR i IN 1 .. 3 LOOP
v_out := v_out || to_char(v_ts, 'MS') || ' ';
PERFORM pg_sleep(0.02);
END LOOP;
RETURN v_out;
END;
$$;
SELECT demo_eval_once() AS same_ms; same_ms
-------------
512 512 512
(1 row)三次打印的时间戳完全相同,因为 v_ts 只在函数开始时求值一次——这与前一节示例中「内层块每轮重新求值」形成鲜明对比。
常见陷阱清单:
| 陷阱 | 错误写法 | 正确写法 |
|---|---|---|
| 默认值引用后声明的变量 | v_a int := v_b; v_b int := 1; | 调整声明顺序 |
NOT NULL 无默认值 | v_x int NOT NULL; | v_x int NOT NULL DEFAULT 0; |
| 未初始化变量参与运算 | v_sum := v_sum + 1; | v_sum integer := 0; |
SELECT INTO 多行不报错 | SELECT ... INTO v | 需要唯一性时用 INTO STRICT |
| 空数组长度判断 | array_length(v,1) = 0 | cardinality(v) = 0 |
忘记 FOUND 被覆盖 | 连续两条语句后用 FOUND | 紧邻语句之后立即使用 |
实战示例
示例一:用 %TYPE / %ROWTYPE 写一个类型安全的自增编号生成器
需求:从编号表中取当前最大值并加一,返回新编号。列类型变化时函数应自动跟随。
CREATE TABLE IF NOT EXISTS demo_code_seq(
prefix varchar(8) NOT NULL,
seq integer NOT NULL DEFAULT 0,
PRIMARY KEY (prefix)
);
INSERT INTO demo_code_seq(prefix, seq) VALUES ('SKU', 100)
ON CONFLICT (prefix) DO NOTHING;
CREATE OR REPLACE FUNCTION app_next_code(p_prefix varchar)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_prefix demo_code_seq.prefix%TYPE;
v_seq demo_code_seq.seq%TYPE;
v_row demo_code_seq%ROWTYPE;
BEGIN
v_prefix := p_prefix;
UPDATE demo_code_seq
SET seq = seq + 1
WHERE prefix = v_prefix
RETURNING * INTO v_row; -- 整行写入 %ROWTYPE 变量
IF NOT FOUND THEN
RAISE EXCEPTION 'prefix % not found', p_prefix USING ERRCODE = 'P0002';
END IF;
RETURN format('%s-%s', v_row.prefix, lpad(v_row.seq::text, 5, '0'));
END;
$$;
SELECT app_next_code('SKU') AS code1, app_next_code('SKU') AS code2; code1 | code2
-------------+-------------
SKU-00101 | SKU-00102
(1 row)示例二:用数组变量收集批量查询结果
需求:给定一组 id,返回其中确实存在的 id 列表,用数组变量收集结果。
CREATE OR REPLACE FUNCTION app_filter_existing(VARIADIC p_ids integer[])
RETURNS integer[]
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_exist integer[] := '{}'; -- 空数组初始化
v_id integer;
BEGIN
FOREACH v_id IN ARRAY p_ids LOOP
IF EXISTS (SELECT 1 FROM demo_emp WHERE id = v_id) THEN
v_exist := array_append(v_exist, v_id);
END IF;
END LOOP;
RETURN v_exist;
END;
$$;
SELECT app_filter_existing(1, 5, 2, 8) AS existing_ids; existing_ids
--------------
{1,2}
(1 row)注意事项
CONSTANT与NOT NULL都必须有默认值,否则创建函数时就会报错,不是运行时才发现。%TYPE不带约束。列的NOT NULL、DEFAULT、CHECK都不会被复制,需要自己补。SELECT INTO非STRICT时多行不报错,只会取任意一行并丢弃其余。要求唯一性时务必用INTO STRICT。FOUND会被下一条语句覆盖。SELECT INTO、UPDATE、PERFORM等都会重设它,取值必须紧跟在关心它的语句之后。GET DIAGNOSTICS与GET STACKED DIAGNOSTICS不是一回事。前者读当前语句统计,后者只能在EXCEPTION块内读异常详情。array_length(空数组, 1)返回NULL,判断空数组请用cardinality(v) = 0或v = '{}'。数组下标从 1 开始,读越界元素返回
NULL而不报错,容易掩盖逻辑错误。EXECUTE不会改变FOUND,动态 SQL 后想判断有无结果,需要通过INTO或额外查询。默认值表达式在每次进入块时求值,不是函数编译时求值一次。
ALIAS FOR主要用于可读性,典型场景是把位置参数$1、$2换成有意义的名字(旧式写法或动态 SQL 生成的函数体中常见)。
常见错误一览:
| 错误现象 | SQLSTATE | 常见原因 |
|---|---|---|
constant "x" must have a default value | 42601 | CONSTANT 声明未给初值 |
variable "x" must have a default value | 42601 | NOT NULL 声明未给非空初值 |
assignment to constant variable | 42601 | 给 CONSTANT 变量再次赋值 |
null value cannot be assigned to variable "x" | 22004 | NOT NULL 变量被赋 NULL |
query returned no rows | P0002 | SELECT INTO STRICT 返回 0 行 |
query returned more than one row | P0003 | SELECT INTO STRICT 返回多行 |
column reference "x" is ambiguous | 42702 | 变量名与列名冲突且开启严格检查 |
subquery used as an expression returned more than one row | 21000 | 标量子查询返回多行 |
variable "x" must be of type ... | 42804 | 赋值的类型与声明不符 |
最佳实践
命名前缀是性价比最高的习惯:参数
p_、局部变量v_、常量c_、游标cur_。它能一次性消灭变量与列名、变量与变量的大部分冲突。sql-- 推荐 DECLARE v_total integer := 0; BEGIN ... END; -- 不推荐:名字与列名一模一样,全靠解析规则兜底 DECLARE total integer := 0; BEGIN ... END;变量一律给初值,即使语义上「稍后赋值」。未初始化的变量是
NULL,参与运算会把NULL传染给整个表达式。sql-- 推荐 DECLARE v_sum integer := 0; v_list text := '';需要「不能被修改的配置值」时用
CONSTANT,让误改在最早时机暴露:sqlDECLARE c_max_retry constant integer := 3;从表列派生类型用
%TYPE,让列类型变更自动传导,避免「改了表、忘了改函数」:sql-- 推荐 DECLARE v_sku demo_stock.sku%TYPE; -- 不推荐:硬编码 varchar(32),表改了这里就不同步 DECLARE v_sku varchar(32);需要整行数据时用
%ROWTYPE,比逐个声明字段更省事,也能配合RETURNING * INTO使用。唯一性查询用
INTO STRICT,并把NO_DATA_FOUND/TOO_MANY_ROWS显式处理成业务语义,而不是让异常穿透到调用方。SELECT INTO后立刻检查FOUND,不要在中隔若干语句后再判断。sql-- 推荐 SELECT qty INTO v_qty FROM demo_stock WHERE sku = v_sku; IF NOT FOUND THEN ... END IF;取值统计一律走
GET DIAGNOSTICS,不要用「再查一次 count(*)」代替——前者零成本。判断空数组用
cardinality(),语义比array_length()明确,也不会有NULL陷阱。打开
plpgsql.extra_warnings让你的隐性错误提前暴露:sqlSET plpgsql.extra_warnings = 'shadowed_variables, strict_multi_assignment, too_many_rows';检查项 捕获什么 shadowed_variables内层变量遮蔽外层 strict_multi_assignmentSELECT INTO列数与变量数不匹配too_many_rows非 STRICT的SELECT INTO返回多行优先用块级作用域隔离临时变量,而不是把整个函数的所有临时变量堆在最外层声明段里。
不要用
SELECT ... INTO做纯赋值。计算类赋值用:=,语义更清晰、也不依赖查询执行:sql-- 推荐 v_total := v_a + v_b; -- 不推荐(语义含混,且会触发一次查询执行) SELECT v_a + v_b INTO v_total;
下一节
下一节我们学习:记录类型(RECORD / ROWTYPE),深入 record 伪类型、%ROWTYPE 的字段访问与整行赋值。