先聊一个特别基础、但很多人一直没搞透的东西:Python操作MySQL时,那个谁都会用、却很少正经分析过的“游标”(cursor)。
我第一次用Python连MySQL写业务代码时,根本不知道自己已经在一个游标上操作了。那时候我只知道要先connection.cursor(),然后cursor.execute(),再fetchall(),一气呵成。直到后来面试被问到“游标到底是什么”,我才发现自己对它的理解其实很模糊。更别提去了新公司接手一个带存储过程的老系统,里面又是DECLARE cur CURSOR FOR又是FETCH NEXT FROM,直接看懵。
这篇内容想把游标这件事掰开揉碎讲清楚:包括Python驱动里游标对象怎么用、MySQL存储过程里SQL游标怎么写、为什么有时不用游标反而更快、以及我在实际项目里踩过的坑。适合刚学Python+MySQL的初学者,也适合工作几年想系统梳理一下的开发者。写的时候以PyMySQL为例,其他驱动比如mysql-connector-python、MySQLdb,核心概念完全一样,代码稍微改改就能跑。
1. 先搞懂游标到底解决了什么问题
1.1 游标是一根“数据水管上的吸管”
关系型数据库处理查询的方式,和你写Python列表完全不一样。Python里一个列表,data = [1, 2, 3],内存直接建好,想访问哪个就访问哪个。但MySQL面对一条SELECT * FROM orders,结果可能有几十万行,服务端不可能一次性把所有数据打包塞给你,网络也扛不住,内存更扛不住。这时候就需要一种“按需取数据”的机制。
游标就是干这个的。你可以把它理解成一根插在数据结果集上的吸管,数据还留在MySQL服务器那头,Python这边通过游标一行一行“吸”过来。吸一口,就拿到一行;再吸一口,再拿到一行。不用的时候把吸管拔掉,服务器端那部分缓冲也就释放了。
这个比喻能解释很多现象:为什么fetchone()每次只拿一行?因为游标在服务器端维护了一个“当前位置”,每次fetch都从这个位置往后取。为什么fetchall()在结果集极大时可能把内存撑爆?因为你一次性把整根水管里的水全抽到了本地Python进程里。
1.2 游标在Python里有“两层身份”
这是新手最容易混淆的地方。
第一层身份,是MySQL存储过程里的“游标对象”。它是在SQL内部直接操作结果集的机制,比如在存储过程里写:
DECLARE cur CURSOR FOR SELECT id, name FROM users; OPEN cur; FETCH cur INTO v_id, v_name; CLOSE cur;这是纯粹在数据库内部跑的,和Python没有直接关系。
第二层身份,是Python数据库驱动(比如PyMySQL)提供给开发者的“游标对象”。它是客户端用来执行SQL、读取结果集的API。你在Python里写的cursor.execute()、cursor.fetchall(),操作的都是这一层。
很多人在存储过程里看到游标、在Python代码里也看到游标,但不知道这俩到底谁是谁。简单总结:Python里的cursor是“会话工具”,存储过程里的cursor是“SQL内部的临时管道”。两者都能解决“结果集按需读取”的问题,只是存在的位置不同。
1.3 你以为的“正常查询”,其实一直在用游标
我给很多新人讲这个问题时会问:“你们有没有用过pymysql连接数据库?”十个人里九个人都说用过。接着问:“那你们有没有写过conn.cursor()?”所有人都说写过。
对,只要你写了cursor = conn.cursor(),你就已经创建了一个游标。后面所有操作,不管你是fetchone()还是for row in cursor,都是在游标上完成的。所以游标不是一个“高级特性”,而是Python操作MySQL最基础、最底层的交互方式。
搞明白这一点之后,后面所有问题就有了抓手:为什么游标用不好会内存暴涨、为什么连接不能随便关、为什么有的查询要分页拉取,根源都在游标的工作机制上。
2. 环境准备:驱动选型和最小连接代码
2.1 Python连MySQL的驱动怎么选
先说驱动。Python连MySQL的主流方案有三个,我用一张表说清楚它们的关系:
| 驱动 | 维护状态 | 纯Python | 常用场景 | 备注 |
|---|---|---|---|---|
| PyMySQL | 活跃 | 是 | 日常开发、生产环境 | 安装最简单,跨平台 |
| mysql-connector-python | 官方维护 | 是 | 需要官方支持的项目 | 某些API和PyMySQL略不同 |
| MySQLdb(mysqlclient) | 维护较慢 | 否 | 老项目、追求极致性能 | 需要编译,Windows安装麻烦 |
我在自己的项目里默认选PyMySQL。原因很简单:一条pip install pymysql就能装好,不依赖C扩展,在Windows、Linux、macOS上都能跑,性能和mysqlclient差距不大,而API又比mysql-connector-python更顺手。
2.2 第一次建立连接需要注意什么
安装好之后,在一个.py文件里写下这段最小代码:
import pymysql conn = pymysql.connect( host="127.0.0.1", port=3306, user="root", password="your_password", database="test_db", charset="utf8mb4", ) try: with conn.cursor() as cursor: cursor.execute("SELECT 1") result = cursor.fetchone() print(result) # 输出 (1,) finally: conn.close()这里有几个细节值得掰一下:
第一,host到底写localhost还是127.0.0.1,很多人不在意,但实际影响很大。localhost在有些系统上会走Unix socket文件连接,比如/var/run/mysqld/mysqld.sock;而127.0.0.1走的是TCP网络协议。如果你的MySQL没启动socket监听,或者Python环境权限不够,写localhost就会报ERROR 2002,改成127.0.0.1反而就好了。这个坑我后面还会重点讲。
第二,charset一定要写utf8mb4,不要写utf8。utf8在MySQL里是历史遗留的“阉割版”,最多存3字节字符,遇到emoji或者生僻字直接报错或乱码。utf8mb4才是完整的UTF-8。
第三,这段代码里的with conn.cursor() as cursor只负责自动关闭游标,不会自动关闭连接。连接必须单独conn.close()。很多新手以为with把连接也关了,结果程序跑完连接还挂着,最后数据库连接数暴涨。注意区分。
2.3 连接参数里的隐藏选项
再看几个常用但总被忽略的连接参数:
autocommit: 默认是False,也就是手动事务模式。你执行INSERT、UPDATE后不写conn.commit(),数据不会真正落库。新手最容易在这卡住:代码跑了没报错,但数据库里就是查不到新数据。cursorclass: 默认返回元组,你可以传pymysql.cursors.DictCursor让它返回字典。这个对代码可读性帮助很大,不用再row[0]、row[1]猜字段位置。read_timeout和write_timeout: 如果数据库查询很慢,默认超时可能不够,需要适当调大。
下面这段代码把两个常用选项一起用上:
import pymysql from pymysql.cursors import DictCursor conn = pymysql.connect( host="127.0.0.1", user="root", password="your_password", database="test_db", charset="utf8mb4", autocommit=True, cursorclass=DictCursor, ) with conn.cursor() as cursor: cursor.execute("SELECT id, name FROM users LIMIT 3") for row in cursor: print(row["id"], row["name"])这样每一行拿回来的都是字典,字段名字直接对着列名取,不容易出错。不过要注意,字典游标拿到的字段顺序不一定和表结构一致,按列名访问才安全。
3. 游标对象的核心操作:从执行到取值一整套
3.1 execute之后先搞清楚行数
很多人写cursor.execute(sql)之后直接fetchall(),完全不管这个execute返回了什么。实际上,execute的返回值在很多驱动里表示影响的行数,但不建议依赖它,因为不同驱动实现有差异。PyMySQL里,execute的返回值是受影响行数,对于SELECT来说,它在某些版本里并不靠谱。更稳妥的做法是看cursor.rowcount。
cursor.execute("SELECT * FROM orders WHERE status = 0") print("未处理订单数:", cursor.rowcount)这个属性在SELECT和UPDATE/DELETE上都有意义。比如你执行了一个UPDATE ... WHERE,想知道到底改了多少行,用cursor.rowcount最直观。但要注意,它表示的是“上一次执行影响的行数”,如果中间穿插了其他查询,旧值就会被覆盖。
3.2 fetchone、fetchmany、fetchall怎么选
这是我日常开发里最常用的三个方法,各自适用场景完全不同。
fetchone()拿一行,返回元组或None。适合只需要一条记录、或者想循环处理大结果集的情况。
cursor.execute("SELECT * FROM users WHERE id = 1") user = cursor.fetchone() if user: print(user) else: print("用户不存在")fetchmany(size)拿指定行数,适合分批读取,既不会一次把所有数据塞进内存,又比一行一行拿要高效。比如导出10万条数据,可以每次取5000条处理:
cursor.execute("SELECT * FROM big_table") while True: rows = cursor.fetchmany(5000) if not rows: break for row in rows: process(row)fetchall()一次拿全部,适合结果集很小的情况。如果你明确知道数据量在几百几千行内,用它最简单。但要是几百万行还直接fetchall(),轻则内存占用飙升,重则进程直接卡死。
3.3 直接遍历游标对象
游标对象本身是可迭代的,for row in cursor其实等价于不断fetchone(),底层实现也是这么做的。不过这种写法有个隐藏好处:代码更简洁,而且处理大结果集时,不会像fetchall()那样一次性加载所有数据。
with conn.cursor() as cursor: cursor.execute("SELECT id, name FROM users") for row in cursor: print(row)我自己写脚本时,如果逻辑简单,都是直接for循环遍历游标。有一点要记住:游标是一个“一次性”的可迭代对象,遍历完了,位置到了末尾,再想从头读就没了。想要重来,只能重新execute()。
3.4 scroll移动游标位置
scroll()可以让游标在结果集里前后移动,支持相对移动和绝对移动两种模式:
cursor.scroll(1) # 相对当前位置,往后移动1行 cursor.scroll(-2) # 相对当前位置,往前移动2行 cursor.scroll(0, mode="absolute") # 移到结果集开头这个操作在实际应用中用得不算多,但在某些需要“来回看”的场景下非常有用。比如你拿到一批数据,先看了第一条,又想让游标回到开头重新处理,用scroll(0, mode="absolute")就能实现。注意,绝对模式里,mode参数要写"absolute",默认是"relative",别把两个参数顺序搞反了。
3.5 拿列名:description属性
有时候你拿到的结果集是元组,但你想知道每一列叫什么,可以用cursor.description。它是一个元组列表,每个元素包含字段名、类型、长度等信息。
cursor.execute("SELECT id, name, created_at FROM users") print([col[0] for col in cursor.description]) # 输出: ['id', 'name', 'created_at']这个属性在做动态数据处理、自动生成表格头、或者写通用ORM工具时非常有用。比如导出CSV前,你可以先通过description拿到列名作为CSV的第一行,再遍历游标输出数据。
4. 参数化查询与批量操作:别再用字符串拼SQL了
4.1 占位符的正确姿势
Python连MySQL执行带参数的SQL时,占位符是%s,这一点和SQL Server完全不一样。SQL Server的Python驱动(pyodbc)用?做占位符,很多人从SQL Server切到MySQL时报1064语法错误,十有八九就是这里出了问题。
正确写法是这样的:
cursor.execute( "SELECT * FROM users WHERE age > %s AND city = %s", (18, "上海") )注意,这里的%s是占位符,不是Python的字符串格式化。参数必须通过第二个参数传递,用一个元组或者列表,不要自己往SQL里拼值。还有个容易踩的坑:如果SQL里本身有%字符,比如LIKE '%北京%',在参数化查询里这个%会和占位符混淆。解决办法是把它也写成参数:
cursor.execute( "SELECT * FROM users WHERE name LIKE %s", ("%北京%",) )这样%成了参数的一部分,不会被当成占位符解析。
4.2 为什么不能直接拼接字符串
很多人图省事,会这么写:
name = input("请输入用户名: ") cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")这种写法在开发环境跑得通,但一旦name的内容被恶意构造,你的整个库都有危险。更可怕的是,正常情况下它还不报错。遇到字段里有单引号的数据,比如输入O'Neil,SQL直接变成:
SELECT * FROM users WHERE name = 'O'Neil'语法错误不说,还暴露出一个更严重的问题:如果你输入的是' OR 1=1 --,整张表的数据全被查出来了。在用参数化查询之后,驱动会自动处理转义和类型转换,这类问题从根上就断了。这是我刚工作时踩过最深刻的坑,后来养成了一条铁律:任何SQL,只要带外部输入,一律用占位符。
4.3 executemany批量插入
批量插入数据时,用executemany()比循环execute()快得多。它的原理是把多条数据一次性发给MySQL执行,减少了网络往返次数。
data = [ ("张三", 25, "北京"), ("李四", 30, "上海"), ("王五", 28, "广州"), ] with conn.cursor() as cursor: cursor.executemany( "INSERT INTO users(name, age, city) VALUES (%s, %s, %s)", data ) conn.commit()注意,executemany()只是“批量发送”,不代表自动提交。如果你没开autocommit,最后还是要conn.commit()。
另外,批量插入超大数据时,不要把整个大列表一次性传给executemany,建议分批,比如每5000条一批,避免单次SQL包过大把MySQL的max_allowed_packet打爆。
4.4 拿到自增ID:lastrowid
插入一条记录之后,经常需要拿到它的自增主键ID,用来关联子表。PyMySQL里,在插入操作之后直接访问cursor.lastrowid就行:
with conn.cursor() as cursor: cursor.execute( "INSERT INTO orders(user_id, amount) VALUES (%s, %s)", (1001, 299.00) ) new_id = cursor.lastrowid conn.commit() print("新订单ID:", new_id)注意一个细节:lastrowid是游标对象的属性,不是连接对象的属性。如果你在同一个连接上创建了多个游标,插完数据后又去另一个游标上查lastrowid,拿到的可能不是你想要的值。
5. 存储过程里的游标:SQL内部的循环玩法
5.1 存储过程为什么要用游标
Python里的游标是给客户端用的,而MySQL存储过程里的游标,是给SQL自己用的。什么时候需要它?典型场景是“对一行数据做一遍处理”:
比如有一张积分明细表,你要按用户汇总积分,再更新到用户表。SQL本身虽然有UPDATE、INSERT,但没法“逐行读数据,再根据不同条件逐个计算”,这时候游标就派上用场了。
但我不建议一上来就设计一堆复杂的存储过程游标。游标在数据库里是出了名的性能杀手,因为它本质上还是逐行循环,每FETCH一次都涉及内存和上下文的切换。能用一条UPDATE解决的,就不要写游标;写游标之前,先想一想能不能用JOIN、子查询、窗口函数来替代。
5.2 声明、打开、抓取、关闭的完整模板
MySQL存储过程里用游标的固定流程是:声明游标 -> 打开游标 -> 循环抓取 -> 关闭游标。下面是一个最简单的例子,遍历user表中的用户,逐个做逻辑处理:
DELIMITER // CREATE PROCEDURE sp_process_users() BEGIN DECLARE done INT DEFAULT 0; DECLARE v_id INT; DECLARE v_name VARCHAR(100); DECLARE cur CURSOR FOR SELECT id, name FROM users WHERE status = 1; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_name; IF done THEN LEAVE read_loop; END IF; -- 这里写你对每一行数据的处理逻辑 UPDATE user_stats SET total_processed = total_processed + 1 WHERE user_id = v_id; END LOOP; CLOSE cur; END // DELIMITER ;这里有几个细节必须注意:
第一,所有DECLARE语句必须放在存储过程BEGIN块的最前面,不能写几条SQL之后再声明变量。这是MySQL的硬性规定,很多人写游标报语法错误,就是忘了这一点。
第二,CONTINUE HANDLER FOR NOT FOUND SET done = 1是循环结束的关键。FETCH到结果集末尾时,MySQL会触发NOT FOUND条件,这时把done设为1,循环里判断done就退出。不写这个handler,游标会无限循环或者直接报错。
第三,游标用完一定要CLOSE。不关的话,存储过程结束时虽然会自动释放,但长时间运行的连接上,游标资源可能一直占着,影响性能。
5.3 从Python调用存储过程
写好了存储过程,在Python里调用有两种常见姿势。
第一种是用cursor.callproc():
with conn.cursor() as cursor: cursor.callproc("sp_process_users")这个方法比较“高级”,但坑也不少。PyMySQL的callproc()执行后会返回一个结果集,但如果你读过一些网络教程,会发现有人取不到数据、有人取到空结果集。原因在于callproc()的行为和直接执行CALL有差异,尤其在存储过程里有多个SELECT时,结果集会分多个批次返回。
更稳的写法是直接用execute()执行CALL语句:
with conn.cursor() as cursor: cursor.execute("CALL sp_process_users()") # 存储过程如果返回结果集,可以 fetchall 读取 result = cursor.fetchall() # 如果还有多个结果集,用 nextset 切换 cursor.nextset()我个人更推荐第二种。它语义直观,行为可控,而且多个结果集的处理逻辑也和普通查询一致:先fetchall()取第一个结果集,再nextset()跳到下一个。
5.4 存储过程里游标和Python游标的关系
如果你在一个Python项目里,既写了存储过程里的游标,又在外层用了Python游标去调用这个存储过程,不妨在脑子里理一下它们的关系:外层Python游标负责“把SQL发给MySQL”,存储过程内部的游标负责“在MySQL里处理结果集”,两者是不同层级的工具,互不冲突。
有一点要注意:存储过程执行期间,同样占用数据库连接。如果一个存储过程内部游标循环跑了很久,外层连接在这段时间内是不能执行其他SQL的。所以存储过程里的游标循环体,尽量只做必要的更新,别在里面再调其他存储过程或者做复杂运算。
6. 性能、资源管理与游标的“隐藏陷阱”
6.1 游标用完必须关吗?
先说结论:必须关。
游标关不关,短期看似乎无所谓,反正Python垃圾回收可能帮你处理。但长期跑的服务里,游标占用的不仅是Python对象,还有MySQL连接上的上下文资源。如果你在同一个连接上反复执行查询,不关游标,一旦达到MySQL的某些资源上限,就会出现莫名其妙的报错。
我一般这么组织代码,保证一定关闭:
conn = pymysql.connect(host="127.0.0.1", user="root", password="123456", database="test_db") with conn.cursor() as cursor: cursor.execute("SELECT * FROM users") rows = cursor.fetchall() conn.close()with conn.cursor()结束时自动调用cursor.close(),conn.close()放在最后。实在搞不清,就记住一排顺序:先关游标,再关连接。别先关连接再操作游标,那样拿到的就是“在关闭的连接上的游标”,报错都不知道从哪里查。
6.2 大结果集:普通游标和SSCursor的区别
前面提到fetchall()可能把内存撑爆,那怎么办?PyMySQL提供了一个服务端游标类:pymysql.cursors.SSCursor,以及配套的SSDictCursor。
普通游标在执行execute()后,驱动会尽量把结果集拉到本地,之后你可以反复fetch。而SSCursor不会一次性把结果集拉到本地,它让MySQL在服务器端保存游标状态,你每次fetch时才真正去数据库取数据。这样内存占用大幅降低,特别适合超大结果集的导出、ETL任务。
import pymysql from pymysql.cursors import SSCursor conn = pymysql.connect( host="127.0.0.1", user="root", password="123456", database="test_db", cursorclass=SSCursor, ) with conn.cursor() as cursor: cursor.execute("SELECT * FROM big_table") for row in cursor: process(row)但SSCursor有个非常坑的限制:在结果集没有全部读完之前,这个连接不能再执行其他SQL。如果你在遍历过程中,又拿同一个连接去执行一条新查询,就会报错。原因是连接底层只有一个socket,旧的游标还在从socket里“吸”数据,新的查询根本插不进来。
所以用SSCursor时,要么专门开一个连接跑大查询,要么确保在遍历结束时,把所有行都读完再干别的。
6.3 事务没提交,数据去哪了
游标只是执行SQL的通道,真正决定数据是否写入数据库的是事务状态。PyMySQL默认autocommit=False,这意味着你执行INSERT、UPDATE之后,如果没调用conn.commit(),数据只是“暂存”在事务里,对别的连接不可见,而且连接关闭时会回滚。
一个比较推荐的用法是:
try: with conn.cursor() as cursor: cursor.execute("UPDATE users SET balance = balance - 100 WHERE id = 1") cursor.execute("UPDATE orders SET status = 'paid' WHERE id = 100") conn.commit() except Exception: conn.rollback() raise把多个操作放在同一个事务里,要么全部成功,要么全部回滚。这里注意:with conn.cursor()结束只关游标,不会提交事务,所以conn.commit()必须单独写。如果你用with conn:包裹整个代码块,行为又不一样——PyMySQL的连接上下文管理器在代码块正常结束时自动commit,异常时自动rollback,但它同样不关闭连接。
我在实际项目中习惯这样处理都用with conn:,因为它天然避免“忘了commit”这种低级事故:
with conn: with conn.cursor() as cursor: cursor.execute("UPDATE ...") cursor.execute("DELETE ...")6.4 游标不是越多越好
有些面试题或者项目里会看到“多个游标”的概念。其实游标是绑定在连接上的,一个连接同时最多只能有一个“读取中的游标”。你可以在同一个连接上创建多个游标对象,但如果你用一个游标执行了查询还没fetch完,又用另一个游标执行新的查询,行为就变得不可控。
这也是为什么我建议:一个连接同时只处理一个查询流。需要并发查询,就开多个连接,而不是在一个连接上搓多个游标。很多“锁等待超时”“connection is busy”的报错,源头都是这个。
7. 常见问题排查:那些年我被游标坑过的瞬间
7.1 ERROR 2002:怎么连都连不上
这个报错几乎每个MySQL新手都遇到过:
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)首先检查MySQL服务起没起:systemctl status mysql或者mysql -u root -p手动连一下。如果命令行能连,Python连不上,多半就是host写成了localhost。在Linux上,localhost会让驱动尝试连socket文件,但你的Python进程可能没有权限访问,或者socket文件的路径不对。改成127.0.0.1走TCP,通常就通了。
还有一种情况:MySQL端口不是默认的3306,而Python没传port参数。检查一下my.cnf里的port配置,再在连接参数里手动指定。
7.2 游标报“programming error”的常见场景
pymysql.err.ProgrammingError是最通用的错误类型,里面包含MySQL的错误码。最常见的两种:
- 1064:SQL语法错误。先检查SQL本身,再检查占位符是不是用了
?而不是%s。 - 1054:字段不存在。如果你用了字典游标,查出来的字段名拼错了就会报这个错,它在SQL里其实不会立刻暴露,因为MySQL接收到的是字符串。
排查这类问题,最快的方式是先把SQL打出来,到命令行或者Navicat里跑一遍。
7.3 存储过程执行了,但是取不到结果
这个问题特别经典。你写了:
cursor.callproc("sp_get_users") rows = cursor.fetchall() print(rows) # 空原因很可能是存储过程里有多个结果集。callproc()之后,第一个“结果集”可能根本不是一个SELECT的结果,而只是个状态指示。你需要用nextset()跳到真正的数据结果集:
cursor.callproc("sp_get_users") cursor.nextset() rows = cursor.fetchall()更省事的方案还是那句话:直接cursor.execute("CALL sp_get_users()"),然后按需要fetchall()+nextset(),可控性高很多。
7.4 读取大表时把Python搞崩了
如果你SELECT *了一张千万行级别的表,又在内存里fetchall(),那Python内存占用会瞬间跑到几个G甚至十几个G,轻则卡顿,重则被系统杀掉。
解决办法就是前面说的SSCursor,或者改成分页查询:
def fetch_paged(cursor, table, page_size=1000, last_id=0): while True: cursor.execute( f"SELECT * FROM {table} WHERE id > %s ORDER BY id LIMIT {page_size}", (last_id,) ) rows = cursor.fetchall() if not rows: break for row in rows: yield row last_id = rows[-1][0]注意,分页查询时ORDER BY id一定要加上,不然每次取出来的“下一页”数据是乱序的,很可能漏数据或者重复数据。
7.5 锁等待超时:1205错误
执行UPDATE时偶尔会遇到:
Lock wait timeout exceeded; try restarting transaction (1205)这个报错通常不是游标本身的锅,而是游标所在的事务没有及时提交,把行锁一直握在手里,其他事务拿不到锁,超时等待。解决办法是:检查代码里是不是有忘了commit()的操作,是不是事务里嵌套了太耗时的外部调用。
游标执行完,尽早提交或回滚,是避免这类问题最朴素也最有效的方法。
7.6 字符集乱码
Python查询出来的中文变成???或者乱码,优先检查三处:连接参数有没有charset="utf8mb4"、数据表本身的字符集是不是utf8mb4、MySQL客户端的default-character-set配置。有时候程序里是对的,但Navicat或命令行查出来乱码,那是客户端显示的问题,不是程序问题。
最后分享一个我自己的使用习惯
写到这里,游标这东西算是讲得差不多了。最后说一个我这些年养成的习惯:只要查询跑完,结果集取完了,我立刻把游标关掉,绝不让它“躺”在连接上过夜。一开始我也觉得无所谓,反正Python有垃圾回收。直到有一次在正式环境里,一个定时任务把游标放在循环里忘了关,跑了几天后MySQL连接数爆满,业务直接挂掉。从那以后,我写任何数据库代码,第一反应就是with上下文管理器,该关的绝不拖到后面。
还有个小技巧分享给你:调试游标问题的时候,不要想当然地在代码里猜。把执行的SQL打出来,把cursor.rowcount打出来,把返回的前几行打出来,问题基本就定位了一半。游标并不神秘,它就是你和MySQL之间那根数据水管上的吸管,学会控制它、善用它,你的Python+MySQL开发就能顺畅很多。
这篇文章的内容基于我自己的项目实践,游标的很多细节在不同驱动下会有些许差异,但核心思路是完全通用的。希望对你有帮助,也欢迎你在评论区分享自己遇到的游标相关的坑。