SQL优化详解与案例、以及索引失效场景;

SQL优化详解与案例
(1)避免使用 *
案例对比
-- ❌ 不推荐
SELECT * FROM users WHERE id = 1;-- ✅ 推荐
SELECT id, username, email, created_at FROM users WHERE id = 1;
原因分析
- 网络传输开销:
*会查询所有列,包括不需要的大字段(如 TEXT、BLOB),增加网络IO - 无法使用覆盖索引:如果只查询索引列,可以直接从索引获取数据,避免回表
- 内存消耗:查询更多数据占用更多内存缓冲区
- 维护性差:表结构变更时,应用程序可能获取到不需要的新列
(2)合理创建索引
案例
-- 表结构
CREATE TABLE orders (id INT PRIMARY KEY,user_id INT,status VARCHAR(20),created_at DATETIME,amount DECIMAL(10,2)
);-- ✅ 为高频查询字段创建索引
CREATE INDEX idx_user_status ON orders(user_id, status);
CREATE INDEX idx_created_at ON orders(created_at);-- 高效查询
SELECT * FROM orders WHERE user_id = 100 AND status = 'paid';
原因分析
- 减少扫描行数:索引是排序的数据结构(B+树),可以快速定位数据
- 避免全表扫描:无索引时需要逐行扫描整张表
- 注意:过多索引会影响写入性能(INSERT/UPDATE/DELETE需要维护索引)
(3)避免 WHERE 中的 NULL 值判断
案例对比
-- ❌ 不推荐(全表扫描)
SELECT * FROM users WHERE age IS NULL;-- ✅ 推荐:设计时避免 NULL,使用默认值
ALTER TABLE users MODIFY COLUMN age INT NOT NULL DEFAULT 0;
SELECT * FROM users WHERE age = 0;
原因分析
-
索引失效:大多数数据库的索引不存储 NULL 值,导致
IS NULL或IS NOT NULL无法使用索引 -
解决方案
:
- 设计表时使用
NOT NULL+ 默认值 - 如果必须用 NULL,可以为该列创建特殊索引(部分数据库支持)
- 设计表时使用
(4)避免使用 OR,用 IN 替换
案例对比
-- ❌ 不推荐(可能全表扫描)
SELECT * FROM products WHERE category_id = 1 OR category_id = 2 OR category_id = 3;-- ✅ 推荐
SELECT * FROM products WHERE category_id IN (1, 2, 3);
原因分析
- OR 导致索引失效:多个 OR 条件可能导致优化器放弃索引,改为全表扫描
- IN 可以使用索引:优化器会将 IN 转换为多个等值查询,能利用索引
- 特殊情况:如果 OR 连接的是不同列,改用 UNION ALL
-- 不同列的 OR
SELECT * FROM users WHERE username = 'admin'
UNION ALL
SELECT * FROM users WHERE email = 'admin@example.com';
(5)LIKE 不以 % 开头
案例对比
-- ❌ 不推荐(全表扫描)
SELECT * FROM products WHERE name LIKE '%手机%';-- ✅ 推荐(可以使用索引)
SELECT * FROM products WHERE name LIKE '苹果%';
原因分析
-
前缀匹配可用索引:索引是按字母顺序排列的,
'苹果%'可以快速定位到"苹果"开头的记录 -
%开头无法用索引:不知道从哪里开始匹配,只能全表扫描 -
解决方案
:
- 如果必须模糊搜索,考虑使用全文索引(FULLTEXT)
- 或使用 Elasticsearch 等专业搜索引擎
(6)避免在 WHERE 中对字段进行表达式操作
案例对比
-- ❌ 不推荐(索引失效)
SELECT * FROM orders WHERE amount + 10 > 100;-- ✅ 推荐
SELECT * FROM orders WHERE amount > 90;
原因分析
- 破坏索引结构:索引是对原始列值建立的,对列进行运算后,索引无法直接匹配
- 优化器无法优化:数据库需要对每一行执行计算,再判断条件
- 正确做法:将表达式移到等式右边
(7)避免在 WHERE 中对字段进行函数操作
案例对比
-- ❌ 不推荐(索引失效)
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
SELECT * FROM users WHERE UPPER(username) = 'ADMIN';-- ✅ 推荐
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';-- 或创建函数索引(MySQL 8.0+)
CREATE INDEX idx_username_upper ON users((UPPER(username)));
原因分析
-
函数计算开销:每行数据都要执行函数
-
索引无法使用:索引是对原始值建立的,函数处理后的值不在索引中
-
解决方案
:
- 改写 SQL,避免函数包裹列
- 使用函数索引或生成列
(8)复合索引遵循最左原则
案例
-- 创建复合索引
CREATE INDEX idx_user_status_date ON orders(user_id, status, created_at);-- ✅ 可以使用索引(最左列 user_id 存在)
SELECT * FROM orders WHERE user_id = 100;
SELECT * FROM orders WHERE user_id = 100 AND status = 'paid';
SELECT * FROM orders WHERE user_id = 100 AND status = 'paid' AND created_at > '2024-01-01';-- ❌ 无法使用索引(跳过了最左列)
SELECT * FROM orders WHERE status = 'paid';
SELECT * FROM orders WHERE created_at > '2024-01-01';
原因分析
- 索引存储结构:复合索引按照 (user_id, status, created_at) 的顺序排序
- 类似电话簿:先按姓氏排序,再按名字排序。如果不知道姓氏,无法快速查找名字
- 优化建议:查询条件频繁的列放在前面
(9)左右外连接遵循小表驱动大表
案例
-- 假设:users 表 1000 行,orders 表 100万 行-- ❌ 不推荐(大表驱动小表)
SELECT * FROM orders o LEFT JOIN users u ON o.user_id = u.id;-- ✅ 推荐(小表驱动大表)
SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id;
原因分析
-
JOIN 算法
:MySQL 使用嵌套循环连接(Nested Loop Join)
- 外层表(驱动表)循环每一行
- 对每一行,在内层表中查找匹配
-
小表驱动大表效率高
:
- 驱动表 1000 行 × 内层表索引查询(快)
- 大表驱动:100万行 × 内层表查询(慢)
-
INNER JOIN:优化器会自动选择小表作为驱动表
(10)避免使用子查询,改用 JOIN
案例对比
-- ❌ 不推荐(子查询)
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');-- ✅ 推荐(INNER JOIN)
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';
原因分析
-
子查询性能问题
:
- MySQL 对子查询优化较弱,可能多次执行子查询
- 需要在内存中创建临时表存储子查询结果
-
JOIN 优势
:
- 优化器可以选择最优执行计划
- 可以利用索引进行连接
- 避免临时表开销
(11)大分页优化
案例对比
-- ❌ 不推荐(深度分页,越往后越慢)
SELECT * FROM orders LIMIT 999980, 20;
-- 需要扫描 1000000 行,丢弃前 999980 行-- ✅ 推荐(使用主键过滤)
SELECT * FROM orders WHERE id > 999980 LIMIT 20;-- ✅ 更好:记录上一页最大 ID
-- 第一页
SELECT * FROM orders ORDER BY id LIMIT 20; -- 返回 id: 1-20
-- 第二页
SELECT * FROM orders WHERE id > 20 ORDER BY id LIMIT 20; -- 返回 id: 21-40
原因分析
- LIMIT 原理:
LIMIT offset, size会扫描offset + size行,然后丢弃前offset行 - 深度分页问题:offset 越大,扫描的无用数据越多
- 优化原理:利用主键索引直接定位,避免扫描大量数据
(12)GROUP BY 优先使用 WHERE 而非 HAVING
案例对比
-- ❌ 不推荐(HAVING 过滤)
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING user_id > 1000;-- ✅ 推荐(WHERE 过滤)
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE user_id > 1000
GROUP BY user_id;
原因分析
-
执行顺序
:
- WHERE:在分组前过滤,减少参与分组的数据量
- GROUP BY:对筛选后的数据分组
- HAVING:对分组后的结果过滤
-
WHERE 优势
:
- 提前过滤,减少数据量
- 可以使用索引
- 减少排序和分组的开销
-
HAVING 使用场景:过滤聚合函数的结果
-- HAVING 的正确用法
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 10; -- 过滤订单数大于 10 的用户
总结表格
| 优化点 | 核心原因 | 关键收益 |
|---|---|---|
| 避免 * | 减少数据传输,启用覆盖索引 | 降低 IO,提升查询速度 |
| 合理索引 | 快速定位数据 | 避免全表扫描 |
| 避免 NULL 判断 | 索引不存储 NULL | 保证索引有效性 |
| IN 替换 OR | 优化器处理更高效 | 更好利用索引 |
| LIKE 不用前缀 % | 保持索引有序性 | 索引范围扫描 |
| 避免列运算/函数 | 保持索引原始结构 | 索引不失效 |
| 最左原则 | 复合索引排序规则 | 充分利用索引 |
| 小表驱动大表 | 减少循环次数 | 减少计算量 |
| JOIN 代替子查询 | 避免临时表,优化器更智能 | 更好的执行计划 |
| 主键分页 | 避免扫描无用数据 | 深度分页不变慢 |
| WHERE 优于 HAVING | 提前过滤数据 | 减少分组开销 |
这些优化的本质都是减少扫描的数据量和充分利用索引,从而提升 SQL 执行效率!
