🪟 第13章 游标的使用
📄 一句话概括

三个过程分工明确:sp_describe_cursor 看状态、_columns 看列、_tables 看表和锁。 排查顺序也是这个:先确认「活着」,再看「吃了什么锁」,最后看「结果集对不对」。 很多「表被别人锁住」的事故,源头就是一个忘了 CLOSE 的游标。

📝 本章和前面几章的关系

13.1 讲概念、13.2 讲操作,这一节讲「怎么查」——游标出问题时,从哪儿下手。 教材 13.3 讲三个系统过程,「小结」和「实践与练习」并进本篇末尾。 这一节没有新语法,全是排查手段。 本沙箱没有这些过程,所以原码照贴; 能对上的部分用 Python 的 cursor.description 演示——它就是「结果集长什么样」这个信息, 跟 sp_describe_cursor_columns 是一个意思。

一、三个过程各管一段

过程 告诉你什么
sp_describe_cursor 游标本身:名字、状态、行数、当前位置、滚动方式
sp_describe_cursor_columns 结果集有哪些列:列名、类型、顺序
sp_describe_cursor_tables 游标引用了哪些表,以及锁的情况

三个都要求先拿到游标的「句柄」,所以标准套路是先 DECLARE @rep CURSOR, 再把过程的结果 OUTPUT 到它里面,然后 FETCH 出来看。

DECLARE @rep CURSOR;
EXEC sp_describe_cursor @cursor_return = @rep OUTPUT;
FETCH NEXT FROM @rep;   -- 这个游标里只有一行,就是状态
GO

看着绕,其实是因为「系统过程返回结果集」这件事在 T-SQL 里只能这么表达。 过程不能像函数那样 RETURN 一张表,只能吐一个游标出来。

二、本沙箱怎么看到同样的信息

Python 的 cursor 有个 description 属性,就是列信息:

cur = c.cursor()
cur.execute(u"SELECT st.sname, sc.subject, sc.score FROM ...")
for d in cur.description:
    print(d[0])      # 列名

演示里把它打成了表:列序、列名、类型码。 三列的名字分别是 sname、subject、score, 跟 SELECT 里写的一模一样。 但类型码那一列全是「—」,因为 Python 的 sqlite3 不返回这个信息。 SQL Server 的 sp_describe_cursor_columns 会给真实的类型名和长度—— 这是它比这里多出来的东西。 这件事本身也说明一个道理: 换个数据库,连「元数据能查到什么程度」都不一样。 写跨库的工具时,别假设哪都能拿到类型信息。

三、游标状态:为什么要专门查

状态 意味着
打开且有效 可以 FETCH
关闭但存在 要 OPEN 才能用
不存在 名字写错了,或者已经被 DEALLOCATE

排查「游标用不了」时,第一步就是查状态。 三类原因对应三种修法: 补 OPEN、改名、重建——别瞎试。 最常见的其实是第三种,而且原因往往不是「名字写错」: 游标默认是「局部」的, 出了那个批、那个存储过程就没了。 你在下一个 GO 之后再去 FETCH,它就是不存在。 全局游标(GLOBAL)能跨批使用,但两个会话同时用同名游标就会打架—— 所以默认是 LOCAL,这是有意的设计,别随便改。

四、三个过程一起用的一次排查

-- 先看状态(0 打开且有效、1 关闭、-1 不存在)
SELECT CURSOR_STATUS('global', 'cur_goods');
-- 再看它引用了哪张表、锁成什么样
EXEC sp_describe_cursor_tables @cursor_return = @rep OUTPUT;
-- 最后看列信息对不对
EXEC sp_describe_cursor_columns @cursor_return = @rep OUTPUT;
GO

顺序有讲究: 先确认「活着」(不然后面两步都没意义),再看「吃了什么锁」, 最后看「结果集对不对」。 第二步最有用。 真实事故里经常是「某张表改不动了」, 查半天发现有个游标打开着、一直没 CLOSE,把行锁攥在手里。 这时候 sp_describe_cursor_tables 一眼就能看出是哪张表、什么锁。 所以「游标用完立刻 CLOSE」不只是代码整洁问题,是运维安全问题。 一个漏掉的 CLOSE,可以让整张业务表在高峰期写不进去。

五、动手 Demo:三个系统过程与它们在本沙箱的对应物

这一段把三个系统过程的原码贴出来, 再用 Python 的 cursor.description 演示「看结果集长什么样」这件事。 最后列一遍游标状态的三种情况,以及排查的顺序。

import sqlite3

c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据 ...

cur = c.cursor()
cur.execute(u"SELECT st.sname, sc.subject, sc.score FROM tb_score sc "
            u"JOIN tb_student st ON st.id = sc.student_id ORDER BY sc.id")

# 相当于 sp_describe_cursor_columns:结果集有哪些列
for i, d in enumerate(cur.description, 1):
    print(i, d[0], d[1])      # 列序、列名、类型码

rows = cur.fetchall()
cur.close()
print(u"取到 %d 行" % len(rows))

实跑输出:

--------------------------------------------------------------------
用系统过程查看游标的状态
--------------------------------------------------------------------


  ① 三个系统过程各管一段
  --------------------------------------------------------------------
