写了三年SQL,我决定把这些反模式拉出去枪毙

2026-07-14 4 0

干后端开发的,谁没写过几千行 SQL?但我观察到一个可怕的事实:大多数人的 SQL 水平,三年前和三年后没有任何区别——不是因为他们没进步,而是因为没人告诉他们写的有多烂。

今天我不讲什么「索引是数据库的灵魂」这种正确的废话。我来讲点真正让人血压飙升的反模式,以及怎么绕过这些坑。

反模式一:SELECT * — 懒狗的万金油

我敢打赌,看这篇文章的人,80% 写过 SELECT * FROM users。别装了,我知道你写过。

问题在哪?

  • 网络传输多了好几倍无用数据,你的 DBA 看到监控会想砍人
  • 表加了新字段,你的后端莫名其妙崩了——因为 JSON 序列化突然多了个未知属性
  • 覆盖索引失效,查询直接从索引查找变成回表,性能腰斩
-- 反模式:SELECT *
SELECT * FROM orders WHERE user_id = 123;

-- 正确姿势:只拿你需要的东西
SELECT order_id, total_amount, created_at 
FROM orders 
WHERE user_id = 123;

有人会说「我就是要全部字段啊」。行,你赢了。但你确定你真的需要那张表里的全部 47 个字段?连个 deleted_atbackup_flag 都要?

反模式二:LIKE '%关键字%' — 索引杀手

这个我必须单独拿出来喷。

-- 索引:请让我好好工作行吗
SELECT * FROM products WHERE name LIKE '%手机%';

前面加了百分号,索引直接报废。B+树在这种情况下一脸懵逼,只能老老实实全表扫描。你的表有 1000 万条数据?好的,请等待 30 秒。

解决方案:

  • 全文索引:FULLTEXT INDEX(name),这个是正经解决方案
  • ES/Solr:数据量大的时候,该上搜索引擎就上,别硬撑
  • 前缀匹配:如果是 LIKE '手机%',普通索引还是能用的(只是大多数人不知道这个区别)

反模式三:隐式类型转换 — 阴险小人

这个坑特别阴,因为它不报错,甚至看起来完全正常,但性能就是差。

-- user_id 是 BIGINT 类型
-- 反模式:传入字符串
SELECT * FROM users WHERE user_id = '12345';

MySQL 看到字符串和数字比较,会偷偷把字符串转成数字。这个过程发生在每一行,意味着索引失效。

怎么发现?用 EXPLAIN 看执行计划,如果 typeALL 或者 rows 远大于预期,就该怀疑这个了。

反模式四:N+1 查询 — 温水煮青蛙

这个问题特别恶心,因为单个请求看起来没问题,等流量上来才发现数据库被打死了。

// 伪代码,后端语言
const users = db.query('SELECT * FROM users LIMIT 100');
for (user of users) {
  // 每次循环都执行一次新查询!!!
  user.orders = db.query(
    'SELECT * FROM orders WHERE user_id = ?', 
    user.id
  );
}
// 100个用户 = 101次数据库往返
// 1000个用户 = 1001次,你服务器凉了

正确做法是 JOIN 或者批量查询:

-- 方案一:JOIN
SELECT u.*, o.order_id, o.total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN (1,2,3...100);

-- 方案二:两次查询 + 程序里组装
const users = db.query('SELECT * FROM users LIMIT 100');
const userIds = users.map(u => u.id);
const orders = db.query(
  'SELECT * FROM orders WHERE user_id IN (?)', 
  [userIds]
);
// 程序里按 user_id 分组组装

方案一简单粗暴,方案二在数据量大的时候性能更好(避免了大表 JOIN 的问题)。

反模式五:没有分页意识 — 内存杀手

我见过最夸张的一个案例:一张表 5000 万数据,前端一个下拉框要显示选项,后端直接 SELECT id, name FROM categories 全部返回。然后 JavaScript forEach 渲染,卡出翔。

-- 反模式:全表返回
SELECT id, name FROM categories;

-- 正确姿势:分页
SELECT id, name FROM categories LIMIT 20 OFFSET 0;

-- 如果是下拉框这种场景,直接缓存吧
-- Redis / 内存缓存,别每次都查数据库

还有一种更阴险的:LIMIT 100000, 20。OFFSET 越大,数据库越慢,因为它要先跳过前 10 万行。如果你的分页慢,试试游标分页(keyset pagination):

-- 游标分页:利用索引,速度恒定
SELECT * FROM orders 
WHERE id > last_seen_id 
ORDER BY id 
LIMIT 20;

反模式六:事务里写网络请求 — 自杀式开发

这个坑,杀伤力极大,但中招的人巨多。

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 调用外部服务,假设网络超时了...
http.post('http://payment-service/api/charge', data);  // 网络请求!
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 此时事务还挂着,数据库连接被占用
-- 并发一高,连接池耗尽,服务雪崩

事务里不要有任何网络请求、日志写入、长计算。能拆就拆,拆不了就用消息队列解耦。

反模式七:忽视慢查询日志 — 掩耳盗铃

MySQL 自带慢查询日志,但多少人真的看过?

-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';

-- 设置慢查询阈值(毫秒)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过1秒的才记录

-- 查看慢查询日志路径
SHOW VARIABLES LIKE 'slow_query_log_file';

有了数据之后,用 pt-query-digest 分析,这才是正确的打开方式。别告诉我你只会 EXPLAIN 然后「嗯,这个扫了全表,优化一下吧」——你连慢在哪里都不知道,优化个寂寞。

说在最后

SQL 优化这件事,归根结底是三件事:

  1. 知道你跑的是什么(EXPLAIN + 慢查询日志)
  2. 知道数据库怎么跑的(索引原理、事务机制、执行计划)
  3. 知道什么不该写(这些反模式)

很多人连第三条都没做到,就急着去学什么「数据库架构设计」「分库分表实战」——兄弟,路还没学会走呢,别急着跑。

数据库是最诚实的:你糊弄它,它就糊弄你。


有问题欢迎留言,数据库相关的坑我踩过不少,问的人多的话,我再出一篇讲讲生产环境里的 SQL 优化真实案例。

相关文章

async/await 正在偷偷谋杀你的 Node.js 服务:我用三次线上事故换来的教训
HTTP keep-alive:那个被你忽视却能让服务快3倍的东西
你的HTTP客户端可能在悄悄”泄漏”连接:一次教科书不会告诉你的保活真相
让你的API错误信息不再像算命先生的签文——HTTP状态码的精准表达与错误处理的艺术
省心省力:让AI工具秒速上线的神奇代部署服务
别让N+1查询悄悄偷走你的性能——一次线上血泪史

发布评论