想象你是一家餐厅的老板。刚开始时,顾客点菜后,服务员要跑去后厨口头传达需求,后厨做好后服务员再端出来。每一道菜,服务员都要来回跑好几趟。客人一多,服务员累得满头大汗,上菜还特别慢。这时候你想:为什么不把做菜的流程直接交给后厨,让后厨按照固定的菜谱批量完成呢?服务员只需要把订单往窗口一递,后厨自己就能搞定所有工序。 这就是存储过程的核心思想——把复杂的业务逻辑从“客户端”搬到“数据库”里执行。在PostgreSQL中,PL/pgSQL(Procedural Language/PostgreSQL)就是写这种“后厨菜谱”的语言。它允许你把多条SQL语句、条件判断、循环、甚至异常处理都封装成一个整体,数据库一次性执行完毕,而不是让客户端和数据库之间来回“传话”。这样既减少了网络开销,又提升了执行效率,尤其在复杂的数据处理任务中效果显著。 本文将带你走进PL/pgSQL的世界,从变量、流程控制到异常处理和触发器,一步步学会“编写菜谱”,让你的数据库成为一个高效运转的“智能后厨”。

岗位关联

掌握PL/pgSQL存储过程开发,是数据库工程师从“能用SQL”进阶到“能设计高性能数据服务”的必经之路。在数据密集型业务和高并发场景下,这项技能能让你的系统扛住更大的压力,大幅提升开发效率和系统稳定性。

目录
  1. 可视化演示
  2. 存储过程概述
  3. 变量与基本语句
  4. 流程控制逻辑
  5. 异常处理机制
  6. 触发器实战
  7. 性能优化技巧
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示PostgreSQL PL/pgSQL存储过程的块结构、流程控制语句和异常处理机制,理解数据库端编程的核心模式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

PL/pgSQL块结构 DECLARE声明变量,BEGIN执行逻辑 EXCEPTION捕获异常,END结束块 DO $$ BEGIN ... END $$; 嵌套块支持独立异常作用域

存储过程概述

想象后厨里最熟练的大厨,他不需要每做一道菜就问一遍服务员“这菜要几分熟”“放不放香菜”“要不要免辣”。一份标准的菜品执行单递进来,他看一眼就能按部就班地完成全部工序,中间不需要任何来回确认。PL/pgSQL存储过程,本质上就是这张标准化的“菜品执行单”——它把一系列数据库操作打包成一个整体,就像一份完整的操作指令,数据库拿到后就能自主执行,无需客户端反复“传话”。

从技术层面看,当你在客户端中逐条发送SQL语句时,每次都是一个“请求-响应”的往返过程,就像服务员在厨房和餐桌之间来回奔波。而调用一个存储过程时,你只需要发送一次请求,数据库就能按预定义的逻辑依次完成查询、更新、判断等一系列操作。这减少的网络往返次数是显而易见的——哪怕省掉一半的“跑腿”,在大量请求并发的情况下,对整个系统的性能提升也是质的飞跃。

存储过程的核心语法结构如下:

简而言之,存储过程把“客户端多次往返”变成了“数据库一次完成”。更重要的是,它把业务规则从应用层下沉到数据层,让数据更靠近计算,减少了数据搬运的成本。就好比把洗菜、切菜全部交给后厨来完成,而不是把食材先运到前厅处理完再送回去。你写的每一个存储过程,都是给数据库这位“大厨”的一份完整执行单,它拿到就能立刻干活,不用中途停下来向你确认什么。

