EXECUTE IMMEDIATE 是 Oracle 动态 SQL 的核心机制:DDL 必须用它执行且自动提交;DML 需显式 COMMIT 并用 USING 绑定值;查询单行用 INTO,多行须用 REF CURSOR 或临时表;对象名拼接必须经 DBMS_ASSERT 校验防注入。

EXECUTE IMMEDIATE 是 Oracle 存储过程中执行动态 SQL 最常用、最直接的方式。它不需预编译、不需游标变量、语法简洁,但不是万能的——用错场景或忽略细节,轻则报错,重则数据异常或 SQL 注入。
DDL 语句必须用 EXECUTE IMMEDIATE,不能写在 PL/SQL 块里直写
Oracle 不允许在 PL/SQL 匿名块或存储过程中直接写 CREATE TABLE、TRUNCATE TABLE、ALTER INDEX 等 DDL 语句,否则编译报错 PLS-00103: Encountered the symbol "CREATE"。
这是因为 DDL 会隐式提交事务,破坏 PL/SQL 的事务上下文。必须包装成字符串后交给 EXECUTE IMMEDIATE 执行:
- DDL 执行后自动
COMMIT,之前未提交的 DML 也会一并提交 - 不支持
INTO或USING子句(DDL 没有绑定变量概念) - 若拼接表名/字段名,务必校验输入合法性(如用
DBMS_ASSERT.SQL_OBJECT_NAME),否则极易被注入
示例:安全建表
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE ' ||
DBMS_ASSERT.SQL_OBJECT_NAME('tmp_log_' || TO_CHAR(SYSDATE, 'yyyymmdd')) ||
' (id NUMBER, msg VARCHAR2(4000))';
END;
EXECUTE IMMEDIATE 执行 DML 时必须显式 COMMIT
和 DDL 不同,INSERT/UPDATE/DELETE 动态执行后**不会自动提交**。如果存储过程结束前没 COMMIT,调用方看到的是未提交状态,回滚后数据就丢了。
常见错误现象:
- 过程执行成功,查不到刚插入的数据(其他会话不可见)
- 过程第二次执行报
ORA-00001: unique constraint violated(其实第一次已插入但未提交,只是自己会话里看不见)
正确做法:
- 明确业务是否需要事务控制 —— 若是独立操作,加
COMMIT;若是大事务一部分,交由调用方统一提交 - 避免在
EXECUTE IMMEDIATE后直接写COMMIT,应判断是否真执行成功(加EXCEPTION处理) - 不要用字符串拼接值,一律用
USING绑定变量,防注入且支持 CLOB/BLOB
示例:带绑定与异常处理的插入
DECLARE
v_sql VARCHAR2(500) := 'INSERT INTO log_table (ts, level, msg) VALUES (SYSDATE, :lvl, :msg)';
BEGIN
EXECUTE IMMEDIATE v_sql USING 'ERROR', 'Connection timeout';
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;查询单行结果用 INTO,多行必须用 REF CURSOR 或临时表
EXECUTE IMMEDIATE **不支持返回多行结果集**。若强行对 SELECT ... FROM ... WHERE ... 使用 INTO,只取第一行,其余丢弃,且无警告。
典型误用场景:
- 想动态查出所有匹配用户,却写
EXECUTE IMMEDIATE 'SELECT name FROM users WHERE dept=:d' INTO v_name USING p_dept—— 只拿到一个名字,还可能报ORA-01422: exact fetch returns more than requested number of rows - 以为
BULK COLLECT INTO能用于任意动态查询 —— 它只支持单行或多行,但要求目标变量类型严格匹配(如TABLE OF VARCHAR2),且无法直接返回给调用方
真正可交付的方案只有两个:
- 用
REF CURSOR(推荐):声明TYPE cur_type IS REF CURSOR,OPEN cur FOR v_sql,把游标作为OUT参数传出 - 写入全局临时表(
GTTS):适合中间计算,但需提前建表、注意事务可见性
示例:返回动态查询结果集
CREATE OR REPLACE PROCEDURE get_users_by_dept( p_dept IN VARCHAR2, p_cursor OUT SYS_REFCURSOR ) AS v_sql VARCHAR2(500); BEGIN v_sql := 'SELECT empno, ename, job FROM emp WHERE deptno = :dept'; OPEN p_cursor FOR v_sql USING p_dept; END;
USING 和 RETURNING INTO 的边界很窄,别硬套
USING 只能传入值(IN),不能传出;RETURNING INTO 只支持 DML 语句(INSERT/UPDATE/DELETE),且仅限单行。这两者组合使用时极易踩坑。
常见问题:
- 试图在
UPDATE中用RETURNING INTO获取多个字段,但变量数量或类型与列不一致 → 报ORA-06502 - 在
USING中传入NULL,而目标字段为NOT NULL→ 直接失败,不会跳过 - 对
INSERT ... SELECT用RETURNING→ 不支持,语法报错
关键限制:
-
RETURNING后只能跟列名,不能是表达式或别名 - 绑定变量顺序必须和
USING列表、RETURNING列表完全一致 - 若 DML 影响 0 行,
RETURNING INTO变量保持原值(不会置空),需手动初始化
示例:安全获取新插入记录的主键
DECLARE
v_new_id NUMBER;
BEGIN
EXECUTE IMMEDIATE
'INSERT INTO orders (cust_id, amt) VALUES (:c, :a) RETURNING id INTO :r'
USING 1001, 299.99, OUT v_new_id;
DBMS_OUTPUT.PUT_LINE('New order ID: ' || v_new_id);
END;动态 SQL 的核心矛盾在于:灵活性以牺牲编译期检查为代价。拼接字符串看着简单,但表名错一个字母、少一个引号、绑定顺序错一位,都会在运行时报错。真正难的不是写出来,而是让每次执行都稳定、可审计、不越权。尤其当输入来自应用层时,DBMS_ASSERT 不是可选项,是必选项。


















