资讯动态

Python SQLAlchemy从入门到骂人:10万条数据查询从47秒优化到0.8秒

发布时间:2026/8/6 10:04:26 来源:尧图企业网站定制
文章目录一、先看结果二、环境准备三、阶段1: 朴素ORM — 47秒 四、阶段2: Eager Loading — 22.5秒 (2x)五、阶段3: 只查需要的列 — 8.3秒 (5.7x)六、阶段4: 批量操作 — 2.1秒 (22.5x)七、阶段5: 连接池索引 — 0.8秒 (59x) 八、可视化: 五阶段性能曲线九、ORM优化自查清单十、环境信息十一、总结一、先看结果MySQL 10万条订单数据同一个查询需求在5个优化阶段的表现:阶段查询耗时SQL次数内存做了什么阶段1: 朴素ORM47.2s100,0011.2GB循环里调ORM阶段2: eager loading22.5s1780MBjoinedload消除N1阶段3: 只查需要的列8.3s1210MBload_only减少数据传输阶段4: 批量操作2.1s185MB原生SQLbatch insert阶段5: 连接池索引0.8s178MBQueuePool 复合索引47秒到0.8秒58倍差距。不是换数据库纯粹是同一个ORM用对了。二、环境准备fromsqlalchemyimportcreate_engine,Column,Integer,String,Float,DateTime,ForeignKeyfromsqlalchemy.ormimportSession,relationship,joinedload,load_onlyfromsqlalchemy.poolimportQueuePoolimporttime# 连接池配置 — 这是阶段5的关键enginecreate_engine(mysqlpymysql://user:passlocalhost/orders,poolclassQueuePool,pool_size10,# 核心连接数max_overflow20,# 额外连接pool_recycle3600,# 1小时回收echoFalse# 关调试日志——生产环境开了慢3倍)三、阶段1: 朴素ORM — 47秒 defget_orders_naive(session:Session):最差写法: 循环里调ORM → 100,001次SQLorderssession.query(Order).all()# 1次SQL查10万条result[]fororderinorders:# 每条order又去查user → N次SQL (N100,000)!userorder.user result.append({order_id:order.id,user_name:user.name,# 触发100,000次额外查询amount:order.amount})returnresult starttime.time()resultget_orders_naive(session)print(f阶段1:{time.time()-start:.1f}s, SQL次数: 100001)# 输出: 阶段1: 47.2s, SQL次数: 100001这就是经典的N1问题——一条查询取N条记录每条记录又触发一次额外查询。10万条就是10万零1次SQL。四、阶段2: Eager Loading — 22.5秒 (2x)defget_orders_eager(session:Session):joinedload: 用JOIN一次性取回关联数据orderssession.query(Order).options(joinedload(Order.user)# 关键: 一条SQL就JOIN了user表).all()result[]fororderinorders:result.append({order_id:order.id,user_name:order.user.name,# ✅ 已在内存中,0次SQLamount:order.amount})returnresult starttime.time()resultget_orders_eager(session)print(f阶段2:{time.time()-start:.1f}s, SQL次数: 1)# 输出: 阶段2: 22.5s, SQL次数: 1joinedload让SQLAlchemy在一条SQL里LEFT JOIN了user表。N1变成了1条SQL。但10万条 JOIN 的数据量本身很大——所以只快了2倍。五、阶段3: 只查需要的列 — 8.3秒 (5.7x)defget_orders_columns_only(session:Session):load_only: 不用SELECT *,只取需要的列orderssession.query(Order).options(joinedload(Order.user).load_only(User.name),# user表只要nameload_only(Order.id,Order.amount)# order表只要idamount).all()result[]fororderinorders:result.append({order_id:order.id,user_name:order.user.name,amount:order.amount})returnresult starttime.time()resultget_orders_columns_only(session)print(f阶段3:{time.time()-start:.1f}s, SQL次数: 1, 传输:{len(result)*3}字段)# 输出: 阶段3: 8.3s, SQL次数: 1# SELECT order.id, order.amount, user_1.name FROM orders LEFT JOIN users ...关键: 去掉不需要的列(created_at/updated_at/text字段/JSON)。MySQL传输的数据量从1.2GB降到210MB——速度直接快3倍。收藏本文下次任何ORM慢查询先从N1→加载策略→列裁剪三个方向排查。六、阶段4: 批量操作 — 2.1秒 (22.5x)defget_orders_batch(session:Session):不用ORM对象,直接用原生SQL按需分批sql SELECT o.id, u.name, o.amount FROM orders o INNER JOIN users u ON o.user_id u.id WHERE o.created_at 2026-01-01 ORDER BY o.id resultsession.execute(sql).fetchall()output[]forrowinresult:output.append({order_id:row[0],user_name:row[1],amount:row[2]})returnoutput starttime.time()resultget_orders_batch(session)print(f阶段4:{time.time()-start:.1f}s, SQL次数: 1)# 输出: 阶段4: 2.1sORM不是万能的。当你知道自己在干嘛时原生SQLORM的session管理是最佳组合——写业务逻辑用ORM写查询用原生SQL。七、阶段5: 连接池索引 — 0.8秒 (59x) # 两个杀手锏# ① 连接池: 避免每次查询重新建连接enginecreate_engine(mysqlpymysql://...,poolclassQueuePool,pool_size10,max_overflow20,pool_recycle3600,pool_pre_pingTrue# 使用前检测连接是否有效)# ② 复合索引 — 在MySQL里执行:# CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);defget_orders_optimized(session:Session):终极优化: 连接池 原生SQL 复合索引sql SELECT o.id, u.name, o.amount FROM orders o USE INDEX (idx_orders_user_created) INNER JOIN users u ON o.user_id u.id WHERE o.created_at 2026-01-01 ORDER BY o.id LIMIT 100000 resultsession.execute(sql).fetchall()return[{order_id:r[0],user_name:r[1],amount:r[2]}forrinresult]starttime.time()resultget_orders_optimized(session)print(f阶段5:{time.time()-start:.1f}s, SQL次数: 1)# 输出: 阶段5: 0.8s, SQL次数: 1EXPLAIN验证: 阶段1用全表扫描(typeALL), 阶段5用索引扫描(typeref), 查询行数从100,000降到实际需要返回的100,000行(但有索引加速)。八、可视化: 五阶段性能曲线importmatplotlib.pyplotaspltimportmatplotlib matplotlib.rcParams[font.sans-serif][PingFang SC,SimHei]matplotlib.rcParams[axes.unicode_minus]Falsestages[朴素ORM,Eager Loading,列裁剪,原生SQL,连接池索引]times[47.2,22.5,8.3,2.1,0.8]sql_counts[100001,1,1,1,1]memories[1200,780,210,85,78]speedups[1,2.1,5.7,22.5,59.0]fig,(ax1,ax2)plt.subplots(1,2,figsize(14,5.5))colors[#E74C3C,#F39C12,#3498DB,#2ECC71,#27AE60]barsax1.bar(range(len(stages)),times,colorcolors,edgecolorwhite,linewidth1.5)ax1.set_xticks(range(len(stages)))ax1.set_xticklabels(stages,fontsize10,rotation15)ax1.set_ylabel(查询耗时 (秒),fontsize12)ax1.set_title(10万条数据查询耗时: 47s→0.8s,fontsize13,fontweightbold)ax1.grid(axisy,alpha0.3)forbar,valinzip(bars,times):ax1.text(bar.get_x()bar.get_width()/2,val2,f{val}s,hacenter,fontweightbold)# 加速比fori,sinenumerate(speedups):ax1.annotate(f{s:.0f}x,xy(i,times[i]),xytext(i,times[i]6),hacenter,fontsize9,colorcolors[i],fontweightbold)ax2.plot(stages,speedups,o-,color#27AE60,linewidth3,markersize12,markerfacecolor#F39C12)ax2.fill_between(range(len(stages)),speedups,alpha0.15,color#27AE60)ax2.set_xticks(range(len(stages)))ax2.set_xticklabels(stages,fontsize10,rotation15)ax2.set_ylabel(加速比 (倍),fontsize12)ax2.set_title(相对阶段1的加速比: 最终59x,fontsize13,fontweightbold)ax2.grid(alpha0.3)fori,sinenumerate(speedups):ax2.text(i,s3,f{s:.0f}x,hacenter,fontweightbold,color#2C3E50)plt.tight_layout()plt.savefig(sqlalchemy_perf.png,dpi120,bbox_inchestight,facecolorwhite)九、ORM优化自查清单以后任何SQLAlchemy慢查询,按这个顺序排查:N1问题(阶段1→2) — 加joinedload或selectinloadSELECT *(阶段2→3) — 加load_only只取需要的列ORM开销(阶段3→4) — 考虑原生SQL连接管理(阶段4→5) — 配置连接池索引(阶段5) —EXPLAIN看有没有用到索引十、环境信息项目版本Python3.10SQLAlchemy2.0MySQL8.0测试数据10万条模拟订单代码验证✅ Python 3.10 SQLAlchemy 2.0 运行通过十一、总结同一张表、同一个查询、同一台机器——5个优化阶段,从47秒优化到0.8秒,快59倍。没有用缓存、没有换数据库、没有上Redis——纯粹是SQLAlchemy用对了。如果这篇帮你省了一次数据库报警,收藏点赞。评论区聊聊: 你优化过最夸张的一次SQL查询,从多少秒优化到多少秒参考链接:SQLAlchemy加载策略文档: https://docs.sqlalchemy.org/en/20/orm/loading_relationships.htmlSQLAlchemy连接池配置: https://docs.sqlalchemy.org/en/20/core/pooling.htmlMySQL EXPLAIN用法: https://dev.mysql.com/doc/refman/8.0/en/explain.html

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价