聚簇索引
聚簇索引(Clustered Index)并不是一种新的数据结构,只是B树索引的一种存储方式。
聚簇索引的特点是完整的数据行就放在B树的叶子结点中,Clustered(聚簇,集群)就表示数据行与对应的键紧凑的存储在一起。
下图是《高性能MySQL》聚簇索引的截图,其中,叶子结点包含了数据行的完整数据,非叶子节点只包含索引列数据。
数据行的逻辑顺序与聚簇索引的顺序一致。B+树中叶子结点以链表的形式串联的,叶子节点中数据行的逻辑顺序只有一种,所以一张表只能有一个聚簇索引。
相反非聚簇索引指的就是数据行和对应的键不存在一起,非聚簇索引中叶子结点一般存的是数据行的引用。
MySQL中索引由存储引擎实现,不同的存储引擎索引的存储结构是不一样的,不是所有的存储引擎都支持聚簇索引。
MySQL支持很多种存储引擎(包括官方的和第三方的),使用最多的就是InnoDB(5.5之后默认的存储引擎)和MyISAM(5.5之前默认的存储引擎)。
InnoDB存储引擎
InnoDB中主键索引默认是聚簇的。
如果表中没有定义主键,InnoDB会选择一个所有列非空的Unique索引作为聚簇索引。
如果表中既没有主键也没有非空的唯一索引,InnoDB内部会生成一个名为GEN_CLUST_INDEX的隐式聚簇索引。这个隐式的聚簇索引中包含每个数据行的RowID,并以RowID作为隐式主键进行排序。RowID是一个6字节的字段,并随着新行的插入而单调递增,隐式索引中的数据行在物理结构上是按插入顺序顺序存储的。
绝大多数情况下我们都会定义主键的,所以上面说的两种没有主键的情况下面都不考虑。
InnoDB的二级索引会在叶子结点保存主键。
当通过二级索引查找时,InnoDB需要通过二级索引的叶子结点获得对应的主键,然后根据主键从主键索引中找对应的行。也就是说InnoDB查找数据行会经过两个B树。这样做有个好处就是减少了数据行移动或数据页分裂时二级索引的维护工作,但是查找速度会略微变慢,有一种解决方法是实现索引覆盖,让二级索引直接覆盖所需的查询字段,这样就没必要查找数据行,也就没必要走主键索引了。
聚簇索引的优点
1. 聚簇索引将索引和数据行保存在同一个B-Tree中,查询通过聚簇索引可以直接获取数据,而非聚簇索引要进行多次I/O,所以聚簇索引通常比非聚簇索引查找更快。
2. 聚簇索引对主键范围查询的效率很高,因为其数据是按照主键排列的
3. 二级索引使用索引覆盖可以直接使用叶节点的主键值。
聚簇索引的缺点
1. 聚簇索引最大限度地提高了I/O密集型应用的性能,但如果数据都存放在内存中,则访问顺序就不那么重要了,聚簇索引也没什么优势。
2. 插入速度严重依赖于插入顺序。按照主键顺序往InnoDB中进行数据导入是最快的。如果不是按照主键插入,最好在导入完成后使用OPTIMIZE TABLE命令重新组织一下表。
3. 聚簇索引在插入新行和更新主键时,可能导致“页分裂”问题:当插入到某个已满的叶子结点时,B+树会分裂成两个页来容纳新插入的行数据。页分裂会导致表占用更多的磁盘空间(不要用UUID或随机数做主键,而应该使用单调递增的值做主键)。
4. 聚簇索引可能导致全表扫描速度变慢,因为可能需要加载物理上相隔较远的页到内存中(需要耗时的磁盘寻道操作)。
5. 二级索引访问数据行需要两次索引查找,由于二级索引保存了主键列,二级索引会占更大的空间(所以选用一个短主键是有利的)。
MyISAM存储引擎
InnoDB通过主键聚集数据,整个聚簇索引就是一张完整的表。MyISAM存储引擎的数据相对简单。
MyISAM(ISAM,indexed sequential access method)的数据行和索引是分开存储的:数据文件以.MYD为后缀,索引文件以.MYI为后缀(Innodb只有一个.idb文件)。
数据行按照数据插入顺序存储在数据文件中,数据行的存储支持多种行存储格式(ROW_FORMAT)。
MyISAM没有聚簇索引,主键索引和二级索引工作方式是一样的:叶子结点存储数据行在数据文件中的物理偏移量(行指针)
MyISAM优点
1. 读取数据行的速度快,特别是当数据行长度固定的时候。
2. 数据行插入容易,新行直接追加到数据文件末尾。
MyISAM缺点
1. 删除操作必须留出空白区域,否则后面的行数据偏移将会发生变化。所以当大量删除MyISAM表数据后,数据文件大小不会发生变化,所以要定期执行OPTIMIZE TABLE操作对MyISAM碎片空间进行整理。
2. 修改操作数据行长度如果变短,也会留白;数据行如果变长,数据将会分段存储。
MyISAM还有几个重要优缺点:
· 支持全文(FULLTEXT)索引
· 不支持事务、不支持外键、不支持行级锁。
InnoDB在5.6.4之后支持了全文索引,不过MySQL的全文索引都很鸡肋,都不支持中文。
InnoDB支持事务,外键,行级锁。这些都是MySQL使用InnoDB替代MyISAM作为默认存储引擎的理由。
聚集索引与非聚集索引的总结
一.索引简介
众所周知,索引是关系型数据库中给数据库表中一列或多列的值排序后的存储结构,SQL的主流索引结构有B+树以及Hash结构,聚集索引以及非聚集索引用的是B+树索引。这篇文章会总结SQL Server以及MySQL的InnoDB和MyISAM两种SQL的索引。
SQL Sever索引类型有:唯一索引,主键索引,聚集索引,非聚集索引。MySQL 索引类型有:唯一索引,主键(聚集)索引,非聚集索引,全文索引。
二.聚集索引
聚集(clustered)索引,也叫聚簇索引。
定义:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。
单单从定义来看是不是显得有点抽象,打个比方,一个表就像是我们以前用的新华字典,聚集索引就像是拼音目录,而每个字存放的页码就是我们的数据物理地址,我们如果要查询一个“哇”字,我们只需要查询“哇”字对应在新华字典拼音目录对应的页码,就可以查询到对应的“哇”字所在的位置,而拼音目录对应的A-Z的字顺序,和新华字典实际存储的字的顺序A-Z也是一样的,如果我们中文新出了一个字,拼音开头第一个是B,那么他插入的时候也要按照拼音目录顺序插入到A字的后面,现在用一个简单的示意图来大概说明一下在数据库中的样子:
地址 | id | username | score |
0x01 | 1 | 小明 | 90 |
0x02 | 2 | 小红 | 80 |
0x03 | 3 | 小华 | 92 |
.. | .. | .. | .. |
0xff | 256 | 小英 | 70 |
注:第一列的地址表示该行数据在磁盘中的物理地址,后面三列才是我们SQL里面用的表里的列,其中id是主键,建立了聚集索引。
结合上面的表格就可以理解这句话了吧:数据行的物理顺序与列值的顺序相同,如果我们查询id比较靠后的数据,那么这行数据的地址在磁盘中的物理地址也会比较靠后。而且由于物理排列方式与聚集索引的顺序相同,所以也就只能建立一个聚集索引了。
聚集索引实际存放的示意图
从上图可以看出聚集索引的好处了,索引的叶子节点就是对应的数据节点(MySQL的MyISAM除外,此存储引擎的聚集索引和非聚集索引只多了个唯一约束,其他没什么区别),可以直接获取到对应的全部列的数据,而非聚集索引在索引没有覆盖到对应的列的时候需要进行二次查询,后面会详细讲。因此在查询方面,聚集索引的速度往往会更占优势。
创建聚集索引
如果不创建索引,系统会自动创建一个隐含列作为表的聚集索引。
1.创建表的时候指定主键(注意:SQL Sever默认主键为聚集索引,也可以指定为非聚集索引,而MySQL里主键就是聚集索引)
create table t1(
id int primary key,
name nvarchar(255)
)
2.创建表后添加聚集索引
SQL Server
create clustered index clustered_index on table_name(colum_name)
MySQL
alter table table_name add primary key(colum_name)
值得注意的是,最好还是在创建表的时候添加聚集索引,由于聚集索引的物理顺序上的特殊性,因此如果再在上面创建索引的时候会根据索引列的排序移动全部数据行上面的顺序,会非常地耗费时间以及性能。
三.非聚集索引
非聚集(unclustered)索引。
定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。
其实按照定义,除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。如果非要把非聚集索引类比成现实生活中的东西,那么非聚集索引就像新华字典的偏旁字典,他结构顺序与实际存放顺序不一定一致。
非聚集索引实际存放的示意图
非聚集索引的二次查询问题
非聚集索引叶节点仍然是索引节点,只是有一个指针指向对应的数据块,此如果使用非聚集索引查询,而查询列中包含了其他该索引没有覆盖的列,那么他还要进行第二次的查询,查询节点上对应的数据行的数据。
如有以下表t1:
id | username | score |
1 | 小明 | 90 |
2 | 小红 | 80 |
3 | 小华 | 92 |
.. | .. | .. |
256 | 小英 | 70 |
以及聚集索引clustered index(id), 非聚集索引index(username)。
使用以下语句进行查询,不需要进行二次查询,直接就可以从非聚集索引的节点里面就可以获取到查询列的数据。
select id, username from t1 where username = '小明'select username from t1 where username = '小明'
但是使用以下语句进行查询,就需要二次的查询去获取原数据行的score:
select username, score from t1 where username = '小明'
在SQL Server里面查询效率如下所示,Index Seek就是索引所花费的时间,Key Lookup就是二次查询所花费的时间。可以看的出二次查询所花费的查询开销占比很大,达到50%。
在SQL Server里面会对查询自动优化,选择适合的索引,因此如果在数据量不大的情况下,SQL Server很有可能不会使用非聚集索引进行查询,而是使用聚集索引进行查询,即便需要扫描整个聚集索引,效率也比使用非聚集索引效率要高。
本人试过在含有30w行表上建立非聚集索引,查询非聚集索引覆盖以外的列就会变成聚集索引的全索引扫描(index scan)查询来避免二次查询,而在另外一张200w行表才会用到非聚集索引seek对应的列再进行kek lookup,有关于SQL Server的有Index seek,index scan, table scan,key LookUp这几个概念,可以查看这个blog,描写比较详细。
但在MySQL里面就算表里数据量少且查询了非键列,也不会使用聚集索引去全索引扫描,但如果强制使用聚集索引去查询,性能反而比非聚集索引查询要差,这就是两种SQL的不同之处。
还有一点要注意的是非聚集索引其实叶子节点除了会存储索引覆盖列的数据,也会存放聚集索引所覆盖的列数据。
如何解决非聚集索引的二次查询问题
复合索引(覆盖索引)
建立两列以上的索引,即可查询复合索引里的列的数据而不需要进行回表二次查询,如index(col1, col2),执行下面的语句
select col1, col2 from t1 where col1 = '213';
要注意使用复合索引需要满足最左侧索引的原则,也就是查询的时候如果where条件里面没有最左边的一到多列,索引就不会起作用。
在SQL Server中还有include的用法,可以把非聚集索引里包含的列包含进来,而不一定需要建立复合索引。
四.总结与使用心得
1. 使用聚集索引的查询效率要比非聚集索引的效率要高,但是如果需要频繁去改变聚集索引的值,写入性能并不高,因为需要移动对应数据的物理位置。
2. 非聚集索引在查询的时候可以的话就避免二次查询,这样性能会大幅提升。
3. 不是所有的表都适合建立索引,只有数据量大表才适合建立索引,且建立在选择性高的列上面性能会更好。