SQL Server 2019 游标 --- 语法知识点及使用方法详解
一、认识游标
1. 游标的概念
游标(Cursor) 是数据库中用于逐行处理结果集的一种机制。它允许应用程序对查询返回的多行数据进行逐行遍历、定位、修改或删除,弥补了标准SQL语句一次操作整个结果集的局限性。
⚠️ 注意:游标是逐行处理,性能通常低于基于集合的操作,应谨慎使用。
2. 游标的优点
- 逐行控制:可对结果集中每一行执行不同逻辑。
- 灵活定位:支持向前、向后、跳转到指定行(取决于游标类型)。
- 支持修改 :可结合
WHERE CURRENT OF子句更新或删除当前行。 - 兼容性好:适用于需要逐行处理的复杂业务逻辑(如报表、审计、数据迁移)。
3. 游标的分类
| 分类维度 | 类型 | 说明 |
|---|---|---|
| 作用域 | LOCAL(局部) |
仅在声明它的批处理、存储过程或触发器中有效 |
GLOBAL(全局) |
在连接级别有效,多个批处理可共享(不推荐) | |
| 滚动方向 | FORWARD_ONLY(默认) |
只能从第一行滚动到最后一行(FETCH NEXT) |
SCROLL |
支持任意方向滚动(NEXT, PRIOR, FIRST, LAST, ABSOLUTE n, RELATIVE n) |
|
| 数据敏感性 | STATIC |
静态快照,不反映后续修改 |
KEYSET |
键集驱动,键值固定,非键列值可变 | |
DYNAMIC |
动态反映所有修改(性能开销大) | |
FAST_FORWARD |
FORWARD_ONLY + READ_ONLY 的优化组合,性能最佳 |
|
| 修改能力 | READ_ONLY |
不允许修改 |
SCROLL_LOCKS |
保证可更新(通过锁) | |
OPTIMISTIC |
乐观并发控制(通过时间戳或比较值) |
二、游标的基本操作
游标操作五步曲:
- 声明游标 (
DECLARE CURSOR) - 打开游标 (
OPEN) - 读取数据 (
FETCH) - 关闭游标 (
CLOSE) - 释放游标 (
DEALLOCATE)
案例1:基本游标操作 --- 遍历员工表并打印信息
sql
-- 创建测试表
CREATE TABLE dbo.Employees (
EmployeeID INT PRIMARY KEY,
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
Salary DECIMAL(10,2)
);
GO
INSERT INTO dbo.Employees VALUES
(1, '张', '三', 8000.00),
(2, '李', '四', 9500.00),
(3, '王', '五', 7200.00);
GO
-- 声明变量用于接收游标数据
DECLARE @EmpID INT, @FirstName NVARCHAR(50), @LastName NVARCHAR(50), @Salary DECIMAL(10,2);
-- 1. 声明游标
-- 使用 FAST_FORWARD(只读、只向前)提高性能
DECLARE emp_cursor CURSOR FAST_FORWARD FOR
SELECT EmployeeID, FirstName, LastName, Salary
FROM dbo.Employees
ORDER BY Salary DESC; -- 按薪资降序排列
-- 2. 打开游标
OPEN emp_cursor;
-- 3. 读取第一行数据
FETCH NEXT FROM emp_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
-- 循环读取所有行
WHILE @@FETCH_STATUS = 0 -- 0=成功, -1=失败或行不存在, -2=行已删除
BEGIN
-- 打印员工信息
PRINT '员工ID: ' + CAST(@EmpID AS NVARCHAR) +
', 姓名: ' + @FirstName + @LastName +
', 薪资: ' + CAST(@Salary AS NVARCHAR);
-- 读取下一行
FETCH NEXT FROM emp_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
END
-- 4. 关闭游标
CLOSE emp_cursor;
-- 5. 释放游标资源
DEALLOCATE emp_cursor;
GO
注释:
FAST_FORWARD是性能最优的只读、只向前游标。@@FETCH_STATUS是全局变量,指示上一次FETCH操作状态。FETCH NEXT读取下一行(对于FORWARD_ONLY是唯一选项)。- 必须显式
CLOSE和DEALLOCATE,否则会占用资源。
三、游标的运用
1. 使用游标变量
游标可以声明为变量类型 CURSOR,用于动态传递或作为存储过程参数。
sql
-- 声明游标变量
DECLARE @MyCursor CURSOR;
-- 将游标赋值给变量
SET @MyCursor = CURSOR FAST_FORWARD FOR
SELECT EmployeeID, FirstName, LastName
FROM dbo.Employees
WHERE Salary > 8000;
-- 打开游标变量
OPEN @MyCursor;
DECLARE @ID INT, @FN NVARCHAR(50), @LN NVARCHAR(50);
FETCH NEXT FROM @MyCursor INTO @ID, @FN, @LN;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT '高薪员工: ' + @FN + @LN + ' (ID: ' + CAST(@ID AS NVARCHAR) + ')';
FETCH NEXT FROM @MyCursor INTO @ID, @FN, @LN;
END
CLOSE @MyCursor;
DEALLOCATE @MyCursor; -- 释放变量游标
GO
注释:
- 游标变量需用
SET或SELECT赋值。- 使用方式与普通游标相同。
- 适用于模块化编程或动态SQL场景。
2. 使用游标为变量赋值(多行转单值处理)
sql
-- 场景:获取薪资最高的3名员工姓名拼接成字符串
DECLARE @TopEmployees NVARCHAR(MAX) = '';
DECLARE @Name NVARCHAR(100);
DECLARE top_emp_cursor CURSOR FAST_FORWARD FOR
SELECT TOP 3 FirstName + ' ' + LastName AS FullName
FROM dbo.Employees
ORDER BY Salary DESC;
OPEN top_emp_cursor;
FETCH NEXT FROM top_emp_cursor INTO @Name;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @TopEmployees = @TopEmployees + @Name + '; ';
FETCH NEXT FROM top_emp_cursor INTO @Name;
END
CLOSE top_emp_cursor;
DEALLOCATE top_emp_cursor;
-- 输出结果
PRINT '薪资最高的员工: ' + LEFT(@TopEmployees, LEN(@TopEmployees) - 2); -- 去掉末尾分号
GO
注释:
- 游标常用于聚合字符串等集合函数无法直接实现的操作。
- 替代方案:SQL Server 2017+ 可用
STRING_AGG()函数。
3. 使用 ORDER BY 子句改变游标中行的顺序
sql
-- 声明游标时使用 ORDER BY 控制遍历顺序
DECLARE emp_by_name_cursor CURSOR SCROLL FOR -- SCROLL允许任意滚动
SELECT EmployeeID, FirstName, LastName, Salary
FROM dbo.Employees
ORDER BY LastName ASC, FirstName ASC; -- 按姓氏、名字升序
OPEN emp_by_name_cursor;
-- 读取第一行
FETCH FIRST FROM emp_by_name_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
PRINT '第一行: ' + @LastName + @FirstName;
-- 读取最后一行
FETCH LAST FROM emp_by_name_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
PRINT '最后一行: ' + @LastName + @FirstName;
-- 读取前一行(从最后一行往前)
FETCH PRIOR FROM emp_by_name_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
PRINT '倒数第二行: ' + @LastName + @FirstName;
-- 跳转到绝对位置(第2行)
FETCH ABSOLUTE 2 FROM emp_by_name_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
PRINT '第2行: ' + @LastName + @FirstName;
-- 相对当前位置移动(+1 下一行)
FETCH RELATIVE 1 FROM emp_by_name_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
PRINT '第3行: ' + @LastName + @FirstName;
CLOSE emp_by_name_cursor;
DEALLOCATE emp_by_name_cursor;
GO
注释:
SCROLL游标支持FIRST,LAST,PRIOR,ABSOLUTE n,RELATIVE n。ABSOLUTE n:n>0 从第一行开始,n<0 从最后一行开始。RELATIVE n:n>0 向后,n<0 向前。- 性能低于
FAST_FORWARD,仅在需要时使用。
4. 使用游标修改数据(UPDATE ... WHERE CURRENT OF)
sql
-- 为薪资低于8000的员工加薪10%
DECLARE @CurrentSalary DECIMAL(10,2);
-- 声明可更新游标(必须包含主键或唯一索引列)
DECLARE update_cursor CURSOR FOR
SELECT EmployeeID, Salary -- 包含主键EmployeeID用于定位
FROM dbo.Employees
WHERE Salary < 8000
FOR UPDATE OF Salary; -- 指定可更新的列
OPEN update_cursor;
FETCH NEXT FROM update_cursor INTO @EmpID, @CurrentSalary;
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @NewSalary DECIMAL(10,2) = @CurrentSalary * 1.10;
-- 更新当前行
UPDATE dbo.Employees
SET Salary = @NewSalary
WHERE CURRENT OF update_cursor; -- 关键:WHERE CURRENT OF
PRINT '员工ID ' + CAST(@EmpID AS NVARCHAR) +
' 薪资从 ' + CAST(@CurrentSalary AS NVARCHAR) +
' 更新为 ' + CAST(@NewSalary AS NVARCHAR);
FETCH NEXT FROM update_cursor INTO @EmpID, @CurrentSalary;
END
CLOSE update_cursor;
DEALLOCATE update_cursor;
-- 查看更新结果
SELECT * FROM dbo.Employees;
GO
注释:
FOR UPDATE [OF column_list]声明游标可更新。WHERE CURRENT OF cursor_name定位到当前游标行进行更新。- 游标查询必须包含足够信息定位行(如主键)。
5. 使用游标删除数据(DELETE ... WHERE CURRENT OF)
sql
-- 删除薪资低于7500的员工(谨慎操作!)
DECLARE delete_cursor CURSOR FOR
SELECT EmployeeID, FirstName, LastName, Salary
FROM dbo.Employees
WHERE Salary < 7500
FOR UPDATE; -- 可省略列,表示所有列可更新(含删除)
OPEN delete_cursor;
FETCH NEXT FROM delete_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
WHILE @@FETCH_STATUS = 0
BEGIN
-- 删除当前行
DELETE FROM dbo.Employees
WHERE CURRENT OF delete_cursor;
PRINT '已删除员工: ' + @FirstName + @LastName + ' (原薪资: ' + CAST(@Salary AS NVARCHAR) + ')';
-- 注意:删除后@@FETCH_STATUS可能为-2,需重新FETCH
FETCH NEXT FROM delete_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
END
CLOSE delete_cursor;
DEALLOCATE delete_cursor;
-- 查看剩余员工
SELECT * FROM dbo.Employees;
GO
注释:
DELETE ... WHERE CURRENT OF删除当前游标行。- 删除后当前行消失,下次
FETCH可能失败(@@FETCH_STATUS = -2)。- 生产环境慎用,建议先备份或使用逻辑删除。
四、使用系统存储过程管理游标
SQL Server 提供系统存储过程用于查看游标元数据:
1. sp_cursor_list --- 列出当前连接的游标
sql
-- 声明一个游标(不打开)
DECLARE test_cursor CURSOR FOR SELECT * FROM dbo.Employees;
-- 列出当前连接的所有游标
-- @cursor_scope: 1=本地游标, 2=全局游标, 3=全部
EXEC sp_cursor_list @cursor_return = 0 OUTPUT, @cursor_scope = 3;
GO
输出列 :
cursor_handle,cursor_name,cursor_scope,status,model,concurrency,scrollable,open_status等。
2. sp_describe_cursor --- 描述游标属性
sql
-- 声明并打开游标
DECLARE emp_desc_cursor CURSOR SCROLL FOR
SELECT EmployeeID, FirstName, LastName FROM dbo.Employees;
OPEN emp_desc_cursor;
-- 描述游标属性
EXEC sp_describe_cursor
@cursor_return = 0 OUTPUT,
@cursor_source = N'local', -- 'local', 'global', 'variable'
@cursor_identity = N'emp_desc_cursor';
-- 关闭释放
CLOSE emp_desc_cursor;
DEALLOCATE emp_desc_cursor;
GO
输出:游标的声明方式、滚动性、可更新性、状态等详细信息。
3. sp_describe_cursor_columns --- 描述游标结果集列
sql
DECLARE col_cursor CURSOR FOR
SELECT EmployeeID, FirstName + ' ' + LastName AS FullName, Salary
FROM dbo.Employees;
OPEN col_cursor;
-- 描述游标结果集的列信息
EXEC sp_describe_cursor_columns
@cursor_return = 0 OUTPUT,
@cursor_source = N'local',
@cursor_identity = N'col_cursor';
CLOSE col_cursor;
DEALLOCATE col_cursor;
GO
输出列 :
column_name,ordinal_position,data_type,max_length,precision,scale等。
4. sp_describe_cursor_tables --- 描述游标引用的表
sql
DECLARE table_cursor CURSOR FOR
SELECT e.EmployeeID, e.FirstName, d.DepartmentName
FROM dbo.Employees e
JOIN dbo.Departments d ON e.DeptID = d.DeptID; -- 假设有部门表
-- 创建部门表(如果不存在)
IF OBJECT_ID('dbo.Departments') IS NULL
BEGIN
CREATE TABLE dbo.Departments (
DeptID INT PRIMARY KEY,
DepartmentName NVARCHAR(50)
);
INSERT INTO dbo.Departments VALUES (1, 'IT'), (2, 'HR');
ALTER TABLE dbo.Employees ADD DeptID INT DEFAULT 1;
UPDATE dbo.Employees SET DeptID = 1;
END
GO
OPEN table_cursor;
-- 描述游标引用的基表
EXEC sp_describe_cursor_tables
@cursor_return = 0 OUTPUT,
@cursor_source = N'local',
@cursor_identity = N'table_cursor';
CLOSE table_cursor;
DEALLOCATE table_cursor;
GO
输出:游标查询涉及的基表名称、所有者、更新能力等。
五、综合性案例
综合案例1:使用游标生成员工薪资调整报告(含历史记录)
sql
-- 创建薪资历史表
CREATE TABLE dbo.SalaryHistory (
HistoryID INT IDENTITY(1,1) PRIMARY KEY,
EmployeeID INT,
OldSalary DECIMAL(10,2),
NewSalary DECIMAL(10,2),
AdjustmentDate DATETIME2 DEFAULT GETDATE(),
Reason NVARCHAR(100)
);
GO
-- 创建存储过程:根据绩效调整薪资
CREATE PROCEDURE dbo.usp_AdjustSalaryByPerformance
@PerformanceLevel NVARCHAR(10), -- 'High', 'Medium', 'Low'
@AdjustmentRate DECIMAL(3,2) -- 调整比率,如 0.1 表示10%
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EmpID INT, @CurrentSalary DECIMAL(10,2), @NewSalary DECIMAL(10,2);
DECLARE @AdjustmentReason NVARCHAR(100) = '年度绩效调整 - ' + @PerformanceLevel;
-- 声明可更新游标
DECLARE salary_cursor CURSOR FOR
SELECT EmployeeID, Salary
FROM dbo.Employees
WHERE Performance = @PerformanceLevel -- 假设有Performance列
FOR UPDATE OF Salary;
-- 添加Performance列(如果不存在)
IF COL_LENGTH('dbo.Employees', 'Performance') IS NULL
BEGIN
ALTER TABLE dbo.Employees ADD Performance NVARCHAR(10) DEFAULT 'Medium';
UPDATE dbo.Employees SET Performance =
CASE WHEN Salary > 9000 THEN 'High'
WHEN Salary > 7500 THEN 'Medium'
ELSE 'Low' END;
END
OPEN salary_cursor;
FETCH NEXT FROM salary_cursor INTO @EmpID, @CurrentSalary;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @NewSalary = @CurrentSalary * (1 + @AdjustmentRate);
-- 更新当前员工薪资
UPDATE dbo.Employees
SET Salary = @NewSalary
WHERE CURRENT OF salary_cursor;
-- 记录薪资调整历史
INSERT INTO dbo.SalaryHistory (EmployeeID, OldSalary, NewSalary, Reason)
VALUES (@EmpID, @CurrentSalary, @NewSalary, @AdjustmentReason);
PRINT '员工ID ' + CAST(@EmpID AS NVARCHAR) +
' 薪资从 ' + CAST(@CurrentSalary AS NVARCHAR) +
' 调整为 ' + CAST(@NewSalary AS NVARCHAR);
FETCH NEXT FROM salary_cursor INTO @EmpID, @CurrentSalary;
END
CLOSE salary_cursor;
DEALLOCATE salary_cursor;
PRINT '薪资调整完成。';
END
GO
-- 执行薪资调整
EXEC dbo.usp_AdjustSalaryByPerformance @PerformanceLevel = 'Low', @AdjustmentRate = 0.15;
EXEC dbo.usp_AdjustSalaryByPerformance @PerformanceLevel = 'High', @AdjustmentRate = 0.05;
-- 查看结果
SELECT * FROM dbo.Employees;
SELECT * FROM dbo.SalaryHistory;
GO
注释:
- 结合游标实现复杂业务逻辑(绩效分级调整)。
- 使用
WHERE CURRENT OF精确更新。- 记录历史便于审计。
综合案例2:游标用于数据清洗与标准化
sql
-- 场景:清洗客户表中的姓名字段(去除空格、统一大小写)
CREATE TABLE dbo.Customers (
CustomerID INT IDENTITY(1,1) PRIMARY KEY,
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
Email NVARCHAR(100)
);
GO
INSERT INTO dbo.Customers (FirstName, LastName, Email) VALUES
(' john ', ' doe ', 'JOHN.DOE@EXAMPLE.COM '),
(' Jane ', ' Smith ', ' jane.smith@example.com'),
(' Bob ', ' Johnson ', 'BOB.JOHNSON@EXAMPLE.COM');
-- 声明游标进行数据清洗
DECLARE clean_cursor CURSOR FOR
SELECT CustomerID, FirstName, LastName, Email
FROM dbo.Customers
FOR UPDATE OF FirstName, LastName, Email;
DECLARE @CID INT, @FName NVARCHAR(50), @LName NVARCHAR(50), @Email NVARCHAR(100);
OPEN clean_cursor;
FETCH NEXT FROM clean_cursor INTO @CID, @FName, @LName, @Email;
WHILE @@FETCH_STATUS = 0
BEGIN
-- 清洗逻辑
DECLARE @NewFName NVARCHAR(50) = LTRIM(RTRIM(@FName));
DECLARE @NewLName NVARCHAR(50) = LTRIM(RTRIM(@LName));
DECLARE @NewEmail NVARCHAR(100) = LOWER(LTRIM(RTRIM(@Email)));
-- 如果有变化则更新
IF @FName <> @NewFName OR @LName <> @NewLName OR @Email <> @NewEmail
BEGIN
UPDATE dbo.Customers
SET
FirstName = @NewFName,
LastName = @NewLName,
Email = @NewEmail
WHERE CURRENT OF clean_cursor;
PRINT '客户ID ' + CAST(@CID AS NVARCHAR) + ' 数据已清洗。';
END
FETCH NEXT FROM clean_cursor INTO @CID, @FName, @LName, @Email;
END
CLOSE clean_cursor;
DEALLOCATE clean_cursor;
-- 查看清洗后数据
SELECT * FROM dbo.Customers;
GO
注释:
- 游标用于逐行数据清洗和标准化。
- 仅在数据实际变化时更新,减少日志和锁开销。
- 替代方案:可使用单条
UPDATE语句,但游标提供更精细控制。
综合案例3:使用 SCROLL 游标实现分页浏览功能
sql
-- 创建存储过程:模拟分页浏览员工数据
CREATE PROCEDURE dbo.usp_BrowseEmployees
@PageSize INT = 5, -- 每页行数
@PageNumber INT = 1 -- 页码(从1开始)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EmpID INT, @FirstName NVARCHAR(50), @LastName NVARCHAR(50), @Salary DECIMAL(10,2);
DECLARE @Counter INT = 0;
DECLARE @StartRow INT = (@PageNumber - 1) * @PageSize + 1;
DECLARE @EndRow INT = @PageNumber * @PageSize;
-- 声明 SCROLL 游标
DECLARE browse_cursor SCROLL CURSOR FOR
SELECT EmployeeID, FirstName, LastName, Salary
FROM dbo.Employees
ORDER BY EmployeeID;
OPEN browse_cursor;
-- 定位到起始行
FETCH ABSOLUTE @StartRow FROM browse_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
PRINT '第 ' + CAST(@PageNumber AS NVARCHAR) + ' 页数据(每页 ' + CAST(@PageSize AS NVARCHAR) + ' 行):';
PRINT REPLICATE('-', 50);
WHILE @@FETCH_STATUS = 0 AND @Counter < @PageSize
BEGIN
PRINT 'ID: ' + CAST(@EmpID AS NVARCHAR) +
', 姓名: ' + @FirstName + ' ' + @LastName +
', 薪资: ' + CAST(@Salary AS NVARCHAR);
SET @Counter = @Counter + 1;
FETCH NEXT FROM browse_cursor INTO @EmpID, @FirstName, @LastName, @Salary;
END
CLOSE browse_cursor;
DEALLOCATE browse_cursor;
END
GO
-- 测试分页
EXEC dbo.usp_BrowseEmployees @PageSize = 2, @PageNumber = 1;
EXEC dbo.usp_BrowseEmployees @PageSize = 2, @PageNumber = 2;
GO
注释:
SCROLL游标支持FETCH ABSOLUTE直接跳转到指定行。- 模拟分页功能,适用于需要逐页浏览的场景。
- 实际应用中,大数据量分页推荐使用
OFFSET FETCH。
六、最佳实践与注意事项
- 优先使用集合操作 :99% 的场景可用
UPDATE/DELETE+WHERE或JOIN替代游标。 - 选择合适游标类型 :
FAST_FORWARD性能最佳,仅在需要时使用SCROLL。 - 及时释放资源 :务必
CLOSE和DEALLOCATE,避免内存泄漏。 - 避免在事务中长时间持有游标:减少锁争用。
- 监控性能:游标可能导致表扫描和阻塞。
- 考虑替代方案 :
- 字符串聚合 →
STRING_AGG() - 分页 →
OFFSET FETCH - 逐行计算 → 窗口函数或递归CTE
- 字符串聚合 →
- 测试大数据量:确保游标在生产数据规模下性能可接受。
✅ 以上内容涵盖 SQL Server 2019 游标的核心语法、管理操作及实战案例,可直接用于学习、开发与调优。游标虽强大,但请牢记:"能用集合操作解决的问题,不要用游标!"