CREATE_PROCEDURE:CREATE PROCEDURE+过程名+参数列表+LANGUAGE+plpgsql\text{CREATE\_PROCEDURE} : \text{CREATE PROCEDURE} + \text{过程名} + \text{参数列表} + \text{LANGUAGE} + \text{plpgsql}
这个公式帮你认清创建存储过程的基本骨架:先写CREATE PROCEDURE,接着是过程名和参数列表,最后指定语言为plpgsql。
  1. CREATE PROCEDURE这是创建存储过程的起始关键字,告诉数据库“我要定义一个可重复调用的过程”。
    CREATE PROCEDURECREATE \ PROCEDURE
  2. 过程名与参数给这个过程取一个有意义的名字,并声明它需要哪些输入参数,就像菜谱上写明菜名和所需食材。
    过程名+(参数列表)\text{过程名} + (\text{参数列表})
  3. LANGUAGE plpgsql表明这个存储过程使用PL/pgSQL语言编写,它是PostgreSQL专门为编写存储过程提供的扩展语言。
    LANGUAGE plpgsqlLANGUAGE \ plpgsql

变量与基本语句

后厨的任何菜品制作,都少不了预先准备食材、调料和工具。在PL/pgSQL中,这些“准备工作”就对应着变量和基本语句。你需要在执行任何逻辑之前,先声明好要用的变量、定义合适的类型,就像把各种食材切好、分装、贴上标签,等到火烧起来时才能从容下锅。

在PL/pgSQL中,变量声明使用DECLARE关键字,赋值使用:=操作符。变量有自己的作用域和生命周期,就像厨房里备好的食材,有的在特定菜品的制作中使用,有的可以在整个晚市期间通用。

理解变量之后,你会发现存储过程的功能远不止变量。参数模式(IN、OUT、INOUT)让存储过程的输入输出更加灵活——IN参数像“顾客点单传入的信息”,OUT参数像“端出菜品时带回的信息”,INOUT则像“一份可以修改后再返回的订单备注”。这些机制共同构成了存储过程与外部世界交互的语言基础,让你的“后厨”既能接收指令,又能反馈结果。

声明语法:DECLARE→变量名→数据类型→(:=初值)?\text{声明语法} : \text{DECLARE} \rightarrow \text{变量名} \rightarrow \text{数据类型} \rightarrow (:= \text{初值})?
声明一个变量,名字在前、类型在后,可选地给它一个初值。这就像在厨房里把“葱花”装进贴着标签的碗里,勺子就放在旁边等着用。
  1. DECLARE声明区存储过程的声明段,所有变量都要在这里“登记入册”,就像列出今天需要的采购清单。
    DECLARE 变量声明区DECLARE \ \text{变量声明区}
  2. 变量名与数据类型变量名是你在代码中引用这个“食材碗”的方式,数据类型则规定了碗里能装什么类型的东西。
    变量名 数据类型\text{变量名} \ \text{数据类型}
  3. 赋值操作符=:把右侧的值装进左侧的变量中,注意这个操作符与SQL中的等号作用不同。
    变量名:=值\text{变量名} := \text{值}

流程控制逻辑

后厨做菜时,大厨需要根据实际情况不断做决策:如果客人备注了不要辣,就跳过放辣椒的步骤;如果炉灶的火力不够,就多焖一会儿。PL/pgSQL流程控制,就是这样一套“决策语言”,让存储过程能根据数据的具体情况灵活调整执行路径。

条件分支使用IF-THEN-ELSE结构,循环则支持LOOP、WHILE和FOR三种形式。IF的条件判断就像大厨检查食材是否足够,循环就像反复颠勺直到菜品达到标准——这些都是编写复杂业务逻辑不可或缺的工具。

循环结构同样重要:LOOP配合EXIT实现“先做题后检查”直到满足条件才退出;WHILE是“先检查后执行”;FOR则能像遍历菜单一样逐条处理数据集。这些控制结构给予存储过程强大的表达力——当业务规则多到一定程度,仅靠SQL语句堆砌已经无法清晰表达时,流程控制就是帮你把这些散落的“做饭步骤”组织成连贯“菜谱”的关键。

