Mysql优化之创建高性能索引(三)
聚蔟索引
聚蔟索引并不是一种单独的索引类型,而是一种数据存储方式。Innodb的聚蔟索引在同一结构保存了B-Tree索引和数据行。
当表有聚蔟索引时,它的数据行实际上存放在索引的叶子页中。下图展示了聚蔟索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据,但是节点页只包含了索引列。
聚集的数据有一些重要的优点:
- 可以把相关的数据保存在一起。减少查找到ID后回行产生多一次的I/O
- 数据访问更快。聚蔟索引把数据页和索引保存在同一个B-Tree结构,比非聚蔟索引在查找数据时少一次回表查询
- 使用覆盖索引扫描的查询可以直接使用页节点中的主键值
当然,聚蔟索引也有一些缺点:
- 如果数据放在内存中,聚蔟索引就没什么优势
- 插入速度严重依赖于插入顺序。如果不是按主键顺序加载数据,那么加载完后最好使用OPTIMIZE TABLE 命令重新组织一下表。
- 更新聚蔟索引列的代价很高,因为更新的行需要移动到新的位置
- 基于聚蔟索引的表在插入新行或者主键被更新需要移动行时,会产生页分裂的问题。页分裂会导致表占用更多的磁盘空间。
- 聚蔟索引可能会使全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候
- 二级索引(非聚蔟索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
- 二级索引访问需要两次索引查找,而不是一次。
InnoDB和MyISAM的数据分布对比
聚蔟索引和非聚蔟索引的数据分布有区别,以及对于的主键索引和二级索引的数据分别以也有区别。来看看InnoDB和MyISAM是如何存储下面这个表的:
CREATE TABLE layout_test( col1 int NOT NULL, col2 int NOT NULL, PRIMARY KEY(col1), KEY(col2) );
假设该表的主键取值1--100001,随机插入并使用OPTIMIZE TABLE 命令做优化。也就是说,数据在磁盘的存储方式已经最优,但行的顺序是随机的。列col2的值是从1--100之间随机赋值,所以有很多重复的值。
MyISAM的数据分布比较简单,如下图:
在行的旁边显示了行号,从0递增,所以MyISAM可以从表的开头跳过所需的字节找到所需的行。这种分布方式很容易创建索引。下面显示的一系列图,因此了页的物理细节,只显示索引中的“节点”,索引中的每个叶子节点包含行号。图一为主键分布,图二为col2索引列分布
InnoDB的数据分布。因为InnoDB支持聚蔟索引,所以用不同的方式存储同样的数据。下图为InnoDB表的主键分布。
聚蔟索引的每一个叶子节点都包含了主键值,事务ID,用于事务和MVCC(多版本控制)的回滚指针以及所有的剩余列。
还有一点与MyISAM不同的是,InnoDB的二级索引和聚蔟索引很不相同。
这样做的好处是减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当作指针会让二级索引占用更多的空间,换来的好处是,InnoDB在移动行是无需更新二级索引中的这个“指针”。
下图是描述InnoDB和MyISAM如何存放表的抽象图。
在InnoDB表中按主键顺序插入行
最好避免随机的聚蔟索引,特别是I/O密集型的应用。例如,从性能的角度考虑,使用UUID来作为聚蔟索引则会很糟糕,它使得聚蔟索引的插入变得完成随机。
为了演示这一点,我们做如下两个基准测试。第一个使用整数ID插入userinfo表:
CREATE TABLE userinfo ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL DEFAULT '', email varchar(64) NOT NULL DEFAULT '', password varchar(64) NOT NULL DEFAULT '', dob date DEFAULT NULL, address varchar(255) NOT NULL DEFAULT '', city varchar(64) NOT NULL DEFAULT '', state_id tinyint unsigned NOT NULL DEFAULT '0', zip varchar(8) NOT NULL DEFAULT '', country_id smallint unsigned NOT NULL DEFAULT '0', gender enum('M','F') NOT NULL DEFAULT 'M', account_type varchar(32) NOT NULL DEFAULT '', verified tinyint NOT NULL DEFAULT '0', allow_mall tinyint unsigned NOT NULL DEFAULT '0', parrent_account int unsigned NOT NULL DEFAULT '0', closest_airport varchar(3) NOT NULL DEFAULT '', PRIMARY KEY(uuid), UNIQUE KEY email (email), KEY country_id (country_id), KEY state_id (state_id), KEY state_id_2 (state_id,city,address) )ENGINE=InnoDB;
第二个例子是userinfo_uuid表,除了主键改为uuid varchar(32),其余和前面的userinfo表完全相同。
我们测试这两个表,先各自向表中插入100万条数据,然后再插入300万条数据。
userinfo插入100万条的时间 (插入时间随测试机的性能而异,大家看时间差距就好)
userinfo_uuid插入100万条的时间
userinfo插入300万条的时间
userinfo_uuid插入300万条的时间
向UUID主键插入行不仅花费时间长,而且索引占用空间更大。这一方面由于主键字段更长,另一方面毫无疑问是由于页分裂和碎片导致。
明白了原因,我们来看看第一个表插入数据时,索引发生了什么变化。下图显示了插满一个页面后继续插入相邻的下一个页面的场景。
再来看看第二个UUID表的插入有什么不同
可以看到,在插入UUID表中,移动行的情况非常频繁,这样会增加很多额外的工作,并导致数据分布不够优化。下面是总结的一些缺点:
- 写入的目标页可能已经刷到磁盘并从缓存中删除,或者是还没有加载到缓存中,InnoDB在插入之前不得不先从磁盘读取目标页到内存中,这样会导致大量随机I/O。
- 因为写入是乱序的,页分裂频繁。
- 由于频繁的页分裂,页会变得稀疏并被不规则填充,最终导致数据有碎片。
在把这些随机值载入到聚蔟索引后,也许需要做一次OPTIMIZE TABLE 来重建表并优化页的填充。
从这个案例可以看到,使用InnoDB时应尽可能按主键顺序插入数据,并且尽可能地使用单调递增的聚蔟键的值来插入新行。