1. pyodbc 调 SQL Server 存储过程,为什么 for row in cursor 一行都不打印
你写了一段很典型的 Python 脚本:pyodbc.connect拼连接串,conn.cursor()建游标,cursor.execute("存储过程名称")调存储过程,最后for row in cursor: print(row)想拿到查询结果。结果跑起来要么直接报错,要么循环体一次都不进,屏幕上干干净净,连个空行都没有。
这个场景我太熟了。它通常不是"存储过程写错了",而是三处细节里至少有一处对不上:连接串的 DRIVER 写法、execute 的调用方式、游标遍历的时机。SQL Server 的驱动版本不同,连接串关键字就不同;存储过程如果带参数或者内部有SET NOCOUNT ON,返回结果集的行为也会变;而 pyodbc 的游标在execute之后如果没正确进入结果集状态,for row in cursor就是空转。
这篇按排障视角来写:把原始代码和本地报错整理好,交给 Codex 逐行对照,重点盯连接串、execute、游标遍历这三处差异。TaoToken 在这里只负责提供 Key 和兼容通道,不参与 Python 代码执行,也不替 pyodbc 连数据库——数据库连接始终是你本地环境的事。下面从环境准备到实跑验证,一步步来。
2. 前置准备:TaoToken 提供 Key 与兼容通道,不碰你的数据库
先把边界说清楚,避免误解。TaoToken 是一个模型调用的兼容通道,你在这里拿到 Key、配好 Base URL,就能让 Codex 这类编码助手正常发起请求。它不会去连你的 SQL Server,也不会执行你的 pyodbc 脚本。你的数据库账号、服务器地址、驱动版本,全部留在本地。
所以这一步只做两件事:注册拿 Key,把 Codex 的接入地址配好。
打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号,进入控制台创建 API Key。创建入口在 https://taotoken.net/api-keys ,建议给这个 Key 起个能认出来的名字,比如codex-pyodbc-debug,方便后面区分用途。
拿到 Key 之后,Codex 的 Base URL 填https://taotoken.net/api。注意这里不带/v1,也不要加任何查询参数。很多人习惯性补/v1,结果请求路径拼错,报 404 或者模型列表拉不出来。填完保存,重启一下 Codex 让配置生效。
如果你后面不只是排这一个 bug,而是想长期用编码助手改项目、跑 Agent 任务,可以看看 Coding Plan 页面 https://taotoken.net/coding-plan ,按用量选合适的档位。只是临时排障的话,按量用就行。
配好之后先别急着改代码。回到你的本地环境,把那段调存储过程的脚本原样跑一遍,确认通道可用、请求能正常发出去,再进入逐行对照环节。这个顺序很重要:先确认工具链通,再动业务代码,不然报错来源会混在一起。
3. 可复制配置:连接串、execute、游标遍历三处对照
现在进入正题。把下面这段原始代码和你的本地报错一起贴给 Codex,让它逐行对照。原始写法大概是这样:
import pyodbc conn = pyodbc.connect('DRIVER={SQL Server};SERVER=数据库服务器名称;DATABASE=要连接的数据库的名称;' 'UID=数据库账户名;PWD=数据库密码') cursor = conn.cursor() cursor.execute("存储过程名称") for row in cursor: print(row) cursor.close() conn.close()这段代码能跑通的前提很苛刻。下面按三处差异拆开讲。
3.1 连接串:DRIVER 写错是最常见的坑
DRIVER={SQL Server}是旧版 ODBC 驱动的名字,对应的是 SQL Server 2000 时代的驱动。现在大多数环境装的是ODBC Driver 17 for SQL Server或ODBC Driver 18 for SQL Server。驱动名对不上,pyodbc.connect会直接抛pyodbc.Error: ('IM002', '[IM002] [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序')。
先查本机装了哪些驱动:
import pyodbc print(pyodbc.drivers())输出里如果有ODBC Driver 17 for SQL Server,连接串就改成:
conn = pyodbc.connect( 'DRIVER={ODBC Driver 17 for SQL Server};' 'SERVER=你的服务器地址,1433;' 'DATABASE=你的数据库名;' 'UID=你的账号;PWD=你的密码;' 'TrustServerCertificate=yes;' )几个细节:SERVER后面带端口用逗号,不是冒号;TrustServerCertificate=yes在自签证书或本地开发环境经常需要,否则会报 SSL 相关错误;如果用的是 18 版驱动,加密默认开启,不加这个参数更容易连不上。
3.2 execute:存储过程名和参数写法
cursor.execute("存储过程名称")这种写法,如果存储过程不带参数,某些情况下能跑;但更稳妥的是显式用EXEC或者{CALL ...}语法。带参数时,直接拼字符串既容易出错又有注入风险,应该用参数占位:
cursor.execute("{CALL 你的存储过程名 (?, ?)}", param1, param2)或者:
cursor.execute("EXEC 你的存储过程名 @p1=?, @p2=?", param1, param2)如果你的存储过程内部有SET NOCOUNT ON,它不会影响结果集返回,但会影响cursor.rowcount,别拿 rowcount 判断有没有数据。
3.3 游标遍历:execute 之后要正确取结果集
for row in cursor本身没问题,问题往往出在"这个游标当前有没有结果集"。如果存储过程先执行了一堆INSERT/UPDATE,最后才SELECT,pyodbc 默认可能停在第一个受影响行数的状态,你需要用nextset()跳到真正的结果集:
cursor.execute("EXEC 你的存储过程名") while True: if cursor.description is not None: for row in cursor: print(row) if not cursor.nextset(): breakcursor.description为None表示当前不是结果集(比如是行数统计),跳过它继续nextset()。这样无论存储过程内部有几个语句,都能定位到真正的查询结果。
把这三处对照整理成表格,贴给 Codex 时更清晰:
| 位置 | 原始写法 | 常见问题 | 建议写法 |
|---|---|---|---|
| 连接串 | DRIVER={SQL Server} | 驱动名过时,IM002 报错 | DRIVER={ODBC Driver 17 for SQL Server} |
| execute | execute("存储过程名称") | 带参数时拼接易错 | {CALL 名 (?, ?)}或EXEC 名 @p1=? |
| 遍历 | for row in cursor | 多结果集时停在非结果集状态 | 配合cursor.description+nextset() |
4. 验证请求:实跑脚本,确认返回行正常打印
配置改完,回到本地实跑。先写一个最小验证脚本,只做连接和一次简单查询,确认通道和数据库都通:
import pyodbc conn = pyodbc.connect( 'DRIVER={ODBC Driver 17 for SQL Server};' 'SERVER=你的服务器地址,1433;' 'DATABASE=你的数据库名;' 'UID=你的账号;PWD=你的密码;' 'TrustServerCertificate=yes;' ) cursor = conn.cursor() cursor.execute("SELECT TOP 3 name FROM sys.tables") for row in cursor: print(row) cursor.close() conn.close()如果这三行表名能打印出来,说明连接串和游标遍历的基本链路没问题。然后再换成你的存储过程调用:
cursor.execute("{CALL 你的存储过程名 (?, ?)}", param1, param2) while True: if cursor.description is not None: columns = [col[0] for col in cursor.description] print("列名:", columns) for row in cursor: print(row) if not cursor.nextset(): break跑通后你会看到列名和每一行数据依次打印。如果列名打出来了但行是空的,那问题在存储过程本身的数据逻辑,不在 pyodbc 这一层。如果列名都没打出来,说明cursor.description一直是None,回到 3.3 检查nextset()的循环。
这一步的意义在于:先用一个已知能返回数据的简单查询确认通道可用,再逐步替换成真实存储过程。这样一旦出问题,你能立刻判断是环境问题还是代码问题。
5. 本篇常见错排查
排障时按报错信息对号入座,比盲目改代码快得多。
IM002 未发现数据源名称:驱动名写错或没装。跑pyodbc.drivers()看实际名字,照抄进连接串。
Login failed for user:账号密码或认证模式问题。确认 SQL Server 开了混合认证,账号有对应库的权限。
SSL Provider 相关错误:18 版驱动默认加密。加TrustServerCertificate=yes,或配好证书。
for row in cursor 不执行:先看cursor.description是不是None,是的话用nextset()往后找结果集。
存储过程有返回但 print 不出:检查是不是SET NOCOUNT ON加上多语句,导致游标停在行数状态。
Codex 请求报 404 或模型列表为空:Base URL 填成了带/v1的地址。改回https://taotoken.net/api,不带后缀。
改了配置但没生效:Codex 需要重启,配置是启动时读取的。
把报错原文和你的连接串(记得把密码打码)一起贴给 Codex,让它对照第 3 节的表格逐项检查,通常几分钟就能定位到具体哪一处。
6. 继续用 Codex 排障时的接入入口
排完这个 bug,如果你还想让 Codex 帮你继续改 pyodbc 相关代码、加参数校验、处理多结果集,接入入口整理在这里:
- 创建和管理 Key:https://taotoken.net/api-keys
- 接入文档与 Base URL 说明:https://taotoken.net/doc
- 想直接在网页里对话验证模型:https://taotoken.net/model-chat
- 长期编码和 Agent 任务:https://taotoken.net/coding-plan
Base URL 统一用https://taotoken.net/api,不带/v1。数据库连接始终在你本地,TaoToken 只负责把 Codex 的请求通道打通。按第 4 节的最小脚本先验证通道,再逐步替换成真实存储过程,排障会顺很多。