Python SQLAlchemy内存泄漏疑问:循环写入DataFrame内存持续增长
我在循环写入DataFrame到数据库时,通过tracemalloc追踪到了内存泄漏问题:
- 前50次循环中,内存变化主要来自
tracemalloc自身代码,无关紧要; - 50次循环后,内存增长的核心来源是
%userprofile%\AppData\Roaming\Python\Python39\site-packages\sqlalchemy\sql\elements.py:1551,每次循环内存增加5KiB,对象计数增长5,和DataFrame的规模完全匹配。
原本以为循环中每次重新赋值df、完成写入后对象会被回收,不会出现内存泄漏,但实际使用SQLAlchemy时问题确实存在,且处理(1000000, 100)的大型DataFrame时,泄漏会变得非常显著。
示例代码
import pandas as pd import sqlalchemy as sa import tracemalloc tracemalloc.start(100) engine = sa.create_engine( 'mssql+pyodbc://' + UN + ':' + PW + '@' + HOST + '/' + DB +"?driver=ODBC Driver 17 for SQL Server&Encrypt=Yes", pool_pre_ping=True, fast_executemany=True) things = ['a', 'b', 'c'] do_count = 0 def get_some_data(): df = pd.DataFrame([['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no'], ['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no'], ['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no'], ['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no'], ['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no'], ['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no'], ['Ajitesh', 84, 183, 'no'], ['Shailesh', 79, 186, 'yes'], ['Seema', 67, 158, 'yes'], ['Nidhi', 52, 155, 'no']]) return df def write_the_data(tablename, collected_data): collected_data.to_sql(name=tablename, con=engine) snapshot1 = tracemalloc.take_snapshot() for thing in things: while do_count < 5000: df = get_some_data() write_the_data(tablename = thing, collected_data = df) do_count += 1 print("Biggest DIFF: " + str(tracemalloc.take_snapshot().compare_to(snapshot1, 'lineno')[0]))
内存泄漏的核心原因
1. SQLAlchemy 元数据缓存累积
pandas的to_sql每次调用时,都会通过SQLAlchemy自动检测或反射目标表的元数据(列结构、类型等)。SQLAlchemy默认会缓存这些元数据对象,循环中反复触发元数据解析时,新创建的表结构对象、列对象会被SQLAlchemy的内部缓存持有,无法被Python垃圾回收(GC)及时清理。
你追踪到的elements.py:1551正是SQLAlchemy构造列元素、SQL表达式的核心位置,每次循环都会生成新的SQL插入语句相关对象,这些对象被关联到元数据缓存中持续累积。
2. 连接池上下文残留
你启用了连接池(pool_pre_ping=True),每次to_sql会从池里获取连接,使用后归还。但连接池中的连接可能会持有SQL语句编译缓存、参数绑定对象等上下文状态,随着循环次数增加,这些缓存不断堆积,尤其是开启fast_executemany=True时,批量插入的参数绑定对象会占用更多内存且难以被回收。
3. pandas to_sql的重复对象创建
如果没有提前指定表结构或重用SQLAlchemy的Table对象,to_sql每次都会重新创建Table实例、插入语句对象,这些临时对象虽然看似应该被回收,但由于被SQLAlchemy的元数据或连接上下文引用,GC无法及时清理,最终导致内存泄漏。
4. GC延迟回收
Python的垃圾回收是惰性触发的,循环执行速度快时,新对象不断创建,GC还没来得及处理旧对象的引用,就会导致内存持续增长。大型DataFrame会放大这个问题,因为每次循环创建的中间对象体积更大。
解决建议
重用SQLAlchemy
Table对象:提前定义或反射表结构,避免每次to_sql都重新创建元数据对象:from sqlalchemy import MetaData, Table metadata = MetaData() # 提前反射表结构,或者手动定义列 target_table = Table('a', metadata, autoload_with=engine) def write_the_data(tablename, collected_data): # 指定if_exists避免报错,提前定义表结构减少元数据创建 collected_data.to_sql( name=tablename, con=engine, if_exists='append', index=False, dtype={col: target_table.c[col].type for col in collected_data.columns} )显式管理连接上下文:手动控制连接的获取与释放,确保上下文及时清理:
def write_the_data(tablename, collected_data): with engine.begin() as conn: collected_data.to_sql( name=tablename, con=conn, if_exists='append', index=False )定期触发GC:在循环中手动调用垃圾回收,缓解内存增长速度:
import gc # 每100次循环触发一次GC if do_count % 100 == 0: gc.collect()直接使用SQLAlchemy Core写入:绕过pandas的
to_sql,用Core API批量插入,更精准控制对象生命周期:def write_the_data(tablename, collected_data): metadata = MetaData() table = Table(tablename, metadata, autoload_with=engine) data_records = collected_data.to_dict('records') with engine.begin() as conn: conn.execute(table.insert(), data_records)
内容的提问来源于stack exchange,提问作者JasperKPI