流程控制:IF+条件+THEN+语句+[ELSEIF+语句]+[ELSE+语句]+END IF\text{流程控制} : \text{IF} + \text{条件} + \text{THEN} + \text{语句} + [\text{ELSEIF} + \text{语句}] + [\text{ELSE} + \text{语句}] + \text{END IF}
IF-THEN-ELSE是最基本的条件分支,就像大厨的“if 库存没虾 then 换鱼 else 做虾”决策逻辑。
  1. IF与条件判断IF后面跟着一个布尔表达式,如果为真就执行THEN后面的语句,这就像检查“食材还剩多少”。
    IF 条件 THENIF \ \text{条件} \ THEN
  2. ELSEIF 分支当首个条件不满足时,依次检查其他条件,相当于根据库存做出B计划。
    ELSEIF 条件 THENELSEIF \ \text{条件} \ THEN
  3. ELSE 兜底分支所有条件都不满足时执行的默认路径,就像“都没货了就只能做鸡蛋炒饭”。
    ELSE 语句ELSE \ \text{语句}
  4. END IF 结束标记整个条件分支结构的结束,告诉处理器“这个决策完了,继续往下走”。
    END IFEND \ IF

异常处理机制

哪怕是最有经验的厨师,也免不了遇到意外:食材突然变质、火候失控、盛菜的盘子打碎。在PL/pgSQL中,这些“意外”就是异常。一个好的存储过程,不仅在正常流程下工作,更能在异常发生时优雅地应对,而不是让整个“后厨”停摆。

PL/pgSQL的异常处理机制以EXCEPTION块为核心。当BEGIN块内的语句执行出错时,控制权会立即转移到EXCEPTION块中匹配的错误处理分支。若没有合适的异常处理,事务将自动回滚——就像一道菜做坏了,整个订单作废,但后厨总结经验,下次改进。

异常处理引入了一个重要的底层机制:子事务(subtransaction)与回滚。当BEGIN块内出错时,块内已完成的操作会全部回滚,如同这份菜做砸了,厨房里这个工作台所有“未出菜”的半成品一并处理掉,但块外已提交的操作不受影响。这正是“局部失误,整体安全”的保障——你的“后厨”不会因为一道菜的失误就开不了门。

EXCEPTIONWHEN 错误类型 THEN 错误处理语句\text{EXCEPTION} \\\text{WHEN } \text{错误类型} \ THEN \ \text{错误处理语句}
当BEGIN块中出现错误时,EXCEPTION捕获它;具体是哪种错误,由WHEN后面的错误类型来匹配。
  1. BEGIN 执行区正常的业务逻辑在此执行,就像大厨正常按菜谱做菜。
    BEGIN 正常流程语句BEGIN \ \text{正常流程语句}
  2. EXCEPTION 异常区当BEGIN块内部抛出错误时,执行立即跳到此处,跳过后会结束该块的执行。
    EXCEPTION 错误捕获段EXCEPTION \ \text{错误捕获段}
  3. WHEN 错误匹配指定要捕获哪一类错误,比如unique_violation(唯一约束冲突)、division_by_zero(除零错误)等。
    WHEN 错误类型 THEN 处理方法WHEN \ \text{错误类型} \ THEN \ \text{处理方法}
  4. GET STACKED DIAGNOSTICS收集错误详情(如错误消息、错误码),就像大厨记录这次失误的原因和教训。
    GET STACKED DIAGNOSTICS 变量=错误属性GET \ STACKED \ DIAGNOSTICS \ \text{变量} = \text{错误属性}

触发器实战

触发器就像一位隐藏在厨房里的“监工”——他并不主动做菜,但只要有人动刀、开火、装盘,他都会按照预设的规则自动响应。比如食材入库时自动记录来源、菜品出锅时自动评估质量。PL/pgSQL触发器,就是挂在数据表上的自动化响应机制。

创建触发器需要两个步骤:先创建一个返回类型为trigger的存储函数,定义触发器触发时要执行的逻辑;再用CREATE TRIGGER将触发器绑定到指定表的事件(INSERT、UPDATE、DELETE)上。

触发器的典型场景包括自动审计日志——任何人修改了薪资表,自动记录谁在什么时间改了什么;也有数据完整性维护——订单表插入新行时,自动更新客户的总消费额。作为存储过程的高级形态,触发器让你的数据库具备“敏感而自动”的数据守护能力,这在高合规性、数据敏感度高的业务系统中极具价值。