| 过程 | 告诉你什么 |
|---|---|
| sp_describe_cursor | 游标本身:名字、状态、行数、当前位置、滚动方式 |
| sp_describe_cursor_columns | 结果集有哪些列:列名、类型、顺序 |
| sp_describe_cursor_tables | 游标引用了哪些表,以及锁的情况 |

  三个都要求先知道游标的「句柄」或者名字,所以通常配合 CURSOR_STATUS 一起用。

  [教材原码 · 本沙箱不执行]
  | DECLARE @rep CURSOR;
  | EXEC sp_describe_cursor @cursor_return = @rep OUTPUT;
  | FETCH NEXT FROM @rep;   -- 这个游标里只有一行,就是状态
  | GO


  ② 本沙箱怎么看到同样的信息
  --------------------------------------------------------------------
  Python 的 cursor 也有对应物:description 给列信息,rowcount 给行数。
| 列序 | 列名 | 类型码 |
|---:|---|---|
| 1 | sname | — |
| 2 | subject | — |
| 3 | score | — |

  类型码那一列全是「—」,因为 Python 的 sqlite3 不返回这个信息。
  SQL Server 的 sp_describe_cursor_columns 会给出真实的类型名和长度——那是它比这里多出来的东西。
  这就是 sp_describe_cursor_columns 干的事:告诉你结果集长什么样。
  取到 8 行。当前游标已经到底,再 FETCH 会返回 -1(越界)。

  ③ 游标状态:为什么要专门查
  --------------------------------------------------------------------
| 状态 | 意味着 |
|---|---|
| 打开且有效 | 可以 FETCH |
| 关闭但存在 | 要 OPEN 才能用 |
| 不存在 | 名字写错了,或者已经被 DEALLOCATE |

  排查「游标用不了」时,第一步就是查状态——三类原因对应三种修法,别瞎试。
  最常见的其实是第三种:游标是「局部」的,出了那个批或那个存储过程就没了。
  ! 全局游标(GLOBAL)能跨批使用,但两个会话同时用同名游标就会打架——所以默认是 LOCAL。

  ④ 三个过程一起用的一次排查
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | -- 先看状态(0 打开且有效、1 关闭、-1 不存在)
  | SELECT CURSOR_STATUS('global', 'cur_goods');
  | -- 再看它引用了哪张表、锁成什么样
  | EXEC sp_describe_cursor_tables @cursor_return = @rep OUTPUT;
  | -- 最后看列信息对不对
  | EXEC sp_describe_cursor_columns @cursor_return = @rep OUTPUT;
  | GO

  顺序是这样:先确认「活着」,再看「吃了什么锁」,最后看「结果集对不对」。
  锁那一步在排查「表被别人锁住」时特别有用——很多时候锁的源头就是一个没关的游标。

description 那三行输出的列名是 sname、subject、score。 这就是「结果集长什么样」——sp_describe_cursor_columns 干的是同一件事, 只不过它还会告诉你类型和长度。 类型码那一列打出来是 None(演示里显示成「—」)。 Python 的 sqlite3 不返回类型信息,这是它的取舍:SQLite 本身就是动态类型(5.1 讲过)。 fetchall() 取到 8 行,然后 close()。 这两句把「取完」和「关掉」放在一起, 是个好习惯——省得后面忘了关。 真实环境里排查游标问题,第一个动作永远是查状态。 状态不看就去猜原因,十有八九猜错方向。

30 秒实验

  1. 把 cur.close() 挪到 fetchall() 前面,跑一次。 会报错——游标关了就不能再取。这就是「CLOSE 之后不能 FETCH」的直观感受。
  2. 在 cursor() 之后、execute() 之前打印 cur.description,看是什么。 是 None——还没执行查询,游标里什么都没有。
  3. 想一想:如果一张表在高峰期「莫名其妙改不动」,你会先查什么? 先查有没有长事务和没关的游标——不是先重启数据库。重启能解决,但原因还在。

常见误区

  1. 「游标用完系统会自己关」。连接不断、会话不结束,它就一直开着。锁和内存都挂在上面。
  2. 「查游标状态没意义,直接重建一个就行」。重建能绕过问题, 但「谁把锁吃掉了」这个疑问没解决——下次还会发生。
  3. 「游标是透明的,不会影响别人」。它拿着行锁。 一个没关的游标能让整张业务表写不进去——这是实际发生过的事故。
自测三问(点开对答案) 1. 三个 `sp_describe_cursor*` 分别给什么信息? —— **`sp_describe_cursor` 给游标本身(状态、行数、位置);`_columns` 给列信息;** **`_tables` 给引用的表和锁。** 2. 游标「不存在」最常见的原因是什么? —— **不是名字写错,是作用域。** 游标默认是 `LOCAL`, **出了那个批或存储过程就没了**——下一个 `GO` 之后再去 `FETCH` 就会说不存在。 3. 为什么「游标没关」会导致业务事故? —— **因为它一直拿着锁。** 一个忘了 `CLOSE` 的游标, 可以让整张表在高峰期写不进去——**而报错信息里根本不会提「游标」两个字。**

第 13 章到此结束。 下一篇进第 14 章 14.1,讲索引: 它是「书的目录」——没有它,查一行要翻完整本书;有了它,一次就能定位。 但索引本身也占空间、拖慢写入。

相关笔记