你的 ORM 正在慢慢杀死你的应用:一个老后端的血泪吐槽

2026-08-23 12 0

你的 ORM 正在慢慢杀死你的应用:一个老后端的血泪吐槽

我见过太多团队在应用性能崩溃时疯狂加机器、加缓存、加 Redis,以为是架构不够高大上。结果呢?机器加了三倍,数据库还是慢得像蜗牛。最后排查一圈,问题80%都出在 ORM 上。

今天我不写什么"ORM 入门教程",那种东西烂大街了。我来扒一扒 ORM 那些让人又爱又恨的设计,以及怎么在真实项目里不被它坑死。

第一宗罪:N+1 查询,你的数据库在无声尖叫

先问个问题:你在代码里写过类似这样的东西吗?

users = User.all()
for user in users:
    print(user.company.name)

看起来很正常对吧?但如果你去数据库监控里看执行日志,你会发现它执行了:1次查 users 表,然后 N 次查 companies 表(N = users 的数量)。这就是臭名昭著的 N+1 问题。

你一个"简单"的循环,让数据库多执行了几十甚至几百条 SQL。数据库在日志里疯狂重试,你的应用却浑然不觉,还觉得 ORM 真方便真好用。

正确的做法:

users = User.query.options(joinedload(User.company)).all()

或者用 select_in 预加载,把 N+1 优化成 2 条 SQL。这是基本功,但据我观察,至少一半的 Python/Java 后端都踩过这个坑,而且踩完还不知道问题在哪。

第二宗罪:事务边界被悄悄模糊

ORM 的事务封装让一切看起来都很简单。你不需要写 BEGINCOMMIT,ORM 自动帮你管了。但问题来了——

当你在一个业务方法里调用多个 ORM 操作时,ORM 会把它们放进同一个事务吗?大多数情况下会。但当你在业务逻辑里夹杂了第三方 API 调用、定时任务、消息队列的时候,事务边界就变得模糊了。

我见过最离谱的案例:业务代码在一个方法里做了三件事——更新订单状态、发消息给 MQ、调用仓储服务减库存。结果运维同事在某个高峰期发现数据库里订单状态变了,但库存没扣掉。查了半天发现是仓储服务超时抛异常了,事务已经 commit 了一半。

教训:事务边界必须显式管理,永远不要假设 ORM 会帮你做对。重要操作用 with_for_update() 显式加锁,业务层代码要能清晰看到事务范围。

第三宗罪:批量操作时,你以为的优化其实是假象

当你需要插入或更新 10000 条记录时,你大概会这么写:

for item in items:
    db.session.add(Item(**item))
db.session.commit()

看起来只 commit 了一次,很合理对吧?但 ORM 在这个过程中会为每一条记录维护一个持久化对象快照,这个内存开销在数据量大的时候会非常恐怖。

真实案例:我之前参与的一个数据导入功能,导入 5 万条订单记录。用 ORM 的方式跑,跑了 40 分钟还报内存溢出。后来换成原生 SQL:

values = [tuple(item.values()) for item in items]
placeholders = ",".join(["%s"] * len(items[0]))
sql = f"INSERT INTO orders (field1, field2, ...) VALUES {placeholders}"
cursor.executemany(sql, values)

结果:2.3 秒,内存占用稳定在 80MB。

这不是说永远不要用 ORM,而是说当你需要批量操作时,原生 SQL 是你最好的朋友。

第四宗罪:关系管理让表结构越来越难懂

ORM 的关系抽象(has_many、belongs_to、many_to_many)让新手也能快速上手,但代价是:你的数据库表结构设计变得越来越不直观。

我见过一个项目,用 ORM 定义了 12 张表之间的关系链,代码看起来清晰明白。但实际去查数据库时,发现有三张表实际上是冗余的——它们只是 ORM 用来实现 many_to_many 的"连接表",数据团队每次做报表都要多 join 三张表,查询时间翻倍。

一个建议:始终让你的数据库表结构设计独立于 ORM 定义。画 ER 图的时候用数据库思维,而不是 ORM 思维。ORM 是工具,不是数据模型本身。

第五宗罪:分页被悄悄"优化"成了全表扫描

很多 ORM 框架的分页 API 是这样的:

users = User.query.offset(10000).limit(20).all()

你觉得这是从第 10000 行取 20 条?数据库会聪明地跳过前 10000 行?错了。

在 MySQL 里,OFFSET 越大,查询越慢,因为数据库还是要扫描到第 10000 行然后扔掉它。OFFSET 100000 和 OFFSET 0 的查询时间可能差几十倍。

正确姿势:用游标分页代替偏移量分页。

users = User.query.filter(User.id > last_id).limit(20).all()

基于 ID 的游标分页,时间复杂度恒定在 O(1),不管翻到第几页,查询时间都差不多。这是 Pinterest、YouTube 这些大厂在用的方案,不是什么黑科技。

怎么和 ORM 和平共处

说了这么多 ORM 的坏话,不是让你抛弃它。ORM 在快速原型、CRUD 业务、团队协作方面确实有巨大价值。问题是:你要知道它的边界在哪,在边界处果断换档。

我的经验:

  • 用 ORM 处理单对象读写、简单查询、关系不复杂业务。这部分 ORM 体验最好,出错概率也低。
  • 用原生 SQL 处理批量操作、复杂报表、深度优化场景。特别是涉及到几十张表 join 的分析型查询,ORM 生成的 SQL 往往不够智能。
  • 始终监控你的 SQL 执行日志。在开发环境里打开 ORM 的 SQL 日志输出,每一个慢查询背后都是一个被忽视的 N+1 或全表扫描。
  • 定期做数据库执行计划分析。EXPLAIN 看你的查询有没有走索引,扫描了多少行。ORM 写的 SQL 不一定是最优的,你要有能力识别和修正。

最后说一句

ORM 是工具,工具本身没有对错。但如果你只会用它,而不懂它背后发生了什么,那你就成了它的奴隶。数据库不是你的 ORM 的私有财产,它是一个独立的系统,有自己的语言(SQL),自己的优化器,有自己的游戏规则。

尊重数据库,它才会尊重你的应用性能。

下次再遇到性能问题,别急着加机器。先打开数据库日志,看看 ORM 背后在干什么。

你会惊讶的。

相关文章

当群里突然@你,空气突然凝固了
AI圈最近在搞什么:从让程序员进群聊天,到用线性代数省掉一个亿
后端 CRUD 写到手残?我从“增删改查机器”到“架构师”的骚操作进化论
月薪3800,钱包却死得比我还惨:我是如何把记账变成悬疑小说的
休息日躺平实录:我是如何把咸鱼人设修炼到极致的
当小龙虾遇上OpenClaw:我和这个赛博打工人相处的日子

发布评论