CREATE TRIGGER:CREATE TRIGGER+触发器名+触发时机+事件+触发粒度+ON+表名+FOR EACH+粒度+EXECUTE FUNCTION+函数名()\text{CREATE TRIGGER} : \text{CREATE TRIGGER} + \text{触发器名} + \text{触发时机} + \text{事件} + \text{触发粒度} + \text{ON} + \text{表名} + \text{FOR EACH} + \text{粒度} + \text{EXECUTE FUNCTION} + \text{函数名}()
触发器由事件驱动,可以指定在操作前(BEFORE)还是操作后(AFTER)触发,以及是行级触发还是语句级触发。
  1. 触发器名与时机触发器名字标识身份,触发时机指定是在事件发生前还是发生后响应。
    CREATE TRIGGER 名 BEFORE/AFTER 事件CREATE \ TRIGGER \ \text{名} \ BEFORE/AFTER \ \text{事件}
  2. 事件类型INSERT、UPDATE、DELETE,这些是触发器“关心”的操作行为。
    事件 ON 表名\text{事件} \ ON \ \text{表名}
  3. FOR EACH 粒度FOR EACH ROW表示每行变化都触发,FOR EACH STATEMENT表示整条SQL只触发一次。
    FOR EACH ROW/STATEMENTFOR \ EACH \ ROW/STATEMENT
  4. EXECUTE FUNCTION指定触发器执行哪个存储函数,这个函数才是“监工”的真正行动逻辑。
    EXECUTE FUNCTION 函数名()EXECUTE \ FUNCTION \ \text{函数名}()

性能优化技巧

回到我们的餐厅比喻:聪明的老板会怎么提升后厨效率?不是让厨师多跑几趟,而是优化菜品的制作流程——该批量预处理的一次性准备好,该并行处理的不相互等待。PL/pgSQL的性能优化,正是沿着这条思路展开的。

一条万能建议是:推荐使用CREATE OR REPLACE PROCEDURE并利用PL/pgSQL的预编译机制。存储过程在首次调用时即被解析和编译,之后执行计划会缓存复用,这大大减少了重复SQL语句逐条解析的开销。另外,在存储过程内部尽量使用循环批量处理数据,而不是在客户端循环发SQL——就像后厨一次性备好十份宫保鸡丁的料,而不是每接到一份订单才洗一次菜切一次丁。

值得一提的是,合理划分事务边界同样关键。存储过程默认在一个事务中执行,如果中间某个步骤失败,所有操作要么全部提交要么全部回滚。善用子事务和异常处理,可以把事务控制在最合理的粒度,既保证原子性,又避免过长事务持有锁导致并发下降。真正高效的“后厨”,会用最少的等待、最小的成本、最合理的流程,把每一份菜做得又快又好。

性能提升=减少往返+预编译+批量操作+合理事务\text{性能提升} = \text{减少往返} + \text{预编译} + \text{批量操作} + \text{合理事务}
存储过程的性能优势来自四个方面共同作用:减少客户端-服务器往返、预编译的计划重用、批量处理替代逐条执行、以及事务的合理划分。
  1. 减少往返次数客户端一次调用替代多条SQL逐条发送,这是性能提升的最大来源。
    总次数调用=1≪总次数SQL往返=n\text{总次数}_{\text{调用}} = 1 \ll \text{总次数}_{\text{SQL往返}} = n
  2. 预编译计划重用第一次调用编译后,后续调用直接使用已有执行计划,免去重复解析优化。
    每调用:无 parse成本\text{每调用}: \text{无} \ \text{parse成本}
  3. 批量操作替代逐条在过程内部使用集合操作或循环批量写入,减少不必要的重复开销。
    写次数=数据行数/每批次行数\text{写次数} = \text{数据行数} / \text{每批次行数}

AI 应用场景

金融风控中的高频账务处理

阶段:数据预处理与特征计算

