三个过程分工明确: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 秒实验
- 把
cur.close()挪到fetchall()前面,跑一次。 会报错——游标关了就不能再取。这就是「CLOSE 之后不能 FETCH」的直观感受。 - 在
cursor()之后、execute()之前打印cur.description,看是什么。 是None——还没执行查询,游标里什么都没有。 - 想一想:如果一张表在高峰期「莫名其妙改不动」,你会先查什么? 先查有没有长事务和没关的游标——不是先重启数据库。重启能解决,但原因还在。
常见误区
- 「游标用完系统会自己关」。连接不断、会话不结束,它就一直开着。锁和内存都挂在上面。
- 「查游标状态没意义,直接重建一个就行」。重建能绕过问题, 但「谁把锁吃掉了」这个疑问没解决——下次还会发生。
- 「游标是透明的,不会影响别人」。它拿着行锁。 一个没关的游标能让整张业务表写不进去——这是实际发生过的事故。
自测三问(点开对答案)
1. 三个 `sp_describe_cursor*` 分别给什么信息? —— **`sp_describe_cursor` 给游标本身(状态、行数、位置);`_columns` 给列信息;** **`_tables` 给引用的表和锁。** 2. 游标「不存在」最常见的原因是什么? —— **不是名字写错,是作用域。** 游标默认是 `LOCAL`, **出了那个批或存储过程就没了**——下一个 `GO` 之后再去 `FETCH` 就会说不存在。 3. 为什么「游标没关」会导致业务事故? —— **因为它一直拿着锁。** 一个忘了 `CLOSE` 的游标, 可以让整张表在高峰期写不进去——**而报错信息里根本不会提「游标」两个字。**第 13 章到此结束。 下一篇进第 14 章 14.1,讲索引: 它是「书的目录」——没有它,查一行要翻完整本书;有了它,一次就能定位。 但索引本身也占空间、拖慢写入。
- ☐ (本篇完,下一篇 14.1 索引的概念)