MySQL索引踩坑合集从入门到精通
作者:程序员牧羊
本文详细介绍了MySQL索引的使用,包括索引的类型、创建、使用、优化技巧及最佳实践,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
MySQL索引完整教程:从入门到入土(附实战踩坑指南)
“没有索引的查询就像在图书馆里不用目录,一本本翻书找资料——等你找到,项目都上线了 😅”
一、索引是什么?为什么需要它?
1.1 什么是索引?
索引(Index)就像书籍的目录,可以帮助数据库快速定位到数据,而不需要全表扫描。
想象一下:
- 没有索引:就像在一本1000页的字典里,从第一页开始逐页查找"MySQL"这个词
- 有索引:直接翻到"M"开头的部分,瞬间找到
1.2 为什么需要索引?
让我们看一个真实的场景:
-- 假设有一个用户表,有1000万条数据 SELECT * FROM users WHERE phone = '13800138000';
没有索引的情况:
- 需要扫描全表:1000万行
- 时间复杂度:O(n)
- 执行时间:可能几秒甚至几十秒
- 用户体验:😱 “这页面卡死了吗?”
有索引的情况:
- 通过B+树快速定位:几层查找
- 时间复杂度:O(log n)
- 执行时间:几毫秒
- 用户体验:😊 “秒开,真快!”
二、索引的类型
2.1 按数据结构分类
1. B+树索引(最常用)
B+树索引是MySQL的默认索引类型,适用于大部分场景。
特点:
- 所有数据都存储在叶子节点
- 叶子节点之间用指针连接(便于范围查询)
- 树的高度低,查询效率高
[50]
/ \
[25] [75]
/ \ / \
[10][30] [60][80]2. Hash索引
特点:
- 等值查询极快,O(1)时间复杂度
- 不支持范围查询
- 不支持排序
- 仅Memory存储引擎支持
适用场景:
- 等值查询频繁
- 不需要范围查询
-- Memory引擎的Hash索引
CREATE TABLE user_hash (
id INT PRIMARY KEY,
username VARCHAR(50)
) ENGINE=MEMORY;
CREATE INDEX idx_username ON user_hash(username) USING HASH;
3. 全文索引(FULLTEXT)
特点:
- 用于全文搜索
- 支持中文分词(MySQL 5.7+)
- 仅MyISAM和InnoDB支持
-- 创建全文索引
CREATE FULLTEXT INDEX ft_content ON articles(content);
-- 使用全文索引搜索
SELECT * FROM articles
WHERE MATCH(content) AGAINST('MySQL 索引' IN NATURAL LANGUAGE MODE);
2.2 按字段数量分类
1. 单列索引
-- 在单个列上创建索引 CREATE INDEX idx_username ON users(username);
2. 复合索引(联合索引)
-- 在多个列上创建索引 CREATE INDEX idx_name_phone ON users(name, phone);
⚠️ 踩坑点1:复合索引的顺序很重要!
-- 假设有索引 idx_name_phone(name, phone) -- ✅ 可以使用索引 SELECT * FROM users WHERE name = '张三'; SELECT * FROM users WHERE name = '张三' AND phone = '13800138000'; -- ❌ 无法使用索引(违背最左前缀原则) SELECT * FROM users WHERE phone = '13800138000';
为什么会这样? 就像查字典,先按拼音首字母,再按第二个字母。你直接查第二个字母,目录就帮不上忙了!
2.3 按唯一性分类
1. 普通索引
允许重复值,最常用。
CREATE INDEX idx_email ON users(email);
2. 唯一索引
不允许重复值,但允许NULL(可以有多个NULL)。
CREATE UNIQUE INDEX idx_phone ON users(phone);
3. 主键索引
特殊的唯一索引,不允许NULL,每个表只能有一个。
-- 创建表时自动创建
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键索引
username VARCHAR(50)
);
三、索引的创建和使用
3.1 创建索引的几种方式
方式1:创建表时创建
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
phone VARCHAR(20),
INDEX idx_username(username), -- 普通索引
UNIQUE INDEX idx_email(email), -- 唯一索引
INDEX idx_name_phone(username, phone) -- 复合索引
);
方式2:使用ALTER TABLE
ALTER TABLE users ADD INDEX idx_username(username); ALTER TABLE users ADD UNIQUE INDEX idx_email(email);
方式3:使用CREATE INDEX
CREATE INDEX idx_username ON users(username); CREATE UNIQUE INDEX idx_email ON users(email);
3.2 删除索引
-- 方式1 DROP INDEX idx_username ON users; -- 方式2 ALTER TABLE users DROP INDEX idx_username;
3.3 查看索引
-- 查看表的索引 SHOW INDEX FROM users; -- 查看创建索引的SQL SHOW CREATE TABLE users;
四、工作中常见的踩坑点(血泪教训)
🐛 踩坑点1:索引不是越多越好
错误示例:
-- 新手:每个字段都加索引,美其名曰"优化"
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
phone VARCHAR(20),
age INT,
gender TINYINT,
city VARCHAR(50),
INDEX idx_username(username),
INDEX idx_email(email),
INDEX idx_phone(phone),
INDEX idx_age(age),
INDEX idx_gender(gender),
INDEX idx_city(city)
-- ... 还有10个字段,每个都加索引
);问题:
- 占用存储空间:每个索引都需要额外存储
- 降低写性能:INSERT/UPDATE/DELETE需要维护所有索引
- 查询优化器可能选错索引:MySQL会纠结用哪个索引
正确做法:
- 只为经常用于查询条件的字段创建索引
- 不要为数据分布均匀的字段创建索引(如性别、状态)
🐛 踩坑点2:字符串索引长度设置不当
错误示例:
-- 为很长的文本字段创建完整索引 CREATE INDEX idx_content ON articles(content); -- content是TEXT类型,可能几KB
问题:
- 索引文件巨大
- 查询效率低
- 浪费存储空间
正确做法:使用前缀索引
-- 只对前100个字符创建索引
CREATE INDEX idx_content ON articles(content(100));
-- 如何确定前缀长度?
-- 计算不同前缀长度的选择性
SELECT
COUNT(DISTINCT LEFT(content, 10)) / COUNT(*) AS sel10,
COUNT(DISTINCT LEFT(content, 50)) / COUNT(*) AS sel50,
COUNT(DISTINCT LEFT(content, 100)) / COUNT(*) AS sel100
FROM articles;
-- 选择性越接近1越好,但也要考虑索引大小🐛 踩坑点3:在WHERE子句中对索引列使用函数
错误示例:
-- 有索引 idx_created_at(created_at) SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01'; -- ❌ 无法使用索引
问题:
- MySQL无法使用索引,因为需要对每行数据执行函数
- 导致全表扫描
正确做法:
-- ✅ 使用范围查询 SELECT * FROM orders WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-01-02 00:00:00'; -- ✅ 或者在函数计算列上创建索引(MySQL 5.7+) ALTER TABLE orders ADD INDEX idx_created_date((DATE(created_at))); SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
🐛 踩坑点4:LIKE查询使用不当
错误示例:
-- 有索引 idx_username(username) SELECT * FROM users WHERE username LIKE '%admin%'; -- ❌ 无法使用索引 SELECT * FROM users WHERE username LIKE '%admin'; -- ❌ 无法使用索引
问题:
- 前导通配符
%导致无法使用索引 - 全表扫描,性能极差
正确做法:
-- ✅ 只有后缀通配符可以使用索引 SELECT * FROM users WHERE username LIKE 'admin%'; -- ✅ 可以使用索引 -- 如果必须使用前导通配符,考虑: -- 1. 使用全文索引 -- 2. 使用搜索引擎(Elasticsearch) -- 3. 反序存储(如:将'admin'存储为'nimda',然后查询'%nimda'变成'admin%')
🐛 踩坑点5:OR条件导致索引失效
错误示例:
-- 有索引 idx_username(username) 和 idx_email(email) SELECT * FROM users WHERE username = 'admin' OR email = 'admin@example.com'; -- ❌ 可能无法使用索引
问题:
- MySQL可能无法同时使用两个索引
- 优化器可能选择全表扫描
正确做法:
-- ✅ 使用UNION SELECT * FROM users WHERE username = 'admin' UNION SELECT * FROM users WHERE email = 'admin@example.com'; -- ✅ 或者创建复合索引 CREATE INDEX idx_username_email ON users(username, email);
🐛 踩坑点6:NULL值处理不当
错误示例:
-- 有索引 idx_email(email),但email字段允许NULL SELECT * FROM users WHERE email IS NULL; -- ⚠️ 可能无法使用索引 SELECT * FROM users WHERE email IS NOT NULL; -- ⚠️ 可能无法使用索引
问题:
- NULL值在索引中的处理比较复杂
- 可能导致索引使用效率低下
正确做法:
-- ✅ 尽量避免NULL,使用默认值
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) NOT NULL DEFAULT '', -- 使用空字符串而不是NULL
INDEX idx_email(email)
);
-- ✅ 如果必须使用NULL,考虑覆盖索引
CREATE INDEX idx_email_id ON users(email, id);
SELECT id FROM users WHERE email IS NULL; -- 可以使用覆盖索引🐛 踩坑点7:隐式类型转换
错误示例:
-- phone字段是VARCHAR类型,有索引 idx_phone(phone) SELECT * FROM users WHERE phone = 13800138000; -- ❌ 类型不匹配,无法使用索引
问题:
- MySQL会进行隐式类型转换
- 导致无法使用索引
正确做法:
-- ✅ 确保类型匹配 SELECT * FROM users WHERE phone = '13800138000'; -- ✅ 使用字符串
🐛 踩坑点8:ORDER BY和索引
错误示例:
-- 有索引 idx_username(username),但没有包含age SELECT * FROM users WHERE username = 'admin' ORDER BY age; -- ❌ 需要额外的排序操作(filesort)
问题:
- 如果ORDER BY的字段不在索引中,需要额外的排序
- 如果数据量大,排序会很慢
正确做法:
-- ✅ 创建包含ORDER BY字段的索引 CREATE INDEX idx_username_age ON users(username, age); SELECT * FROM users WHERE username = 'admin' ORDER BY age; -- ✅ 可以使用索引,避免filesort
五、索引优化技巧
5.1 使用EXPLAIN分析查询
EXPLAIN是优化查询的神器!
EXPLAIN SELECT * FROM users WHERE username = 'admin';
关键字段:
- type:访问类型
ALL:全表扫描(最差)😱index:全索引扫描range:范围扫描ref:非唯一索引扫描const:常量查询(最好)😊
- key:使用的索引
- rows:扫描的行数(越少越好)
- Extra:额外信息
Using index:使用了覆盖索引(很好)✨Using filesort:需要额外排序(不好)⚠️Using temporary:需要临时表(很不好)🚫
5.2 覆盖索引(Covering Index)
覆盖索引:索引包含了查询所需的所有字段
-- 普通索引 CREATE INDEX idx_username ON users(username); -- 查询需要回表 SELECT id, username, email FROM users WHERE username = 'admin'; -- 1. 通过索引找到username = 'admin'的行 -- 2. 根据主键回表查询email(额外IO) -- 覆盖索引 CREATE INDEX idx_username_email ON users(username, email); -- 查询不需要回表 SELECT username, email FROM users WHERE username = 'admin'; -- 1. 通过索引找到username = 'admin'的行 -- 2. 索引中已经有email,直接返回(不需要回表)✨
优势:
- 减少IO操作
- 提高查询性能
- 特别是在InnoDB中,可以减少随机IO
5.3 索引下推(Index Condition Pushdown,ICP)
MySQL 5.6+支持索引下推优化
-- 有索引 idx_name_phone(name, phone) SELECT * FROM users WHERE name LIKE '张%' AND phone = '13800138000'; -- 没有ICP(MySQL 5.6之前): -- 1. 通过索引找到name LIKE '张%'的所有行 -- 2. 回表查询 -- 3. 过滤phone = '13800138000' -- 有ICP(MySQL 5.6+): -- 1. 通过索引找到name LIKE '张%'的所有行 -- 2. 在索引中直接过滤phone = '13800138000'(索引下推) -- 3. 只回表查询匹配的行 -- 减少了回表次数!✨
5.4 索引选择性(Cardinality)
索引选择性 = 不同值的数量 / 总行数
选择性越高,索引效果越好。
-- 查看索引选择性
SHOW INDEX FROM users;
-- 或者
SELECT
COUNT(DISTINCT username) / COUNT(*) AS username_sel,
COUNT(DISTINCT gender) / COUNT(*) AS gender_sel
FROM users;
-- username_sel接近1,适合创建索引
-- gender_sel接近0.5(只有男/女),不适合创建索引经验法则:
- 选择性 > 0.1:适合创建索引
- 选择性 < 0.1:不适合创建索引(如性别、状态等)
5.5 索引合并(Index Merge)
MySQL可以将多个索引合并使用
-- 有索引 idx_username(username) 和 idx_email(email) SELECT * FROM users WHERE username = 'admin' OR email = 'admin@example.com'; -- MySQL可能使用索引合并: -- 1. 使用idx_username查找username = 'admin'的行 -- 2. 使用idx_email查找email = 'admin@example.com'的行 -- 3. 合并结果
但要注意:
- 索引合并的效率通常不如单个复合索引
- 如果经常这样查询,考虑创建复合索引
六、索引设计最佳实践
6.1 索引设计原则
- 最左前缀原则
- 复合索引要遵循最左前缀原则
- 将选择性高的字段放在前面
- 避免冗余索引
-- ❌ 冗余:idx_username已经包含在idx_username_email中 CREATE INDEX idx_username ON users(username); CREATE INDEX idx_username_email ON users(username, email); -- ✅ 正确:只需要复合索引 CREATE INDEX idx_username_email ON users(username, email);
- 考虑查询模式
- 根据实际查询场景设计索引
- 不要盲目创建索引
- 定期分析和优化
-- 分析表,更新索引统计信息 ANALYZE TABLE users; -- 查看未使用的索引(MySQL 5.7+) SELECT * FROM sys.schema_unused_indexes;
6.2 索引命名规范
-- 推荐命名方式 CREATE INDEX idx_username ON users(username); -- 单列索引 CREATE INDEX idx_username_email ON users(username, email); -- 复合索引 CREATE UNIQUE INDEX uk_email ON users(email); -- 唯一索引 CREATE INDEX idx_created_at ON orders(created_at); -- 时间索引
6.3 索引维护
-- 重建索引(InnoDB) ALTER TABLE users DROP INDEX idx_username; ALTER TABLE users ADD INDEX idx_username(username); -- 或者使用OPTIMIZE TABLE OPTIMIZE TABLE users;
总结
索引使用 checklist ✅
- 为经常用于WHERE条件的字段创建索引
- 为经常用于ORDER BY的字段创建索引
- 遵循最左前缀原则设计复合索引
- 使用EXPLAIN分析查询性能
- 避免在索引列上使用函数
- 避免前导通配符的LIKE查询
- 定期分析和优化索引
- 删除未使用的索引
- 监控索引使用情况
还是那句话
索引是一把双刃剑:
- 用得好:查询速度飞起,用户体验up up up 🚀
- 用得不好:写性能下降,存储空间浪费,还可能选错索引 😱
记住:
- 不要过度索引:索引不是越多越好
- 根据实际场景设计:不要盲目创建索引
- 定期监控和优化:索引需要持续维护
- 使用EXPLAIN分析:不要凭感觉优化
希望这篇文章能帮到你,避免在工作中踩坑。大家都踩过什么坑呢,欢迎留言讨论!
参考资料:
到此这篇关于MySQL索引踩坑合集从入门到精通的文章就介绍到这了,更多相关mysql索引内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