解决的问题:在银行交易系统中,每笔交易都需要同时更新账户余额、写入流水记录、检查风控阈值。传统方式下,每笔交易需要客户端发送3-4次SQL请求,不仅耗时容易超时,而且分散的事务难以保证数据一致性。

具体案例:某银行核心系统将所有账务操作封装为PL/pgSQL存储过程sp_credit_debit,传入账号、金额、交易类型、流水号,存储过程在一个事务内完成:余额更新、流水写入、累计额度校验、异常检测上报。单笔处理时长从直接SQL执行的35毫秒降低到5毫秒,配合批处理循环可支撑数万笔/秒的并发交易处理。

电商平台订单统计与库存联动

阶段:多表事务协调与业务规则执行

解决的问题:电商下单时,需要同时检查库存、扣减数量、生成订单、更新统计数据、记录日志。若使用多条独立SQL,任何一步失败都可能导致数据不一致,且在网络往返中容易出错。

具体案例:某电商平台的后端服务调用PL/pgSQL存储过程sp_create_order,一次性完成库存判断与扣减、订单生成、销售统计、日志写入。所有操作共享同一事务,失败自动回滚。通过触发器trg_order_audit自动记录每次订单变更的操作人和时间,满足了审计合规要求。

Python 演示

python41 行
import psycopg2 def call_transfer_funds(src_acc, dst_acc, amount):    """    模拟调用PL/pgSQL存储过程的Python封装。    注意: PL/pgSQL代码由数据库执行, 这里展示连接级调用方式。    """    conn = psycopg2.connect(dbname="testdb", user="postgres", password="secret")    try:        conn.autocommit = False        with conn.cursor() as cur:            # 创建存储过程(若已存在则替换)            cur.execute("""                CREATE OR REPLACE PROCEDURE transfer_funds(                    p_src INT, p_dst INT, p_amount NUMERIC)                LANGUAGE plpgsql                AS $$                BEGIN                    IF p_amount <= 0 THEN                        RAISE EXCEPTION '金额必须为正数';                    END IF;                    UPDATE accounts SET balance = balance - p_amount WHERE id = p_src;                    UPDATE accounts SET balance = balance + p_amount WHERE id = p_dst;                    INSERT INTO transaction_log(src, dst, amount)                    VALUES (p_src, p_dst, p_amount);                END;                $$;                """            )            # 调用存储过程——一次调用完成所有操作            cur.execute("CALL transfer_funds(%s, %s, %s)", (src_acc, dst_acc, amount))        conn.commit()        print(f"转账成功: {src_acc} -> {dst_acc}, 金额 {amount}")    except psycopg2.Error as e:        conn.rollback()        print(f"转账失败, 已回滚: {e}")    finally:        conn.close() if __name__ == "__main__":    call_transfer_funds(1001, 1002, 500.00)
目的:演示通过PL/pgSQL存储过程实现资金转账的原子性操作——所有步骤在一个数据库事务中完成,任何一步失败都会整体回滚。
  1. CREATE OR REPLACE PROCEDURE transfer_funds创建名为transfer_funds的存储过程,用PL/pgSQL语言编写,参数为源账号、目标账号和转账金额
  2. IF p_amount <= 0 THEN RAISE EXCEPTION在存储过程内部使用条件判断检查金额合法性,非法则抛出异常
  3. UPDATE accounts SET balance = balance - p_amount WHERE id = p_src实现扣款操作的内部SQL,使用传入参数作为条件
  4. CALL transfer_funds(%s, %s, %s)这是Python调用存储过程的唯一入口,所有业务逻辑在数据库内完成
  5. conn.rollback()异常发生时回滚整个事务,确保余额不会出现“扣了对方没收”的不一致状态

常见误区

误区我一直觉得存储过程是“祖师爷时代”的技术,现在ORM和SQL直查又快又方便,存储过程多包了一层函数调用,肯定更慢。我从来不用存储过程。

