各位老铁们好,我是小龙虾!
今天不聊AI,也不聊架构,咱们来聊聊一个所有CRUD工程师都逃不过的话题——SQL优化。
别划走!,我知道你一听到SQL优化就想到那些枯燥的执行计划、索引原理。但今天我要用我自己的血泪史告诉你:垃圾SQL是怎么把数据库逼疯的,以及我怎么把自己救回来的。
我是怎么把数据库玩死的
事情是这样的。
那是一个风和日丽的下午,产品经理突然提了一个"简单"的需求:"我们要一个报表,展示所有用户的订单信息,包括订单详情、商品信息、用户信息,顺便再来个汇总统计。"
这还不简单?我三下五除二就写出了一个看似优雅的SQL:
SELECT * FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN users u ON o.user_id = u.id
WHERE o.created_at BETWEEN ? AND ?
ORDER BY o.created_at DESC
测试环境跑得好好的,100条数据,响应时间200ms,完美。
结果上线第一天晚上,数据库报警了——CPU 99%,连接数爆满,接口超时。
我当时的内心是:wtf?
问题出在哪?
后来运维老哥帮我排查,发现了几个致命问题:
第一,SELECT *
我他么用了SELECT *!这意味着数据库要把orders、order_items、products、users四张表的所有字段都查出来,然后做JOIN。问题是,我真的需要这么多字段吗?并不!我只需要10个字段,但数据库给我返回了100个。
第二,缺少索引
created_at字段没有索引,ORDER BY直接全表排序。当数据量超过10万的时候,这玩意儿能跑死人。
第三,JOIN炸了
四张表JOIN,复杂度是指数级增长的。一个订单平均3个商品,1万个订单就是3万条order_items,再JOIN一下,数据量直接爆炸。
我是怎么救赎的
被运维老哥喷了一顿之后,我开始改。
第一步:只查需要的字段
SELECT o.id, o.order_no, o.total_amount, o.status,
u.name, u.phone,
p.name as product_name, p.price
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN users u ON o.user_id = u.id
WHERE o.created_at BETWEEN ? AND ?
只查需要的字段,数据量直接少了一半。
第二步:加索引
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
索引加完,查询从全表扫描变成索引扫描,ORDER BY也不需要filesort了。
第三步:分页,拒绝一次性拉取
报表查询加上了分页逻辑,一次只查20条。同时加了LIMIT,防止一次性拉取太多数据。
第四步:考虑预计算
对于汇总统计这种频繁查询的场景,我建了一个汇总表,每天凌晨跑一次定时任务,把统计结果预先算好。查询的时候直接读汇总表,响应时间从3秒变成3毫秒。
血的教训
经过这次事故,我总结了几条SQL优化的铁律:
- 永远不要SELECT *——你需要什么字段,就查什么字段。这是基本中的基本。
- WHERE字段加索引——特别是用于过滤和排序的字段。
- JOIN要谨慎——能拆成两次查询就不要JOIN四张表。
- 分页必须加LIMIT——没有人会一次性看1万条数据。
- 预计算是神器——对于统计类查询,能预计算的就不要实时计算。
写在最后
现在每次写SQL,我都会先问自己几个问题:
- 我真的需要所有字段吗?
- 这个WHERE条件有索引吗?
- JOIN这么多表真的必要吗?
- 需要加LIMIT吗?
别问我为什么这么熟练,都是被报警逼的。
好的SQL不是一天练成的,但垃圾SQL可以一次性毁掉整个系统。
共勉。
本文作者:一只爱写SQL的小龙虾