1. 从一段匿名块说起执行语句到底在数据库里干了什么很多人学PL/SQL学到“执行语句”这一节时会下意识把它当成“写一句能跑的话”这么简单。我刚接触这块的时候也是这么想的直到有一次帮同事排查一个存储过程的问题才发现自己对“执行语句”的理解有多浅。那段代码大概长这样BEGIN UPDATE orders SET status DONE WHERE id 1001; IF SQL%ROWCOUNT 0 THEN INSERT INTO orders(id, status) VALUES (1001, DONE); END IF; END;看起来平平无奇但当时的问题是这段逻辑在并发场景下偶尔会插入重复数据。我盯着看了半天才意识到问题不在UPDATE或INSERT本身而在于我没搞清楚PL/SQL里“执行语句”的执行模型——它和单纯在客户端敲一条SQL底层走的路径是不一样的。PL/SQL里的执行语句本质上是把SQL语句嵌入到过程化代码块中由PL/SQL引擎和SQL引擎协同完成。PL/SQL引擎负责流程控制、变量管理、异常处理SQL引擎负责真正的数据操作。两者之间通过一个叫“上下文切换”的机制来回传递控制权。这个切换不是免费的每次切换都有开销。所以理解执行语句不只是知道语法怎么写更要明白它在引擎层面是怎么跑的、什么时候会触发切换、怎么组织语句才能减少不必要的开销。这篇笔记面向的是已经写过一些PL/SQL、但对执行语句的底层行为和实战细节还不够清晰的朋友。我会从执行语句的分类讲起把赋值、SQL语句、流程控制、动态SQL这几类逐一拆开重点讲清楚每类语句在实际项目里容易踩的坑以及我这些年总结下来的一些写法习惯。如果你正在写存储过程、函数、触发器或者准备做批量数据处理这些内容应该能帮你少走一些弯路。2. 赋值语句看似最简单却最容易埋下精度和空值的雷2.1 赋值语句的两种写法与选择逻辑PL/SQL里的赋值语句主要有两种形式一种是直接用:给变量赋值另一种是通过SELECT INTO从数据库里取值赋给变量。前者是纯PL/SQL引擎内部的操作不涉及SQL引擎速度极快后者需要发起一次查询走SQL引擎有上下文切换开销。-- 方式一直接赋值纯PL/SQL引擎操作 DECLARE v_count NUMBER : 0; v_name VARCHAR2(50); BEGIN v_count : v_count 1; v_name : test; END; -- 方式二SELECT INTO涉及SQL引擎 DECLARE v_total NUMBER; BEGIN SELECT SUM(amount) INTO v_total FROM orders WHERE status DONE; END;选择哪种方式核心判断标准是数据来源在哪里。如果值已经在PL/SQL变量里或者可以通过简单运算得到就用:如果值存在数据库表里就必须用SELECT INTO。我见过有人在循环里反复用SELECT INTO去取一些其实可以用变量累加的值结果性能差了好几倍。比如统计某个分类下的订单总额完全可以在遍历游标时用:累加而不是每轮都去查一次数据库。2.2 空值传播赋值时最隐蔽的陷阱赋值语句里最容易被忽视的问题是空值传播。在PL/SQL中任何包含NULL的算术运算结果都是NULL任何与NULL的比较结果都是UNKNOWN。这意味着如果你不小心把一个NULL赋给了变量后续所有基于这个变量的计算都会变成NULL而且不会报错。DECLARE v_a NUMBER : 10; v_b NUMBER; -- 未初始化默认为NULL v_c NUMBER; BEGIN v_c : v_a v_b; -- v_c 结果是 NULL不是 10 IF v_c 0 THEN DBMS_OUTPUT.PUT_LINE(positive); ELSE DBMS_OUTPUT.PUT_LINE(not positive); -- 会走这里因为 NULL 0 是 UNKNOWN END IF; END;这段代码的输出是not positive因为NULL 0的结果是UNKNOWNIF条件不成立走ELSE分支。很多人第一次遇到这种情况会非常困惑明明v_a是10怎么加了个v_b就变成“不大于0”了。我的习惯是所有参与运算的变量在声明时就给一个默认值尤其是数值型变量给0字符串给空串。如果业务上确实需要区分“未赋值”和“值为0”那就用额外的标志位来标记而不是依赖NULL。DECLARE v_a NUMBER : 10; v_b NUMBER : 0; -- 显式初始化 v_c NUMBER; BEGIN v_c : v_a v_b; -- 结果是 10 END;2.3 类型转换的隐式陷阱赋值时另一个常见问题是隐式类型转换。PL/SQL允许你把字符串赋给数值变量只要字符串能转成数字但它不会在编译期报错而是在运行期才可能抛异常。DECLARE v_num NUMBER; BEGIN v_num : 123; -- 可以隐式转换成功 v_num : abc; -- 运行期报错ORA-06502 END;更隐蔽的是日期和字符串之间的转换。v_date : 2024-01-01能不能成功取决于当前会话的NLS_DATE_FORMAT设置。如果换了一个环境这个赋值可能就失败了。我的做法是涉及日期赋值时一律用TO_DATE显式转换并指定格式串涉及字符串转数字时用TO_NUMBER并做好异常捕获。DECLARE v_date DATE; v_num NUMBER; BEGIN v_date : TO_DATE(2024-01-01, YYYY-MM-DD); v_num : TO_NUMBER(123); EXCEPTION WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE(转换失败请检查输入格式); END;提示在赋值语句中显式转换永远比隐式转换可靠。隐式转换的规则依赖会话参数换环境就可能出问题。3. SQL语句在PL/SQL中的执行上下文切换与批量处理的取舍3.1 上下文切换的真实成本前面提到PL/SQL引擎和SQL引擎之间的切换是有开销的。这个开销到底有多大可以这样理解PL/SQL引擎像一个调度员SQL引擎像一个工人。调度员每次让工人干活都要把任务交代清楚工人干完再把结果交回来。如果只有一两个任务这个交代过程可以忽略但如果有几万个小任务交代本身的时间就超过了干活的时间。-- 逐行处理每次循环都触发一次上下文切换 BEGIN FOR rec IN (SELECT id, amount FROM orders WHERE status PENDING) LOOP UPDATE orders SET status DONE WHERE id rec.id; END LOOP; END; -- 批量处理一次上下文切换完成所有更新 BEGIN FORALL i IN 1..v_ids.COUNT UPDATE orders SET status DONE WHERE id v_ids(i); END;上面两种写法的功能完全一样但性能差距可能达到几十倍甚至上百倍。逐行处理时每一行都要经历“PL/SQL引擎发起UPDATE → SQL引擎执行 → 返回结果 → PL/SQL引擎继续循环”这个完整过程。而FORALL会把整个集合一次性交给SQL引擎只切换一次。3.2 什么时候该用FORALL什么时候不该用FORALL不是万能的。它适合“对集合中的每个元素执行同一条DML语句”的场景。如果每次执行的语句不一样或者需要根据前一次执行的结果决定下一次怎么做FORALL就不适用了。我一般用这个标准来判断如果循环体里只有一条DML语句且这条语句不依赖循环体内的其他逻辑就优先考虑FORALL。如果循环体里有IF判断、有多个DML、有异常处理分支那就老老实实用普通FOR循环。-- 适合FORALL循环体只有一条UPDATE BEGIN FORALL i IN 1..v_ids.COUNT UPDATE orders SET status DONE WHERE id v_ids(i); END; -- 不适合FORALL循环体有IF判断和多个操作 BEGIN FOR rec IN (SELECT id, amount, type FROM orders WHERE status PENDING) LOOP IF rec.type VIP THEN UPDATE orders SET status DONE, priority 1 WHERE id rec.id; INSERT INTO vip_log(order_id, action) VALUES (rec.id, PROCESSED); ELSE UPDATE orders SET status DONE WHERE id rec.id; END IF; END LOOP; END;3.3 隐式游标属性SQL%ROWCOUNT、SQL%FOUND的实战用法执行完一条DML语句后PL/SQL会提供几个隐式游标属性最常用的是SQL%ROWCOUNT、SQL%FOUND和SQL%NOTFOUND。这些属性反映的是最近一次DML语句的执行结果。BEGIN UPDATE orders SET status DONE WHERE id 1001; IF SQL%ROWCOUNT 0 THEN DBMS_OUTPUT.PUT_LINE(没有找到ID为1001的订单); ELSE DBMS_OUTPUT.PUT_LINE(更新了 || SQL%ROWCOUNT || 条记录); END IF; END;这里有个细节需要注意SQL%ROWCOUNT在DML语句执行后立即有效但如果中间又执行了别的SQL语句它就会被覆盖。所以如果你需要多次使用这个值应该先把它赋给一个变量。DECLARE v_count NUMBER; BEGIN UPDATE orders SET status DONE WHERE id 1001; v_count : SQL%ROWCOUNT; -- 先保存下来 INSERT INTO log_table(action, cnt) VALUES (UPDATE_ORDER, v_count); -- 此时 SQL%ROWCOUNT 已经变成 INSERT 的影响行数了 END;另一个容易踩的坑是SQL%FOUND和SQL%NOTFOUND在DML语句没有影响任何行时SQL%FOUND为FALSESQL%NOTFOUND为TRUE。但如果在DML之前没有执行过任何SQL语句这两个属性的值是NULL不是FALSE。所以不要依赖它们在DML之前的初始值。4. 流程控制语句IF、CASE、LOOP在真实业务里的写法4.1 IF语句的嵌套与扁平化IF语句是PL/SQL里最基础的流程控制结构但嵌套层数一多代码就变得难以维护。我见过一个存储过程里IF嵌套了七层读起来像在走迷宫。-- 嵌套过深难以阅读 IF v_type A THEN IF v_amount 1000 THEN IF v_status ACTIVE THEN -- 处理逻辑 END IF; END IF; END IF; -- 扁平化写法提前返回或跳过 IF v_type ! A THEN RETURN; END IF; IF v_amount 1000 THEN RETURN; END IF; IF v_status ! ACTIVE THEN RETURN; END IF; -- 处理逻辑扁平化的核心思路是把不满足条件的情况提前排除掉让主逻辑保持在最外层。这样读代码的人不需要在脑子里维护一个“当前在哪一层”的栈顺着往下读就行。4.2 CASE语句比IF更适合多分支等值判断当判断条件是“某个变量等于某个值”时CASE语句比IF-ELSIF链更清晰而且执行效率也略高因为CASE是跳转表式的匹配而IF-ELSIF是顺序比较。-- IF-ELSIF 链 IF v_level 1 THEN v_discount : 0.1; ELSIF v_level 2 THEN v_discount : 0.2; ELSIF v_level 3 THEN v_discount : 0.3; ELSE v_discount : 0; END IF; -- CASE 语句 v_discount : CASE v_level WHEN 1 THEN 0.1 WHEN 2 THEN 0.2 WHEN 3 THEN 0.3 ELSE 0 END;CASE还有一个优势它可以用在SQL语句内部而IF不行。所以在写SELECT或UPDATE时如果需要对不同值做不同处理CASE是唯一的选择。SELECT id, CASE status WHEN PENDING THEN 待处理 WHEN DONE THEN 已完成 ELSE 未知 END AS status_desc FROM orders;4.3 LOOP的三种形式与退出条件PL/SQL里有三种循环基本LOOP、FOR LOOP、WHILE LOOP。选择哪种取决于你知不知道循环次数、以及退出条件是什么。基本LOOP适合“至少执行一次根据内部条件决定是否继续”的场景。FOR LOOP适合“明确知道循环范围”的场景。WHILE LOOP适合“条件满足时持续循环”的场景。-- 基本LOOP至少执行一次 LOOP v_count : v_count 1; EXIT WHEN v_count 10; END LOOP; -- FOR LOOP范围明确 FOR i IN 1..10 LOOP DBMS_OUTPUT.PUT_LINE(i); END LOOP; -- WHILE LOOP条件驱动 WHILE v_count 10 LOOP v_count : v_count 1; END LOOP;我在实际项目里用得最多的是FOR LOOP尤其是配合游标使用。基本LOOP一般只在需要“先执行再判断”的场景下用比如从队列里取任务取到了就处理取不到就退出。注意在FOR LOOP里不要修改循环变量。PL/SQL的FOR循环变量是只读的试图修改会报错。如果需要根据条件跳过某次循环用CONTINUE如果需要提前退出用EXIT。5. 动态SQLEXECUTE IMMEDIATE的正确打开方式5.1 什么时候必须用动态SQL静态SQL在编译期就确定了语句结构而动态SQL是在运行期才拼接出来的。需要动态SQL的场景主要有三类一是表名或列名不固定需要根据参数决定二是DDL语句CREATE、DROP、ALTER不能直接写在PL/SQL里必须用动态SQL执行三是需要拼接复杂的WHERE条件。-- 表名不固定必须用动态SQL CREATE OR REPLACE PROCEDURE count_rows(p_table_name IN VARCHAR2) IS v_sql VARCHAR2(500); v_count NUMBER; BEGIN v_sql : SELECT COUNT(*) FROM || p_table_name; EXECUTE IMMEDIATE v_sql INTO v_count; DBMS_OUTPUT.PUT_LINE(p_table_name || 共有 || v_count || 行); END;5.2 绑定变量动态SQL的安全底线动态SQL最大的风险是SQL注入。如果直接把用户输入拼接到SQL字符串里恶意输入就可能改变语句结构。解决办法是使用绑定变量。-- 危险写法直接拼接 v_sql : SELECT * FROM orders WHERE id || p_id; EXECUTE IMMEDIATE v_sql; -- 安全写法使用绑定变量 v_sql : SELECT * FROM orders WHERE id :1; EXECUTE IMMEDIATE v_sql INTO v_rec USING p_id;绑定变量不仅安全还能提高性能。因为使用绑定变量的SQL语句数据库可以复用执行计划不需要每次重新解析。对于频繁执行的动态SQL这个差异非常明显。5.3 EXECUTE IMMEDIATE的INTO和USING子句EXECUTE IMMEDIATE支持INTO和USING两个子句。INTO用于接收查询结果USING用于传入绑定变量。需要注意的是USING默认是IN模式如果需要传出参数要显式指定OUT模式。DECLARE v_sql VARCHAR2(500); v_name VARCHAR2(50); v_count NUMBER; BEGIN -- INTO 接收单行单列结果 v_sql : SELECT name FROM users WHERE id :1; EXECUTE IMMEDIATE v_sql INTO v_name USING 1001; -- USING 传入多个参数 v_sql : UPDATE orders SET status :1 WHERE id :2; EXECUTE IMMEDIATE v_sql USING DONE, 1001; -- OUT 模式传出参数 v_sql : BEGIN get_order_count(:1, :2); END;; EXECUTE IMMEDIATE v_sql USING IN 1001, OUT v_count; END;这里有个细节如果动态SQL返回多行INTO子句会报错。这时候需要用OPEN FOR配合游标变量来处理。DECLARE v_sql VARCHAR2(500); v_cur SYS_REFCURSOR; v_id NUMBER; v_name VARCHAR2(50); BEGIN v_sql : SELECT id, name FROM users WHERE status :1; OPEN v_cur FOR v_sql USING ACTIVE; LOOP FETCH v_cur INTO v_id, v_name; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_id || : || v_name); END LOOP; CLOSE v_cur; END;5.4 动态SQL的性能考量动态SQL虽然灵活但每次执行都需要解析、编译、生成执行计划开销比静态SQL大。如果一段动态SQL在循环里反复执行性能问题会非常突出。我的做法是能静态就静态必须动态时尽量减少动态SQL的执行次数。比如需要根据不同的表名做统计可以先把所有表名收集到一个集合里然后批量处理而不是每张表都单独发起一次动态SQL。-- 不推荐循环内反复执行动态SQL FOR rec IN (SELECT table_name FROM config_tables) LOOP EXECUTE IMMEDIATE SELECT COUNT(*) FROM || rec.table_name INTO v_count; -- 处理 v_count END LOOP; -- 推荐先收集再批量处理如果业务允许 -- 或者至少把动态SQL的拼接放在循环外减少重复解析提示动态SQL里的字符串拼接一定要对表名、列名做白名单校验。不要相信任何外部传入的标识符哪怕它来自内部配置表。6. 执行语句的异常处理什么时候该捕获什么时候该放手6.1 异常传播的基本规则PL/SQL里的异常如果不捕获会一直向上传播直到被某个外层块捕获或者传到客户端。传播过程中异常的类型和错误信息会保留但异常发生的具体位置信息会丢失。BEGIN BEGIN UPDATE orders SET status DONE WHERE id 1001; -- 假设这里抛出了异常 EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(内层捕获 || SQLERRM); RAISE; -- 重新抛出让外层也能感知 END; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(外层捕获 || SQLERRM); END;RAISE关键字在不带参数时会重新抛出当前异常。这个用法在需要记录日志但又不想吞掉异常的场景下非常有用。6.2 WHEN OTHERS的正确用法与误用WHEN OTHERS能捕获所有异常但它也是最容易被误用的。我见过很多代码用WHEN OTHERS THEN NULL;来“忽略所有错误”结果出了问题完全查不到原因。-- 危险写法吞掉所有异常 BEGIN UPDATE orders SET status DONE WHERE id 1001; EXCEPTION WHEN OTHERS THEN NULL; -- 出错了也不知道 END; -- 正确写法记录日志后重新抛出 BEGIN UPDATE orders SET status DONE WHERE id 1001; EXCEPTION WHEN OTHERS THEN INSERT INTO error_log(proc_name, error_msg, error_time) VALUES (update_order, SQLERRM, SYSDATE); RAISE; -- 让调用方知道出错了 END;如果确实需要忽略某些特定异常应该明确指定异常类型而不是用WHEN OTHERS。BEGIN SELECT name INTO v_name FROM users WHERE id 1001; EXCEPTION WHEN NO_DATA_FOUND THEN v_name : 未知用户; -- 明确处理“没找到”的情况 END;6.3 自定义异常与RAISE_APPLICATION_ERROR除了系统预定义的异常PL/SQL还允许自定义异常。自定义异常适合表达业务逻辑上的错误比如“余额不足”“订单状态不允许此操作”等。DECLARE e_insufficient_balance EXCEPTION; v_balance NUMBER : 100; v_amount NUMBER : 200; BEGIN IF v_amount v_balance THEN RAISE e_insufficient_balance; END IF; EXCEPTION WHEN e_insufficient_balance THEN DBMS_OUTPUT.PUT_LINE(余额不足); END;如果希望错误信息能传到客户端用RAISE_APPLICATION_ERROR更合适。它可以指定错误码-20000到-20999之间和错误信息。BEGIN IF v_amount v_balance THEN RAISE_APPLICATION_ERROR(-20001, 余额不足当前余额 || v_balance); END IF; END;注意RAISE_APPLICATION_ERROR的错误码必须在这个范围内否则会和系统错误码冲突。错误信息长度不能超过2048字节。7. 批量执行语句的实战从逐行到批量的改造过程7.1 一个真实的改造案例之前有个数据迁移的存储过程需要把一张表里的几十万条记录逐条更新到另一张表。最初的写法是逐行循环跑一次要四十多分钟。后来改造成批量处理时间降到了两分钟以内。-- 改造前逐行处理 BEGIN FOR rec IN (SELECT id, name, amount FROM source_table) LOOP INSERT INTO target_table(id, name, amount) VALUES (rec.id, rec.name, rec.amount); END LOOP; COMMIT; END; -- 改造后批量处理 DECLARE TYPE t_id IS TABLE OF source_table.id%TYPE; TYPE t_name IS TABLE OF source_table.name%TYPE; TYPE t_amount IS TABLE OF source_table.amount%TYPE; v_ids t_id; v_names t_name; v_amounts t_amount; BEGIN SELECT id, name, amount BULK COLLECT INTO v_ids, v_names, v_amounts FROM source_table; FORALL i IN 1..v_ids.COUNT INSERT INTO target_table(id, name, amount) VALUES (v_ids(i), v_names(i), v_amounts(i)); COMMIT; END;改造的核心是BULK COLLECT和FORALL的配合使用。BULK COLLECT一次性把查询结果全部加载到集合变量里FORALL一次性把集合里的所有数据提交给SQL引擎。整个过程只有两次上下文切换一次查询一次插入。7.2 BULK COLLECT的LIMIT子句BULK COLLECT默认会把所有结果都加载到内存里。如果数据量特别大比如几百万行可能会耗尽PGA内存。这时候需要用LIMIT子句分批加载。DECLARE CURSOR c_source IS SELECT id, name FROM source_table; TYPE t_rec IS TABLE OF c_source%ROWTYPE; v_recs t_rec; BEGIN OPEN c_source; LOOP FETCH c_source BULK COLLECT INTO v_recs LIMIT 1000; EXIT WHEN v_recs.COUNT 0; FORALL i IN 1..v_recs.COUNT INSERT INTO target_table(id, name) VALUES (v_recs(i).id, v_recs(i).name); COMMIT; END LOOP; CLOSE c_source; END;LIMIT 1000表示每次只取1000行处理完再取下一批。这样内存占用可控同时批量处理的性能优势也保留了。具体LIMIT设多少需要根据每行数据的大小和可用的PGA内存来调整。我的经验是每行数据不大的话1000到5000比较合适如果每行有很长的字符串或LOB字段就降到100到500。7.3 FORALL的异常处理SAVE EXCEPTIONSFORALL默认是“全有或全无”的如果集合中任何一行执行失败整个FORALL语句都会回滚。但有时候我们希望“能成功的先成功失败的记录下来”。这时候可以用SAVE EXCEPTIONS子句。DECLARE e_bulk_errors EXCEPTION; PRAGMA EXCEPTION_INIT(e_bulk_errors, -24381); BEGIN FORALL i IN 1..v_ids.COUNT SAVE EXCEPTIONS INSERT INTO target_table(id, name) VALUES (v_ids(i), v_names(i)); EXCEPTION WHEN e_bulk_errors THEN FOR j IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP DBMS_OUTPUT.PUT_LINE(第 || SQL%BULK_EXCEPTIONS(j).ERROR_INDEX || 行失败 || SQL%BULK_EXCEPTIONS(j).ERROR_CODE); END LOOP; END;SQL%BULK_EXCEPTIONS是一个集合记录了每一行失败的索引和错误码。通过遍历这个集合可以知道哪些行出了问题然后针对性地处理。8. 执行语句的性能诊断从等待事件到执行计划8.1 用DBMS_PROFILER定位慢语句当一段PL/SQL跑得慢时第一步是定位到底哪条语句慢。DBMS_PROFILER可以按行统计执行时间精确到每一条SQL语句。BEGIN DBMS_PROFILER.START_PROFILER(my_profiling); -- 这里放需要分析的代码 DBMS_PROFILER.STOP_PROFILER; END;跑完之后查询PLSQL_PROFILER_DATA表按TOTAL_TIME降序排列就能看到哪条语句占用的时间最多。这个工具在排查“存储过程整体慢但不知道慢在哪”的问题时特别有用。8.2 执行计划理解SQL引擎的选择对于单条SQL语句最直接的诊断手段是看执行计划。在PL/SQL里可以用EXPLAIN PLAN或者查询V$SQL_PLAN来获取。EXPLAIN PLAN FOR SELECT * FROM orders WHERE status PENDING; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);执行计划会告诉你SQL引擎选择了全表扫描还是索引扫描、连接方式是嵌套循环还是哈希连接、预估行数和实际行数是否偏差很大。如果预估行数和实际行数差了几个数量级通常意味着统计信息过期了需要重新收集。8.3 常见的性能反模式在PL/SQL执行语句层面有几个反复出现的性能反模式反模式问题改进方案循环内逐行DML上下文切换次数等于行数用BULK COLLECT FORALL循环内COMMIT频繁提交导致日志同步等待批量处理完后统一COMMIT在SQL中调用PL/SQL函数每行都要切换引擎尽量用SQL内置函数替代动态SQL不带绑定变量每次都要硬解析使用USING子句绑定变量SELECT INTO取多行运行期报TOO_MANY_ROWS用游标或加ROWNUM限制其中“在SQL中调用PL/SQL函数”这一条特别隐蔽。比如SELECT my_function(col) FROM big_table看起来是一条SQL实际上每一行都会触发一次PL/SQL引擎调用。如果my_function里还有SQL语句那就是嵌套的上下文切换性能会非常差。9. 一些让我少熬几个夜的写法习惯写了这么多年的PL/SQL有几个习惯是踩过坑之后慢慢养成的分享出来供参考。第一个习惯所有变量声明时给默认值。数值给0字符串给空串日期给SYSDATE或一个明确的基准日期。这样就不会因为NULL传播导致莫名其妙的逻辑错误。第二个习惯DML语句执行后立即检查SQL%ROWCOUNT。如果预期应该影响一行但实际影响了零行或多行马上抛异常或记录日志。不要等到后面数据对不上了才回头查。第三个习惯动态SQL的拼接部分和绑定变量部分分开写。拼接部分只放表名、列名这些标识符并且做白名单校验值一律用绑定变量。这样既安全又高效。第四个习惯批量处理时先小批量试跑。比如LIMIT 1000先跑一批看看结果对不对再放大到全量。直接跑全量如果逻辑有问题回滚都要等半天。第五个习惯异常处理里永远不要写WHEN OTHERS THEN NULL。要么记录日志后重新抛出要么明确指定要忽略的异常类型。吞掉异常等于把问题埋起来迟早会炸。第六个习惯在循环里避免COMMIT。把COMMIT放在循环外面或者按批次COMMIT。频繁COMMIT不仅慢还会导致“提交了但后续步骤失败”的数据不一致问题。这些习惯看起来都是小事但每一个背后都有过真实的教训。PL/SQL的执行语句语法层面并不复杂难的是在正确的场景下选择正确的写法并且对边界情况有足够的预判。希望这篇笔记能帮你在写执行语句时多一分笃定少一分试错。