本文系统介绍了Oracle数据库中的游标与动态SQL技术。
主要内容包括:
- 游标概念与分类(隐式/显式游标)及其四大属性(ISOPEN/FOUND/NOTFOUND/ROWCOUNT);
- 显式游标的使用方法,包括定义、遍历和参数传递;
- 动态SQL的EXECUTE IMMEDIATE语法,重点说明SELECT...INTO在动态SQL中的特殊写法;
- 批量数据处理技巧和注意事项。
文章通过丰富示例演示了游标遍历、批量提交、动态表操作等典型场景,并对比了静态SQL与动态SQL的差异,为数据库开发提供了实用指导。
(课堂笔记)游标与动态SQL
🧩 一、游标(CURSOR)
1. 什么是游标?
-
游标是指向数据库表中每一行数据的指针。
-
用于逐行处理查询结果集。
2. 游标分类
| 类型 | 说明 |
|---|---|
| 隐式游标 | 自动创建,执行 DML(INSERT/UPDATE/DELETE)或 SELECT INTO 时自动生成 |
| 显式游标 | 开发者手动定义,指向一个 SELECT 语句的结果集 |
隐式游标
| 项目 | 说明 |
|---|---|
| 触发时机 | 执行 DML(UPDATE/DELETE/INSERT)或 SELECT INTO 时自动创建 |
| 开发者操作 | 无需定义,直接使用 |
| 属性引用 | SQL%属性名 |
显式游标
| 项目 | 说明 |
|---|---|
| 定义方式 | CURSOR 游标名 IS SELECT 语句; |
| 本质 | 类似一个大的变量 ,用于接收一个表格(结果集) |
| 使用场景 | 批量提交、批量更新、逐行处理数据 |
| 属性引用 | 游标名%属性名 |
3. 游标的四大属性(非常重要)
| 属性 | 类型 | 说明 |
|---|---|---|
%ISOPEN |
布尔值 | 游标是否打开 |
%FOUND |
布尔值 | 是否找到数据行 |
%NOTFOUND |
布尔值 | 是否未找到数据行 |
%ROWCOUNT |
数字 | 已遍历的行数 |
使用方式:
-
隐式游标:
SQL%属性名 -
显式游标:
游标名%属性名
🧪 二、隐式游标示例
sql
BEGIN
UPDATE EMP SET SAL = SAL + 100 WHERE DEPTNO = 10;
IF SQL%FOUND THEN
DBMS_OUTPUT.put_line('有数据被更新!');
DBMS_OUTPUT.PUT_LINE('总共有:' || SQL%ROWCOUNT || ' 行数据被更新。');
END IF;
COMMIT;
END;
🛠 三、显式游标
创建语法(放在 BEGIN 之前)
sql
CURSOR 游标名(参数名 参数类型)
IS
SELECT 语句;
示例1:遍历 20 号部门员工
sql
CREATE OR REPLACE PROCEDURE P_005 IS
CURSOR CUR_001 IS
SELECT * FROM EMP WHERE DEPTNO = 20;
BEGIN
FOR I IN CUR_001 LOOP
DBMS_OUTPUT.PUT_LINE(I.ENAME || '--' || I.SAL);
END LOOP;
END;
示例2:根据传入员工编号,打印其所在部门所有员工
sql
CREATE OR REPLACE PROCEDURE P_007(P_NUM NUMBER) IS
VVVV NUMBER;
CURSOR CUR_002(P_DEPTNO NUMBER) IS
SELECT ENAME FROM EMP WHERE DEPTNO = P_DEPTNO;
BEGIN
SELECT DEPTNO INTO VVVV FROM EMP WHERE EMPNO = P_NUM;
FOR I IN CUR_002(VVVV) LOOP
DBMS_OUTPUT.PUT_LINE(I.ENAME);
END LOOP;
END;
示例3:返回员工所在部门的所有员工姓名(拼接)
sql
CREATE OR REPLACE FUNCTION KKK(P_EMPNO NUMBER) RETURN VARCHAR2 IS
V_DEPTNO NUMBER;
CURSOR CUR_001(P_DEPT NUMBER) IS
SELECT ENAME FROM EMP WHERE DEPTNO = P_DEPT;
V_RES VARCHAR2(1000) := '';
BEGIN
SELECT DEPTNO INTO V_DEPTNO FROM EMP WHERE EMPNO = P_EMPNO;
FOR I IN CUR_001(V_DEPTNO) LOOP
V_RES := V_RES || I.ENAME || ',';
END LOOP;
V_RES := SUBSTR(V_RES, 1, LENGTH(V_RES) - 1);
RETURN V_RES;
END;
对比记忆表
| 对比项 | 隐式游标 | 显式游标 |
|---|---|---|
| 是否需定义 | ❌ 不需要 | ✅ 需要 CURSOR...IS... |
| 触发方式 | 自动(DML/SELECT INTO) | 手动(OPEN/FOR循环) |
| 结果集大小 | 通常单行 | 多行 |
| 典型场景 | 单行查询、DML操作 | 批量遍历、批量提交 |
| 属性引用 | SQL%... |
游标名%... |
⚠️ 四、变量 vs 参数
| 类型 | 可否在内部赋值 |
|---|---|
| 参数(入参) | ❌ 不能二次赋值 |
变量(IS 中定义) |
✅ 可以任意赋值 |
sql
CREATE OR REPLACE PROCEDURE PPP(P_NUM NUMBER) IS
BEGIN
-- P_NUM := 2222; ❌ 错误,参数不能赋值
DBMS_OUTPUT.PUT_LINE(P_NUM);
END;
📦 五、综合练习(游标 + 批量插入)
创建空表并插入数据
sql
-- 创建空表
CREATE TABLE EMP_0508 AS SELECT * FROM EMP WHERE 1=2;
-- 存储过程:插入指定部门数据
CREATE OR REPLACE PROCEDURE P_000(P_DEPT NUMBER) IS
CURSOR CUR_001(T_DEPTNO NUMBER) IS
SELECT * FROM EMP WHERE DEPTNO = T_DEPTNO;
BEGIN
FOR I IN CUR_001(P_DEPT) LOOP
INSERT INTO EMP_0508 VALUES
(I.EMPNO, I.ENAME, I.JOB, I.MGR, I.HIREDATE, I.SAL, I.COMM, I.DEPTNO);
END LOOP;
COMMIT;
END;
🔁 六、批量提交(避免 UNDO 表空间不足)
sql
CREATE OR REPLACE PROCEDURE P_012 IS
CURSOR CUR_012 IS SELECT * FROM EMP;
BEGIN
FOR I IN CUR_012 LOOP
INSERT INTO EMP_0508 VALUES (...);
IF MOD(CUR_012%ROWCOUNT, 100000) = 0 THEN
COMMIT;
END IF;
END LOOP;
COMMIT;
END;
✅ 每 10 万条提交一次,适合大数据量操作。
⚡ 七、动态 SQL
使用场景:
-
SQL 语句结构不固定(如动态表名、条件)
-
执行 DDL 语句(CREATE / DROP / TRUNCATE)
| 场景 | 说明 | 示例 |
|---|---|---|
| 场景1 | SQL中存在变量,且变量在单引号内部 | 动态表名、动态条件 |
| 场景2 | 需要执行DDL语句 | TRUNCATE、DROP、CREATE |
sql
-- ❌ 错误写法(变量在单引号内,不会被解析)
V_SQL := 'SELECT * FROM EMP WHERE ENAME = 'ENAME'';
-- ✅ 正确写法:变量用 || 拼接
V_SQL := 'SELECT * FROM EMP WHERE ENAME = ''' || V_ENAME || '''';
| 要点 | 说明 |
|---|---|
| 单引号处理 | 用 ''''(4个单引号)或 CHR(39) 表示一个单引号 |
| 变量/参数 | 严禁放在单引号里面,必须用 ` |
核心语法:
sql
sql
-- 语法1:执行无返回值的SQL(DDL、DML)
EXECUTE IMMEDIATE SQL语句字符串;
-- 语法2:执行有返回值的SELECT,结果存入变量
EXECUTE IMMEDIATE SELECT语句字符串 INTO 变量1, 变量2...;
示例1:批量创建 100 张表
sql
CREATE OR REPLACE PROCEDURE P_CRE_TABLE IS
V_STR VARCHAR2(100);
BEGIN
FOR I IN 1..100 LOOP
V_STR := 'CREATE TABLE EMP__' || LPAD(I, 3, 0) ||
' AS SELECT * FROM EMP WHERE 1=2';
EXECUTE IMMEDIATE V_STR;
END LOOP;
END;
示例2:批量删除表
sql
CREATE OR REPLACE PROCEDURE P_013 IS
V_SQL VARCHAR2(1000);
BEGIN
FOR I IN 1..100 LOOP
V_SQL := 'DROP TABLE EMP__' || LPAD(I, 3, 0);
EXECUTE IMMEDIATE V_SQL;
END LOOP;
END;
示例3:TRUNCATE 表
sql
CREATE OR REPLACE PROCEDURE P_014 IS
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE EMP_0508';
INSERT INTO EMP_0508 SELECT * FROM EMP;
COMMIT;
END;
示例4:动态 SELECT INTO
sql
EXECUTE IMMEDIATE 'SELECT ENAME FROM EMP WHERE EMPNO = 7369' INTO V_ENAME;
🧠 常见考点 & 面试题
Q:游标的使用场景?
-
批量提交
-
批量更新
-
逐行处理数据
Q:隐式游标和显式游标的区别?
| 隐式游标 | 显式游标 | |
|---|---|---|
| 定义 | 自动 | 手动 CURSOR |
| 使用场景 | DML / SELECT INTO | 多行结果集遍历 |
| 属性引用 | SQL%... |
游标名%... |
Q:四大属性是什么?
-
%ISOPEN -
%FOUND -
%NOTFOUND -
%ROWCOUNT
✅ 复习建议
-
掌握隐式和显式游标的区别及使用方式
-
熟练写出显式游标的定义 + FOR 循环遍历
-
理解四大属性的含义和输出时机
-
掌握动态 SQL 的写法,尤其是字符串拼接中的引号处理
-
会写批量提交的逻辑
如果 动态执行的 是 SELECT .. INTO 语句 则需要改写成 EXECUTE IMMEDIATE SQL语句字符串 INTO 变量1,变量2... 是什么意思
详细解释:动态 SQL 中的 SELECT ... INTO
你的笔记中这句话的意思是:
当你用动态SQL执行一个查询语句(SELECT)并且想把查询结果存储到变量中时,需要使用
EXECUTE IMMEDIATE ... INTO ...语法
📌 为什么要这样写?
普通情况(静态SQL)
sql
DECLARE
V_ENAME VARCHAR2(100);
BEGIN
-- 直接使用 SELECT INTO,结果存入变量
SELECT ENAME INTO V_ENAME
FROM EMP
WHERE EMPNO = 7369;
DBMS_OUTPUT.PUT_LINE(V_ENAME); -- 输出:SMITH
END;
动态SQL情况
sql
DECLARE
V_ENAME VARCHAR2(100);
V_SQL VARCHAR2(1000);
BEGIN
-- SQL 语句被写成了字符串
V_SQL := 'SELECT ENAME FROM EMP WHERE EMPNO = 7369';
-- 必须用 EXECUTE IMMEDIATE ... INTO
EXECUTE IMMEDIATE V_SQL INTO V_ENAME;
DBMS_OUTPUT.PUT_LINE(V_ENAME); -- 输出:SMITH
END;
🎯 核心语法
sql
EXECUTE IMMEDIATE 'SELECT 语句字符串' INTO 变量1, 变量2, ...;
示例1:查询单个值
sql
DECLARE
V_ENAME VARCHAR2(100);
V_SQL VARCHAR2(1000);
BEGIN
V_SQL := 'SELECT ENAME FROM EMP WHERE EMPNO = 7499';
EXECUTE IMMEDIATE V_SQL INTO V_ENAME;
DBMS_OUTPUT.PUT_LINE(V_ENAME); -- 输出:ALLEN
END;
示例2:查询多个值
sql
DECLARE
V_ENAME VARCHAR2(100);
V_SAL NUMBER;
V_SQL VARCHAR2(1000);
BEGIN
V_SQL := 'SELECT ENAME, SAL FROM EMP WHERE EMPNO = 7499';
EXECUTE IMMEDIATE V_SQL INTO V_ENAME, V_SAL;
DBMS_OUTPUT.PUT_LINE(V_ENAME || '的工资是:' || V_SAL);
END;
💡 你笔记中的实际案例解析
sql
CREATE OR REPLACE PROCEDURE P_021(X VARCHAR2) IS
V_CNT NUMBER;
V_SQL VARCHAR2(1000);
V_ENAME VARCHAR2(100);
BEGIN
-- 先统计有多少符合条件的员工
SELECT COUNT(1) INTO V_CNT
FROM EMP WHERE INSTR(ENAME, X) > 0;
IF V_CNT = 0 THEN
DBMS_OUTPUT.PUT_LINE('没有符合条件的员工!');
ELSE
-- 循环取出每个员工
FOR I IN 1..V_CNT LOOP
-- 动态构造分页查询的SQL字符串
V_SQL :=
'SELECT ENAME
FROM
(SELECT ENAME, ROW_NUMBER() OVER (ORDER BY 1) AS RN
FROM EMP
WHERE ENAME LIKE ' || CHR(39) || '%' || X || '%' || CHR(39) || ')
WHERE RN = ' || I;
-- ⭐ 关键:将动态查询的结果存入变量 V_ENAME
EXECUTE IMMEDIATE V_SQL INTO V_ENAME;
DBMS_OUTPUT.PUT_LINE(V_ENAME);
END LOOP;
END IF;
END;
⚠️ 注意事项
1. 查询结果必须只有一行
-
EXECUTE IMMEDIATE ... INTO要求查询结果只能返回一行 -
如果返回多行会报错
ORA-01422: exact fetch returns more than requested number of rows
2. 变量数量必须匹配
sql
-- ❌ 错误:2个变量但只查询1列
EXECUTE IMMEDIATE 'SELECT ENAME FROM EMP WHERE EMPNO=7369' INTO V_ENAME, V_SAL;
-- ✅ 正确:查询2列对应2个变量
EXECUTE IMMEDIATE 'SELECT ENAME, SAL FROM EMP WHERE EMPNO=7369' INTO V_ENAME, V_SAL;
3. 字符串中的引号处理
sql
-- 两种写法等价
-- 方法1:用两个单引号表示一个单引号
V_SQL := 'SELECT ENAME FROM EMP WHERE ENAME LIKE ''%S%''';
-- 方法2:用 CHR(39) 表示单引号(更清晰)
V_SQL := 'SELECT ENAME FROM EMP WHERE ENAME LIKE ' || CHR(39) || '%S%' || CHR(39);
📊 对比总结
| 场景 | 语法 | 示例 |
|---|---|---|
| 静态SQL | SELECT ... INTO |
SELECT ENAME INTO V_ENAME FROM EMP... |
| 动态SQL | EXECUTE IMMEDIATE ... INTO |
EXECUTE IMMEDIATE V_SQL INTO V_ENAME |
✅ 一句话记忆
动态SQL是字符串,要存结果加INTO,变量数量要匹配,查询必须单一行。
关联阅读推荐