正解恰恰相反,在复杂、多步骤、有逻辑判断的数据操作场景下,存储过程通常更快。原因是存储过程减少了客户端和数据库之间的所有中间往返——每次网络往返的时间可能是几毫秒到几十毫秒,而存储过程内部的语句执行是内存级的微秒操作。更重要的是,存储过程预处理(预编译)让重复调用不需要重复解析SQL。仅当只执行一条简单SQL时,两者差异才不明显。

为何常见:初学者接触的SQL都比较简单,单条执行和存储过程没有可比性,同时ORM工具的普及让大家习惯于把所有逻辑写在应用层,自然容易低估存储过程的价值。

误区我看到存储过程里可以写EXCEPTION块,觉得这就是最保险的写法。不管什么操作,我都用一个大BEGIN包起来,中间任何一步出错,EXCEPTION里统一处理一下就行。

正解PL/pgSQL的异常捕获有两个重要限制:一是EXCEPTION会触发子事务,带来明显的性能开销,不应该无脑包裹每一段代码;二是进入EXCEPTION块后,当前块内已做的所有更改都会回滚,如果你错误地认为“出错后继续执行后续语句”,实际行为会完全不同。合理做法是:只在真正需要异常处理的临界区域使用,且把EXCEPTION块内的逻辑写得尽量简洁。

为何常见:很多编程语言里try-catch是轻量级的,开发者自然而然把这种“性能预期”带到数据库语言中,忽略了数据库事务模型带来的语义差异和开销。

误区写触发器的时候,我觉得FOR EACH ROW一定更精确、更强大。每当有数据变化,我需要每一行都触发一次处理,这样肯定比批量触发一次更靠谱,所以我永远选择FOR EACH ROW。

正解FOR EACH ROW和FOR EACH STATEMENT各有所长。FOR EACH ROW触发更频繁,适合需要逐行处理(如审计每条记录的变更)的场景;但它的开销较大,一次UPDATE影响十万行时会触发十万次函数调用。FOR EACH STATEMENT只触发一次,适合统计型逻辑(比如“员工表被更新后记录一次变动事件”)。选择粒度应考虑实际业务语义,避免“一刀切”。

为何常见:初学者容易陷入“越细越安全”的思维误区,忽略数据库是面向集合处理的设计哲学——有时一次处理是整个集合,逐行处理反而违背初衷。

误区存储过程在数据库里,不像应用代码那样还要经过编译、部署、单元测试这些环节,应该不太需要严格测试吧,数据库自己保证正确性。

正解事实恰恰相反,存储过程更需要完整的测试。存储过程代码难以调试,错误信息在复杂的嵌套调用中容易被吞掉。加上业务逻辑集中、数据变更影响面大,一个存储过程的小逻辑错误可能在大批量数据上被无限放大。推荐做法是用事务包裹测试,即测试完成后回滚,模拟生产数据,并创建边界测试(空表、大量数据、异常数据等)。

为何常见:因为看到存储过程创建后立即“生效”且不需要重新编译,许多人误以为它有“免检”特权。实际上,恰是因为它贴近数据核心层,任何缺陷影响都更严重。

面试题

Q1. PL/pgSQL存储过程最核心的性能优势是什么?

单选题 · easy

  • A.减少客户端与数据库之间的网络往返次数← 正确答案
  • B.因为使用了更快的编程语言
  • C.存储过程会压缩数据节省存储空间
  • D.存储过程不需要SQL解析和优化

答案:A。存储过程把多次SQL往返封装为一次数据库内部流程,减少了昂贵的网络开销。它仍使用SQL解析,也不压缩数据,B、C、D均不属于存储过程的根本优势。

Q2. PL/pgSQL中声明变量使用哪个关键字?

单选题 · easy

  • A.VAR
  • B.DECLARE← 正确答案
  • C.LET
  • D.DEFINE

答案:B。PL/pgSQL的变量声明区使用DECLARE关键字开始,后面跟着变量名和类型定义。VAR和LET是其他语言的关键字,DEFINE在PL/pgSQL中专用于定义光标。

