1. 从业务需求到表设计的思考过程
当接到一个查询需求时,很多开发者会直接开始写SQL,但真正高效的查询往往始于合理的表设计。让我们从一个真实的电商订单查询需求开始:
假设我们需要查询"某个用户最近3个月待发货的订单,按下单时间倒序排列,并支持分页"。这个看似简单的需求实际上包含了多个查询维度:用户ID、订单状态、时间范围和排序要求。
1.1 识别核心查询模式
首先需要分析这个查询的访问模式:
- 高频查询条件:user_id、status、create_time
- 排序要求:create_time DESC
- 分页需求:LIMIT offset, size
根据这些特征,我们的表设计应该考虑:
sql复制CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`status` tinyint(4) NOT NULL COMMENT '0-待支付 1-待发货 2-已发货',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`amount` decimal(10,2) NOT NULL,
-- 其他字段...
PRIMARY KEY (`id`),
KEY `idx_user_status_time` (`user_id`,`status`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1.2 索引设计的黄金法则
这个案例中我们创建了联合索引(user_id, status, create_time),这基于几个重要原则:
-
最左前缀原则:MySQL索引使用从左到右的顺序匹配。我们的查询条件正好是user_id → status → create_time的顺序
-
区分度优先:把区分度高的字段放在前面。user_id的区分度通常高于status
-
覆盖排序:create_time放在最后可以避免filesort,因为查询已经按索引顺序读取数据
经验分享:实际项目中,我遇到过开发者在每个查询字段上都建单列索引的情况,这会导致索引合并(Index Merge)效率低下。正确的做法是分析查询模式,设计合理的联合索引。
2. SQL编写与执行计划分析
有了合理的表结构,接下来我们编写查询SQL:
sql复制SELECT * FROM orders
WHERE user_id = 123
AND status = 1
AND create_time >= DATE_SUB(NOW(), INTERVAL 3 MONTH)
ORDER BY create_time DESC
LIMIT 0, 10;
2.1 使用EXPLAIN验证执行计划
执行EXPLAIN可以看到:
code复制+----+-------------+--------+------+--------------------------+--------------------------+---------+-------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+--------------------------+--------------------------+---------+-------------+------+-------------+
| 1 | SIMPLE | orders | ref | idx_user_status_time | idx_user_status_time | 9 | const,const | 125 | Using where |
+----+-------------+--------+------+--------------------------+--------------------------+---------+-------------+------+-------------+
关键指标解读:
- type=ref:使用了非唯一索引扫描
- key=idx_user_status_time:命中了我们设计的联合索引
- rows=125:预估扫描的行数
- Extra中没有Using filesort:说明排序利用了索引
2.2 常见陷阱与规避方法
在实践中,我遇到过几个典型问题:
- 范围查询中断索引:
sql复制-- 错误示例:status条件在create_time之后,导致索引中断
WHERE user_id = 123 AND create_time > '2023-01-01' AND status = 1
- 隐式类型转换:
sql复制-- user_id是bigint但传入字符串,导致索引失效
WHERE user_id = '123' AND status = 1
- 函数操作列:
sql复制-- 对索引列使用函数导致无法使用索引
WHERE user_id = 123 AND DATE(create_time) > '2023-01-01'
3. 分页查询的深度优化
当数据量很大时,分页查询会成为性能瓶颈。传统的LIMIT offset, size方式在offset很大时效率极低:
sql复制-- 性能问题SQL
SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 10000, 10;
3.1 优化方案:游标分页
采用"记住上一页最后一条记录"的方式:
sql复制SELECT * FROM orders
WHERE user_id = 123
AND create_time < '2023-03-20 15:00:00' -- 上一页最后一条记录的create_time
ORDER BY create_time DESC
LIMIT 10;
这种方案的优势:
- 不需要计算总数
- 不受offset增大的影响
- 适合无限滚动场景
3.2 分页优化对比测试
我用100万条订单数据做了对比测试:
| 分页方式 | offset=0耗时 | offset=10000耗时 | offset=100000耗时 |
|---|---|---|---|
| 传统LIMIT | 5ms | 120ms | 1500ms |
| 游标分页 | 5ms | 5ms | 5ms |
4. 高级优化技巧与实践经验
4.1 覆盖索引优化
当查询只需要索引列时,可以使用覆盖索引避免回表:
sql复制-- 只查询索引包含的字段
SELECT user_id, status, create_time FROM orders
WHERE user_id = 123 AND status = 1
ORDER BY create_time DESC
LIMIT 10;
执行计划的Extra会显示"Using index",表示仅使用索引就完成了查询。
4.2 索引条件下推(ICP)
MySQL 5.6+支持ICP优化,可以在存储引擎层过滤数据:
code复制SET optimizer_switch = 'index_condition_pushdown=on';
4.3 实战中的经验总结
-
定期分析慢查询日志:配置long_query_time=1s,每周分析TOP 10慢SQL
-
使用SQL改写技巧:
sql复制-- 原SQL
SELECT * FROM table WHERE a = 1 OR b = 2;
-- 优化为
SELECT * FROM table WHERE a = 1
UNION ALL
SELECT * FROM table WHERE b = 2;
- 监控索引使用情况:定期检查未使用的索引
sql复制SELECT * FROM sys.schema_unused_indexes
WHERE object_schema = 'your_db';
- 避免过度索引:每个额外索引都会降低写性能,维护成本高
5. 真实案例:电商订单查询优化
去年我优化过一个电商系统的订单查询,原始查询需要8秒完成,优化后降至80ms。具体过程:
5.1 原始问题分析
原始SQL:
sql复制SELECT o.*, p.pay_time, s.ship_name
FROM orders o
LEFT JOIN payment p ON o.id = p.order_id
LEFT JOIN shipping s ON o.id = s.order_id
WHERE o.user_id = 123
AND o.status IN (1,2,3)
AND o.create_time BETWEEN '2023-01-01' AND '2023-03-31'
ORDER BY o.create_time DESC
LIMIT 0, 20;
问题诊断:
- 使用了3个表连接
- status IN查询导致索引失效
- 排序字段与WHERE条件不匹配
5.2 优化方案实施
- 重构索引:
sql复制ALTER TABLE orders ADD INDEX idx_user_time_status (user_id, create_time, status);
- 重写SQL:
sql复制SELECT o.*,
(SELECT pay_time FROM payment WHERE order_id = o.id LIMIT 1) AS pay_time,
(SELECT ship_name FROM shipping WHERE order_id = o.id LIMIT 1) AS ship_name
FROM orders o
WHERE o.user_id = 123
AND o.create_time BETWEEN '2023-01-01' AND '2023-03-31'
AND EXISTS (
SELECT 1 FROM order_status os
WHERE os.order_id = o.id AND os.status IN (1,2,3)
)
ORDER BY o.create_time DESC
LIMIT 0, 20;
- 结果:
- 查询时间从8000ms降至80ms
- 扫描行数从10万+降至200
- QPS从5提升到200
这个案例让我深刻理解到:SQL优化不仅是技术活,更需要深入理解业务场景和数据特征。有时候,改变查询方式比单纯加索引更有效。
