组合索引

Posted zqlmianshi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了组合索引相关的知识,希望对你有一定的参考价值。

MySQL的组合索引是指将多个列的索引组合在一起,以提高查询效率。组合索引的创建可以通过CREATE INDEX语句实现。

组合索引的优点是可以提高查询效率,减少磁盘的I/O操作,降低系统的负载。同时,组合索引还可以减少索引的数量,降低索引的存储空间和维护成本。

在使用组合索引时,需要注意以下几点:

  1. 选择合适的列:选择具有较高选择性的列作为组合索引的前缀列,可以减少索引的数量,提高查询效率。

  2. 列的顺序:组合索引的列的顺序也很重要,应该将选择性高的列放在前面,以便优先过滤数据。

  3. 索引长度:组合索引的长度也需要合理设置,过长的索引可能会降低查询效率,同时也会占用更多的存储空间。

  4. 避免过多索引:过多的组合索引也会降低系统的性能,因此应该避免创建过多的组合索引。

下面是一个创建组合索引的示例:

CREATE INDEX idx_name_age ON student(name, age);

这里创建了一个组合索引,包括name和age两列。这个组合索引可以加快根据name和age进行查询的速度。

 

当创建(col1,col2,col3)联合索引时,相当于创建了(col)单列索引,(clo1,clo2)联合索引以及(col1,col2,col3)联合索引想要索引生效,只能使用col1和col1,col2和col1,col2,col3三种组合;当然,col1,col3组合也可以,但实际上只用到了col1的索引,col3并没有用到!

第22期:索引设计(组合索引适用场景)

建立在多个列上的索引即组合索引(联合索引),适用在多个列必须一起使用或者是从左到右方向部分连续列一起使用的业务场景。

组合索引和单值索引类似,索引上的每个键值按照一定的大小排序。比如针对三个字段的组合索引有以下组合:

  1. (f1, f2, f3)
  2. (f1, f2, f3 desc)
  3. (f1, f2 desc, f3)
  4. (f1 desc, f2, f3)
  5. (f1 desc, f2 desc, f3 desc)
  6. ...

今天讨论的组合索引只基于默认排序方式,也就是 (f1,f2,f3),等价于 (f1 asc, f2 asc, f3 asc)。

组合索引的语法:

alter table t1 add key idx_multi(f1 [asc/desc],f2 [asc/desc],f3 [asc/desc]) [using btree/using hash]

MySQL 里,组合索引最大支持 16 个列。可以基于 B+ 树,也可以基于哈希,这篇主要讨论基于 B 树,并且索引顺序默认升序,基于 HASH 只有一种用法,就是所有列的都必须等值过滤【仅限于下面 SQL 3】。

使用组合索引的必备条件为:列 f1 必须存在于 SQL 语句过滤条件中!也就是说组合索引的第一个列(最左列)在过滤条件中必须存在,而且最好是等值过滤。

考虑以下 15 条 SQL 语句, 分别对表 t1 字段 f1、f2、f3 有不同的组合过滤,并且都包含了列 f1,也就是说满足了组合索引使用的必备条件。

# SQL 1
select * from t1 where f1 = 1;

# SQL 2
select * from t1 where f1 = 1 and f2 = 1;

# SQL 3
select * from t1 where f1 = 1 and f2 = 1 and f3 = 1 ;

# SQL 4
select f1,f2 from t1 where 1 order by f1,f2;

# SQL 5
select f1,f2,f3 from t1 where 1 order by f1,f2,f3;

# SQL 6
select f1,f2,count(*) from t1 group by f1,f2;

# SQL 7
select f1,f2,f3,count(*) from t1 group by f1,f2,f3;

# SQL 8
select * from t1 where f1 = 10 and f2 = 5 and f3 > 10

# SQL 9
select  * from t1 where f1 = 10 and f2 > 5;

# SQL 10
select * from t1 where f1 < 10;

# SQL 11
select * from t1 where f1 < 10 and f2 > 5;

# SQL 12
select * from t1 where f1 < 10 and f2 > 5 and f3 < 10;

# SQL 13
select * from t1 where f1 < 10 and f2 = 5 and f3 < 10;

