ORM这个温柔的陷阱,毁掉了无数年轻程序员的数据库功底
先说个真实故事。我带过一个实习生,985研究生,代码写得漂亮。有一天我让他写个用户流失分析,他对着电脑坐了二十分钟,最后跟我说:"我用ORM查不出来,能帮我看看吗?"
我凑过去一看,他写的是这样的:
User.objects.filter(is_active=False).count()
"这不对吗?"他理直气壮,"我在查不活跃用户啊。"
是的,用ORM的眼光看,完全正确。但他的数据库有两千万用户,这一条count()跑了47秒。
ORM的温柔是个陷阱
ORM没有错。错的是它给开发者制造了一种"你不需要了解数据库"的错觉。
我见过太多程序员,用了三年ORM,却连什么是索引失效、什么是回表查询都不知道。他们以为代码写对了,性能就对了。
Too young, too naive.
ORM的工作方式是:把你的对象操作翻译成SQL。你以为在操作数据,其实你在操作一层抽象。而抽象,往往意味着你看不见底层发生了什么。
三个血淋淋的案例
案例一:N+1查询的温柔一刀
看看这段代码:
users = User.objects.all()[:100]
for user in users:
print(user.orders.count())
看起来很正常对吧?取100个用户,每个打印订单数量。但执行起来是101次数据库往返。
而你只要加一个annotate:
from django.db.models import Count
users = User.objects.annotate(order_count=Count('orders')).all()[:100]
一条SQL,两张表JOIN,10毫秒搞定。但——谁教你这个?
案例二:分页的隐形杀手
很多框架的分页:
SELECT * FROM orders ORDER BY id LIMIT 100 OFFSET 10000000
数据库说:"好的,我先跳过这一千万条,然后给你取下一百条。"
OFFSET越大越慢,OFFSET 0和OFFSET 10000000,成本差了十万八千里。
正确做法是游标分页:
SELECT * FROM orders WHERE id > last_id ORDER BY id LIMIT 100
无论翻到第几页,性能都稳定如一。但这种写法,ORM标准封装根本不会给你。
案例三:批量插入的坑
想插入一万条数据?
for item in items:
Model.objects.create(field=item)
一万次INSERT,一万次网络往返。
正确做法:
Model.objects.bulk_create([Model(field=i) for i in items])
一条SQL,一万行。天地之别。
我的救赎之路
被ORM坑了两次大的之后,我做了一个决定:所有复杂查询,全部手写SQL。
一开始很痛苦。像学走路一样重新学JOIN,重新理解索引失效的原理,重新看懂EXPLAIN。但当我真正理解之后,我打开了新世界的大门。
我开始理解:
- 为什么有时候走索引反而更慢?
- 什么是覆盖索引,什么情况下能避免回表?
- 为什么COUNT(*)和COUNT(id)在InnoDB里完全不同?
- 什么是隐式类型转换,如何让你的索引悄悄失效?
这些问题,ORM永远教不了你。
我的妥协方案
CRUD用ORM,复杂查询手写SQL。
普通的新增、修改、删除,用ORM省时省力。但凡涉及到多表关联、需要优化性能、批量处理、分页逻辑——一律手写SQL。
在Go里我常用sqlx,在Python里我直接用pymysql,在Node里我倾向直接上knex.js。放弃过度封装,拥抱原始力量。
给年轻程序员的三条忠告
第一,学SQL,越早越好。不是会SELECT * FROM table那种,是能看懂执行计划、能分析慢查询、能设计合理索引的SQL。这是你吃饭的本事。
第二,不要迷信ORM的性能。它帮你省了时间,但代价是你的认知。遇到性能问题,你连SQL都看不到,怎么优化?
第三,始终保持对底层的好奇心。你用的框架在干什么?你的代码最终变成了什么?多问几个为什么,你会比同事成长得快得多。
最后
ORM是个好工具,但它不是银弹。它是给初学者入门的拐杖,不是你赖以生存的腿。
哪天摔了,你得知道怎么自己站起来。那些劝你"不用懂SQL,ORM够用"的人,要么是蠢,要么是坏。
别成为他们,也别被他们误导。