MySQL 索引和执行计划

索引的作用是优化查询。

一、索引分类(按算法)

1. B 树

  • 家族

image-1765956820351

  • B-Tree:上层节点保存下层节点的最小值
  • B+:对范围查找优化
  • B*(默认):对范围查找进一步优化

2. R 树

3. Hash

  • 自适应 Hash 索引

4. FullText

  • 全文索引

5. GIS

  • 类似 MongoDB

二、索引分类(按功能)

1. 辅助索引

  • 创建过程
    • 将索引列的值取出并排序
    • 将排序后的数据均匀存储到索引的叶子节点(叶子节点存储对应 id)
    • 生成枝节点和根节点
  • 细分
    • 单列辅助索引
    • 联合索引(覆盖索引)
    • 唯一索引

2. 聚集索引

  • 创建过程
    • 自动将主键作为聚集索引列(无主键则选唯一键,均无则生成隐藏主键)
    • 存储数据时按顺序存储数据行
    • 将源表的数据页作为叶子节点,提取索引列向上生成枝节点和根节点

3. 区别

  • 任何列都可创建辅助索引;聚集索引只能有一个列
  • 辅助索引的叶子节点只存储索引列的有序值和对应 id;聚集索引的叶子节点存储有序的整行数据
  • MySQL 的表数据是聚集索引组织表

三、索引树高度

1. 高度要求

  • 越低越好,3-4 层最佳

2. 影响因素

  • 数据行数
    • 分表(partition):目前用得较少
    • 分片(分布式架构)
  • 字段长度
    • 尽量选择字符长度短的列作为索引列
    • 采用前缀索引
  • 数据类型
    • char/varchar:char 创建索引时占用更大空间
    • enum:占用更少空间

四、索引操作

1. 创建索引

  • 单列辅助索引

    alter table user add index idx_password(password);
    

    Note:会导致锁表。

    查询 SQL:

    select * from user where password = '954002';
    
  • 联合索引

    alter table user add index idx_user_password(user, password);
    

    查看:

    mysql> show index from user;
    +-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | Table | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
    +-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | user  |          0 | PRIMARY           |            1 | id          | A         |    16295990 |     NULL | NULL   |      | BTREE      |         |               |
    | user  |          1 | idx_user_password |            1 | user        | A         |    10324053 |     NULL | NULL   |      | BTREE      |         |               |
    | user  |          1 | idx_user_password |            2 | password    | A         |    16295990 |     NULL | NULL   |      | BTREE      |         |               |
    +-------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    mysql> desc user;
    +----------+--------------+------+-----+---------+----------------+
    | Field    | Type         | Null | Key | Default | Extra          |
    +----------+--------------+------+-----+---------+----------------+
    | id       | int(11)      | NO   | PRI | NULL    | auto_increment |
    | user     | varchar(255) | NO   | MUL | NULL    |                |
    | password | varchar(255) | NO   |     | NULL    |                |
    +----------+--------------+------+-----+---------+----------------+
    
  • 唯一索引

    alter table user add unique index uidx_user(user);
    

    查询 SQL:

    select * from user where id = '145324';
    
  • 前缀索引

    alter table user add index idx_user(user(5));
    

    Note:使用 user 列的前 5 个字符创建索引。

2. 查看索引

  • desc
    key 列的值表明索引类型:

    desc user;
    

    输出结果:

    +----------+--------------+------+-----+---------+----------------+
    | Field    | Type         | Null | Key | Default | Extra          |
    +----------+--------------+------+-----+---------+----------------+
    | id       | int(11)      | NO   | PRI | NULL    | auto_increment |
    | user     | varchar(255) | NO   |     | NULL    |                |
    | password | varchar(255) | NO   | MUL | NULL    |                |
    +----------+--------------+------+-----+---------+----------------+
    
    • PRI:主键索引
    • MUL:辅助索引
    • UNI:唯一索引
  • show

    show index from user;
    

    输出结果:

    +-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | Table | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
    +-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | user  |          0 | PRIMARY      |            1 | id          | A         |    16295990 |     NULL | NULL   |      | BTREE      |         |               |
    | user  |          1 | idx_password |            1 | password    | A         |     1002924 |     NULL | NULL   |      | BTREE      |         |               |
    +-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    