# SQL 14
select * from t1 where f1 < 10 and f2 = 5 and f3 = 10;

# SQL 15
select * from t1 where f1 = 1 and f3 = 1;

SQL 1、SQL 2、 SQL 3 三条 SQL 分别基于组合索引 idx_multi 过滤后回表;其中 SQL 3 是组合索引中每个字段都能过滤到的最完美查询。来看看 SQL 3的执行计划:

(127.0.0.1:3400)|(ytt)>explain  select * from t1 where f1 = 1 and f2 = 1 and f3 = 1\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_multi
          key: idx_multi
      key_len: 15
          ref: const,const,const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

SQL 3 的执行计划趋近完美,当然最完美的是索引 idx_multi 是主键或者唯一索引。比如下面对表 t3 查询,t3 的索引 udx_multi 是一个唯一索引。

(127.0.0.1:3400)|(ytt)>explain  select * from t3 where f1 = 9 and f2 = 52 and f3 = 35\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t3
   partitions: NULL
         type: const
possible_keys: udx_multi
          key: udx_multi
      key_len: 15
          ref: const,const,const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

SQL 4、SQL 5、SQL 6、SQL 7 四条 SQL 走覆盖索引扫描,不用回表,利用索引 idx_multi 升序输出结果。随便打印下其中 SQL 7 的执行计划看看:

127.0.0.1:3400)|(ytt)>explain
    -> select f1,f2,f3,count(*) from t1 group by f1,f2,f3\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: index
possible_keys: idx_multi
          key: idx_multi
      key_len: 15
          ref: NULL
         rows: 32194
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

结果 type 列 为 index,直接索引返回结果。

其实这里遇到一个非常常见的疑问:SQL 1 过滤条件里只有字段 f1, SQL 2 过滤字段里只有 f1,f2,针对这两种场景可否应该建立如下单值索引让查询运行的更加高效?

alter table t1 add key idx_f1(f1);

alter table t1 add key idx_multi_sub(f1,f2);

其实针对列 f1 单独建立索引没有必要,因为 f1 为索引 idx_multi 的第一个字段,查询时如果仅仅包含字段 f1,那 MySQL 也仅仅只使用 f1 的索引数据,不会让索引 idx_multi 的所有列都被使用;

同理,基于字段 (f1,f2) 再建立一个组合索引也没有必要,(f1, f2) 可以看作以这样的方式存在与组合索引中:((f1, f2), f3)

那分别看下 SQL 2 用索引 idx_multi 和 idx_multi_sub 的执行计划来证实以上的说法:

(127.0.0.1:3400)|(ytt)>explain select * from t1 where f1 = 1 and f2 = 1\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_multi,idx_f1,idx_multi_sub
          key: idx_multi
      key_len: 10
          ref: const,const
         rows: 3
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

(127.0.0.1:3400)|(ytt)>explain select * from t1 force index (idx_multi_sub) where f1 = 1 and f2 = 1\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_multi_sub
          key: idx_multi_sub
      key_len: 10
          ref: const,const
         rows: 3
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

从以上结果来看,执行计划没有任何不同,所以如果查询过滤条件为组合索引里第一个列或者是包含第一个列的连续前缀列,不需要单独再建立部分字段的组合索引,保留原来组合索引即可。

(127.0.0.1:3400)|(ytt)>alter table t1 drop key idx_multi_sub, drop key idx_f1;
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

那接下来看看 SQL 8。SQL 8 里前两列也是连续的,不同的是后面列 f3 的过滤条件是一个范围,那看下 SQL 8 执行计划:

(127.0.0.1:3400)|(ytt)>explain select * from t1 where f1 = 10 and f2 = 5 and f3 > 10\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: idx_multi
          key: idx_multi
      key_len: 15
          ref: NULL
         rows: 154
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

SQL 8 的执行计划显示:由于列f3是过滤条件是一个范围,并不影响使用组合索引,因为前两列 f1,f2 是连续的。

SQL 9 和 SQL 8 一样,因为列 f1 等值过滤,之后是 f2 范围过滤。

