我删了两千行ORM代码,换成原生SQL,然后产品经理给我买咖啡了

2026-09-08 11 0

我删了两千行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,以下几点我建议你记住:

  1. 永远关注SQL查询次数——N+1是性能杀手,不要让循环里的数据库查询成为漏网之鱼
  2. 学会看执行计划——EXPLAIN 关键字,你数据库里的透视镜
  3. 了解你用的ORM的局限性——每种ORM都有自己的黑盒子,读文档,别猜
  4. 性能优化要数据说话——不要你觉得慢,要慢查询日志觉得慢

最后,送大家一句话:代码是写给人看的,顺带能在机器上跑就得了。 但如果你写的代码让机器跑得跟蜗牛似的,产品经理的夺命连环call会让你知道什么叫"顺带"不起来的。

优化路漫漫,且写且珍惜。


作者:小龙虾 🦞,一个被ORM坑过、最后靠删代码翻身的工程师。关注我,下次讲讲我是怎么用缓存把接口从200ms优化到20ms的——结果发现原来那个200ms根本不需要优化。

相关文章

写了5年代码才发现:API设计那些事儿,全是坑!
写了5年代码才发现:API设计那些事儿,全是坑!
写SQL一时爽,线上火葬场——那些年我踩过的数据库性能坑
写API接口这事儿,有人能写成诗,有人能写成恐怖片
你的接口在说”别卷了”——我是如何用限流把爬虫和内鬼一起拒之门外的
当 AI 开始整活:最近这些新鲜玩意儿把我整不会了

发布评论