3. 删除索引

alter table user drop index idx_password;

五、索引规范

1. 创建索引规范

  • 建表必须有主键,一般为无关列且自增长
  • 索引列最好是经常作为 where 条件的列(包括排序条件 order by、group by、union 等)
  • 推荐使用唯一值多的列作为联合索引的第一位
  • 列值长度较长的列,建议使用索引前缀
  • 降低索引数量(不建无用索引,清理不常用索引)
  • 索引维护要避开业务高峰期
  • 小表不建索引

2. 不走索引的情况

  • 没有查询条件或没有建索引

    mysql> desc select * from user2 limit 5;
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------+
    |  1 | SIMPLE      | user2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 9682283 |   100.00 | NULL  |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> desc select * from user2 where qq = '234234234';
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    |  1 | SIMPLE      | user2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 9682283 |    10.00 | Using where |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    1 row in set, 1 warning (0.01 sec)
    
  • 当查询扫描的数据超过全表的 25%,优化器会选择全表扫描(固定百分比不是唯一的判定条件)

  • 索引失效(统计信息不准):同一 SQL 语句突然变慢可能是此原因

  • 索引列的查询条件上使用了函数或运算(+、-、*、/、!)

    mysql> desc select * from user2 where user + 1 = '34234324324';
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    |  1 | SIMPLE      | user2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 9682283 |   100.00 | Using where |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    
    mysql> desc select * from user2 where floor(user) = '34234324324';
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    |  1 | SIMPLE      | user2 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 9682283 |   100.00 | Using where |
    +----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    
  • 索引列上有隐式转换(如字符串和数字的转换)

    mysql> desc select * from user2 where user = 34234324324;
    +----+-------------+-------+------------+------+-------------------+------+---------+------+---------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys     | key  | key_len | ref  | rows    | filtered | Extra       |
    +----+-------------+-------+------------+------+-------------------+------+---------+------+---------+----------+-------------+
    |  1 | SIMPLE      | user2 | NULL       | ALL  | idx_user_password | NULL | NULL    | NULL | 9682283 |    10.00 | Using where |
    +----+-------------+-------+------------+------+-------------------+------+---------+------+---------+----------+-------------+
    1 row in set, 3 warnings (0.00 sec)
    
  • 对于辅助索引,索引列中包含 !=、<> 和 not in 时不走索引(只有主键索引的 != 会走索引)

六、压力测试

在创建索引前后进行压力测试,对比查询效率提升。

1. 模拟环境

create table user(
id int not null auto_increment primary key,
user varchar(255) not null,
password varchar(255) not null
) engine=InnoDB;

insert into user(user, password) values('张三', '123456');

insert into user(
 user
 ,password
 )
select
 md5(rand())
 ,floor(rand(100)*1000000)
from
 user;

2. 测试

mysqlslap --defaults-file=/etc/my.cnf \
--concurrency=100 --iterations=1 --create-schema='test' \
--query="select * from user where user = '6518617c3a54d10028287abec8db13d2' and password = '694125';" engine=innodb \
--number-of-queries=2000 -uroot -proot -verbose

Note:创建索引前后分别记录查询时长,可看到明显性能提升。

七、执行计划

1. 查看执行计划

目的是进行 SQL 优化。

mysql> explain select * from user limit 5;
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 16295990 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------+
1 row in set, 1 warning (0.02 sec)

mysql> desc select * from user where user = '6518617c3a54d10028287abec8db13d2' and password = '694125';
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 16295990 |     1.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
  • 方式:desc + SQL 语句 或 explain + SQL 语句

