索引(Index)是帮助MySQL高效获取数据的数据结构——索引是一种数据结构,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。一般来说索引本身占用内存空间也很大,不可能全部存储在内存中,因此索引往往以文件形式存储在硬盘上,占据一定的物理空间。
更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。可以简单理解为排好序的快速查找数据结构",即索引 = 排序 + 查找,建立索引之后,查找起来会变快。
假设有如下查询语句经常被使用:
SELECT * FROM user WHERE NAME = 'xxx';
可以通过建立索引,使得杂乱的数据变得有序、易查询。
CREATE INDEX idx_user_name ON USER (name); # idx_user_name:约定俗成的命名规范,idx表示这是一个索引,user表示在user表建立索引,name是建立索引的字段
复合索引就是给多个字段建立索引:
CREATE INDEX idx_user_name ON USER (name,email);
索引的优点:
索引的缺点:
主键索引:数据列不允许重复,不允许为NULL,一个表只能有一个主键。
ALTER TABLE table_name ADD PRIMARY KEY(column);
创建主键索引唯一索引:数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
ALTER TABLE table_name ADD UNIQUE (column);
创建唯一索引ALTER TABLE table_name ADD UNIQUE (column1,column2);
创建唯一复合索引普通索引:基本的索引类型,没有唯一性的限制,即一个索引只包含单个列,允许为NULL值。一个表可以有多个单列索引;建议一张表索引不要超过5个。
ALTER TABLE table_name ADD INDEX index_name (column);
创建普通索引ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);
创建复合索引全文索引: 是目前搜索引擎使用的一种关键技术。
ALTER TABLE table_name ADD FULLTEXT (column);
创建全文索引CREATE [UNIQUE] INDEX indexName ON mytable(columnname(length)); # 或者 ALTER mytable ADD [UNIQUE] INDEX [indexName] ON(columnname(length)); # 如果是CHAR和VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定length。
DROP INDEX [indexName] ON mytable;
\G
表示将查询到的横向表格纵向输出,方便阅读
SHOW INDEX FROM table_name\G
索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引。
如下图的一颗 B 树, 浅蓝色的块我们称之为一个磁盘块, 可以看到每个磁盘块包含几个数据项(深蓝色所示) 和指针(黄色所示)。比如磁盘块 1 包含数据项 17 和 35, 包含指针 P1、 P2、 P3。P1 表示小于 17 的磁盘块, P2 表示在 17 和 35 之间的磁盘块, P3 表示大于 35 的磁盘块,以此类推。
如果要查找数据项 29, 那么首先会把磁盘块 1 由磁盘加载到内存, 此时发生一次 IO, 在内存中用二分查找确定 29在 17 和 35 之间, 锁定磁盘块 1 的 P2 指针, 内存时间因为非常短(相比磁盘的 IO) 可以忽略不计
通过磁盘块 1的 P2 指针的磁盘地址把磁盘块 3 由磁盘加载到内存, 发生第二次 IO, 29 在 26 和 30 之间, 锁定磁盘块 3 的 P2 指针
通过指针加载磁盘块 8 到内存, 发生第三次 IO, 同时内存中做二分查找找到 29, 结束查询, 总计三次 IO。
B-树的关键字(数据项)和记录是放在一起的; B+树的非叶子节点中只有关键字和指向下一个节点的索引, 记录只放在叶子节点中。
在 B 树中, 越靠近根节点的记录查找时间越快, 只要找到关键字即可确定记录的存在; 而 B+ 树中每个记录的查找时间基本是一样的, 都需要从根节点走到叶子节点, 而且在叶子节点中还要再比较关键字。从这个角度看 B 树的性能好像要比 B+ 树好, 而在实际应用中却是 B+ 树的性能要好些。 因为 B+ 树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比 B 树多, 树高比 B 树小, 这样带来的好处是减少磁盘访问次数。
尽管 B+ 树找到一个记录所需的比较次数要比 B 树多, 但是一次磁盘访问的时间相当于成百上千次内存比较的时间, 因此实际中B+ 树的性能可能还会好些, 而且 B+树的叶子节点使用指针连接在一起, 方便顺序遍历(范围搜索), 这也是很多数据库和文件系统使用 B+树的缘故。
下图是一个B+树的示例:
B+tree性质:
B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加
B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样
叶子节点本身依关键字的大小自小而大顺序链接。左边结尾数据都会保存右边节点开始数据的指针。
总的来说,B+树查找的效率要比B树更高、更稳定。由于非叶子节点并不是最终指向文件内容的节点, 而只是叶子节点中关键字的索引, 所以任何关键字的查找必须走一条从根节点到叶子节点的路。 所有关键字查询的路径长度相同,使得每一个数据的查询效率相当。
类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash冲突(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储(拉链法),如图:
索引的创建最好符合以下原则:
a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。另外,索引的选择性是指索引列中不同值的数目与表中记录数的比。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99。一个索引的选择性越接近于1,这个索引的效率就越高。
聚簇索引:将数据与索引存放到一起,找到索引就可以直接找到数据。
非聚簇索引:将数据存储与索引分开,索引结构的叶子节点存储的是对应数据的地址,并不能直接获取到数据。
回表:举个例子,非聚簇索引中找到了叶子节点,获取叶子节点存储的地址,根据这个地址再去查找一次,这个过程叫做回表。
下图为一个聚簇索引:
一个表建立后,如果有主键,主键就是默认的聚簇索引。如果表中没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。
由于存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引(比如MyISAM不支持聚簇索引),MyISAM使用非聚簇索引,如下图:
需要注意:
聚簇索引不是一种单独的数据类型,而是一种数据存储方式。
InnoDB的聚簇索引实际上在同一结构中保存了B+Tree索引和数据,当表有聚簇索引时,它的数据行实际上存放在索引的叶子节点中。
因为无法同时把数据行放在两个不同的地方,所以一个表只能有一个聚簇索引。
InnoDB中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引。辅助索引叶子节点存储的不再是行的物理位置,而是主键值
InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14" 这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。
若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)
聚簇索引优点:
聚簇索引缺点:
下图展示了使用uuid主键和自增主键的区别:
非聚簇索引不一定进行回表查询。
这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行SELECT age FROM employee WHERE age < 20
的查询时,在索引的叶子节点上,已经包含了age信息,就不会再次进行回表查询。
覆盖索引:
如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在
select
后只写必要的查询字段,以增加索引覆盖的几率。这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。
MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
如图,我们在三个列上建立了一个联合索引:整形id、字符串name、日期birthDate。那么索引的排序为:先按照id 排序,如果id 相同,则按照name排序,如果name的值也相等,则按照birthDate进行排序。
当进行查询时,此时索引仅仅按照id严格有序,因此必须首先使用id字段进行等值查询,之后对于匹配到的列而言,其按照name字段严格有序,此时可以使用name字段用做索引查找,以此类推。如果直接按照name查找: select* from employee where name='Staff'
就不会走索引,因为name是依靠着id进行排序的。
因此,在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。