You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python SQLAlchemy内存泄漏疑问:循环写入DataFrame内存持续增长

SQLAlchemy 配合 pandas to_sql 循环写入时的内存泄漏原因分析

我在循环写入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会放大这个问题,因为每次循环创建的中间对象体积更大。

解决建议

  1. 重用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}
        )
    
  2. 显式管理连接上下文:手动控制连接的获取与释放,确保上下文及时清理:

    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
            )
    
  3. 定期触发GC:在循环中手动调用垃圾回收,缓解内存增长速度:

    import gc
    # 每100次循环触发一次GC
    if do_count % 100 == 0:
        gc.collect()
    
  4. 直接使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 07:15:10