概念
- 术语聚簇表示数据行和相邻的键值紧凑地存储在一起,一个表只有一个。
- 聚簇索引不是一种单独的数据类型,而是一种数据存储方式。
- InnoDB的聚簇索引实际上在同一结构中保存了B+Tree索引和数据,当表有聚簇索引时,它的数据行实际上存放在索引的叶子节点中。
- 因为无法同时把数据行放在两个不同的地方,所以一个表只能有一个聚簇索引(覆盖索引可模拟多个聚簇索引的情况,后面会介绍)
- 因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引。
本节课主要关注InnoDB,但是这里讨论的原理对于任何支持聚簇索引的存储引擎都是适用的。
聚簇索引中的记录是如何存放的?
叶子节点包含了全部数据,其他节点只包含索引列。InnoDB将通过主键聚集数据,也就是说上图中的“被索引的列”就是主键列。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引InnoDB会隐式定义一个主键来作为聚簇索引。
聚簇索引优点
- 可以把相关数据保存在一起。
例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有则每封邮件都有可能导致一次磁盘I/O。 - 数据访问速度快。
聚簇索引将索引和数据保存在同一个B+Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。 - 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。
聚簇索引缺点
聚簇索引最大限度地提高了IO密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没么重要了,聚簇索引也就没什么优势了。
插入速度严重依赖插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。当对MySQL进行大量的增删改操作的时候,很容易产生一些碎片,这些碎片占据着空间,所以可能会出现删除很多数据后,数据文件大小变化不大的现象。当然新插入的数据仍然会利用这些碎片。但过多的碎片,对数据的插入操作是有一定影响的,此时,我们可以通过optimize来对表的优化。
跟新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置
基于聚簇索引的表在插入新行,或者主键或者主键被更新导致需要移动行的时候,可能面临“页分裂”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键值,会导致非叶子节点占用占用更多的物理空间
- 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
所以建议使用int的auto_increment作为主键
主键的值是顺序的,所以 InnoDB 把每一条记录都存储在上一条记录的后面。当达到页的最大值时,下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满。
聚簇索引的数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。如果主键不是自增id,那么可以想 象,它会干些什么,不断地调整数据的物理地址、分页,当然也有其他一些措施来减少这些操作,但却无法彻底避免。但,如果是自增的,那就简单了,它只需要一 页一页地写,索引结构相对紧凑,磁盘碎片少,效率也高。
因为MyISAM的主索引并非聚簇索引,那么他的数据的物理地址必然是凌乱的,拿到这些物理地址,按照合适的算法进行I/O读取,于是开始不停的寻道不停的旋转。聚簇索引则只需一次I/O。(强烈的对比)
不过,如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。
-
二级索引访问需要两次索引查找,而不是一次。因为聚簇索引一张表就有一个,二级索引叶子节点保存着主键列,查询时首先根据索引找到对应的主键列,然后根据主键列到聚簇索引中查找数据。
InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。
若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。
MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
所以说,聚簇索引性能最好而且具有唯一性,所以非常珍贵,必须慎重设置。一般要根据这个表最常用的SQL查询方式来进行选择,某个字段作为聚簇索引,或组合聚簇索引,这个要看实际情况。
InnoDB和MyISAM的数据分布对比
聚簇索引和非聚簇索引的数据分布有区别,主键索引和二级索引的数据分布也有区别,通常会让人感到困扰和以外,下面通过一个列子来讲解InnoDB和MyISAM是如何存储数据的:
CREATE TABLE `layout_test` (
`col1` int(10) NOT NULL,
`col2` int(10) DEFAULT NULL,
PRIMARY KEY (`col1`),
KEY `col2` (`col2`) USING BTREE
) ENGINE=InnoDB ;
该表的主键取值1~10000,按照随机顺序插入并使用optimize table命令做了优化。换句话说,数据在磁盘上的存储方式已是最优,但行的顺序是随机的。列col2的值是从1~100之间随机赋值,所以有很多重复的值。
MyISAM的数据分布
MyISAM的数据分布很简单,所以先介绍它。MyISAM按照数据插入的顺序存储在磁盘上,如下图所示:
在行的旁边显示行号,从0开始递增。因为行是定长的,所以MyISAM可以从表的开头跳过所需的字节找到需要的行。
col2上的索引
事实上,MyISAM中主键索引和其他索引在结构上没有什么不同。主键索引就是一个名为PRIMARY的唯一非空索引。
InnoDB的数据分布
InnoDB支持聚簇索引,所以使用不同的方式存储同样的数据。
第一眼看上去,感觉和前面的没什么区别,但是该图显示了整个表,而不是只有索引。因为在InnoDB中,聚簇索引就是表,所以不像MyISAM那样需要独立的行存储,这也是为什么MyISAM索引和数据结构是分开的。
聚簇索引的每一个叶子节点都包含了主键值。事务ID、用于事务和MVCC的回滚指针以及所有的剩余列。如果主键是一个列前缀索引,InnoDB也会包含完整的主键列和剩下的其他列。
还有一点和MyISAM不同的是,InnoDB的二级索引和聚簇索引很不相同。InnoDB的二级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的“指针”。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当作指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动时无需更新二级索引中的这个“指针”。
我们在来看一下col2索引。
每一个叶子节点包含了索引列(这里是col2),紧接着是主键值(col1),上图我们省略了非叶子节点这样的细节。InnoDB非叶子节点包含了索引列和一个指向下一级节点的指针。
最后,以一张图表示InnoDB和MyISAM保存数据和索引的区别。
在InnoDB表中按主键顺序插入行
前面讲过,最好使用AUTO_INCREMENT自增列来聚集数据,避免随机的、不连续的、值分布范围大的列做聚簇索引,特别是对于I/O密集型的应用。例如,从性能角度考虑,使用UUID来作为聚簇索引则会很糟糕:他使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
为了演示这一点,我们做两个基准测试:
1、使用证书ID插入userinfo表,和uuid作为主键的userinfo_uuid表
CREATE TABLE `userinfo` (
`id` int(32) NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`email` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
`dob` date DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
`city` varchar(64) DEFAULT NULL,
`state_id` tinyint(1) DEFAULT '0',
`zip` varchar(8) DEFAULT NULL,
`country_id` smallint(10) DEFAULT '0',
`gender` enum('M','F') CHARACTER SET latin1 DEFAULT 'M',
`account_type` varchar(32) DEFAULT NULL,
`verified` tinyint(1) DEFAULT '0',
`allow_mail` tinyint(1) DEFAULT '0',
`parrent_account` int(10) DEFAULT NULL,
`closest_airport` varchar(3) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`) USING BTREE,
KEY `country_id` (`country_id`) USING BTREE,
KEY `state_id` (`state_id`) USING BTREE,
KEY `state_id_2` (`state_id`,`city`,`address`) USING HASH
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
userinfo_uuid表跟userinfo表除了主键给为UUID,其他字段都一样
CREATE TABLE `userinfo_uuid` (
`uuid` varchar(36) NOT NULL,
...
测试这两个表的设计,首先在一个有足够内存容纳索引的服务器上向这两个表各插入100万条记录。然后向两个表继续插入300万数据,使索引的大小超过服务器的内存容量。测试结果如下:
向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长,另一方面毫无疑问是由于页分裂和碎片导致的。
为了明白为什么会这样,来看看往第一个表中插入数据时,索引发生了什么变化。
自整型主键插入
因为主键是顺序的,所以InnoDB把每一条记录都存在上一条记录的后面。当达到页的最大容量后,下一条记录就会写入到新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果。
UUID插入
因为新行的主键值不一定比之前插入的大,所以InnoDB无法简单的总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置,通常是已有数据的中间位置,并且分配空间。这会正价很多的额外工作,并导致数据分布不够优化。
缺点:
写入的目标也可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机IO。
因为写入时乱序的,InnoDB不得不频繁的做页分裂操作,以便为新的行分配空间。页分裂将会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
由于频繁的页分裂,页会变得稀疏并被不规则的填充,所以最终数据会有碎片。
把这些随机值载入到聚簇索引后,也许需要做一次OPTIMIZE TABLE来重建表并优化页的填充。
结论:使用InnoDB时应尽可能地按主键顺序插入数据,并且尽可能地单调增加聚簇键的值来插入新行。