2. 分析执行计划

  • type(查询类型)

    • 全表扫描(ALL)

      mysql> explain select * from user limit 5;
      +----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------+
      | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra |
      +----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------+
      |  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 16295990 |   100.00 | NULL  |
      +----+-------------+-------+------------+------+---------------+------+---------+------+----------+----------+-------+
      
    • 索引扫描

      性能排序:index(全索引扫描)< range(索引范围扫描)< ref(辅助索引等值查询)< eq_ref(联表查询时,主键或唯一键的等值查询)< const(主键或唯一键的等值查询)< NULL(所查找的数据在表中不存在)

      • index(全索引扫描):遍历整个索引树

        mysql> desc select * from user;
        +----+-------------+-------+------------+-------+---------------+-------------------+---------+------+----------+----------+-------------+
        | id | select_type | table | partitions | type  | possible_keys | key               | key_len | ref  | rows     | filtered | Extra       |
        +----+-------------+-------+------------+-------+---------------+-------------------+---------+------+----------+----------+-------------+
        |  1 | SIMPLE      | user  | NULL       | index | NULL          | idx_user_password | 2044    | NULL | 16295990 |   100.00 | Using index |
        +----+-------------+-------+------------+-------+---------------+-------------------+---------+------+----------+----------+-------------+
        
      • range(索引范围扫描):用于 >、<、>=、<=、between and、or、in、like 等范围查询(>、like 性能优于 or、in;in 一般改写成 union all)

        mysql> desc select * from user where user like '000005561e686de93b46%';
        +----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
        | id | select_type | table | partitions | type  | possible_keys     | key               | key_len | ref  | rows | filtered | Extra                    |
        +----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
        |  1 | SIMPLE      | user  | NULL       | range | idx_user_password | idx_user_password | 1022    | NULL |    2 |   100.00 | Using where; Using index |
        +----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
        

        in 与 union all 对比:

        mysql> desc select * from user where user in ('3242342werwerwe','000003f8fb93a54bf1d37d2b23a5cdef');
        +----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
        | id | select_type | table | partitions | type  | possible_keys     | key               | key_len | ref  | rows | filtered | Extra                    |
        +----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
        |  1 | SIMPLE      | user  | NULL       | range | idx_user_password | idx_user_password | 1022    | NULL |    3 |   100.00 | Using where; Using index |
        +----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
        1 row in set, 1 warning (0.00 sec)
        
        mysql> desc
            -> select * from user where user = '3242342werwerwe' 
            -> union all
            -> select * from user where user = '000003f8fb93a54bf1d37d2b23a5cdef'
            -> ;
        +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
        | id | select_type | table | partitions | type | possible_keys     | key               | key_len | ref   | rows | filtered | Extra       |
        +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
        |  1 | PRIMARY     | user  | NULL       | ref  | idx_user_password | idx_user_password | 1022    | const |    1 |   100.00 | Using index |
        |  2 | UNION       | user  | NULL       | ref  | idx_user_password | idx_user_password | 1022    | const |    2 |   100.00 | Using index |
        +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
        2 rows in set, 1 warning (0.01 sec)
        

        Note:in 使用 range 索引类型,union all 使用 ref 索引类型。

      • ref(辅助索引等值查询)

        mysql> desc select * from user where user = '000003f8fb93a54bf1d37d2b23a5cdef';
        +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
        | id | select_type | table | partitions | type | possible_keys     | key               | key_len | ref   | rows | filtered | Extra       |
        +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
        |  1 | SIMPLE      | user  | NULL       | ref  | idx_user_password | idx_user_password | 1022    | const |    2 |   100.00 | Using index |
        +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
        
      • eq_ref(联表查询时,主键或唯一键的等值查询):多表连接时,子表使用主键列或唯一列作为连接条件(一般小表作为驱动表)

        mysql> desc
            -> select * 
            -> from user t1
            -> left join user t2
            -> on t1.id = t2.id
            -> ;
        +----+-------------+-------+------------+--------+---------------+-------------------+---------+------------+----------+----------+-------------+
        | id | select_type | table | partitions | type   | possible_keys | key               | key_len | ref        | rows     | filtered | Extra       |
        +----+-------------+-------+------------+--------+---------------+-------------------+---------+------------+----------+----------+-------------+
        |  1 | SIMPLE      | t1    | NULL       | index  | NULL          | idx_user_password | 2044    | NULL       | 16295990 |   100.00 | Using index |
        |  1 | SIMPLE      | t2    | NULL       | eq_ref | PRIMARY       | PRIMARY           | 4       | test.t1.id |        1 |   100.00 | NULL        |
        +----+-------------+-------+------------+--------+---------------+-------------------+---------+------------+----------+----------+-------------+
        
      • const(主键或唯一键的等值查询)

        mysql> desc select * from user where id = '2342343';
        +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
        | id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
        +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
        |  1 | SIMPLE      | user  | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
        +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
        
      • NULL(所查找的数据在表中不存在):索引中无对应数据,无需到叶子节点查找

        mysql> desc select * from user where id = '0';
        +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
        | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                          |
        +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
        |  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL |     NULL | no matching row in const table |
        +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
        
  • possible_keys:可能用到的索引

  • key:实际选择的索引

  • key_len(索引覆盖长度):可判断联合索引中哪些字段被使用(联合索引覆盖越全越好)

    假设字符集为 utf8mb4(NULL 占用 1 个标志位,若字段允许 NULL):

    • varchar(10):1(NULL) + 4×10 + 2 = 43
    • char(10):1(NULL) + 4×10 = 41
    • int:1(NULL) + 4 = 5

