Skip to content

变量声明与赋值 ​

DECLARE 段是 PL/pgSQL 的「数据准备区」:所有变量、常量、游标都在这里登记,进入块时按声明顺序初始化。本节讲透变量声明语法(含 CONSTANT、NOT NULL、COLLATE、ALIAS FOR)、内置类型与 %TYPE / %ROWTYPE 派生类型、三种赋值方式、作用域与遮蔽、保留字冲突的处理、SELECT INTO 的陷阱,以及 FOUND 与 GET DIAGNOSTICS 的用法。

前置知识

阅读本节前,建议先了解:函数结构(DECLARE / BEGIN / END)

基础概念 ​

变量(Variable)是块内为某个数据起的名字,它有类型、可选默认值、可选约束(CONSTANT、NOT NULL),并在块的作用域内可见。

概念术语说明
变量VariableDECLARE 中声明的具名存储位置
常量Constant带 CONSTANT 的声明,赋值一次后不可改
类型Type变量的数据类型,可以是内置类型、复合类型或派生类型
默认值Default / Initial ValueDEFAULT、:= 或 = 后跟的表达式
派生类型Derived Type%TYPE、%ROWTYPE 从列或表推导出的类型
别名AliasALIAS FOR 为旧名字或 $n 参数建立新名字
作用域Scope变量可见的代码范围,块是作用域边界
遮蔽Shadowing内层同名声明覆盖外层声明
赋值Assignment:= 或 =,把表达式的值写入变量
结果诊断DiagnosticsGET DIAGNOSTICS 读取上条语句的统计信息

声明段的整体位置 ​

text
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;
$$;

变量声明语法全景 ​

text
名称 [ 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 ...;把查询结果赋给变量推荐(赋值来自查询时)
sql
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;
text
      result
------------------
 a=1 b=2 c=3 d=6
(1 row)

语法与用法 ​

变量声明的完整语法原型:

text
名称 [ CONSTANT ] 类型 [ COLLATE 排序规则 ] [ NOT NULL ] [ { DEFAULT | := | = } 表达式 ];

派生类型与别名的语法:

text
变量 表名.列名%TYPE;      -- 与某列同类型
变量 变量名%TYPE;         -- 与另一变量同类型(含其类型修饰)
变量 表名%ROWTYPE;        -- 表整行的复合类型
变量 复合类型名%ROWTYPE;  -- 复合类型的整行类型
别名 ALIAS FOR 源名称;     -- 为 $n 或旧名建立别名

最小可运行示例——覆盖普通变量、常量、NOT NULL 与 %TYPE:

sql
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;
text
                        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;给位置参数起名

每次进入块时,默认值表达式都会重新求值一次,而不是只在函数首次执行时求值一次。这一点在循环体内声明变量时影响很大:

sql
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;
$$;
text
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?否)只取类型,不取约束
sql
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;
text
           result
----------------------------
 v_sku=SKU-9 v_copy=SKU-9 v_qty=5
(1 row)

%TYPE 只复制类型,不复制列的 NOT NULL 约束与默认值。若需要非空语义,得自己加 NOT NULL。

作用域与遮蔽 ​

块是作用域边界。变量在声明它的块及其所有内层块可见;内层块退出后,内层变量立即失效。

规则说明
内层可读外层内层块能访问外层块与函数参数
内层可写外层内层块能修改外层变量(除非是 CONSTANT)
外层不可见内层内层块结束后,其变量不可再引用
同名遮蔽内层同名声明遮蔽外层,外层需用 外层标签.名字 访问
参数也在作用域内函数参数相当于最外层块的变量
sql
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;
text
 sum_1_to_5
------------
         15
(1 row)

保留字冲突与 variable_conflict ​

当变量名与列名同名时,PL/pgSQL 默认优先解释为变量,但这是一个容易出错的灰色地带。可选的处理方式:

方式效果推荐度
命名前缀(p_ / v_)从源头消除冲突强烈推荐
限定列名(表别名.列名)明确指定为列推荐
#variable_conflict use_column全局改为优先列名不推荐(易失控)
#variable_conflict use_variable全局改为优先变量不推荐
#variable_conflict error冲突时报错局部可用
sql
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;
$$;

若确实需要在不改名的情况下让解析器明确按「列」处理,可在块内使用编译指令:

sql
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。两者都有「必须提供初值」的前置要求。

sql
-- 错误!CONSTANT 变量必须有默认值
CREATE FUNCTION demo_bad_const() RETURNS void LANGUAGE plpgsql AS $$
DECLARE
  c_max integer CONSTANT;    -- 没有初值
BEGIN
  RAISE NOTICE '%', c_max;
