MySQL索引进阶用法

一、索引的创建(多种姿势)

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

工作流程:

  1. 把要删除的索引设为INVISIBLE
  2. 观察几天,确认查询性能没下降
  3. 确认没问题后,真正DROP INDEX
  4. 如果性能下降,设回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; 调试用
相关推荐
舒一笑6 小时前
程序员效率神器:一文掌握 tmux(服务器开发必备工具)
运维·后端·程序员
0xDevNull6 小时前
MySQL索引用法
mysql
UIUV7 小时前
Splitter学习笔记(含RAG相关流程与代码实践)
后端·langchain·llm
cipher7 小时前
HAPI + 设备指纹认证:打造更安全的远程编程体验
前端·后端·ai编程
雨中飘荡的记忆7 小时前
保证金系统入门到实战
java·后端
秋水无痕8 小时前
从零搭建个人博客系统:Spring Boot 多模块实践详解
前端·javascript·后端
用户9003486133468 小时前
GO语言基础:反射
后端
用户1474853079748 小时前
Git-stash产生的冲突
后端
UrbanJazzerati8 小时前
Python Scrapling反爬虫小技巧之Referer
后端·面试