1. 先复现:fetchall 之后 fetchone 为什么是 None
你写了一段查询,execute之后先fetchall()拿到全部结果,接着想再取一行做校验,结果fetchone()返回None,fetchmany(3)返回空元组()。这不是 PyMySQL 的 bug,而是游标(Cursor)的取值位置在起作用。
PyMySQL 的Cursor对象内部维护一个结果集指针,fetchone()、fetchmany(size)、fetchall()三个方法共享同一个指针位置。每次取值都会把指针往后推:fetchall()会把指针推到结果集末尾,之后再调用fetchone(),指针已经在末尾之外,自然返回None;fetchmany(3)同理,返回空元组。
这个现象在原文 3.5 节只写了「注意」和一段示例输出,没有给排查路径。实际项目里,它常常和另外几个动作混在一起,让人误以为是连接断了或者事务提前结束:
- 查询中途调用了
ping(reconnect=True),连接被重建,游标失效; - 在只读查询后误调了
commit()或rollback(),事务边界变化; - 用了
SSCursor(不缓存游标),结果集是流式读取的,指针行为更敏感。
这篇按排障视角走一遍:先把出问题的调用顺序和实际输出整理好,再让走 TaoToken 通道的 Codex 对照 PyMySQL 的 Cursor / SSCursor 差异,检查execute、fetchall、fetchmany、fetchone的调用顺序,判断是重开游标还是换取值方法,最后在本地跑同一段查询确认返回值不再为空。
适合谁看:正在用 PyMySQL 做数据查询、被游标取值顺序坑过、想搞清楚缓存游标和不缓存游标差异的 Python 开发者。核心检索词就是 PyMySQL、fetchall、fetchone、游标位置、SSCursor。
2. 前置:注册 TaoToken 并创建 Key
排障过程中需要让 Codex 帮你对照 PyMySQL 的游标实现和调用顺序,这里走 TaoToken 通道。先打开官网注册账号:
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=注册完成后进入控制台创建 API Key:
https://taotoken.net/console创建 Key 的入口在 API Keys 页面:
https://taotoken.net/api-keys拿到 Key 之后,把 Codex 的 Base URL 填成:
https://taotoken.net/api注意两点:不带/v1,不加 UTM 参数。Key 就填刚创建的那把。配置完成后,Codex 的请求会走 TaoToken 通道,你可以直接让它读你贴进去的 PyMySQL 调用代码和输出,对照游标位置做分析。
如果你后面要长期做编码和 Agent 类任务,可以了解 Coding Plan:
https://taotoken.net/coding-plan需要验证模型对话效果,用模型对话入口:
https://taotoken.net/model-chat接入文档在这里:
https://taotoken.net/doc3. 可复制配置:把出问题的调用和输出整理成可分析片段
排障第一步不是改代码,而是把现场固定下来。你需要准备三段内容:连接与游标创建、execute 与 fetch 调用顺序、实际输出。下面是一段最小可复现脚本,你可以直接复制到本地跑。
import pymysql conn = pymysql.connect( host="127.0.0.1", user="root", password="your_password", database="testdb", charset="utf8mb4", cursorclass=pymysql.cursors.Cursor, # 默认缓存游标 ) try: with conn.cursor() as cursor: sql = "SELECT id, name FROM code ORDER BY id" affected = cursor.execute(sql) print("execute 返回受影响行数:", affected) all_rows = cursor.fetchall() print("fetchall 结果:", all_rows) one_row = cursor.fetchone() print("fetchall 之后 fetchone:", one_row) many_rows = cursor.fetchmany(3) print("fetchall 之后 fetchmany(3):", many_rows) finally: conn.close()跑出来的输出大概率是这样:
execute 返回受影响行数: 5 fetchall 结果: ((1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E')) fetchall 之后 fetchone: None fetchall 之后 fetchmany(3): ()把这段脚本和输出一起贴给走 TaoToken 通道的 Codex,让它对照 PyMySQL 的Cursor与SSCursor实现,检查调用顺序。你可以这样提问:
这是 PyMySQL 的查询代码和实际输出。execute 返回 5 行, fetchall 拿到全部结果后,fetchone 返回 None,fetchmany(3) 返回空元组。 请对照 pymysql.cursors.Cursor 和 SSCursor 的取值实现, 说明游标指针在 fetchall 之后的位置,并判断应该重开游标还是换取值方法。 同时检查 ping(reconnect=True)、commit()、rollback() 是否会让结果提前结束。Codex 会指出:Cursor的fetchall()内部循环调用fetchone()直到返回None,指针已经到末尾;SSCursor不缓存结果,fetchall()同样会耗尽流。两者在「取值位置共享」这一点上一致,区别在于内存占用和读取时机。
4. 验证请求:改完在本地跑同一段查询
知道原因后,改法有两种,取决于你的业务需求。
第一种:需要多次遍历同一结果集,就重开游标或重新执行查询。注意execute会重置游标位置,但会重新发一次 SQL。
with conn.cursor() as cursor: cursor.execute("SELECT id, name FROM code ORDER BY id") all_rows = cursor.fetchall() print("第一次全量:", all_rows) # 需要再取一行,重新执行 cursor.execute("SELECT id, name FROM code ORDER BY id") first_row = cursor.fetchone() print("重开后的第一行:", first_row)第二种:本来就想分页取值,那就不要先fetchall(),直接用fetchone()或fetchmany(size)按顺序取。
with conn.cursor() as cursor: cursor.execute("SELECT id, name FROM code ORDER BY id") first = cursor.fetchone() print("第一行:", first) next_three = cursor.fetchmany(3) print("接下来三行:", next_three) rest = cursor.fetchall() print("剩余全部:", rest)输出会按指针顺序推进:
第一行: (1, 'A') 接下来三行: ((2, 'B'), (3, 'C'), (4, 'D')) 剩余全部: ((5, 'E'),)如果你用的是SSCursor,取值方式一样,但要注意:不缓存游标在结果集很大时省内存,可一旦fetchall()把流读完,后面同样取不到。验证时把cursorclass换成pymysql.cursors.SSCursor再跑一遍,确认行为一致。
关于ping(reconnect=True):它用于检查连接是否存活,连接关闭时会重连。重连后旧游标失效,如果你在查询中途调用,可能让结果提前结束。排查时把它从查询逻辑里挪出去,只在获取连接后、执行查询前做一次健康检查。
关于commit()和rollback():它们结束当前事务。只读查询本身不依赖提交,但如果你在fetch之间调用了它们,事务边界变化可能影响游标状态。排查时先把这两个调用从查询段里去掉,确认返回值正常后再决定放回哪里。
改完在本地跑同一段查询,确认fetchone()不再返回None、fetchmany(3)不再返回空元组,就说明游标位置问题解决了。
5. 本篇常见错排查
5.1 fetchall 之后 fetchone 返回 None
这是最典型的游标位置问题。fetchall()把指针推到末尾,后续取值自然为空。判断方法:看调用顺序里fetchall()是否在fetchone()之前。解决:重开游标重新execute,或调整取值顺序,先fetchone再fetchall。
5.2 fetchmany(3) 返回空元组
和上面同源。如果fetchmany(3)之前已经fetchall(),指针在末尾,返回()。如果之前只取了部分行,fetchmany(3)会从当前位置继续取,剩余不足 3 行时返回实际剩余行数,不会报错。
5.3 SSCursor 下取值行为异常
SSCursor不缓存结果,适合大结果集。但它对连接状态更敏感:连接中断、ping(reconnect=True)触发重连、事务提前结束,都可能让流式读取中断。排查时先用默认Cursor复现,确认是游标位置问题还是连接问题,再切回SSCursor。
5.4 ping(reconnect=True) 让结果提前结束
ping本身是健康检查,但reconnect=True时如果连接已关闭会重建连接。重建后旧游标不可用,继续fetch会拿不到数据。排查方法:把ping调用注释掉,看结果是否恢复正常。如果恢复,说明是重连导致,把ping移到查询开始之前。
5.5 commit / rollback 影响查询结果
只读查询通常不需要commit。如果你在fetch之间调用了commit()或rollback(),事务结束可能让游标状态变化。排查时先移除这两个调用,确认查询正常后再按业务需要放回。写操作才需要commit,回滚用于异常恢复。
5.6 execute 返回值被误当成结果行数
execute()返回的是受影响行数,不是查询结果。查询结果要通过fetchone、fetchmany、fetchall获取。如果你把execute的返回值当成数据,会得到行数而不是行内容。排查时打印execute返回值和fetch返回值,对照类型。
6. 接入与排障入口
排障和接入相关的操作,走 API Keys 和接入文档:
https://taotoken.net/api-keys https://taotoken.net/doc需要验证模型对话效果,用模型对话入口:
https://taotoken.net/model-chat长期做编码和 Agent 类任务,看 Coding Plan:
https://taotoken.net/coding-planClaudeCodeAnthropic 相关入口:
https://taotoken.net/claude-code-anthropic回到这次排障本身:PyMySQL 的fetchall、fetchmany、fetchone共享同一个游标位置,这是设计行为不是故障。排查时先把调用顺序和实际输出固定下来,再让走 TaoToken 通道的 Codex 对照Cursor与SSCursor的取值实现,判断是重开游标还是换取值方法,顺带检查ping(reconnect=True)、commit()、rollback()是否让结果提前结束。改完在本地跑同一段查询,确认返回值不再为空,问题就闭环了。