END;
$$;
text
ERROR:  constant "c_max" must have a default value
sql
-- 错误!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;
$$;
text
ERROR:  variable "v_x" must have a default value
sql
-- 推荐:两者都带合适的初值
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
sql
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;
text
              r1              |                r2
------------------------------+-----------------------------------
 id=1 name=Alice (rows=1, FOUND=t) | id=99 not found (rows=0, FOUND=f)
(1 row)

SELECT INTO STRICT 版本:

sql
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;
text
  ok
------
 Bob
(1 row)
sql
-- 0 行:抛 NO_DATA_FOUND
SELECT demo_into_strict(99);
text
ERROR:  query returned no rows
CONTEXT:  PL/pgSQL function demo_into_strict(integer) line 5 at SELECT INTO variables
sql
-- 多行:抛 TOO_MANY_ROWS
SELECT demo_into_strict(NULL);   -- 条件恒不成立,用于演示另一种写法

使用 STRICT 时可以用 EXCEPTION 捕获:

sql
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;
text
   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 则能取到更细的统计量。

手段类型含义何时被设置
FOUNDboolean上条语句是否找到行SELECT INTO / PERFORM / UPDATE / INSERT / DELETE / FETCH / FOR 循环
ROW_COUNTbigint上条语句影响的行数INSERT / UPDATE / DELETE / SELECT / FETCH / MOVE / COPY
PG_ROUTINE_OIDoid当前函数自身的 OID任何时刻
RETURNED_SQLSTATEtext异常发生时的 SQLSTATE仅在 EXCEPTION 处理块内有效

FOUND 的行为按语句类型而异,这是最容易踩坑的一点:

语句0 行时有行时
SELECT INTOFOUND = falseFOUND = true
PERFORMFOUND = falseFOUND = true
UPDATE / DELETEFOUND = falseFOUND = true
INSERT恒为 true(除非 RETURNING 无行)true
FETCHFOUND = falseFOUND = true
FOR 循环循环结束后 FOUND = false循环体内为 true
EXECUTE不改变 FOUND不改变 FOUND
sql
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;
text
NOTICE:  UPDATE 影响行数 = 1,FOUND = t
NOTICE:  SELECT INTO 后 FOUND = t
 result
--------
 Alice
(1 row)

GET DIAGNOSTICS 支持的诊断项(常用部分):

诊断项类型说明
ROW_COUNTbigint上条 SQL 处理的行数
PG_ROUTINE_OIDoid当前函数 OID
PG_CONTEXTtext当前调用栈文本,用于日志定位
RETURNED_SQLSTATEtext异常的 SQLSTATE,仅在异常处理块内
COLUMN_NAME / CONSTRAINT_NAME 等text异常的详细信息,需在异常处理块内
sql
-- 用一个异常处理器时,可以用 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;
text
                       err
--------------------------------------------------
 SQLSTATE=22012 MESSAGE=division by zero
(1 row)

注意:在 EXCEPTION 处理块内取异常诊断项,必须用 GET STACKED DIAGNOSTICS;GET DIAGNOSTICS 只能取当前活跃语句的统计项。

默认值求值时机与常见陷阱 ​

默认值在每次进入块时求值,这一行为带来两个实用结论:

结论说明
循环体内声明会反复求值想「只求一次」的值必须放在循环外
默认值可以引用前面的变量与参数但不能引用后面声明的变量(尚未求值)
sql
-- 推荐:需要「每次调用求值一次」的值放在最外层块
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;
text
   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) = 0cardinality(v) = 0
忘记 FOUND 被覆盖连续两条语句后用 FOUND紧邻语句之后立即使用

实战示例 ​

示例一:用 %TYPE / %ROWTYPE 写一个类型安全的自增编号生成器 ​

需求:从编号表中取当前最大值并加一,返回新编号。列类型变化时函数应自动跟随。

sql
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;
text
    code1    |    code2
-------------+-------------
 SKU-00101   | SKU-00102
(1 row)

示例二:用数组变量收集批量查询结果 ​

需求:给定一组 id,返回其中确实存在的 id 列表,用数组变量收集结果。

sql
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;
text
 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 value42601CONSTANT 声明未给初值
variable "x" must have a default value42601NOT NULL 声明未给非空初值
assignment to constant variable42601给 CONSTANT 变量再次赋值
null value cannot be assigned to variable "x"22004NOT NULL 变量被赋 NULL
query returned no rowsP0002SELECT INTO STRICT 返回 0 行
query returned more than one rowP0003SELECT INTO STRICT 返回多行
column reference "x" is ambiguous42702变量名与列名冲突且开启严格检查
subquery used as an expression returned more than one row21000标量子查询返回多行
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,让误改在最早时机暴露:

    sql
    DECLARE 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 让你的隐性错误提前暴露:

    sql
    SET 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 的字段访问与整行赋值。

参考链接 ​