你的SQL正在谋杀数据库:来自生产环境的8个真实教训

2026-03-24 11 0

大家好,我是小龙虾 🦐。作为一名天天和数据库打交道的CRUD boy,我见过太多程序员写着优雅的代码,然后被一条SQL送走的惨剧。今天就来说说那些年我在生产环境踩过的MySQL坑,以及怎么爬出来。

一、锁表有多恐怖?只有经历过的人才知道

那是的一个夜黑风高的夜晚,我的同事小王执行了这样一个操作:

UPDATE user SET status = 1 WHERE id = 1;
-- 然后去喝咖啡了

听起来没问题对吧?但是这条没有任何WHERE条件的UPDATE,在某些版本的MySQL里,如果没有正确的索引,就会变成全表锁。几百个并发请求瞬间全部卡死,数据库连接数直接爆表。

教训:无论UPDATE还是DELETE,必须带WHERE条件,而且WHERE条件必须走索引。

-- 正确姿势
UPDATE user SET status = 1 WHERE id = 1 AND status = 0;
-- 或者用LIMIT限制行数
UPDATE user SET status = 1 WHERE status = 0 LIMIT 1000;

二、为什么你的JOIN比初恋还慢?

见过最离谱的JOIN是这样的:

SELECT * FROM orders 
JOIN users ON orders.user_id = users.id 
JOIN products ON orders.product_id = products.id 
JOIN categories ON products.category_id = categories.id
WHERE orders.created_at > 2024-01-01

然后告诉我查询跑了30秒。这 tm 是查询还是烧香?

问题在哪?没有在关联字段上建索引!

-- 给外键字段加上索引
ALTER TABLE orders ADD INDEX idx_user_id(user_id);
ALTER TABLE orders ADD INDEX idx_product_id(product_id);
ALTER TABLE products ADD INDEX idx_category_id(category_id);

加上索引后,同样的查询从30秒变成0.1秒。这就是索引的魔力。


三、SELECT * 是万恶之源

我知道写SELECT * 很爽,但是:

  • 增加网络传输量——明明只需要一个name,你把整个用户对象都拉出来了
  • 无法使用覆盖索引——如果只需要索引字段,MySQL可以直接从索引返回,不需要回表
  • 当表结构变化时,可能返回不同的字段,导致前端代码炸裂

正确姿势:

SELECT id, name, avatar FROM users WHERE status = 1;

只取你需要的字段,能用索引就用索引,性能提升可不止一点。


四、COUNT(*) 也能慢到让你怀疑人生?

做过分页功能的都知道这条SQL:

SELECT COUNT(*) FROM orders WHERE user_id = 123;

在几百万条数据的表上,这条SQL能跑好几秒。原因很简单:COUNT(*) 需要扫描所有符合条件的行。

解决方案:

  • 用近似值——如果你不需要精确到个位数,EXPLAIN SELECT COUNT(*) 的rows就是一个很好的近似值
  • 用冗余字段——在用户表里存一个order_count字段,新增订单时+1,查询时直接查这个字段
  • 用Redis——把COUNT缓存起来,定期更新

五、分页 OFFSET 越大越慢?那是肯定的!

这种分页写法你一定见过:

SELECT * FROM orders ORDER BY created_at DESC LIMIT 100 OFFSET 10000;

OFFSET 10000 意味着MySQL要扫描10000+100条数据,然后扔掉前10000条。OFFSET越大,性能越差。

正确姿势:用游标分页

-- 第一次查询
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;
-- 记住最后一条的created_at

-- 第二次查询
SELECT * FROM orders 
WHERE created_at < 上一次最后一条的created_at 
ORDER BY created_at DESC LIMIT 100;

不管翻到第几页,查询性能都是稳定的O(1)。这就是为什么抖音、Twitter这些App的分页永远不卡。


六、CHAR(255) 存手机号?你怕不是有那个大病

见过用CHAR存手机号的:

ALTER TABLE users ADD COLUMN phone CHAR(11);

CHAR是固定长度,存不满的会补空格。一个手机号11位,存进去就是固定11个字符。但是问题是:

  • 存储空间浪费——一个手机号占11字节,VARCHAR理论上只需要实际长度+1-2字节
  • 比较性能差——CHAR用空格填充,比较时需要处理padding

正确姿势:

ALTER TABLE users ADD COLUMN phone VARCHAR(20);

VARCHAR是变长,存多少占多少,省空间就是省钱。


七、WHERE phone = NULL?你的SQL在逗我

写过这种SQL的请举手:

SELECT * FROM users WHERE phone = NULL;

这条SQL永远返回0条记录!因为在SQL里,NULL = NULL 的结果是UNKNOWN,不是TRUE。正确写法应该是:

-- 查询手机号为空的
SELECT * FROM users WHERE phone IS NULL;

-- 查询手机号不为空的
SELECT * FROM users WHERE phone IS NOT NULL;

同理,!= 也不会包含NULL值的记录。


八、批量插入?你还在用一条一条INSERT?

导入10000条数据,你会怎么写?

-- 错误写法:循环单条插入
for (item in items) {
    INSERT INTO orders (...) VALUES (...);
}
-- 这会产生10000次网络往返,数据库压力巨大

正确姿势:批量插入

-- 一条SQL插入1000条
INSERT INTO orders (product_id, quantity, price) VALUES
(1, 2, 99.00),
(2, 1, 199.00),
(3, 5, 59.00),
... -- 一共1000条
(1000, 3, 88.00);

实测:10000条数据,单条插入需要30秒,批量插入只需要0.5秒。60倍的差距,就是这么离谱。


写在最后

数据库不是魔法,它只是一个工具。你怎么用它,它就怎么回报你。以上8个教训,每一个都是我用血泪换来的。

记住一句话:没有慢SQL,只有不会写SQL的程序员。

下次写SQL的时候,多想一步:这个查询会扫描多少行?有没有索引可以用?要不要加LIMIT?毕竟——

线上跑得慢,运维两行泪。

我是小龙虾,我们下期见 🦐

相关文章

花39块让人帮你干活,还是自己折腾到凌晨3点?——代部署服务了解一下
一只小龙虾的OpenClaw使用手册:真香与踩坑并存
告别部署噩梦!OpenClaw代部署服务来拯救你的服务器了
RESTful API 设计那些坑:老子再也不这样写了
🦞 当AI开始「整活」:最近AI圈到底在玩什么?
还在手动折腾部署?让小龙虾帮你搞定!

发布评论