一、索引的创建(多种姿势)
1.1 建表时创建(推荐)
sql
-- 方式1:列定义时直接加(只适合主键、唯一、普通索引)
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键索引
email VARCHAR(100) UNIQUE, -- 唯一索引
phone VARCHAR(20),
name VARCHAR(50),
age INT,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-- 普通索引(方式2:表级别定义)
INDEX idx_phone (phone),
-- 组合索引(最常用)
INDEX idx_name_age (name, age),
-- 唯一组合索引
UNIQUE INDEX uk_email_status (email, status),
-- 前缀索引(邮箱前8位)
INDEX idx_email_prefix (email(8)),
-- 函数索引(MySQL 8.0.13+)
INDEX idx_func ((LOWER(name))), -- 忽略大小写查询
-- 降序索引(MySQL 8.0+,优化DESC排序)
INDEX idx_time_desc (create_time DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 查看建表语句(确认索引创建成功)
SHOW CREATE TABLE user\G
1.2 建表后添加(ALTER TABLE)
sql
-- 添加主键(如果建表时没指定)
ALTER TABLE user ADD PRIMARY KEY (id);
-- 添加唯一索引
ALTER TABLE user ADD UNIQUE INDEX uk_phone (phone);
-- 添加普通索引
ALTER TABLE user ADD INDEX idx_status (status);
-- 添加组合索引
ALTER TABLE user ADD INDEX idx_age_status (age, status);
-- 添加全文索引(MyISAM和InnoDB 5.6+支持)
ALTER TABLE article ADD FULLTEXT INDEX ft_content (content);
-- 添加空间索引(GIS数据)
ALTER TABLE map_location ADD SPATIAL INDEX sp_geo (geo_point);
-- 指定索引算法(MySQL 8.0默认InnoDB用B-tree,可以显式指定)
ALTER TABLE user ADD INDEX idx_algo (name) USING BTREE;
1.3 使用CREATE INDEX语句(CREATE INDEX)
sql
-- 语法更简洁,推荐用于后期维护
CREATE INDEX idx_name ON user(name);
-- 创建唯一索引
CREATE UNIQUE INDEX uk_email ON user(email);
-- 指定索引类型(MySQL 8.0)
CREATE INDEX idx_name ON user(name) USING BTREE;
1.4 在线DDL(Online DDL,MySQL 5.6+,不锁表)
sql
-- 大表加索引怕锁表?用ALGORITHM=INPLACE, LOCK=NONE
ALTER TABLE big_table ADD INDEX idx_col (col),
ALGORITHM=INPLACE, LOCK=NONE;
-- ALGORITHM选项:
-- COPY:复制表数据(最慢,锁表)
-- INPLACE:原地修改(快,不锁表或短暂锁)
-- INSTANT:瞬间完成(仅部分操作支持,MySQL 8.0+)
-- LOCK选项:
-- NONE:不锁表
-- SHARED:允许读,阻塞写
-- EXCLUSIVE:读写都阻塞(默认最保守)
⚠️ 注意: Online DDL虽然好,但大表(千万级)加索引还是建议在业务低峰期 操作,或者使用pt-online-schema-change工具。
二、索引的查看(知己知彼)
2.1 查看表的所有索引
sql
-- 方式1:SHOW INDEX(最常用)
SHOW INDEX FROM user;
-- 或
SHOW KEYS FROM user;
-- 输出字段说明:
-- Table: 表名
-- Non_unique: 0=唯一索引,1=非唯一
-- Key_name: 索引名(PRIMARY是主键)
-- Seq_in_index: 索引中的列顺序(组合索引有用)
-- Column_name: 列名
-- Collation: A=升序,NULL=无排序(HASH索引)
-- Cardinality: 基数(该列不重复值的大致数量,越高越好)
-- Sub_part: 前缀索引长度(如8表示前8字符)
-- Packed: 是否压缩存储
-- Null: 是否允许NULL
-- Index_type: BTREE/FULLTEXT/HASH/RTREE
-- Comment: 注释
-- Index_comment: 索引注释
-- Visible: 是否可见(MySQL 8.0,INVISIBLE索引优化用)
-- 方式2:INFORMATION_SCHEMA(程序化查询)
SELECT * FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'user';
-- 方式3:SHOW CREATE TABLE(看完整建表语句)
SHOW CREATE TABLE user\G
2.2 查看索引使用情况(诊断神器)
sql
-- 查看索引是否被使用(重启后清零)
SHOW STATUS LIKE 'Handler_read%';
-- 关键指标:
-- Handler_read_key: 使用索引读取行的次数(越高越好)
-- Handler_read_rnd_next: 全表扫描或排序后读取下一行的次数(越低越好)
-- 如果Handler_read_key很低而Handler_read_rnd_next很高,说明索引没用好
-- MySQL 8.0:查看索引统计信息
SELECT * FROM mysql.innodb_index_stats
WHERE table_name = 'user';
-- 查看执行计划(EXPLAIN,前面讲过)
EXPLAIN SELECT * FROM user WHERE name = '张三'\G
2.3 查看冗余索引(清理无用索引第一步)
ini
-- 使用pt-duplicate-key-checker工具(Percona Toolkit)
pt-duplicate-key-checker --host=localhost --user=root --password=xxx --database=your_db
-- 或手动查询INFORMATION_SCHEMA找重复索引
SELECT
t1.TABLE_SCHEMA,
t1.TABLE_NAME,
t1.INDEX_NAME AS '重复索引',
t2.INDEX_NAME AS '被包含索引'
FROM INFORMATION_SCHEMA.STATISTICS t1
JOIN INFORMATION_SCHEMA.STATISTICS t2
ON t1.TABLE_SCHEMA = t2.TABLE_SCHEMA
AND t1.TABLE_NAME = t2.TABLE_NAME
AND t1.COLUMN_NAME = t2.COLUMN_NAME
AND t1.SEQ_IN_INDEX = t2.SEQ_IN_INDEX
WHERE t1.SEQ_IN_INDEX = 1
AND t1.NON_UNIQUE = t2.NON_UNIQUE
AND t1.INDEX_NAME != t2.INDEX_NAME
AND t1.CARDINALITY <= t2.CARDINALITY;
三、索引的修改(改名、重建、优化)
3.1 重命名索引(MySQL 5.7+)
sql
-- 方式1:ALTER TABLE ... RENAME INDEX(推荐,Online DDL)
ALTER TABLE user RENAME INDEX idx_old_name TO idx_new_name;
-- 方式2:先删后建(会锁表,小表可用)
-- 大表千万别这么干!
3.2 重建索引(解决碎片、优化性能)
索引用久了会产生碎片(尤其频繁UPDATE/DELETE后),需要重建:
sql
-- 方式1:ALTER TABLE ... ENGINE=InnoDB(重建整个表,最彻底)
-- 会锁表,大表慎用!
ALTER TABLE user ENGINE=InnoDB;
-- 方式2:OPTIMIZE TABLE(推荐,会重建表和索引)
-- 等价于 ALTER TABLE ... ENGINE=InnoDB + ANALYZE TABLE
OPTIMIZE TABLE user;
-- 方式3:DROP + ADD INDEX(针对单个索引)
-- 先删
ALTER TABLE user DROP INDEX idx_name;
-- 再加(可以顺便修改索引定义)
ALTER TABLE user ADD INDEX idx_name (name) USING BTREE;
-- 方式4:MySQL 8.0的INSTANT算法(瞬间完成,不锁表)
-- 仅部分操作支持,如添加/删除列的默认值等
3.3 修改索引类型(唯一 ↔ 普通)
sql
-- 不能直接修改类型,只能先删后建
-- 普通索引 → 唯一索引
ALTER TABLE user DROP INDEX idx_email;
ALTER TABLE user ADD UNIQUE INDEX uk_email (email);
-- 唯一索引 → 普通索引
ALTER TABLE user DROP INDEX uk_email;
ALTER TABLE user ADD INDEX idx_email (email);
3.4 修改组合索引的列顺序(最左前缀调整)
sql
-- 原索引:idx_name_age(name, age)
-- 想改成:idx_age_name(age, name) 因为age选择性更高
-- 步骤1:创建新索引(在线DDL,不锁表)
ALTER TABLE user ADD INDEX idx_age_name (age, name),
ALGORITHM=INPLACE, LOCK=NONE;
-- 步骤2:验证新索引效果(跑几天生产流量观察)
-- 用EXPLAIN确认查询都用新索引
-- 步骤3:删除旧索引(确认没问题后再删!)
ALTER TABLE user DROP INDEX idx_name_age;
3.5 不可见索引(Invisible Index,MySQL 8.0+)
用途: 测试删除索引后的性能影响,而不真正删除
sql
-- 创建时指定不可见
CREATE INDEX idx_test ON user(name) INVISIBLE;
-- 或修改现有索引为不可见
ALTER TABLE user ALTER INDEX idx_name INVISIBLE;
-- 修改回可见
ALTER TABLE user ALTER INDEX idx_name VISIBLE;
-- 查询时强制使用不可见索引(测试用)
SELECT /*+ INDEX(user idx_test) */ * FROM user WHERE name = 'xxx';
-- 查看索引可见性
SHOW INDEX FROM user;
-- 看Visible列:YES/NO
工作流程:
- 把要删除的索引设为INVISIBLE
- 观察几天,确认查询性能没下降
- 确认没问题后,真正DROP INDEX
- 如果性能下降,设回VISIBLE,保留索引
四、索引的删除(清理无用索引)
4.1 删除单条索引
sql
-- 方式1:ALTER TABLE ... DROP INDEX(推荐)
ALTER TABLE user DROP INDEX idx_name;
-- 方式2:DROP INDEX语句
DROP INDEX idx_name ON user;
-- 删除主键(特殊)
ALTER TABLE user DROP PRIMARY KEY;
-- 删除前确认索引存在(避免报错)
-- 可以先用SHOW INDEX确认,或用IF EXISTS(MySQL不支持,需程序判断)
4.2 批量删除索引(清理冗余)
sql
-- 场景:发现5个冗余索引,要批量删除
-- 方法1:多条ALTER合并(减少表扫描)
ALTER TABLE user
DROP INDEX idx_redundant_1,
DROP INDEX idx_redundant_2,
DROP INDEX idx_redundant_3;
-- 方法2:脚本生成删除语句
SELECT CONCAT('ALTER TABLE ', TABLE_NAME, ' DROP INDEX ', INDEX_NAME, ';')
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'your_db'
AND INDEX_NAME LIKE 'idx_old_%';
-- 把结果复制出来执行
4.3 删除索引的注意事项
sql
-- ⚠️ 危险操作!生产环境务必:
-- 1. 先备份(逻辑备份或快照)
mysqldump -u root -p your_db user > user_backup.sql
-- 2. 检查索引是否被使用(MySQL 8.0的performance_schema)
SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_NAME = 'user' AND INDEX_NAME IS NOT NULL;
-- 3. 低峰期操作
-- 4. 先在测试环境验证
-- 5. 准备好回滚方案(删除语句提前写好,必要时重建)
4.4 智能删除:基于使用频率
vbnet
-- MySQL 8.0:查询索引使用次数(sys schema)
SELECT
OBJECT_SCHEMA AS '数据库',
OBJECT_NAME AS '表名',
INDEX_NAME AS '索引名',
COUNT_FETCH AS '读取次数',
COUNT_INSERT AS '插入使用',
COUNT_UPDATE AS '更新使用',
COUNT_DELETE AS '删除使用'
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'your_db'
AND INDEX_NAME IS NOT NULL -- 排除主键
AND COUNT_FETCH = 0 -- 从未被读取过
AND COUNT_INSERT > 0 -- 但插入时维护过(纯成本)
ORDER BY (COUNT_INSERT + COUNT_UPDATE + COUNT_DELETE) DESC;
-- 这些索引可以考虑删除:只有维护成本,没有查询收益
五、索引的维护(日常保养)
5.1 更新统计信息(ANALYZE TABLE)
MySQL优化器依赖统计信息选择索引,数据变化大后需要更新:
sql
-- 更新表和索引的统计信息(不锁表,很快)
ANALYZE TABLE user;
-- 批量更新整个库
ANALYZE NO_WRITE_TO_BINLOG TABLE
user, order, product, log_2024;
-- 什么时候执行?
-- 1. 大批量导入数据后
-- 2. 删除大量数据后
-- 3. 每周定时任务(数据变化大的表)
-- 查看最后分析时间
SHOW TABLE STATUS LIKE 'user';
-- 看Collation列旁边的Create_time和Update_time
5.2 检查表和索引完整性
sql
-- 检查表是否有错误(包括索引损坏)
CHECK TABLE user;
-- 修复表(如果CHECK发现问题)
REPAIR TABLE user; -- MyISAM有效,InnoDB会自动修复
-- InnoDB索引损坏通常需要:
-- 1. 从备份恢复
-- 2. 或 ALTER TABLE ... ENGINE=InnoDB 重建
5.3 监控索引大小(磁盘空间管理)
sql
-- 查看索引占用的磁盘空间
SELECT
TABLE_NAME,
INDEX_NAME,
ROUND(SUM(DATA_LENGTH)/1024/1024, 2) AS '索引大小(MB)',
ROUND(SUM(DATA_FREE)/1024/1024, 2) AS '碎片空间(MB)'
FROM INFORMATION_SCHEMA.INNODB_SYS_INDEXES i
JOIN INFORMATION_SCHEMA.INNODB_SYS_TABLES t ON i.TABLE_ID = t.TABLE_ID
WHERE t.NAME = 'your_db/user'
GROUP BY INDEX_NAME;
-- 或简单方式
SHOW TABLE STATUS LIKE 'user';
-- Data_length: 数据大小
-- Index_length: 索引大小
-- Data_free: 碎片大小
5.4 索引碎片整理(定期OPTIMIZE)
sql
-- 查看碎片率(Data_free / (Data_length + Index_length) > 30%需要优化)
SELECT
TABLE_NAME,
DATA_FREE,
DATA_LENGTH + INDEX_LENGTH AS TOTAL_SIZE,
ROUND(DATA_FREE / (DATA_LENGTH + INDEX_LENGTH) * 100, 2) AS FRAGMENTATION_PCT
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'your_db'
AND DATA_FREE / (DATA_LENGTH + INDEX_LENGTH) > 0.3;
-- 整理碎片(会锁表,大表用pt-online-schema-change)
OPTIMIZE TABLE user;
-- 大表在线整理(Percona Toolkit)
pt-online-schema-change --alter "ENGINE=InnoDB" \
D=your_db,t=user,u=root,p=password --execute
六、索引的监控与诊断(慢查询分析)
6.1 开启慢查询日志
ini
-- 查看当前设置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 临时开启(重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 永久配置(my.cnf)
[mysqld]
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = 1 -- 记录没走索引的查询(重要!)
6.2 分析慢查询(mysqldumpslow + EXPLAIN)
bash
# 命令行分析慢日志
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log # 按时间排序,前10条
# pt-query-digest更强大(Percona Toolkit)
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
慢查询分析流程:
sql
-- 1. 找到慢SQL
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 5;
-- 2. EXPLAIN分析执行计划
EXPLAIN SELECT * FROM user WHERE phone = '13800138000'\G
-- 3. 确认是否用索引
-- 看key列:NULL表示没走索引
-- 看rows列:扫描行数是否过大
-- 看Extra:Using filesort/Using temporary都是坏信号
-- 4. 添加或优化索引
-- 5. 再次EXPLAIN验证
-- 6. 观察慢查询日志是否消失
6.3 实时性能监控(Performance Schema)
sql
-- MySQL 8.0启用Performance Schema(默认开启)
-- 查看当前正在执行的慢查询
SELECT * FROM performance_schema.events_statements_current
WHERE TIMER_WAIT > 1 * 10^12; -- 超过1秒
-- 查看历史慢查询
SELECT
SQL_TEXT,
TIMER_WAIT/10^9 AS '耗时(ms)',
CREATED_TMP_TABLES,
CREATED_TMP_DISK_TABLES,
NO_INDEX_USED,
NO_GOOD_INDEX_USED
FROM performance_schema.events_statements_history_long
WHERE NO_INDEX_USED = 1 OR NO_GOOD_INDEX_USED = 1
ORDER BY TIMER_WAIT DESC
LIMIT 10;
七、实战案例:索引优化全流程
案例背景
电商系统,订单表order有5000万数据,查询越来越慢:
less
CREATE TABLE `order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`order_no` VARCHAR(32) UNIQUE,
`status` TINYINT DEFAULT 0, -- 0待支付 1已支付 2已发货 3已完成 4已取消
`amount` DECIMAL(10,2),
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`pay_time` DATETIME,
`express_no` VARCHAR(32),
INDEX `idx_user` (`user_id`), -- 旧索引
INDEX `idx_status` (`status`), -- 旧索引(问题!)
INDEX `idx_time` (`create_time`) -- 旧索引
) ENGINE=InnoDB;
问题诊断
vbnet
-- 查看慢查询
SELECT * FROM mysql.slow_log WHERE sql_text LIKE '%order%' LIMIT 5;
-- 发现慢SQL:
-- 1. SELECT * FROM order WHERE user_id = 100 AND status = 1 ORDER BY create_time DESC LIMIT 10;
-- 2. SELECT * FROM order WHERE status = 1 AND create_time > '2024-01-01';
-- 3. SELECT COUNT(*) FROM order WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';
-- EXPLAIN分析
EXPLAIN SELECT * FROM order WHERE user_id = 100 AND status = 1 ORDER BY create_time DESC LIMIT 10\G
-- 结果:type=ref, key=idx_user, rows=10000, Extra=Using where; Using filesort
-- 问题:只用了idx_user,status和排序都没用上,内存排序10000行!
优化步骤
Step 1:清理垃圾索引
sql
-- status字段只有5个值,选择性极低,单独索引没用
ALTER TABLE `order` DROP INDEX `idx_status`; -- 删除低选择性索引
-- 检查索引使用情况(确认idx_status真的没被用)
-- 从performance_schema确认后删除
Step 2:创建组合索引(覆盖查询)
sql
-- 优化SQL1:用户查自己的待支付订单,按时间倒序
-- 查询条件:user_id + status,排序:create_time DESC
-- 最左前缀:user_id → status → create_time
ALTER TABLE `order` ADD INDEX `idx_user_status_time`
(`user_id`, `status`, `create_time` DESC);
-- 验证:EXPLAIN看到Using index(覆盖索引,不用回表!)
EXPLAIN SELECT id, order_no, amount, create_time -- 只查索引有的列
FROM `order`
WHERE user_id = 100 AND status = 1
ORDER BY create_time DESC
LIMIT 10;
-- 结果:type=ref, key=idx_user_status_time, rows=10, Extra=Using where; Using index
-- 完美!直接从索引取10行,不用排序,不用回表
Step 3:优化时间范围查询
sql
-- 优化SQL2和SQL3:按时间范围查,但通常带其他条件
-- 创建时间+状态组合索引
ALTER TABLE `order` ADD INDEX `idx_time_status`
(`create_time`, `status`);
-- 如果经常只按时间查(如统计),保留单列索引
-- 但通常时间范围查询都带user_id或status,组合索引更好
Step 4:处理分页深分页问题
vbnet
-- 用户历史订单分页,翻到100页后极慢
-- 原SQL:SELECT * FROM order WHERE user_id = 100 ORDER BY create_time DESC LIMIT 100000, 10;
-- 优化:延迟关联 + 覆盖索引
SELECT o.*
FROM `order` o
JOIN (
SELECT id
FROM `order`
WHERE user_id = 100
ORDER BY create_time DESC
LIMIT 100000, 10
) tmp ON o.id = tmp.id;
-- 或者:记录上次位置(游标分页,推荐)
SELECT * FROM `order`
WHERE user_id = 100 AND create_time < '上次最后的时间'
ORDER BY create_time DESC
LIMIT 10;
Step 5:定期维护
sql
-- 每周定时任务
ANALYZE TABLE `order`; -- 更新统计信息
-- 每月检查碎片
OPTIMIZE TABLE `order`; -- 或pt-online-schema-change在线整理
-- 监控慢查询
-- 配置Alert,慢查询超过阈值发邮件
优化结果对比
表格
| 查询场景 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 用户查订单列表 | 2-3秒 | 20ms | 100倍 |
| 时间范围统计 | 15秒 | 200ms | 75倍 |
| 深分页(100页) | 10秒+ | 100ms | 100倍 |
| 索引维护成本 | 3个索引 | 2个高效索引 | 写性能提升30% |
附录:索引管理速查表
| 操作 | SQL语句 | 注意事项 |
|---|---|---|
| 创建索引 | CREATE INDEX idx ON table(col); |
大表用Online DDL |
| 创建组合索引 | CREATE INDEX idx ON table(a,b,c); |
注意最左前缀 |
| 创建唯一索引 | CREATE UNIQUE INDEX uk ON table(col); |
列值必须唯一 |
| 查看索引 | SHOW INDEX FROM table; |
看Cardinality和Index_type |
| 重命名 | ALTER TABLE t RENAME INDEX old TO new; |
MySQL 5.7+ |
| 删除索引 | ALTER TABLE t DROP INDEX idx; |
先确认无查询使用 |
| 重建索引 | OPTIMIZE TABLE t; |
会锁表,大表用pt-osc |
| 更新统计 | ANALYZE TABLE t; |
大批量数据变更后执行 |
| 设为不可见 | ALTER TABLE t ALTER INDEX idx INVISIBLE; |
MySQL 8.0,测试用 |
| 强制使用 | SELECT /*+ INDEX(t idx) */ * FROM t; |
调试用 |