开启SQL跟踪/扩展事件时pandas to_sql性能骤降问题排查
问题背景
我们日常用pandas to_sql将CSV文件导入SQL Server现有表,开启fast_executemany=True时性能表现正常——40MB(35万条记录)的CSV仅需10秒就能完成导入。但近期发现,即便启用该参数,to_sql的执行速度变得异常缓慢,排查后确认问题与后台运行的SQL Sentry SQL Monitor(或手动开启的SQL跟踪/扩展事件捕获)直接相关,复现步骤如下:
- 在启用SQL Sentry跟踪的生产服务器上,同一文件导入耗时超10分钟(无阻塞,仅能看到表记录数缓慢增长)
- 在未部署SQL Sentry的开发服务器上,相同文件10-15秒即可完成导入
- 在开发服务器上开启扩展事件捕获或Profiler跟踪后,导入速度立刻变得和生产环境一样慢,暂停跟踪则立即恢复正常速度
核心疑问:为什么SQL跟踪会对导入性能产生如此巨大的影响?是否是因为生成了大量sp_execute语句?有哪些可行的解决办法?
补充信息:我们计划与DBA沟通,确认生产环境捕获的事件类型并尝试降低监控开销(该监控为全天运行的套件);另外发现,开启跟踪时使用to_sql的chunksize参数会得到不同的性能结果。
环境信息
- pandas 2.0(1.4版本也可复现问题)
- pyodbc 4.0.35
- SQL Server 2017
代码示例
import urllib import sqlalchemy as sa import pandas as pd host = 'my_server' schema = 'workdb' params = urllib.parse.quote_plus("DRIVER={ODBC Driver 17 for SQL Server};" "SERVER=" + host + ";" "DATABASE=" + schema + ";" "trusted_connection=yes;") engine = sa.create_engine("mssql+pyodbc:///?odbc_connect={}".format(params), fast_executemany=True) csv_path = r'C:\Users\me\Desktop\somefile.csv' # 40mb file df = pd.read_csv(csv_path, dtype_backend='pyarrow') # pyarrow for pandas 2.0+. df.to_sql(con=engine, name="target_table", schema="import", index=False, if_exists='append')
CSV文件示例
day,ds,gender,age_group,country,device,dormancy_cohort,reg_id,uid 2023-04-17,20230417,1,0,GBR,Android,4,03f9dfza868sb58zza0s8cd0d6f4,b2406ea4da557s9a65926az804
原因分析
sp_execute高频触发放大跟踪开销
当启用fast_executemany=True时,pyodbc会将批量数据拆分成多条sp_execute参数化执行语句。如果SQL跟踪/扩展事件捕获了RPC:Completed、SQL:BatchCompleted这类事件,每一条sp_execute都会触发跟踪工具的日志写入、事件分析操作。35万条记录会生成大量的sp_execute请求,跟踪工具需要对每一条请求进行采集、处理、存储,这会消耗SQL Server大量的CPU、IO资源,直接拖慢导入操作的处理速度。跟踪事件范围过宽加剧性能损耗
如果SQL Sentry或手动跟踪捕获了过多高开销的事件类型(比如执行计划、统计信息更新等),每一次sp_execute都会产生大量的跟踪数据,进一步加剧服务器资源消耗。尤其是全天运行的监控,服务器资源长期被占用,导入操作的资源优先级被压低,最终导致性能暴跌。
可行的解决办法
优化跟踪的事件捕获范围
和DBA协作,缩小SQL Sentry的监控范围:- 移除对
RPC:Completed、SQL:BatchCompleted这类高频事件的全局捕获,改为仅针对特定数据库、登录名或应用进行过滤 - 关闭对执行计划、统计信息更新等高开销事件的捕获,除非业务有强制需求
- 调整跟踪采样率,只捕获部分请求,降低资源消耗
- 移除对
调整
to_sql的chunksize参数
开启跟踪时chunksize会影响性能,原因是更大的chunksize能减少sp_execute的调用次数。比如将chunksize设置为10000甚至更高(根据服务器内存情况调整),这样批量请求的数量会大幅减少,跟踪工具需要处理的事件数也会显著降低,从而缓解性能问题。修改后的to_sql调用示例:
df.to_sql(con=engine, name="target_table", schema="import", index=False, if_exists='append', chunksize=10000)
改用SQL Server原生导入工具
如果跟踪配置无法调整,考虑绕过to_sql的参数化批量导入,改用SQL Server原生的高效导入方式:- 使用
bcp命令行工具直接导入CSV,原生批量导入的性能几乎不受跟踪影响(因为是单批次或极少批次操作) - 通过SQL Server的
BULK INSERT语句导入,配合临时表使用,性能远高于参数化批量插入 - 用pandas处理数据后生成CSV,再通过
subprocess模块调用bcp或BULK INSERT,实现自动化导入
- 使用
临时调整服务器资源优先级
如果服务器资源充足,可以临时提高导入操作的CPU、IO优先级,或者给SQL Server分配更多内存,减少跟踪与导入操作的资源竞争。不过这只是临时缓解方案,核心解决思路还是优化跟踪配置。
内容的提问来源于stack exchange,提问作者Gabe