3. 联合索引的执行计划

假设表结构为 t1(id, k1, k2, k3, k4),联合索引为 idx(k1, k2, k3, k4)。

  • 当 where 查询有多个等值条件(如 where k1 = ‘xx’ and k2 = ‘xx’ and k3 = ‘xx’):联合索引顺序与 where 条件顺序无关,优化器会自动排序;若条件不连续(如 k1, k3, k4),仅走 k1 索引;建索引时,唯一值多的列放第一位。

  • 若 where 条件缺少最左列(如仅 k2 = ‘xx’):不走联合索引(例外:表中所有列均在联合索引中时,每个列的查询条件会走索引)。

  • 若 where 条件有范围查询(>、< 等)或模糊匹配(like):建索引时,等值条件放前,不等值条件放后。

  • 多子句查询(如 select * from user where k1=‘xx’ order by k2):应创建索引 idx(k1, k2)

    mysql> desc select * from user2 where user = '000005561e686de93b4681562b951f2b' order by qq2;
    +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+---------------------------------------+
    | id | select_type | table | partitions | type | possible_keys     | key               | key_len | ref   | rows | filtered | Extra                                 |
    +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+---------------------------------------+
    |  1 | SIMPLE      | user2 | NULL       | ref  | idx_user_password | idx_user_password | 1022    | const |    2 |   100.00 | Using index condition; Using filesort |
    +----+-------------+-------+------------+------+-------------------+-------------------+---------+-------+------+----------+---------------------------------------+
    
    • Extra: Using filesort:说明排序条件未合理应用索引(关注 key_len),常见于 order by、group by、distinct、union 等场景。

八、数据库性能优化

1. 应急性的慢(突然夯住,资源耗尽)

  • 获取导致卡住的语句

    show processlist;
    

    输出结果:

    +-----+------+-----------+------+---------+------+--------------+--------------------------------------------------------------------------------------+
    | Id  | User | Host      | db   | Command | Time | State        | Info                                                                                 |
    +-----+------+-----------+------+---------+------+--------------+--------------------------------------------------------------------------------------+
    |   6 | root | localhost | test | Query   |    0 | starting     | show processlist                                                                     |
    | 413 | root | localhost | NULL | Sleep   |    4 |              | NULL                                                                                 |
    | 414 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 415 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 416 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 417 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 418 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 419 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 420 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 421 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 422 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 423 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    | 424 | root | localhost | test | Query   |    4 | Sending data | select * from user2 where qq = '6518617c3a54d10028287abec8db13d2' and qq2 = '694125' |
    +-----+------+-----------+------+---------+------+--------------+--------------------------------------------------------------------------------------+
    
  • 分析 SQL 执行计划(是否走索引、索引类型)

  • 创建索引或优化 SQL

2. 一段时间慢(持续性的)

  • 分析慢日志
  • 分析 SQL 执行计划
  • 创建索引或优化 SQL