作者:陌北有棵树

数据库是一个只要从事后端开发,就永远离不开的技术,大部分企业选择的数据库都是MySQL,所以需要我们对MySQL有着足够的了解。

而MySQL索引,我们都知道提高性能要加索引,也知道索引的结构是B-Tree,也都可以说出几条加索引的原则,但再深入一点,往往就会词穷,这可能就是知其然而不知其所以然的结果了。这会让我们在实际的开发中,涉及到究竟要给哪个字段加索引,就“拄杖落手心茫然”了。

于是我就问了自己这样几个问题,索引究竟是什么呢?索引是存在哪里的呢?MySQL是怎样通过索引,就优化了性能呢?

本文的研究对象是InnoDB存储引擎,其他存储引擎并没有涉及。

首先想要了解索引究竟存在哪里,如何发挥作用,就不能只研究索引,要结合整个数据库的存储结构,运行流程,来分析索引是如何优化查询过程的。

【一】MySQL的逻辑存储结构

mysql sql索引执行计划 type filtered_MySQL

  • 表空间(tablespace):可以默认为存储引擎逻辑结构的最高层,存放所有的数据。
  • 段(segment):包括数据段、索引段、回滚段,对于段的管理,由存储引擎自身完成。
  • 区(extent):1MB,一个区64个页,InnoDB一次从磁盘申请4~5个区,一个区中有连续的64个页。
  • 页(page)/块(block):每个页固定大小16KB。
  • 行(row):有Compact和Redundant两种行存储格式。

关于一个页的具体组成,可以从下图中有一个更直观的认识:

mysql sql索引执行计划 type filtered_MySQL_02

各个数据页之间通过双向链表连接,每个页内各条行记录用单链表连接。

如果没有任何索引优化,在查询数据时只能先遍历双向链表找到对应的页,加了索引,就可以通过索引的B+树结构定位对应的页。

【二】实验MySQL的数据存储

为了有更清晰的认识,我们在数据库中做一个实验,

在数据库创建member表,结构如下

