使我日渐消瘦的MySQL索引

本文深入探讨了数据库索引的工作原理,分析了不同类型的索引及其优缺点,提供了索引优化的实用技巧,包括如何选择合适的索引字段,避免索引失效的情况,以及如何利用覆盖索引来减少查询成本。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

若不是生活所迫,我怎会把自己弄得一身才华。

数据结构索引之杀死攻略(一)

mysql 索引

0 那些年我们听过的索引优化手段

  • 不用 select *,写明具体查询字段
  • 避免在 where 索引字段中使用  != 或 <> 操作符
  • 模糊匹配 like %_%,like %_ 可能用不到索引
  • 索引并非越多越好
  • ......

为了成为一只知其然知其所以然的闲狗,我开始了下面的探索之路。

1 索引可以大幅提升查询效率?

先来个小测试,我先挖来一张有 100 0000 条记录的用户表 user

这张表目前只在 id 上有一个主键索引,其他字段都没有索引。下面在一个没有索引的字段 account_name 做一个查询操作

给这个字段创建一个普通索引,耗时 1.960000s

ALTER TABLE t_user ADD INDEX idx_nick_name(nick_name);

然后再执行相同的查询操作,神速啊 !

2 索引到底是个啥呢?

索引(indexing):是一种数据结构,是把一个个关键码与它对应的数据记录位置相关联的过程,用来协助快速查询,更新数据库表中数据。

数据结构:计算机存储,组织数据的一种方式。直白地理解,就是研究数据的逻辑结构和存储结构。

索引的出现其实就是为了提高数据查询的效率,就像书的目录一样。一本 500 页的书,如果你想快速找到其中的某一个知识点,在不借助目录的情况下,估计得找一会儿。同样,对于数据库的表而言,索引其实就是它的“目录”。

既然索引是一种数据结构,那它也会以一定的方式来存储自己的数据,并且存储在磁盘上。(索引的缺点)

好比书籍的目录也占用了纸张。

查看表的对象信息,会发现索引也占用大量的存储空间,所以不适合放在内存。

原来没有索引的时候,检索数据时需要遍历整张表的数据,根据每条数据在磁盘存储的顺序,一行一行去对比。

自从有了索引,一切都不一样了。如果在 id 字段建立了索引,id字段的值会被加到索引里,其他字段同理。然后检索的时候,只需要根据 id 的值去检索索引,得到数据的磁盘地址,根据磁盘地址就可以快速地获取到数据。(索引的优点)

3 索引类型

(1)按索引存储结构划分: B Tree  索引、 Hash  索引、 FULLTEXT  全文索引、 R Tree  索引
(2)按应用层次划分:普通索引、唯一索引、主键索引、复合索引
(3)按索引键值类型划分:主键索引、辅助索引(二级索引)
(4)按数据存储和索引键值逻辑关系划分:聚集索引(聚簇索引)、非聚集索引(非聚簇索引)

3.1 Normal 普通索引

基本的索引类型,没有唯一性的限制,允许为 NULL 值。

3.2 Unique 唯一索引

索引字段的值必须唯一,但允许有一个 NULL 值。

验证:login_name 为唯一索引,设置两个 NIULL 值会报错。

缺点:唯一索引在插入之前要先遍历一遍数据,保证没有重复,所以会导致插入速度变慢。

3.3 主键索引

它是一种特殊的唯一索引,不允许有空值,每个表只能有一个主键。

3.4 复合索引

复合索引指在多个列上建立索引,复合索引可以替代多个单一索引,相比多个单一索引,复合索引开销更小。

索引同时有两个概念叫做窄索引和宽索引,窄索引是指索引列为1-2列的索引,宽索引也就是索引列超过2列的索引,能用窄索引就不用宽索引,因为窄索引往往比组合索引更有效。

复合索引使用注意事项:
如果表已经建立了 (col1 col2) ,就没有必要再单独建立( col1 );如果现在有 (col1) 索引,如果查询需要col1 col2 条件,可以建立 (col1,col2) 复合索引,对于查询有一定提高。

3.5 Full text 全文索引

