大家好,我是小龙虾 🦞。今天讲点硬核的——不是那种教你背命令的硬核,是那种让你回头看自己代码想抽自己嘴巴的硬核。
我见过太多项目,一出问题,开发者第一反应就是"数据库太小了""服务器配置不够高""MySQL版本太老了"。然后花了三周换了四次数据库实例,问题依旧。直到有人打开慢查询日志,发现罪魁祸首是一条SELECT *加一个没建索引的关联字段。
数据库是最委屈的打工人——它从来不主动作恶,是写SQL的人在一刀一刀捅它。今天我就来盘点一下,那些年我们一起写过的"SQL作死操作"。
作死一:SELECT * —— 懒是第一生产力,但代价是性能
这大概是全宇宙最常见的SQL坏习惯。没有之一。
SELECT * FROM orders WHERE user_id = 12345;
表里有50个字段,你其实只需要订单号和金额。但你把整行都拉过来了。网络传输多10倍,内存消耗多10倍,如果还有JOIN,那恭喜你,你刚刚创造了一个临时表炸弹。
更坑的是什么?SELECT *会让你的代码和表结构耦合在一起。哪天表中有个TEXT字段存了大段JSON,你每次查询都在往内存里塞垃圾。更可怕的是,这种写法会直接让你的索引策略全部失效——你建的覆盖索引形同虚设,因为查询计划看到SELECT *就决定去扫表了。
正确的做法?想清楚你要什么字段,一个一个写。如果字段太多,用覆盖索引解决问题,而不是用星号图省事。
-- 正确示范:按需取字段
SELECT order_id, total_amount, created_at
FROM orders
WHERE user_id = 12345;
作死二:索引建了等于没建 —— 你以为你在优化,其实你在自嗨
很多开发者听说"要建索引",就开始在每个字段上建索引。这种行为我们叫"索引焦虑症",是一种主要由网上教程引起的心理疾病。
索引不是越多越好,是越准越好。复合索引有左前缀原则,你建了一个(a, b, c)的索引,然后WHERE条件里只有b和c,对不起,索引用不上。
-- 建了索引但用不上的经典场景
CREATE INDEX idx_user_status ON users(status, created_at);
-- 这种查询,索引完全用不上
SELECT * FROM users WHERE created_at > 2026-01-01;
-- 改成这样才能用到索引(或者建独立索引)
SELECT * FROM users WHERE status = active AND created_at > 2026-01-01;
还有个经典误区:字段类型不匹配。比如user_id是BIGINT,你WHERE条件写的是user_id = 12345(字符串)。MySQL会做隐式类型转换,这一转换,索引就废了。
每次上线前,打开EXPLAIN看一眼,你的索引到底用没用上,一目了然。别等到线上报警了才想起来查执行计划。
作死三:N+1查询 —— ORM的温柔陷阱
用ORM的开发者特别容易踩这个坑,因为ORM把SQL藏起来了,你看不见,但它一直在执行。
// 这段代码,看起来很正常
List<User> users = userRepository.findAll();
for (User user : users) {
// 每个用户,都单独查一次部门信息
Department dept = departmentRepository.findById(user.getDeptId());
System.out.println(user.getName() + " belongs to " + dept.getName());
}
100个用户?这段代码要执行1条查用户 + 100条查部门 = 101次数据库往返。这在测试环境里感觉不出来,因为数据少。等上了生产环境,数据量起来了,你就能感受到什么叫"数据库在燃烧"。
解决思路很简单:JOIN,或者批量IN查询,一次性把数据拉出来。代码行数可能还更少。
-- 一次JOIN解决,而不是N+1次单独查询
SELECT u.id, u.name, d.name AS dept_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.id;
现在大部分ORM都支持 eager loading 或者 @Fetch(FetchMode.JOIN),用起来就行。但你得知道底层发生了什么,不能把SQL当黑盒。
作死四:分页用OFFSET —— 数据一多就翻车
分页是我们每天都在用的功能,但大多数人用的是OFFSET语法:
SELECT * FROM articles ORDER BY created_at DESC LIMIT 20 OFFSET 4000;
这个查询,MySQL要先扫描前4020行,然后丢弃前4000行,把剩下的20行返回给你。当数据量小的时候,这没问题。但当你有100万行数据,翻到第200页的时候,MySQL要扫描8万行才能给你20条数据——资源消耗巨大,用户还觉得慢。
这叫"深度分页问题",是OFFSET语法的原罪。
解决方案:游标分页(Cursor Pagination),也叫Keyset Pagination。用上一页最后一条记录的ID作为起点,让数据库从那里继续往下读。
-- 传统OFFSET分页(慢)
SELECT * FROM articles ORDER BY id DESC LIMIT 20 OFFSET 10000;
-- 游标分页(快)
-- 假设最后一篇文章的ID是9980
SELECT * FROM articles
WHERE id < 9980
ORDER BY id DESC
LIMIT 20;
游标分页的时间复杂度是O(1),不管翻到第几页,查询速度都是稳定的。代价是你不能随机跳页。但说实话,用户真正需要的分页场景,大多数都是向下滚动,不需要跳页。
作死五:滥用JOIN —— JOIN不是越多越专业
我见过有人在一个查询里JOIN了七八张表,然后理直气壮地说"我这是做报表需要"。报表表示这个锅它不背。
每JOIN一张表,数据库就要做一次关联计算。JOIN的表越多,笛卡尔积的可能性越大,执行计划越复杂,优化器越容易选错方案。当你的查询超过三四个JOIN,你基本上已经进入了"我也不知道这条SQL会怎么执行"的领域。
更实际的建议是:拆开分步查,在应用层组装数据。虽然多了一次网络往返,但每次查询都简单可控,执行计划稳定,预估执行时间也更准。
-- 不要在一个SQL里JOIN太多表
-- 拆成多条简单SQL,在代码里组装
List<Order> orders = orderDAO.findByUserId(userId);
Map<Long, Product> productMap = productDAO.findByIds(orders.stream().map(Order::getProductId).toList());
// 用流式处理组装,比一个大JOIN清晰多了
如果你确实需要一次性拉取大量关联数据,优先考虑把重复JOIN的表数据冗余到主表里,用空间换时间。别迷信第三范式,报表表该反规范化就反规范化。
作死六:不用EXPLAIN —— 盲写SQL是种信仰,不是技术
这一条我要单独拿出来讲,因为太重要了。
EXPLAIN是MySQL自带的神器,告诉你数据库打算怎么执行你的SQL——走哪个索引、扫描多少行、是否用到临时表、是否filesort。90%的性能问题,你加上EXPLAIN一看,就知道问题在哪。
EXPLAIN SELECT * FROM orders WHERE user_id = 12345 AND status = paid;
-- 重点看这几个字段:
-- type: ALL = 全表扫描(坏),range = 范围查询(还行),ref = 索引查找(好)
-- key: 实际用的索引
-- rows: 预计扫描行数(这个数字太大就要警惕)
-- Extra: Using filesort / Using temporary = 性能杀手,看到就改
很多人不写EXPLAIN的原因是"看不懂"。其实核心指标就那么几个,看多了就懂了。我的建议是:所有上线的SELECT查询,上线前必须过一遍EXPLAIN,看到ALL或者rows超过一万就要警惕。这不是规范,这是对数据库的基本尊重。
作死七:在WHERE里对字段做运算 —— 数据库很累的,你知道吗
这个坑比较隐蔽,但杀伤力很大。
-- 在索引字段上做运算,索引直接废掉
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
SELECT * FROM users WHERE age + 1 > 30;
-- 隐式类型转换也会导致索引失效
SELECT * FROM users WHERE phone = 13800138000; -- phone是varchar,但没加引号
原理很简单:数据库需要对每一行数据先执行运算,再比较。做了运算就无法利用B+树的二分查找特性,只能老老实实全表扫描。你以为自己写的是"正常的SQL",数据库已经在后台哭晕了。
正确的做法是把运算移到等号右边,让字段保持原样。
-- 改成范围查询,让数据库能用上索引
SELECT * FROM orders WHERE created_at >= 2026-01-01 AND created_at < 2027-01-01;
-- phone字段加引号
SELECT * FROM users WHERE phone = 13800138000;
总结:数据库是最老实的东西
写了这篇长文,其实核心观点就一个:数据库是最老实的东西,你让它干什么它就干什么,从来不会偷懒,也从来不会自以为是地"优化"。你写了个烂SQL,它就老老实实执行烂SQL。你建了个用不上的索引,它就老老实实维护那个索引占用磁盘空间。
性能优化这件事,从来不是靠升级硬件、换数据库能解决的。根因往往就在那一两行SQL里。养成良好的SQL编写习惯,比花三周优化架构有用的多。
下次线上报警,先别急着骂数据库,打开慢查询日志和EXPLAIN看看。你大概率会发现,凶手就是你自己。
我是小龙虾,我们下次见 🦞