SQL 10 只用到了列 f1, 这点类似于单值索引 (f1)。

SQL 11、 SQL 12 这两条 SQL 和 SQL 10 是类似的,虽然过滤字段顺序和索引字段顺序一样,但是由于第一个列是一个范围,只能用到组合索引的第一列。

SQL 13 、SQL 14 有点不一样,虽然列f1是范围过滤,但是 SQL 13 里 f2 是等值过滤,SQL 14 里 f2,f3 是等值过滤。

所以如果 SQL 13、SQL 14 也运行较为频繁的话,可以另外加一个组合索引 (f2, f3)。

(127.0.0.1:3400)|(ytt)>alter table t1 add key idx_multi2(f2,f3);
Query OK, 0 rows affected (0.38 sec)
Records: 0  Duplicates: 0  Warnings: 0

看下 SQL 13 和 SQL 14 的执行计划:

(127.0.0.1:3400)|(ytt)>explain  select * from t1 where f1 < 10 and f2 = 5 and f3 < 10\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: idx_multi,idx_multi2
          key: idx_multi2
      key_len: 10
          ref: NULL
         rows: 174
     filtered: 7.99
        Extra: Using index condition; Using where
1 row in set, 1 warning (0.00 sec)

(127.0.0.1:3400)|(ytt)>explain  select * from t1 where f1 < 10 and f2 = 5 and f3 = 10\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_multi,idx_multi2
          key: idx_multi2
      key_len: 10
          ref: const,const
         rows: 14
     filtered: 7.99
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

(127.0.0.1:3400)|(ytt)>

那此时上面两条 SQL 又回到了组合索引的经典使用场景。

再看下最后一条 SQL,SQL 15。SQL 15 过滤条件只有 (f1=1 and f3=1),也就是不匹配组合索引的过滤连续性特征,但是由于列 f1 是等值过滤,所以也可以使用组合索引 idx_multi, 看下执行计划:

(127.0.0.1:3400)|(ytt)>explain  select * from t1 where f1 = 1 and f3 = 1\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_multi
          key: idx_multi
      key_len: 5
          ref: const
         rows: 312
     filtered: 10.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

从执行计划结果看出,也只用到了列 f1,因为 f2 不存在。所以这条 SQL 如果运行也很频繁,可以再次建立一个新的组合索引 (f1, f3)。

(127.0.0.1:3400)|(ytt)>alter table t1 add key idx_multi3(f1,f3);
Query OK, 0 rows affected (0.36 sec)
Records: 0  Duplicates: 0  Warnings: 0

再看下执行计划:

(127.0.0.1:3400)|(ytt)>explain  select * from t1 where f1 = 1 and f3 = 1\\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_multi,idx_multi3
          key: idx_multi3
      key_len: 10
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

那此时又回到了组合索引的过滤连续性特征场景。

文中示例用到的表结构:

(127.0.0.1:3400)|(ytt)>show create table t1\\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `id` int NOT NULL,
  `f1` int DEFAULT NULL,
  `f2` int DEFAULT NULL,
  `f3` int DEFAULT NULL,
  `f4` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_multi` (`f1`,`f2`,`f3`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

(127.0.0.1:3400)|(ytt)>show create table t3\\G
*************************** 1. row ***************************
       Table: t3
Create Table: CREATE TABLE `t3` (
  `id` int NOT NULL,
  `f1` int DEFAULT NULL,
  `f2` int DEFAULT NULL,
  `f3` int DEFAULT NULL,
  `f4` int DEFAULT NULL,
  UNIQUE KEY `udx_multi` (`f1`,`f2`,`f3`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

这里讨论了组合索引的种种使用点,日常业务中,如果一个列已经在组合索引,并且在第一位,应当避免建立额外的单个索引。

以上是关于组合索引的主要内容,如果未能解决你的问题,请参考以下文章

第22期:索引设计(组合索引适用场景)

mysql组合索引按顺序吗

组合索引里面的索引单独使用有效吗?

第12天SQL进阶-索引的组合索引(SQL 小虚竹)

oracle 优化之组合索引

索引合并和组合索引的比较 (转)