查询操作在数据量比较少时,可以使用 like 模糊查询,但是对于大量的文本数据检索,效率很低。如果使用全文索引,查询速度会比 like  快很多倍。在  MySQL 5.6  以前的版本,只有  MyISAM 存储引擎支持全文索引,从 MySQL 5.6  开始  MyISAM  和  InnoDB  存储引擎均支持。 和常用的 like  模糊查询不同,全文索引有自己的语法格式,使用 match against 关键字,比如
select * from user where match(name) against('aaa');

全文索引使用注意事项:

(1)全文索引必须在字符串、文本字段上建立;
(2)全文索引字段值必须在最小字符和最大字符之间的才会有效;( innodb 3-84 myisam 4- 84)
(3)全文索引字段值要进行切词处理,按  syntax  字符进行切割,例如  b+aaa ,切分成  和  aaa 全文索引匹配查询,默认使用的是等值匹配,例如 a  匹配  a ,不会匹配  ab,ac 。如果想匹配可以在布尔模式下搜索 a*
select * from user where match(name) against('a*' in boolean mode);

4 MySQL 索引方法(逻辑结构)

4.1 Hash 结构

散列表的检索

Hash 底层通过 Hash 表来实现,通过 <key, value> 存储数据,适用于根据 key 查找 value,但不适用于范围查询,范围查询需要全表扫描。

InnoDB 自适应哈希索引
InnoDB  存储引擎会监控表上各个索引页的查询,当 InnoDB 注意到某些索引值访问非常频繁时,会在内存中基于 B+Tree 索引再创建一个哈希索引, 快速定位访问频繁的索引页。
InnoDB  自适应哈希索引采用的等值查询效率要优于 B+Tree,B+Tree 查询的时间复杂度受树高影响。
show engine innodb status; 
show variables like '%innodb_adaptive%';
#关闭
set global innodb_adaptive_hash_index=0;

4.2 BTREE 结构

猜数字游戏:给定一个1~100的自然数,给你5次机会,你能猜中这个数字吗?你会从多少开始猜?

二分查找

上面猜数字的方法可以利用到二分查找思想,而二分查找通常采用二叉树作为逻辑结构,采用顺序存储结构存储在磁盘。

4.2.1 二叉搜索树(Binary Search Tree)

二叉搜索树

BST的中序遍历序列,必然单调非降。

查找的效率决定于树的高度,若树的高度是h,那么查找过程的时间复杂度就是O(h),等同于二分查找。

极端情况下,时间复杂度会退化到O(n)。如:顺序插入。 BST演示

采用这样的数据结构作为索引的逻辑结构就太恐怖了,pass!

4.2.2 平衡二叉树(AVL Tree - Balance Binary Search Tree)

AVL(平衡二叉搜索)树学习笔记

AVL 演示

特点:左右子树深度差的绝对值不能超过 1。主要通过旋转来维持平衡。

灵魂拷问:用 AVL 作为索引存储数据的逻辑结构可行吗?

先瞅一眼 AVL 的数据存储

每一个节点占用一个磁盘块,存放键值,完整数据所在磁盘块的磁盘地址,子节点的引用。

查找 id = 23 的过程

  • 把 id = 26 的结点从磁盘加载到内存,对比键值,26 > 23
  • 把 id = 19 的结点从磁盘加载到内存,对比键值,19 < 23
  • 把 id = 23 的结点从磁盘加载到内存,对比键值,23 == 23,匹配,通过结点存储的完整数据的磁盘地址,读取对应的数据。

InnoDB 操作磁盘的最小单位是 Page,默认为 16KB(16*1024=16384 B)。即进行一次 I/O 会把 16 KB 的数据从磁盘加载到内存。即每个二叉树结点占用的磁盘大小为 16 KB。16 KB 的结点只存一个键值是不是太浪费了!!!如上图,才 6 条数,查找 id = 23 时就进行了 3 次 I/O 操作。

怎么解决好呢?

难得去一趟磁盘,是不是得多取点数据,即每个结点多存点数据。然后,诞生了 B 树。

4.2.3 多路平衡查找树(Balanced Tree B Tree)

B树学习笔记之B树的查找

动态索引之B树

(1)索引值和  data  数据分布在整棵树结构中
(2)每个节点可以存放多个索引值及对应的 data 数据
(3)树节点中的多个索引值从左到右升序排列

B树演示