Q3. 在PL/pgSQL中,INOUT参数的作用是什么?

单选题 · mid

  • A.只能输入不能输出的参数
  • B.只能输出不能输入的参数
  • C.既作为输入接收值,又能输出返回值← 正确答案
  • D.既不能输入也不能输出的参数

答案:C。INOUT参数结合了IN和OUT的功能——调用时可传入值,过程中可修改,过程结束后该变量的新值对调用者可见。就像“一份可修改的订单备注”,既能传达信息又能带回新信息。

Q4. EXCEPTION块执行时,以下哪项描述是正确的?

单选题 · mid

  • A.块内所有已执行的语句都会提交
  • B.块内已执行的语句会回滚,异常被处理后继续执行块后的语句← 正确答案
  • C.整个事务自动提交
  • D.块内所有未执行语句会被跳过但已执行的不受影响

答案:B。BEGIN块内发生异常时,PL/pgSQL会自动创建一个子事务,进入EXCEPTION块之前内部已完成的操作会回滚,异常被处理后从END处继续执行。为异常处理设计正确的语义是这里的关键。

Q5. 创建触发器时,触发函数必须返回什么类型?

单选题 · mid

  • A.integer
  • B.text
  • C.trigger← 正确答案
  • D.void

答案:C。PostgreSQL的触发函数必须声明为返回类型trigger,这是PL/pgSQL内部的约定类型,编译器会根据这个类型提供行级触发器操作的特殊返回值(如NEW、OLD)。

Q6. 以下哪些属于使用存储过程的性能优化策略?

多选题 · mid

  • A.减少客户端到数据库的往返次数← 正确答案
  • B.在存储过程中逐条发送SQL语句
  • C.充分利用预编译执行计划← 正确答案
  • D.在客户端循环调用存储过程

答案:AC。性能优化核心是减少往返、批量处理、预编译计划的复用。B在存储过程内逐条发SQL违背“过程”初衷,D在客户端循环调用存储过程则会增加往返,这两者都是反模式。

Q7. 在PL/pgSQL中,WHEN unique_violation THEN用于处理什么类型的错误?

单选题 · hard

  • A.除零错误
  • B.违反了唯一约束的错误← 正确答案
  • C.连接超时错误
  • D.语法错误

答案:B。PostgreSQL将错误分类并分配错误码,unique_violation对应唯一约束冲突。这就像后厨识别出“这道菜单已经在做一份了”的重复订单,需要专门处理。

Q8. 下列哪些触发器场景适合使用FOR EACH ROW触发粒度?

多选题 · hard

  • A.审计记录每一行数据的变更详情← 正确答案
  • B.更新工资表后统计一次员工总数
  • C.在每一行插入前填充创建时间字段← 正确答案
  • D.删除数据后修改一次归档状态

答案:AC。FOR EACH ROW适合需要逐行感知数据的场景,如逐行记录变更和填充每行数据字段。统计总数和归档状态属于一次性的集合级操作,用FOR EACH STATEMENT更合适。

Q9. GET STACKED DIAGNOSTICS指令放在哪里使用才能正确获取错误信息?

单选题 · extreme

  • A.BEGIN块内,出错语句之后
  • B.EXCEPTION块内← 正确答案
  • C.DECLARE声明区
  • D.存储过程的最外层

答案:B。GET STACKED DIAGNOSTICS只能在EXCEPTION块内使用,用来获取当前捕获异常的错误详情。它就像后厨里“记录本次出错原因”的专用表单,必须有错误发生时才能填写。

Q10. PL/pgSQL支持的循环结构包括哪些?

多选题 · hard

  • A.LOOP← 正确答案
  • B.WHILE← 正确答案
  • C.FOR← 正确答案
  • D.REPEAT

答案:ABC。PL/pgSQL提供LOOP、WHILE、FOR三种循环。REPEAT是MySQL的循环语法,不属于PL/pgSQL。三种循环各适合不同场景:LOOP适合无条件直到退出,WHILE适合先判断后执行,FOR适合遍历数据集合。