1. 为什么你的 SQLite 查询执行了却取不到数据很多人第一次用 Python 的 sqlite3 时都会遇到一个很迷惑的现象cursor.execute(SELECT ...)明明没报错print(cursor.fetchall())却返回一个空列表[]。代码看起来完全正确数据库里也确实有数据但就是取不出来。这个问题我在本地脚本和轻量 Flask 服务里都踩过最后发现根子都在对 cursor 对象的理解上——它到底是一个语句句柄还是一个结果游标先把结论摆出来SQLite 的 cursor 对象同时承担两个角色。第一个角色是语句句柄你通过它把 SQL 文本和参数交给数据库引擎去解析、编译、执行第二个角色是结果游标当语句产生结果集时它内部维护一个指向当前行的位置指针fetchone、fetchmany、fetchall都是在移动这个指针并读取数据。理解这两层身份是排查取不到数据和游标复用报错的关键。这篇文章面向用 Python sqlite3 做本地脚本、CLI 工具或轻量服务的开发者。我会从建表开始给出可以直接复制运行的参数化查询和游标遍历代码然后用逐行 fetch 和一次性 fetchall 的对比把结果集的行为讲清楚。你跟着敲一遍基本就能建立对 cursor 的直觉。核心检索词先明确SQLite cursor 对象是什么、能做什么、适合谁。它是什么——sqlite3 模块里执行 SQL 并遍历结果的接口对象能做什么——执行查询、参数绑定、逐行或批量取数、获取列描述适合谁——所有用 Python 操作本地 SQLite 文件、又不想引入 ORM 的开发者。下面进入实操。2. TaoToken 前置给本地脚本接一个稳定的模型调用入口在讲 cursor 之前先解决一个实际场景里的前置问题。很多人的 SQLite 脚本不是孤立的而是配合大模型做数据处理比如把查询结果喂给模型做摘要、分类或字段抽取。这时候你需要一个稳定的 API 入口。我目前用的是 TaoToken官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置时别把查询串拼进去。为什么在 SQLite 教程里提这个因为一个典型工作流是cursor 查出数据 → 组装成 prompt → 调用模型 → 把结果写回另一张表。如果模型调用这一环不稳定你会误以为是 cursor 取数出了问题排查方向就偏了。先把调用入口固定下来后面排障才能聚焦。你需要准备三样东西Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 在控制台创建Model ID 按你实际使用的模型填写。控制台入口是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。如果你只是想先验证模型能不能通可以用模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 手动发一条消息试试。这里要强调一个原则TaoToken 是模型调用的接入层不是数据库工具它不替代你的 SQLite 操作。cursor 的取数逻辑该怎样还是怎样两者是流水线上的不同环节。把这条边界划清楚后面遇到查询执行了但结果为空时你就不会去怀疑 API 层。对于长期做编码和 Agent 任务的场景可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到鉴权或参数格式问题先翻文档。如果你用 Claude Code 这类工具Anthropic 兼容入口是 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。前置准备好之后我们回到 cursor 本身。下面所有代码都假设你已经有一个可用的 Python 3 环境sqlite3 是标准库不需要额外安装。3. 可复制配置建表、参数化查询与游标遍历这一节给出完整可运行的代码。先建一个测试库和表插入几行数据然后分别演示参数化查询和两种遍历方式。你可以把下面这段直接存成demo_cursor.py运行。import sqlite3 # 连接数据库文件不存在会自动创建 conn sqlite3.connect(demo.db) # 让查询结果支持按列名访问返回 sqlite3.Row 对象 conn.row_factory sqlite3.Row cur conn.cursor() # 建表 cur.execute( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, city TEXT ) ) # 清空旧数据方便反复运行 cur.execute(DELETE FROM users) # 参数化插入注意用 ? 占位不要用字符串拼接 rows [ (Alice, 30, Beijing), (Bob, 25, Shanghai), (Carol, 35, Shenzhen), (Dave, 28, Hangzhou), ] cur.executemany(INSERT INTO users (name, age, city) VALUES (?, ?, ?), rows) conn.commit() print(插入完成rowcount , cur.rowcount)运行后你会看到rowcount 4。这里有个细节executemany之后rowcount反映的是受影响行数但不同语句类型下这个值的行为不完全一致后面排障章节会讲。接下来是参数化查询。参数化不只是防注入它也让 SQLite 能复用编译后的语句。写法有两种问号占位和命名占位。# 问号占位按位置传参 cur.execute(SELECT id, name, age, city FROM users WHERE age ?, (26,)) print(description:, cur.description) # 命名占位按字典传参 cur.execute( SELECT id, name, age, city FROM users WHERE city :city, {city: Shanghai}, )cur.description返回一个元组列表每个元组描述一列第一个元素是列名。这是你判断这条语句到底有没有结果集的依据之一——如果description是None说明这条语句不产生结果集比如 INSERT 或 UPDATE。现在重点来了遍历结果集。先看逐行 fetchcur.execute(SELECT id, name, age, city FROM users ORDER BY age) print(--- fetchone 逐行 ---) while True: row cur.fetchone() if row is None: break # row 是 sqlite3.Row可以按列名访问 print(row[id], row[name], row[age], row[city])再看一次性 fetchallcur.execute(SELECT id, name, age, city FROM users ORDER BY age) print(--- fetchall 一次性 ---) all_rows cur.fetchall() print(总行数:, len(all_rows)) for row in all_rows: print(row[id], row[name], row[age], row[city])两种写法输出顺序一致但内部行为差别很大。fetchone每次只把指针往前挪一行内存里始终只有当前行fetchall会把剩余所有行一次性读进内存返回一个列表。数据量小的时候感觉不到差异几万行以上时fetchall的内存占用就会明显上升。还有一个容易忽略的点fetchmany(n)介于两者之间每次取 n 行适合分批处理。cur.execute(SELECT id, name FROM users ORDER BY id) while True: batch cur.fetchmany(2) if not batch: break print(本批:, [dict(r) for r in batch])如果你需要把结果集转成字典列表[dict(row) for row in cur.fetchall()]是最常见的写法前提是设置了conn.row_factory sqlite3.Row。关于配置片段如果你用 Cline MCP 或类似工具管理数据库连接配置里通常需要三件套Base URL、Key、Model ID。以 JSON 形式举例{ base_url: https://taotoken.net/api, api_key: 你的APIKey, model_id: 你使用的模型ID }注意 Base URL 不要带 UTM 查询串Key 从控制台复制Model ID 按实际填写。这三件套在 Codex 的auth.json或 CC Switch 的配置里也是同样的结构路径按各工具文档来。4. 验证请求用逐行 fetch 与 fetchall 对比结果集行为光看代码不够得实际跑一遍看输出。我准备了一个对比脚本把同一个查询用两种方式遍历并打印游标位置相关的信息。import sqlite3 conn sqlite3.connect(demo.db) conn.row_factory sqlite3.Row cur conn.cursor() sql SELECT id, name, age FROM users ORDER BY age # 方式一fetchone 逐行 cur.execute(sql) print( fetchone 逐行 ) count 0 while True: row cur.fetchone() if row is None: break count 1 print(f第{count}行: {row[name]}, {row[age]}) print(共读取:, count) # 方式二fetchall 一次性 cur.execute(sql) print( fetchall 一次性 ) rows cur.fetchall() print(共读取:, len(rows)) for r in rows: print(f{r[name]}, {r[age]})预期输出是两组完全相同的四行数据顺序按 age 升序Bob 25、Dave 28、Alice 30、Carol 35。如果你看到的是空列表先别急着改代码往下看排障章节。这里有一个关键验证点同一个 cursor 上execute 会重置结果集指针。也就是说你execute一次之后fetchall拿完了所有行再fetchall一次会返回空列表因为指针已经在末尾了。想重新读必须重新execute。cur.execute(SELECT name FROM users) first cur.fetchall() second cur.fetchall() print(第一次:, len(first)) # 4 print(第二次:, len(second)) # 0指针已到末尾这个行为解释了很多查询执行了却取不到数据的困惑不是查询没执行而是结果集已经被前一次 fetch 消费掉了。cursor 不是快照它是一个有状态的迭代器。再验证一个场景rowcount在 SELECT 上的表现。很多人以为execute完 SELECT 就能用rowcount拿到行数实际上 SQLite 在 SELECT 上不预先计算总行数rowcount往往是 -1直到你 fetch 完才可能更新。cur.execute(SELECT * FROM users) print(fetch 前 rowcount:, cur.rowcount) # 通常是 -1 rows cur.fetchall() print(fetch 后 rowcount:, cur.rowcount) # 可能变成 4 print(实际行数:, len(rows))所以判断结果集行数最可靠的方式是len(cur.fetchall())而不是依赖rowcount。这一点在写分页逻辑时特别重要。如果你把查询结果接到模型调用上验证流程是cursor 取数成功 → 组装 prompt → 调用 API → 检查返回。模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 可以手动验证模型是否正常响应接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有请求格式说明。这样分段验证出问题时能快速定位是取数环节还是调用环节。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来对照。虽然有些报错来自模型调用层但它们经常和 SQLite 脚本混在一起出现导致排查方向混乱。我按错误信息逐条拆。错误一sqlite3.ProgrammingError: Cannot operate on a closed cursor.这是游标复用报错的典型。原因是你调用了cur.close()之后又拿它去execute或fetch。cursor 关闭后不能再用必须重新conn.cursor()创建。常见于把 cursor 存成全局变量某处关闭后别处还在用。cur conn.cursor() cur.execute(SELECT 1) cur.close() # cur.execute(SELECT 1) # 这里会抛 ProgrammingError cur conn.cursor() # 正确做法重新创建 cur.execute(SELECT 1)错误二sqlite3.OperationalError: no such table: users表不存在。检查你的建表语句是否执行过、数据库文件路径是否一致。相对路径demo.db会相对于当前工作目录脚本在不同目录运行时可能连到不同的文件。建议用绝对路径或pathlib固定位置。错误三sqlite3.InterfaceError: Error binding parameter 0 - probably unsupported type.参数类型不支持。SQLite 只接受 None、int、float、str、bytes 这几种基本类型。如果你传了 datetime、dict 或自定义对象需要先转换。datetime 用isoformat()转字符串复杂对象用json.dumps()。错误四401 Unauthorized这个来自模型调用层不是 SQLite。说明 API Key 无效或没带上。检查请求头里的 Authorization 字段确认 Key 从控制台正确复制没有多余空格。API Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以重新生成一个对比测试。错误五local proxy failed或连接被拒绝本地网络配置问题。检查 Base URL 是否写成了https://taotoken.net/api有没有误加路径或查询串。如果你在代码里硬编码了代理设置确认它指向的是可达的地址。这类错误和 cursor 无关别去改 SQL。错误六reading choices相关报错通常是响应解析失败比如返回体不是预期的 JSON 结构。先打印原始响应内容看看确认模型 ID 是否正确、请求体格式是否符合文档。接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有标准请求示例。错误七OAuth相关报错如果你用 Claude Code 或类似工具鉴权走的是 OAuth 流程。检查配置文件里的 Base URL、Key、Model ID 三件套是否齐全。Anthropic 兼容入口在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 配置格式参考对应文档。Codex 的auth.json里同样是这三项路径按工具要求放置。排查顺序建议先确认 SQLite 层表存在、参数类型对、cursor 没关闭再确认调用层Base URL、Key、Model ID。两层分开验证不要混在一起猜。6. 语义一致 CTA把 cursor 用顺之后下一步做什么cursor 用顺了你会发现它其实就是个有状态的迭代器execute 负责编译和执行fetch 系列负责移动指针读数据指针到末尾就返回 None 或空列表。把这个模型记牢查询执行了却取不到数据这类问题基本一眼就能定位——要么指针被前一次 fetch 消费了要么根本没产生结果集description 为 None。如果你在本地脚本里把 SQLite 查询和大模型调用串起来做数据处理建议先把调用入口固定成一套配置Base URL 用https://taotoken.net/apiKey 从控制台拿Model ID 按实际填。需要手动验证模型响应就去模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 遇到接入细节翻文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 长期跑编码和 Agent 任务可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 控制台总入口 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。最后留一个实用技巧处理大结果集时别用 fetchall用for row in cur直接迭代 cursor它内部就是逐行 fetch内存友好且写法更简洁。cur.execute(SELECT id, name FROM users ORDER BY id) for row in cur: print(row[id], row[name])这个迭代方式等价于循环 fetchone但代码更干净。记住一点迭代过程中不要对同一个 cursor 再 execute 别的语句否则指针会被重置迭代就乱了。需要嵌套查询就再开一个 cursor。