DROP TABLE IF EXISTS `member`;CREATE TABLE `member` (  `id` int(10) NOT NULL AUTO_INCREMENT,  `name` varchar(255) DEFAULT NULL,  `sex` smallint(2) DEFAULT NULL,  `desc` text,  PRIMARY KEY (`id`),    UNIQUE KEY(`id`,`NAME`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建表后,member.ibd大小是112KB,可见有7个页,然后用《MySQL技术内幕-InnoDB存储引擎》中提到的python小工具,查看表空间中各个页的信息,其中有offset为3和4的两个数据页,此时还没有索引节点。

mysql sql索引执行计划 type filtered_数据_03

中间出现的小插曲,导入下面的数据,发现页信息是这样的,:

INSERT INTO `member` SELECT '1', '李白', '1', REPEAT('且放白鹿青崖间',500);INSERT INTO `member` SELECT '2', '苏轼', '1', REPEAT('暮云收尽溢清寒',500);INSERT INTO `member` SELECT '3', '白居易', '1',REPEAT('我寄人间雪满头',500);INSERT INTO `member` SELECT '4', '姜夔', '1',REPEAT('淮南皓月冷千山',500);

mysql sql索引执行计划 type filtered_MySQL_04

出现这种情况是因为,当行记录的长度超过行记录最大长度时,变长列(variable-length column)会选择外部溢出页(overflow page,一般是Uncompressed BLOB Page)进行存储,于是调整desc列的长度:

INSERT INTO `member` SELECT '1', '李白', '1', REPEAT('且放白鹿青崖间',300);INSERT INTO `member` SELECT '2', '苏轼', '1', REPEAT('暮云收尽溢清寒',300);INSERT INTO `member` SELECT '3', '白居易', '1',REPEAT('我寄人间雪满头'300);INSERT INTO `member` SELECT '4', '姜夔', '1',REPEAT('淮南皓月冷千山',300);

mysql sql索引执行计划 type filtered_MySQL_05

我们知道,InnoDB中,表是根据主键顺序存储的(索引组织表),

mysql sql索引执行计划 type filtered_数据_06

上图是page offset为3的数据构成,蓝框的部分是File Header和Page Header,红框的部分是Infimum Records,后面是Supremum Records。

通过Infimum Records可以找到主键为1的键值,就是绿框的部分,后面的值00 00 00 05就是数据页页号,再后面分别是主键为2和4对应的Pointer。

mysql sql索引执行计划 type filtered_聚集索引_07

【三】聚集索引

InnoDB存储引擎中,表是索引组织表,所以准确来讲,聚集索引不是一种索引类型,而是一种InnoDB中数据的存储方式,在这种存储结构中,同时保存了索引和数据行。

简要聚集索引特点:

  1. 按照主键构建B+树
  2. 叶子节点就是数据页(存了该行记录的所有数据)
  3. 一张表只能有一个聚集索引
【四】索引的使用
  • 不同应用中有区别:
  1. 因为涉及多表连接,所以建索引是有意义的
  2. 通常对时间字段建索引,因为需要按时间维度筛选数据
  3. OLTP:只需通过索引获取表中少部分数据,建索引才有意义
  4. OLAP:

联合索引

  1. 联合索引的第一列可以直接使用这棵B+树
  2. 对于“where col1=? order by col2 ” 的查询可以直接使用这棵B+树,因为已经对col2做了排序处理
  3. 本质是键值数量大于等于2的B+树
  4. 优点:

覆盖索引

  1. 直接从辅助索引查询,不需查询聚集索引
  2. 辅助索引:键+聚集索引键,通俗解释,会根据该索引列建一棵B+树,键是键索引的列,但值指向该条记录对应的聚集索引的页,
  3. 覆盖索引:查找该条记录时,不用从头遍历聚集索引这棵B+树,可以从辅助索引这棵B+树,直接跳到聚集索引那棵B+树的中间
  4. 由于辅助索引不会存储整行数据的信息,所以大小远远小于聚集索引,所以减少了大量的IO操作

一些老生常谈的索引使用建议,在日常使用时,要知其然并知其所以然,才能将索引用好

  1. 一般为where和join中用到的列建立索引
  2. B-Tree索引生效的场景:<,<=,=,>,>=,BETWEEN,IN,以及不以通配符开始的like
  3. 索引字段尽量是简单数据类型
  4. 尽量不要让索引值默认为NULL
  5. 遵守“最左前缀”原则
  6. order by也要遵守“最左前缀”原则
【五】几种类型索引的常用操作:
  • 普通索引
# 创建索引CREATE INDEX idx_name ON member(name)# 修改表结构ALTER TABLE member ADD INDEX idx_name (name)#创建表直接指定CREATE TABLE mytable(  id INT NOT NULL,  username VARCHAR(16) NOT NULL,  INDEX idx_name (username(2)) )# 删除索引DROP INDEX idx_name ON member;
  • 唯一索引:唯一索引与普通索引的不同点在于,普通索引列允许重复值,唯一索引列值必须唯一,但允许NULL值。
# 创建索引CREATE UNIQUE INDEX idx_name ON member(name)# 修改表结构ALTER TABLE member ADD UNIQUE INDEX idx_name (name)#创建表直接指定CREATE TABLE mytable(  id INT NOT NULL,  username VARCHAR(16) NOT NULL,  UNIQUE INDEX idx_name (username(2)) )
  • 主键索引:是特殊的唯一索引,不允许NULL值,一个表中只有一个。
# 一般在建表时指定CREATE TABLE mytable(  id INT NOT NULL,  username VARCHAR(16) NOT NULL,PRIMARY KEY(id))
  • 组合索引
ALTER TABLE member ADD INDEX idx_name_sex_desc (name,sex,`desc`(10));