用Python连SQL Server,真正的问题往往不在Python代码,而在pymssql这一层。最近我在做一个报表项目,需要按月把业务库的数据拉出来汇总,开发阶段直连测试环境一切正常,代码一到生产环境的服务器上就开始报(20009, 'DB-Lib error message 20009, severity 9: Unable to connect: Adaptive Server is unavailable or does not exist')。如果你也撞上这类SQLServer连接问题,网上能搜到一堆“改这个参数就好了”的碎片方案,但很少有人把底层逻辑串起来讲。这篇文章就把pymssql连接sqlserver的安装、报错、网络链路、加密认证、真实案例一次讲透,适合所有用Python读写SQL Server的人。
1. 先读懂报错:FreeTDS、DB-Lib和pymssql的三层关系
1.1 一条OperationalError里藏着几层信息
很多人第一次看到pymssql报错会懵:错误信息里全是“DB-Lib error message”这种看起来跟Python毫无关系的文字,而且错误本身还是以元组形式出现的,比如(20009, 'DB-Lib error message 20009, severity 9: ...')。要理解这个格式,得先弄清楚pymssql在Python和SQL Server之间扮演的角色。
pymssql并不是纯Python实现的数据库驱动。它底层调用的是FreeTDS这个C语言库,而FreeTDS实现了SQL Server使用的TDS协议——Tabular Data Stream,一种专门用于SQL Server和Sybase数据库的表格数据流协议。pymssql做的只是把Python调用翻译成FreeTDS的函数调用,再把FreeTDS返回的数据结果转成Python对象。所以当你看到报错前缀是“DB-Lib error message”时,这条错误信息根本不是Python生成的,而是FreeTDS最底层的那一层,也就是DB-Lib层给出的。
一条完整的pymssql连接报错通常长这样:
pymssql.OperationalError: (20009, b'DB-Lib error message 20009, severity 9:\\nUnable to connect: Adaptive Server is unavailable or does not exist\\n')拆开来看,第一个数字是错误码,20009代表“服务器不可达”;后面跟着的一长串字符串包含了严重级别和具体描述。其中的severity 9在TDS协议里表示一个非致命错误,但连接已经被中断或无法建立。理解这个结构的意义在于:你可以根据错误码快速判断问题出在链路哪一段,而不是盲目尝试各种网上看来的参数。
1.2 从Python调用到SQL Server的完整链路
一次pymssql连接请求,在成功拿到数据之前,要依次经过下面这些环节:
- pymssql读取
server、port、user、password等连接参数; - FreeTDS根据地址发起TCP连接;
- 双方通过TDS prelogin阶段协商版本、加密方式、字符集;
- SQL Server验证账号密码,返回登录结果;
- 登录成功后切换到指定数据库;
- 之后才能执行查询语句。
每一个环节出问题,报错的表现都不一样。第2步失败,通常就是20009;第3、4步失败,容易看到TDS版本相关错误、加密错误或登录失败;第5步失败,往往是数据库不存在或没有权限;第6步之后的报错才跟SQL语句本身有关。
所以当你拿到一条连接失败信息时,第一件事不是改代码,而是判断它属于哪个环节。我见过太多人在第6步的SQL问题上反复纠结,结果其实是第2步的防火墙把端口挡了,SQL语句背了黑锅。理解了这条链路,后面的排查才有方向。
2. 安装环节的两类典型失败:缺依赖源码编译与Python版本不匹配
2.1 Linux下源码编译报错与FreeTDS开发包依赖
pymssql在Windows和macOS上通常有现成的预编译wheel包,但在部分Linux环境和较新的Python版本上,pip会尝试从源码编译。源码编译最常见的一个报错是这样:
fatal error: freetds.h: No such file or directory这个报错的意思很简单:编译pymssql需要FreeTDS的头文件,但你的系统里只有FreeTDS的运行时库(或者连库都没有),缺少开发包。Debian/Ubuntu系统需要安装freetds-dev,CentOS/RHEL系统对应的是freetds-devel。
以Debian/Ubuntu为例:
sudo apt-get update sudo apt-get install -y freetds-dev build-essential python3-devCentOS/RHEL则这样处理:
sudo yum install -y freetds-devel gcc python3-devel openssl-devel如果你看到的报错是Cython相关的问题,或者提示找不到cythonize,同样需要先安装构建依赖。pymssql从源码构建时要依赖Cython把模板代码生成C源文件,版本不满足就会出现这类错误。较新的pymssql源码包通常会在pyproject.toml里声明构建依赖,但你本地的pip版本如果太老,可能不会自动安装这些构建依赖,这时候升级pip能解决大半问题:
pip install --upgrade pip setuptools wheel2.2 Windows/Mac的wheel安装与版本选型
Windows用户在这个环节踩坑相对少,多数情况下直接pip install pymssql就能装上。但如果你用的是较新的Python版本,比如3.12或3.13,而当前pymssql版本还没有提供对应的预编译wheel,pip会尝试现场编译。Windows现场编译pymssql非常痛苦,你没有Visual Studio的合适工具链,没有FreeTDS开发头文件,基本寸步难行。
我的建议是,遇到这种情况先升级pymssql到最新版本,很多新版本就是为适配新Python而发布的。如果升级之后还是提示No matching distribution found for pymssql,再考虑退回一个更成熟的Python小版本,或者用Docker跑一个官方Python镜像,镜像里的环境通常能顺利装好。
macOS用户则先安装FreeTDS再装pymssql会顺很多:
brew install freetds pip install pymssql装完之后一定记得验证一下安装结果,这个操作很多人忽略:
import pymssql print(pymssql.__version__) print(pymssql.freetds_version)pymssql.freetds_version能显示编译进去的FreeTDS版本号。这个信息在排查加密和TDS版本兼容问题时非常有用,我后面会提到。
3. 连不上服务器的三大根因:网络端口、实例解析与TCP/IP协议
3.1 错误码20009和20002的定位分界线
pymssql连接失败时最常出现的两个错误码是20009和20002。很多人把这两个错误混为一谈,觉得反正都是连不上。但它们代表的故障环节完全不同,分清楚能省掉大量无效排查。
| 错误码 | 典型表现 | 故障环节 | 排查重点 |
|---|---|---|---|
| 20009 | Unable to connect: Adaptive Server is unavailable or does not exist | TCP连接阶段,找不到目标服务器 | 地址写没写错、端口通不通、服务启没启动、实例名能否解析 |
| 20002 | Adaptive Server connection failed | TCP连接已建立,但连接过程中断 | 网络稳定性、TDS/加密协商失败、服务端主动断开、负载过高 |
我自己的经验是:如果报错是20009,先把网络层和服务层查个底朝天,先别碰代码;如果是20002,除了查网络,还要重点关注SQL Server实例的配置和客户端FreeTDS版本。
举例来说,我在某台服务器上排查过一次20009,ping主机能通,telnet 1433端口也通,但pymssql就是报20009。后来发现该服务器上装了多个SQL Server实例,默认实例根本没启用TCP/IP,网络是通的,但那个端口后面没有服务在听。这就是典型的“端口通不等于数据库可达”。
3.2 三步验证:ping通不等于端口通,端口通不等于实例可达
遇到20009,按下面三个步骤排查,基本能覆盖90%的原因。
第一步,验证网络和端口。用telnet或系统自带的工具直接测目标端口:
telnet 192.168.1.100 1433在Windows PowerShell里可以这样:
Test-NetConnection 192.168.1.100 -Port 1433如果端口不通,先看防火墙入站规则有没有放行SQL Server端口,再看SQL Server服务有没有启动,最后看SQL Server配置管理器里TCP/IP协议是不是处于启用状态。SQL Server的默认协议设置里,Shared Memory和Named Pipes可能开着,但TCP/IP未必开了,而pymssql走的恰恰是TCP协议。
第二步,确认实例监听端口和连接写法。SQL Server实例分两种:默认实例监听1433,命名实例则可能监听一个动态端口。命名实例的标准写法是主机名加反斜杠加实例名,比如192.168.1.100\REPORT。如果在Python里把它写成'192.168.1.100/REPORT',或者逗号风格'192.168.1.100,1433',解析就会出问题。
第三步,检查SQL Server Browser服务。命名实例要能被客户端用实例名解析到动态端口,靠的是SQL Server Browser服务,它监听UDP 1434。这个服务没启动,或者UDP 1434被防火墙拦截,pymssql解析不了实例名,只能报20009。最省心的办法是把命名实例改成固定端口,或者干脆在连接字符串里直接用IP加端口,跳过实例名解析。
4. 新版SQL Server最容易踩的暗坑:强制加密与TDS版本
4.1 SQL Server强制加密时客户端连接失败的常见表现
这几年遇到越来越多人问:pymssql连SQL Server 2019或2022时,在旧实例上好好的连接代码突然就报错了。问题大概率出在加密设置上。SQL Server较新的版本默认启用了更严格的加密行为,如果客户端使用的FreeTDS版本过老,不支持服务器要求的加密方式,连接会在TDS prelogin阶段被服务器掐断。
这种故障的表现很有迷惑性:错误码可能是20009,也可能是20002,而且不会在报错信息里明确写“加密失败”几个字。你检查网络、端口、账号密码都没问题,但连接就是不稳定,或者时好时坏。
按我的实践经验,处理这类问题要分两头看。服务器端,最推荐的做法是给实例配置一张受信任的证书,让客户端能完成证书校验。如果只是临时调试,在SQL Server配置管理器里把“强制加密”调整为非强制也可以,但生产环境不建议这么做,因为那相当于把数据传输暴露在明文风险下。
客户端这边,FreeTDS 1.2及以上版本支持通过freetds.conf配置加密行为。在配置文件里加上:
[global] encryption = requestencryption = request表示如果服务器支持加密就使用加密,服务器不要求时也不强制。如果你确认服务器开启了强制加密,可以改成require,表示必须加密连接,不加密就连接失败。改完freetds.conf之后,重启Python进程重新连接。
还有一点要特别提醒:pymssql本身没有直接在连接参数里暴露“信任服务器证书”这种开关,所以大多数加密相关的调整最终都落在FreeTDS配置上。排查前先确认一下pymssql.freetds_version输出的版本号,如果版本低于1.2,建议先升级pymssql,很多老版本连加密协商都做不完整。
4.2 TDS版本选不对:什么时候需要显式指定tds_version
TDS版本是另一个容易被忽略的因素。SQL Server多年迭代,每个阶段引入的新特性需要对应的TDS协议版本支持。pymssql允许在连接时指定tds_version参数,如果这个值设置得低于SQL Server实际所需的最低版本,连接同样会出问题。
常见的TDS版本和SQL Server大致对应关系是这样的:
| TDS版本 | 对应SQL Server年代 | 说明 |
|---|---|---|
| 7.0 | SQL Server 2000 | 老协议,不支持很多新类型 |
| 7.1 | SQL Server 2005 | 支持基础新特性 |
| 7.2 | SQL Server 2008 | 增强了一些数据类型支持 |
| 7.3 | SQL Server 2008 R2 / 2012 | 目前大部分pymssql默认区间 |
| 7.4 | SQL Server 2012及更新版本 | 需要较新的FreeTDS |
| 8.0 | SQL Server 2022及之后 | 新协议,需要FreeTDS 1.4+ |
如果你连的是SQL Server 2016以上,而pymssql默认用的还是7.0或7.1,可能会遇到两类问题:一是某些数据类型在查询时行为异常,比如新版datetime2、date类型返回的结果不对;二是连接建立后执行某些SQL时报错。这类错误不太容易让人联想到是TDS版本问题,因为它的表现五花八门。
我的建议很直接:连接SQL Server 2012以上,显式指定tds_version='7.3';SQL Server 2016以上,尝试tds_version='7.4'。如果tds_version='7.4'报版本不支持,说明你的FreeTDS版本太老,需要升级pymssql或单独升级FreeTDS。
import pymssql conn = pymssql.connect( server='192.168.1.100', user='sa', password='your_password', database='your_db', tds_version='7.3', )5. 18456登录失败与中文乱码的定式解法
5.1 18456背后的认证模式问题与排查顺序
错误码18456是SQL Server层返回的登录失败错误,跟网络和端口无关。它的错误信息里通常包含Login failed for user 'xxx',后面跟着一个状态码。很多人只看到18456就开始改密码,但真正的问题可能出在认证模式上。
SQL Server有两种认证模式:Windows认证模式,只允许Windows系统账户通过集成认证登录;混合认证模式,允许Windows认证和SQL Server账号密码认证同时存在。如果你的服务端处于纯Windows认证模式,那不管sa密码多正确,连接时都会收到18456。
检查认证模式最直观的方式是在SSMS里右键服务器属性,找到“安全性”页,看“服务器身份验证”选的是哪种。命令行环境也可以查注册表:
HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\<实例ID>\MSSQLServer\LoginMode这个键值如果是0,表示Windows认证模式;如果是1,表示混合认证模式。修改后需要重启SQL Server服务才能生效。
确认了认证模式正确,还要检查账号本身的状态。SQL Server登录账号在数据库里可能有这些状态:禁用、密码过期、要求首次登录必须修改密码,或者账号有效但未被授予目标数据库的访问权限。特别是“要求首次登录必须修改密码”这种设置,你如果用密码去连,照样会报18456。
排查顺序我建议固定为:认证模式确认、账号不存在检查、密码正确性检查、密码过期或强制修改检查、数据库用户权限检查。这个顺序能把绝大部分18456问题快速定位,不用一次次瞎试。
5.2 charset与数据库排序规则的配合:不再看到问号
连上数据库之后,第二个让人头疼的问题是中文乱码。pymssql查询出来的中文字段显示成???,或者一堆乱字符,问题通常出在charset参数和SQL Server端排序规则的不匹配上。
pymssql的charset参数有两个作用:一是指定客户端发送查询请求时使用的编码,二是指定客户端期望服务器返回结果时使用的编码。SQL Server端每个数据库都有一个排序规则,比如Chinese_PRC_CI_AS就是常见的中文不区分大小写排序规则,底层编码是GBK/CP936;而Latin1_General_BIN2之类的规则则是基于Latin1。
如果数据库排序规则是中文GBK体系,连接时应该用charset='cp936':
import pymssql conn = pymssql.connect( server='192.168.1.100', user='sa', password='your_password', database='your_db', charset='cp936', )如果数据库用的是SQL Server 2019以上才支持的UTF-8排序规则,则把charset设为utf8:
conn = pymssql.connect( server='192.168.1.100', user='sa', password='your_password', database='your_db', charset='utf8', )我自己处理乱码问题的经验是:先把Python脚本里打印结果的终端编码固定为UTF-8,再排查pymssql的charset。有时候数据本身是对的,只是终端显示编码不对,就会误判成数据库乱码。如果脚本和charset都对了还乱,再用DBCC CHECKDB一类工具确认数据库里的实际字节内容。新建项目的话,我建议直接在当前SQL Server版本支持的前提下,把表和数据库排序规则统一成UTF-8相关规则,连接配charset='utf8',Python侧不用再做任何编码转换,整套链路最干净。
6. 一次生产环境连接故障的完整排查链路:命名实例与动态端口
6.1 把20009当网络问题处理,差点带偏方向
某次遇到一个报表服务(模拟项目X),部署到生产服务器后,日志里频繁出现pymssql抛出20009错误,而且不是一直失败,是间歇性的:有时一小时内失败几次,有时整个上午都正常。开发环境完全复现不了,因为开发环境直连的是一台测试库,测试库用的是默认实例加固定端口1433。
按照常规思路,我第一轮排查把重心放在网络上。ping生产数据库主机,延迟正常;telnet1433端口,每次都通;防火墙规则也确认过,TCP入站规则对1433端口是放行的。到这里,网络层看起来干净得不能再干净了。既然端口能通,为什么pymssql还说服务器不可达?
后来仔细看了生产环境的连接日志,发现连接字符串里写的是server=r'prod-server\REPORT'这样的命名实例写法。问题一下子变得清晰:命名实例的端口不一定固定是1433,它可能由SQL Server在启动时动态分配一个端口,然后用SQL Server Browser服务在UDP 1434上广播实例名到端口的映射关系。换句话说,网络层测试的1433端口可能压根不是目标实例正在监听的端口。
6.2 根因确认:SQL Server Browser服务与实例名解析
我在生产数据库服务器上运行netstat -ano | findstr 1434和netstat -ano | findstr LISTENING查看SQL Server的实际监听状态,结果发现目标实例确实监听着一个动态端口,而且还开着UDP 1434的广播端口。但应用服务器上测试UDP 1434到生产服务器不通,进一步检查发现生产服务器的防火墙入站规则只放行了TCP端口,UDP 1434没有放行。
这就解释了整个故障链路:应用服务器每次发起连接,需要先把实例名REPORT解析成实际端口,这个解析过程依赖UDP 1434的通畅。UDP被防火墙拦截后,实例名解析失败,pymssql拿不到真实端口,于是报20009“服务器不可达”。开发环境之所以正常,是因为开发机器到测试库的网络路径上没有拦截UDP。
这个坑最隐蔽的地方在于:你会下意识觉得“端口都通了,问题肯定在数据库本身”,结果在数据库配置上反复折腾,完全没想到是UDP 1434被防火墙掐断了。
修复方案有两个方向。第一个方向是在SQL Server配置管理器里把命名实例的端口固定下来,强制它监听1433,这样连接字符串改成IP加端口的形式,彻底绕开UDP依赖;第二个方向是放行防火墙的UDP 1434,让SQL Server Browser服务能正常应答实例名解析请求。
我当时选了第一个方案,因为改动最小,而且后续所有连接代码可以统一成server='192.168.1.100,1433'这种直连格式,少一层动态解析就少一个故障点。
6.3 修复后的验证方法
修复完并不是立刻收工,要验证三件事。第一,确认实例实际监听的端口已经变成固定端口,netstat里能看到监听状态;第二,从应用服务器重新执行telnet 192.168.1.100 1433,确认TCP连通;第三,实际跑一次pymssql连接脚本,连续执行多轮,确认不再复现20009。
import pymssql conn = pymssql.connect( server=('192.168.1.100', 1433), user='x_report_user', password='your_password', database='ReportDB', tds_version='7.3', login_timeout=10, ) cursor = conn.cursor() cursor.execute("SELECT 1") print(cursor.fetchone()) conn.close()这次经验之后,我每次接触新的SQL Server环境,都会先问一句:这是默认实例还是命名实例?如果是命名实例,网络层放行TCP 1433其实不能保证连接成功,必须把UDP 1434或者固定端口一起纳入规划。这个教训在文档里很难看到,踩过一次就记得特别牢。
7. 稳定连接的配置模板和运维习惯
7.1 一份能直接复用的pymssql连接参数模板
把上面所有经验总结成一份连接参数模板。我给一个适合大多数生产环境的配置示例:
import pymssql conn_params = { "server": ("192.168.1.100", 1433), "user": "report_user", "password": "your_password", "database": "ReportDB", "charset": "utf8", "tds_version": "7.3", "login_timeout": 10, "timeout": 30, "as_dict": True, "appname": "report-service", } conn = pymssql.connect(**conn_params)| 参数 | 推荐值 | 说明 |
|---|---|---|
| server | 元组形式(主机,端口) | 用IP加固定端口最稳,避免实例名解析的UDP依赖 |
| charset | utf8或cp936 | 根据数据库排序规则选择 |
| tds_version | 7.3或7.4 | 根据SQL Server版本调整 |
| login_timeout | 10到15秒 | 不要用默认60秒,故障时要快速暴露问题 |
| timeout | 30秒左右 | 查询超时保护,防止SQL卡死拖垮服务 |
| as_dict | True | 返回字典格式,代码可读性好 |
| appname | 服务名称 | 方便SQL Server侧监控哪个应用在连接 |
如果你要用SQLAlchemy,连接串这样写:
from sqlalchemy import create_engine engine = create_engine( "mssql+pymssql://report_user:your_password@192.168.1.100:1433/ReportDB?charset=utf8" )SQLAlchemy的pymssql方言会自动把URL参数转成pymssql.connect的参数,目前用下来兼容性不错。
7.2 连接池、超时和重试的设计建议
生产环境的服务不能每次请求都新建连接,一定要用连接池。我常用DBUtils的PooledDB配合pymssql,配置一个针对数据库连接池的包装层:
from dbutils.pooled_db import PooledDB import pymssql pool = PooledDB( creator=pymssql, maxconnections=10, mincached=2, blocking=True, server=("192.168.1.100", 1433), user="report_user", password="your_password", database="ReportDB", charset="utf8", tds_version="7.3", login_timeout=10, as_dict=True, )连接池里我建议把login_timeout设得比单次连接短一点,因为连接池创建连接时如果等太久,请求会堆积。blocking=True保证连接数达到上限时,请求是排队而不是直接抛错。
重试策略要区分错误类型。20009和20002这类瞬时网络类错误,值得重试两到三次;18456这类认证错误,重试一百次结果都一样,直接抛给上层处理就好。我写过一个简单的重试封装:
import time import pymssql def connect_with_retry(conn_params, retries=3, delay=2): for attempt in range(retries): try: return pymssql.connect(**conn_params) except pymssql.OperationalError as exc: code = exc.args[0] if exc.args else None if code == 18456 or attempt == retries - 1: raise print(f"连接失败(错误码{code}),{delay}秒后重试") time.sleep(delay)最后分享一个我自己的排查习惯:遇到pymssql连接问题,我从不先去改代码,而是先开三个终端分别做三件事——ping服务器、telnet端口、看SQL Server的ERRORLOG。三分钟定位到网络层还是服务层,就能省下一整天瞎试参数的时间。pymssql本身的重点从来不是Python语法,而是它背后那条从TDS协议到SQL Server实例的网络链路,把这条链路摸透了,绝大多数连接报错都能在五分钟内找到方向。