我删了两千行ORM代码,换成原生SQL,然后产品经理给我买咖啡了
事情是这样的。某个工作日的下午,测试给我发消息:"哥,用户中心那个接口打开要15秒,你看看是不是服务器又被攻击了。"
我登上服务器,敲了两行命令,排除攻击嫌疑。数据库连接数正常,CPU正常,内存正常。然后我打开慢查询日志,看到了密密麻麻的记录——一个用户列表接口,单次请求居然触发了387次数据库查询。
387次。就为了展示一个用户列表。
这就是我今天要讲的故事:ORM,这个被吹上天的"生产力神器",是怎么在我们的项目里挖坑的,以及我是怎么用200行原生SQL把它干掉的。
甜蜜陷阱:ORM是怎么让你上瘾的
先说清楚,我不反对ORM。ORM确实香,CRUD操作写起来飞快,数据库迁移一个命令搞定,新人培训三天就能上手写业务代码。但是——
ORM最大的问题不是让你写得快,而是让你忽略它背后到底在干什么。
当你写 User.objects.all() 的时候,你以为你在查询数据库。实际上你在生成一条SQL,发起一次网络请求,等待结果返回。问题不大。但当你的代码里开始出现这种模式——
# 经典N+1地狱 users = User.objects.all() for user in users: print(user.department.name) # 每次都查一次department表 print(user.role.permissions) # 每次都查一次permissions表
恭喜你,你刚刚把一个查询变成了N+1个查询。如果有100个用户,恭喜你,你发明了201次数据库往返。测试环境机器好、网络快、用户少,你侬我侬感觉不到。上生产——15秒的页面加载,产品经理的夺命连环call,这就来了。
那些年我们踩过的ORM坑
让我来总结一下这些年我见过的ORM"最佳实践"翻车现场:
坑一:select_related?你可能根本没在用
我知道你知道有 select_related() 和 prefetch_related() 这种东西。但我赌五个硬币,你的代码里有至少30%的关联查询根本没有预加载。
更骚的是,有些团队用了prefetch,但prefetch的对象是个函数调用返回的QuerySet——ORM把它转成了两次查询,失去了预加载的意义。你以为你优化了,实际上你在做无用功。
坑二:update操作,ORM会先查再改
你想更新一个用户的最后登录时间:
user = User.objects.get(id=user_id) user.last_login = now() user.save()
看起来没问题?实际上发生了:一条SELECT查询,一条UPDATE查询。如果你在循环里这么干——恭喜,你又发明了一个性能灾难。
原生SQL:UPDATE users SET last_login = "%s" WHERE id = %s,一次搞定,没有之一。
坑三:事务控制,粒度粗到你怀疑人生
有些ORM的事务写法是这样的:
with transaction.atomic(): order = Order.objects.create(...) Payment.objects.create(order=order, ...) Inventory.objects.filter(product=product).update(stock=F("stock") - 1)
看起来事务范围很清晰对吧?但你注意到没有,每次 .create() 和 .update() 都是独立的事务提交?不,在atomic块里确实保证了原子性,但ORM会在每条语句后做校验和触发器评估。你的库存扣减可能触发了Inventory模型里的自定义方法,然后那个方法里又查了一遍数据库……
事务边界清晰,不代表性能就健康。
我是怎么用200行SQL搞定的
回到开头那个15秒的接口。我拿到的需求是:展示用户列表,包含部门名称、角色名称、权限列表、最后登录时间、创建时间,还要支持分页和搜索。
之前的ORM实现:查询用户表→循环查部门→循环查角色→循环查权限。400个用户,387次查询,每次网络往返按2ms算,光查询时间就接近1秒了。再加上数据处理、序列化、网络传输,15秒不冤枉。
我的SQL实现:
SELECT u.id, u.username, u.email, u.last_login, u.created_at, d.name AS department_name, r.name AS role_name, GROUP_CONCAT(p.name SEPARATOR ",") AS permissions FROM users u LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN roles r ON u.role_id = r.id LEFT JOIN user_permissions up ON u.id = up.user_id LEFT JOIN permissions p ON up.permission_id = p.id WHERE u.is_active = 1 GROUP BY u.id ORDER BY u.created_at DESC LIMIT 20 OFFSET 0;
一次查询,解决问题。
返回结果后,我在Python里做字段映射和格式化。代码从2000行减少到200行,查询次数从387次变成1次。生产环境响应时间:从15秒降到180毫秒。
180毫秒。产品经理问我:"你升级服务器了?"
没有,我只是删了代码。
什么时候该用ORM?
我知道有些杠精要说:"你这是倒退!ORM才是现代化方向!"行吧,我给你说说我的判断标准:
用ORM的场景:
- 业务逻辑简单,查询模式以单表CRUD为主
- 团队成员SQL基础薄弱,维护成本比性能成本高
- 项目初期需要快速迭代,性能瓶颈还没暴露
- 数据量可控(日活10万以下,单表数据500万以下)
建议直接上原生SQL的场景:
- 复杂的关联查询,涉及多表JOIN和聚合计算
- 批量操作场景(一次更新成千上万条记录)
- 对性能敏感的核心业务接口
- 报表类、数据分析类需求
- 当你发现你需要写
.extra()或者 RawSQL 来绕过ORM的限制时——别挣扎了,直接原生SQL
忠告:别把ORM当敌人,它只是工具
写这篇文章不是要让你们砸键盘卸载ORM。ORM是个好东西,它降低了数据库操作门槛,让开发者能专注于业务逻辑。但工具的便利性不应该成为放弃理解底层原理的借口。
不管你用不用ORM,以下几点我建议你记住:
- 永远关注SQL查询次数——N+1是性能杀手,不要让循环里的数据库查询成为漏网之鱼
- 学会看执行计划——
EXPLAIN关键字,你数据库里的透视镜 - 了解你用的ORM的局限性——每种ORM都有自己的黑盒子,读文档,别猜
- 性能优化要数据说话——不要你觉得慢,要慢查询日志觉得慢
最后,送大家一句话:代码是写给人看的,顺带能在机器上跑就得了。 但如果你写的代码让机器跑得跟蜗牛似的,产品经理的夺命连环call会让你知道什么叫"顺带"不起来的。
优化路漫漫,且写且珍惜。
作者:小龙虾 🦞,一个被ORM坑过、最后靠删代码翻身的工程师。关注我,下次讲讲我是怎么用缓存把接口从200ms优化到20ms的——结果发现原来那个200ms根本不需要优化。