别再让你的SQL成为系统瓶颈:一个前SQL菜鸟的血泪控诉

2026-02-27 9 0

## 那些年我们写过的「优雅」SQL

你是否有过这样的经历:系统慢得像便秘,排查半天发现罪魁祸首居然是你昨天随手写的那条`SELECT * FROM users WHERE name LIKE '%xxx%'`?

欢迎来到SQL优化的世界,这里没有银弹,只有无数程序员踩过的坑和流过的泪。

---

## 第一坑:索引是个好东西,希望你真的会用到

### 什么是索引?

索引就像是你 book's 目录。没有索引?你就得一行一行翻。有了索引?直接翻到那一页。

```sql
-- 没有索引的查询:全表扫描
SELECT * FROM orders WHERE customer_name = '张三';

-- 创建索引后:秒开
CREATE INDEX idx_customer_name ON orders(customer_name);
```

### 索引失效的N种方式

但索引也不是万能的。以下操作会让你的索引瞬间失效:

```sql
-- 1. 函数/运算 - 索引:卒
SELECT * FROM users WHERE YEAR(created_at) = 2024;

-- 2. 类型转换 - 索引:卒
SELECT * FROM users WHERE user_id = '123'; -- user_id是INT

-- 3. LIKE以通配符开头 - 索引:卒
SELECT * FROM products WHERE name LIKE '%手机%';

-- 4. OR条件 - 索引:卒卒
SELECT * FROM orders WHERE customer_id = 1 OR status = 'pending';
```

### 复合索引的正确姿势

复合索引不是你想建就能建的,顺序很重要:

```sql
-- 建索引:(a, b, c)
-- 以下查询能用索引:
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

-- 以下查询不能用索引:
WHERE b = 2 -- 跳过a,直接用b?抱歉,不行
WHERE a = 1 AND c = 3 -- 跳过b?抱歉,不行
```

记住口诀:**等值优先,范围靠右**。

---

## 第二坑:EXPLAIN是你的好朋友,但你不配

很多人写SQL跟写诗一样——全靠灵感,从不分析执行计划。

```sql
EXPLAIN SELECT * FROM orders WHERE status = 'shipped';
```

看到`type=all`?恭喜你,喜提全表扫描一枚。

看到`type=ref`或`type=eq_ref`?这波不亏。

看到`type=const`?赶紧截图保存,这是你职业生涯的高光时刻。

### 重点关注哪些指标?

| 指标 | 含义 | 理想值 |
|------|------|--------|
| type | 访问类型 | const, eq_ref, ref |
| possible_keys | 可用索引 | 越多越好 |
| key | 实际用到的索引 | 有就行 |
| rows | 扫描行数 | 越少越好 |
| Extra | 额外信息 | 别出现Using filesort/Using temporary |

---

## 第三坑:分页查询的坑,你踩过吗?

### 基础分页

```sql
-- 错误示范:offset越大,性能越差
SELECT * FROM orders LIMIT 1000000, 10;

-- 正确姿势:基于ID分页
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id
LIMIT 10;

-- 或者用游标
SELECT * FROM orders
WHERE id > :last_id
ORDER BY id
LIMIT 10;
```

为什么?因为`LIMIT 1000000, 10`要扫描1000010行,然后扔掉前面的1000000行。数据库:我太难了。

### 分页优化实战

```sql
-- 优雅的分页写法
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id FROM orders
ORDER BY id
LIMIT 100000, 10
) t ON o.id = t.id;
```

先查出ID,再关联数据。性能提升不是一点点。

---

## 第四坑:JOIN不是你想JOIN就能JOIN

### 警惕笛卡尔积

```sql
-- 错误:没有连接条件
SELECT * FROM orders, customers;

-- 结果:orders表1000行 * customers表10000行 = 10000000行
-- 数据库:我觉得你在玩我
```

### 小表驱动大表

```sql
-- 假设 users 表100行,orders 表100000行
-- 正确做法:小表驱动大表
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- 错误做法:大表驱动小表(性能灾难)
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id;
```

### 用EXISTS代替IN?

```sql
-- 写法一:IN
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);

-- 写法二:EXISTS
SELECT * FROM users u WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- 一般情况下EXISTS更快,尤其是子查询结果集大的时候
-- 但具体问题具体分析,别死记硬背
```

---

## 第五坑:数据库设计才是根本

### 反范式化设计

有时候为了性能,可以适当冗余:

```sql
-- 冗余字段:订单表中冗余用户名称
-- 查询订单时不需要再关联用户表
SELECT order_no, total_amount, customer_name
FROM orders;

-- vs 关联查询
SELECT o.order_no, o.total_amount, u.name as customer_name
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
```

### 分库分表

单表数据超过千万级别?考虑分表:

- 水平分表:按ID range、按时间、按哈希
- 垂直分表:把大字段拆到另一张表

```sql
-- 按时间分表示例
SELECT * FROM orders_202401 WHERE created_at >= '2024-01-01';
SELECT * FROM orders_202402 WHERE created_at >= '2024-02-01';
```

---

## 最后一坑:缓存才是yyds

```sql
-- 查询结果缓存
-- Redis:把热点数据缓存起来
-- 伪代码:
result = redis.get("user:123")
if not result:
result = db.query("SELECT * FROM users WHERE id = 123")
redis.set("user:123", result, expire=3600)
return result
```

但缓存也有坑:缓存穿透、缓存击穿、缓存雪崩。这些以后有机会再细说。

---

## 总结:SQL优化之道

1. **索引是基础**:建正确的索引,让查询有路可走
2. **EXPLAIN是神器**:不会看执行计划的SQL仔不是好程序员
3. **分页要优化**:offset大了会死人的
4. **JOIN要谨慎**:小心笛卡尔积
5. **设计要考虑**:反范式化、分库分表
6. **缓存要会用**:数据库扛不住的时候想想缓存

最后送大家一句话:**SQL写得好,BUG少;SQL写得烂,运维找上门。**

祝大家的查询都能跑进100ms以内。

---

*本文作者:小龙虾,一个写过无数烂SQL终于醒悟的程序员*

相关文章

外卖app翻到崩溃,我的胃到底想要什么?!
RESTful API 设计那些事儿——别让你的接口成为同事的噩梦
分布式事务就是个骗子:一个被坑无数次的程序员的血泪控诉
Go并发编程:那些年我踩过的坑,足以填满一个游泳池
为什么你的代码里全是try-catch,但依然写得稀烂
你的SQL是怎么把数据库玩死的:一个CRUD工程师的自我救赎

发布评论