4.2.4 加强版多路平衡查找树(B+ Tree)

动态索引之B+树

B+ 树是 B 树的升级版

(1)非叶子节点不存储数据,只存储索引值,这样便于存储更多的索引值。所以索引字段越小越好,这样一次IO操作获取的数据就越多。

假设 1 条记录 1 KB 大小,一个结点 16KB(16*1024=16384 B)

树高为 3 的 B+ 树,即可以存放 2190 2400条记录。

(2)叶子节点包含了所有的索引值和数据

(3)叶子节点用指针连接,提高区间的访问性能

相比 B 树,B+ 树进行范围查找时,只需要查找定位两个节点的索引值,然后利用叶子节点的指针进行遍历即可,不用回到根结点。而 B 树需要遍历范围内所有的节点和数据,显然 B+Tree 效率更高。

5 存储引擎的索引实现

5.1 基本概念

官方文档

存储引擎(表类型,表处理器):负责 MySQL 中数据的存储,提供读写接口。由于数据的存储方式,存储格式和使用方式不同,就会对应产生不同的存储引擎。5.5 版本后默认每张表的存储引擎为 InnoDB。

(1)数据存储方式:有的数据比较稳定,然后存在磁盘;有的数据经常要被访问,需要放在内存。

(2)数据存储格式:经常访问的数据,用 B+ 树的结构去存储;不常访问的数据进行压缩处理,节省空间;有的数据想要看起来比较直观,便于不同系统的交互,就采用 csv 的格式去存储。

(3)数据使用方式不同:有的数据需要有较高的一致性,就需要存储引擎支持事务;有的不需要事务,但需要更高的访问速度,这就需要存储引擎支持 B+ 树。

下面探索一下数据在磁盘以什么样的形式体现

  • 创建两张除了存储引擎不同,其它字段都相同的两张表
