## 那些年我们写过的「优雅」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终于醒悟的程序员*