CREATE TABLE `user_innodb` (
  `Id` int NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户Id',
  `account_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `phone_num` varchar(16) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `nick_name` varchar(32) DEFAULT NULL COMMENT '昵称',
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_myisam` (
  `Id` int NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户Id',
  `account_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `phone_num` varchar(16) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `nick_name` varchar(32) DEFAULT NULL COMMENT '昵称',
  PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

查看 mysql 数据文件存放目录

SHOW VARIABLES LIKE 'datadir';

进入相应的目录,找到以数据库名命名的目录,我的是 test

然后发现,每张表都是以文件的形式存在磁盘。innodb的表有一个文件,而myisam的表有两个文件。

MySQL 8.0 以前,每张表都有一个 frm 文件,用来存放表的元数据(meta),包括表结构的定义等。MySQL 8.0 以后,元数据都存在系统表空间里。

5.2 MyISAM

MyISAM 是 MySQL 自带的存储引擎,但是 MyISAM 没有 crash-safe 的能力,不支持事务。

MyISAM 数据表的索引文件和数据文件是分开存储的,MyISAM 的主键索引与辅助索引没有区别,都存在 .MYI 文件里。

5.3 InnoDB

InnoDB 是另一个公司以插件形式引入 MySQL 的,支持事务,有crash-safe 能力,,可以保证即使数据库发生异常重启,之前提交的记录都不会丢失。

5.3.1 索引模型

1)主键索引

聚集索引文档

完整数据存放在叶子结点,数据与索引在同一棵树,同一个文件里。完整数据的物理存放顺序与主键一致,这样的索引也称为聚簇索引。聚簇索引决定了数据物理存放顺序。如果一张表没有主键索引,InnoDB 内部会优先去查找没有空值的唯一索引,如果也没有InnoDB 内部会为每一行数据都生成一个 rowID,rowID 构建的索引作为聚簇索引,决定数据的物理顺序。

If the table has no PRIMARY KEY or suitable UNIQUE index, InnoDB internally generates a hidden clustered index named GEN_CLUST_INDEX on a synthetic column containing row ID values. The rows are ordered by the ID that InnoDB assigns to the rows in such a table. The row ID is a 6-byte field that increases monotonically as new rows are inserted. Thus, the rows ordered by the row ID are physically in insertion orde.       

查询下 rowid,是真的有诶!

   

2) 辅助索引(普通索引)

完整数据只会存放在主键索引的叶子结点,辅助索引的叶子结点存放对应的字段值和主键值。

InnoDB 的辅助索引会比主键索引多扫描一棵 B+ 树

5.4 索引维护 

B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护,进行一定的分裂或合并,会带来一定的性能问题。

为什么有点规范里要求建表语句里一定要有自增主键?

1)性能层面

自增主键是指自增列上定义的主键,

 NOT NULL PRIMARY KEY AUTO_INCREMENT

插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。

即每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。

而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。在分布式系统里,不采用 UUID 作为分布式 ID,也是由于 UUID 是无序的,会引起索引节点的频繁变动,影响性能。 

2)存储层面

假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?

由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。显然,主键长度越小,辅助索引的叶子节点就越小,辅助索引占用的空间也就越小。

MySQL官方也有明确的建议主键要尽量越短越好

All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index.If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.

6 索引分析与优化

6.1 EXPLAIN

MySQL 提供了一个 EXPLAIN 命令,用来对 SELECT 语句进行分析,并输出 SELECT 执行的详细信息,供开发人员有针对性的优化。
CREATE TABLE `t_resume` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `address` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `phone` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_address` (`address`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

EXPLAIN SELECT * from t_resume WHERE address = "3";

select_type

  • SIMPLE : 表示查询语句不包含子查询或 union
  • PRIMARY:表示此查询是最外层的查询
  • UNION:表示此查询是UNION的第二个或后续的查询
  • DEPENDENT UNIONUNION中的第二个或后续的查询语句,使用了外面查询结果
  • UNION RESULTUNION的结果
  • SUBQUERYSELECT子查询语句
  • DEPENDENT SUBQUERYSELECT子查询语句依赖外层查询的结果

type

  • ALL:表示全表扫描,性能最差
  • index:表示基于索引的全表扫描,先扫描索引再扫描全表数据
  • range:表示使用索引范围查询。使用>>=<<=in等等
  • ref:表示使用非唯一索引进行单值查询
  • eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果
  • const:表示使用主键或唯一索引做等值查询,常量查询
  • NULL:表示不用访问表,速度最快

possible_keys

表示查询时可能能够使用到的索引

key

表示查询时真正使用到的索引

key_len

表示查询使用了索引的字节数量,可以判断是否全部使用了组合索引。

rows

字符串类型

  • 字符串长度跟字符集有关:latin1=1gbk=2utf8=3utf8mb4=4
  • char(n)n*字符集长度
  • varchar(n)n * 字符集长度 + 2字节
数值类型
  • TINYINT1个字节
  • SMALLINT2个字节
  • MEDIUMINT3个字节
  • INTFLOAT4个字节
  • BIGINTDOUBLE8个字节
时间类型
  • DATE3个字节
  • TIMESTAMP4个字节
  • DATETIME8个字节
字段属性
  • NULL属性占用1个字节,如果一个字段设置了NOT NULL,则没有此项

Extra

  • Using where 表示查询需要通过索引回表查询数据
  • Using index 表示查询需要通过索引,索引就可以满足所需数据
  • Using fifilesort 表示查询出来的结果需要额外排序,数据量小在内存,大的话在磁盘,因此有Using fifilesort 建议优化
  • Using temprorary 查询使用到了临时表,一般出现于去重、分组等操作

6.2 列的离散度

离散度公式:count(distinct(column_name)):count(*)  某列不重复值的个数/总数。

实战一下

gender 的离散度要小于 phone

(1)在 gender 字段创建索引

将近扫描了一半的数据。

检索数据时,因为重复值多,所以扫描的行数也多。如果在 B+Tree 里面的重复值太多,MySQL 的优化器发现走索引跟使用全表扫描差不多的时候,就算建了索引,也不一定会走索引。

(2)在 phone 字段创建索引

只需扫描一行。

闲狗言:

优先选择离散度高的字段建立索引

6.3 回表与覆盖索引

6.3.1 回表


mysql> create table T (
ID int primary key,
k int NOT NULL DEFAULT 0, 
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;

insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

思考一下:执行 select * from T where k between 3 and 5,需要执行几次树的搜索操作,会扫描多少行?

执行流程:

  • 在 k 索引树上找到 k=3 的记录,取得 ID = 300;
  • 再到 ID 索引树查到 ID=300 对应的 R3;
  • 在 k 索引树取下一个值 k=5,取得 ID=500;
  • 再回到 ID 索引树查到 ID=500 对应的 R4;
  • 在 k 索引树取下一个值 k=6,不满足条件,循环结束。

在这个过程中,回到主键索引树搜索的过程,称为回表。

InnoDB 的完整数据都在存放在主键索引的叶子节点的,查询主键以外的字段时,需要先查询辅助索引,然后再查询主键索引获取完整数据,比直接查询主键索引多扫描了一棵 B+ 树。

如何避免回表?

6.3.2 覆盖索引

如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,称为覆盖索引。实现索引覆盖最常见的方法就是将被查询的字段,建立到组合索引。

实战一下

(1) 创建 name 和 phone 的组合索引
ALTER TABLE t_user ADD INDEX idx_name_phone(name,phone);

当 select name,phone 时,不需要主键索引里的完整数据,直接查 name 和 phone 的辅助索引就可以获取到 name 和 phone 的值,不用回表。

当 extra 出现 Using index 则表示用到了覆盖索引

(2)

explain select * from t_user where name="tanjinhai";

(3)

explain select name,phone from t_user where name="屿昔" and phone="15505688900";

(4)

explain select phone from t_user where phone="17665453887";

要查的 phone 虽然在辅助索引里,但是违背了联合索引最左匹配原则。MySQL 优化器判断了下回表的代价和直接在辅助索引中过滤数据的代价,还是选择了不回表。

6.4 索引重建优化

对于上面例子中的 InnoDB 表 T,索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。

如果要重建索引 k,你的两个 SQL 语句可以这么写:

alter table T drop index k;
alter table T add index(k);

如果你要重建主键索引,也可以这么写:

alter table T drop primary key;
alter table T add primary key(id);

通过两个 alter 语句重建索引 k,以及通过两个 alter 语句重建主键索引是否合理?

重建索引 k 的做法是合理的,可以达到省空间的目的。但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,可以用这个语句代替 : alter table T engine=InnoDB。

6.5 最左匹配原则

复合索引使用时遵循最左前缀原则,即查询时从最左边的列开始查,就会使用到索引,如果从索引的第二列开始查找,索引将失效。
如果既有联合查询,又有基于 a、b 各自的查询。当查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候不得不维护另外一个索引,也就是说需要同时维护 (a,b)、(b) 这两个索引。
实战一下
(1)创建组合索引 name 和 phone
ALTER TABLE t_user ADD INDEX idx_name_phone(name,phone);

(2)

explain select * from t_user where name="tanjinhai" and phone="15920690159";

(3)

explain select * from t_user where phone="15920690159" and name="tanjinhai";

居然还能用到索引,这不是违背了联合索引最左匹配原则吗?我跟你港,其实 MySQL 内部有一个查询优化器 Optimizer, 它会自动帮我们调整下查询条件的顺序。

(4)

explain select * from t_user where name="tanjinhai";

(5)

explain select * from t_user where phone="15920690159";

6.5 LIKE 查询

(1)索引失效

EXPLAIN SELECT * from t_resume WHERE phone like "%176%";

(2)索引失效

EXPLAIN SELECT * from t_resume WHERE phone like "%176";

(3)索引有效

EXPLAIN SELECT * from t_resume WHERE phone like "176%";

查找到第一个符合条件的记录后,通过指针向后遍历,直到不满足条件为止。

6.6 NULL 查询

MySQL 来说, NULL 是一个特殊的值,从概念上讲, NULL  意味着 一个未知值 ,它的处理方式与其他值有些不同。比如:不能使用 = < 这样的运算符,对  NULL  做算术运算的结果都是  NULL count  时不会包括 NULL  行等,而且  NULL  是占用存储空间的。
select length(NULL), length(''), Length(' ');

  • 空值 (NULL)  的长度是 NULL,不确定占用了多少存储空间,但是占用了存储空间
  • 空字符串 ('') 的长度是 0,不占用存储空间
  • 含一个空格的字符串 (' ') 长度为 1,占用了存储空间
灵魂拷问:如果MySQL表的某一列含有NULL值,那么包含该列的索引是否有效?
真相就是可以使用到索引。
闲狗言:

指定列为 NOT NULL,除非你想存储 NULL。在 MySQL 中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。应该使用-1,0 等特殊值去替代空值,如果是datetime类型,也可以设置系统当前时间或某个固定的特殊值,例如 '1970-01-01 00:00:00'

If an indexed column cannot contain any NULL values, declare it as NOT NULL when you create the table. The optimizer can better determine which index is most effective to use for a query, when it knows whether each column contains NULL values.

官网文档

6.7 排序

DBA 小吕在入职新公司的时候,就发现自己接手维护的库里面,有这么一个表,表结构定义类似这样的:

CREATE TABLE `geek` (
  `a` int(11) NOT NULL,
  `b` int(11) NOT NULL,
  `c` int(11) NOT NULL,
  `d` int(11) NOT NULL,
  PRIMARY KEY (`a`,`b`),
  KEY `c` (`c`),
  KEY `ca` (`c`,`a`),
  KEY `cb` (`c`,`b`)
) ENGINE=InnoDB;

主键包含了 a、b 这两个字段,那意味着单独在字段 c 上创建一个索引,就已经包含了三个字段了呀,为什么要创建“ca”“cb”这两个索引?

同事告诉他,是因为他们的业务里面有这样的两种语句:


select * from geek where c=N order by a limit 1;
select * from geek where c=N order by b limit 1;

为了这两个查询模式,这两个索引是否都是必须的?为什么呢?

解析

主键 a,b 的聚簇索引组织顺序相当于 order by a,b 也就是先按 a 排序,再按 b 排序,c 无序 

索引 ca 的组织是先按 c 排序,再按 a 排序,同时记录主键 b 

索引 cb 的组织是先按 c 排序,再按 b 排序,同时记录主键 a 

对于 select * from geek where c=N order by a limit 1; 这条 sql 走 ca,cb 索引都能定位到满足 c=N 的记录的主键,而且主键的聚簇索引本身就是按 order by a,b 排序,无序重新排序,所以ca索引可以移除。
select * from geek where c=N order by b limit 1;这条 sql 如果只有 c 单个字段的索引,定位 c=N的记录可以走索引,但是 order by b 的顺序与主键顺序不一致,需要通过 cb 索引重新排序

6.8 条件字段函数操作

有一个交易系统,其中交易记录表 tradelog 包含交易流水号(tradeid)、交易员 id(operator)、交易时间(t_modified)等字段。


mysql> CREATE TABLE `tradelog` (
  `id` int(11) NOT NULL,
  `tradeid` varchar(32) DEFAULT NULL,
  `operator` int(11) DEFAULT NULL,
  `t_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `tradeid` (`tradeid`),
  KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

假设,现在已经记录了从 2016 年初到 2018 年底的所有数据,运营部门有一个需求是,要统计发生在所有年份中 7 月份的交易记录总数。


mysql> select count(*) from tradelog where month(t_modified)=7;

结果发现结果很久才返回。为什么条件是 where t_modified='2018-7-1’的时候可以用上索引,而改成 where month(t_modified)=7 的时候就不行了?

查询条件是 where t_modified='2018-7-1’的话,引擎就会按照上面绿色箭头的路线,快速定位到 t_modified='2018-7-1’需要的结果。 

但是,如果计算 month() 函数的话,你会看到传入 7 的时候,在树的第一层就不知道该怎么办了。

对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。 

需要把 SQL 语句改成基于字段本身的范围查询。按照下面这个写法,优化器就能按照我们预期的,用上 t_modified 索引的快速定位能力了。


mysql> select count(*) from tradelog where
    -> (t_modified >= '2016-7-1' and t_modified<'2016-8-1') or
    -> (t_modified >= '2017-7-1' and t_modified<'2017-8-1') or 
    -> (t_modified >= '2018-7-1' and t_modified<'2018-8-1');

6.8 索引并非越多越好

  • 大部分情况下,索引能大幅度提高查询效率,但是数据的变更(增删改)都需要维护索引,更多的索引意味着更多的维护成本,更多的索引也意味着需要更多的空间。好比,一本 100 页的书,却有 50 页目录?
  • 过小的表,建立索引后查询效率可能更低。好比,读个 2 页的宣传手册,你还要先去查目录?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值