索引前言
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。
索引分为聚簇索引和非聚簇索引两种,聚簇索引是按照数据存放的物理位置为顺序的,而非聚簇索引就不一样了;聚簇索引能提高多行检索的速度,而非聚簇索引对于单行的检索很快。
索引分类:
从存储结构上来划分:BTree索引(B-Tree或B+Tree索引),Hash索引,full-index全文索引,R-Tree索引。
从应用层次来分:普通索引,唯一索引,复合索引
从根据中数据的物理顺序与键值的逻辑(索引)顺序关系:聚集索引,非聚集索引。
存储结构所描述的是索引存储时保存的形式,应用层次是索引使用过程中进行的分类,两者是不同层次上的划分。不过平时讲的索引类型一般是指在应用层次的划分。
从索引到主键
我们要了解是聚簇索引和非聚簇索引就要从存放表的数据结构和表的主键开始
主流的RDBMS都是把平衡(非二叉)树当做数据表默认的索引数据结构的,也就是b tree或者 b+ tree。当然, 有的数据库也使用哈希桶作用索引的数据结构。
平时建表的时候都会为表加上主键,但是一个没加主键的表,它的数据无序的放置在磁盘存储器上,一行一行的排列的很整齐。
如果给表上了主键,那么表在磁盘上的存储结构就由整齐排列的结构转变成了树状结构,也就是平衡树(非二叉)结构,换句话说,就是整个表就变成了一个索引,就是「聚集索引」。
为什么一个表只能有一个主键, 一个表只能有一个「聚集索引」,因为主键的作用就是把「表」的数据格式转换成「索引(平衡树)」的格式放置。
索引的类型与增删改查
我们以下面的表为例讲解
mysql> CREATE TABLE `article` ( -> `aid` int(11) NOT NULL AUTO_INCREMENT , -> `title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , -> `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL , -> `time` int(10) NULL DEFAULT NULL , -> PRIMARY KEY (`aid`) -> ); Query OK, 0 rows affected (0.04 sec)
1.索引的添加与索引的类型
1.1. 普通索引
这是最基本的索引,它没有任何限制。
1.直接创建索引 CREATE INDEX index_name ON table(column(length) asc/desc) 2.修改表结构的方式添加索引 ALTER TABLE table_name ADD INDEX index_name(column(length) asc/desc) 3.创建表的时候同时创建索引 CREATE TABLE `article` ( `aid` int(11) NOT NULL AUTO_INCREMENT , `title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL , `time` int(10) NULL DEFAULT NULL , PRIMARY KEY (`aid`), INDEX index_name (title(length) asc/desc) ); 索引默认是asc升序,也是可以省略的。
1.2. 唯一索引
与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值(注意和主键不同)。如果是组合索引,则列值的组合必须唯一,创建方法和普通索引类似。
1.创建唯一索引 CREATE UNIQUE INDEX indexName ON table(column(length)); 2.修改表结构 ALTER TABLE table_name ADD UNIQUE indexName(column(length)); 3.创建表的时候直接指定 CREATE TABLE `table` ( `aid` int(11) NOT NULL AUTO_INCREMENT , `title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL , `time` int(10) NULL DEFAULT NULL , PRIMARY KEY (`aid`), UNIQUE indexName (content(length)) );
索引列的值必须唯一,但允许有空值(注意和主键不同):
mysql> insert into article values (1,1,1,1) ; Query OK, 1 row affected (0.02 sec) mysql> insert into article values (2,1,null,1); Query OK, 1 row affected (0.00 sec) mysql> insert into article values (3,1,null,1); Query OK, 1 row affected (0.01 sec)
mysql> CREATE UNIQUE INDEX article_content ON article(content(10)); Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0
1.3. 多个单列索引
那么是不是我们为表创建多个单列索引表就会有协同作用?
多个单列索引与单个多列索引的查询效果不同,因为执行查询时,MySQL只能使用一个索引,会从多个索引中选择一个限制最为严格的索引。
其实增加索引就是增加key并改变了表结构<毕竟我们用的聚簇索引的innodb数据引擎>,我们可以从下面的添加索引和建表语句的过程中了解:
mysql> CREATE TABLE `article` ( -> `aid` int(11) NOT NULL AUTO_INCREMENT , -> `title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , -> `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL , -> `time` int(10) NULL DEFAULT NULL , -> PRIMARY KEY (`aid`), -> INDEX cr_index_name (title(10) ) -> ); Query OK, 0 rows affected (0.03 sec) mysql> CREATE INDEX index1 ON article(title(10) asc); Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> show create table article; +---------+---------------------------------------------------------------------------------+ | Table | Create Table | +---------+---------------------------------------------------------------------------------+ | article | CREATE TABLE `article` ( `aid` int(11) NOT NULL AUTO_INCREMENT, `title` char(255) CHARACTER SET utf8 NOT NULL, `content` text CHARACTER SET utf8, `time` int(10) DEFAULT NULL, PRIMARY KEY (`aid`), KEY `cr_index_name` (`title`(10) ), KEY `index1` (`title`(10)) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 | +---------+---------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> ALTER TABLE article ADD INDEX index_title(title(10) asc) ; Query OK, 0 rows affected, 1 warning (0.02 sec) Records: 0 Duplicates: 0 Warnings: 1 mysql> show create table article; +---------+---------------------------------------------------------------------------------+ | Table | Create Table | +---------+---------------------------------------------------------------------------------+ | article | CREATE TABLE `article` ( `aid` int(11) NOT NULL AUTO_INCREMENT, `title` char(255) CHARACTER SET utf8 NOT NULL, `content` text CHARACTER SET utf8, `time` int(10) DEFAULT NULL, PRIMARY KEY (`aid`), KEY `cr_index_name` (`title`(10) ), KEY `index1` (`title`(10)), KEY `index_title` (`title`(10)) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 | +---------+---------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> ALTER TABLE article ADD INDEX index_title_content(title(10) asc,content(12)); Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> show create table article; +---------+----------------------------------------------------------------------------------+ | Table | Create Table | +---------+----------------------------------------------------------------------------------+ | article | CREATE TABLE `article` ( `aid` int(11) NOT NULL AUTO_INCREMENT, `title` char(255) CHARACTER SET utf8 NOT NULL, `content` text CHARACTER SET utf8, `time` int(10) DEFAULT NULL, PRIMARY KEY (`aid`), KEY `cr_index_name` (`title`(10) ), KEY `index1` (`title`(10)), KEY `index_title` (`title`(10)), KEY `index_title_content` (`title`(10),`content`(12)) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 | +---------+----------------------------------------------------------------------------------+ 1 row in set (0.00 sec)
本质原因还是下一节
1.4. 组合索引(最左前缀)
平时用的SQL查询语句一般都有比较多的限制条件,所以为了进一步榨取MySQL的效率,就要考虑建立组合索引
例如上表中针对title和time建立一个组合索引:
ALTER TABLE article ADD INDEX index_titme_time(title(50),time(10))
建立这样的组合索引,其实是相当于分别建立了下面两组组合索引:
–title,time
–title
为什么没有time这样的组合索引呢?这是因为MySQL组合索引“最左前缀”的结果。简单的理解就是只从最左面的开始组合。并不是只要包含这两列的查询都会用到该组合索引,如下面的几个SQL所示:
–使用到上面的索引 SELECT * FROM article WHREE title='wqbin博客' AND time=111; SELECT * FROM article WHREE utitle='wqbin博客'; –不使用上面的索引 SELECT * FROM article WHREE time=111;
如果用到了最左前缀而只是颠倒了顺序,也是可以用到索引的,因为mysql查询优化器会判断纠正这条sql语句该以什么样的顺序执行效率最高,最后才生成真正的执行计划。
但我们还是最好按照索引顺序来查询,这样查询优化器就不用重新编译了。
SELECT * FROM article WHREE time=111 AND title='wqbin博客' ;
3. 全文索引(FULLTEXT)
对于content这个column,我们发现他是text类型的无法直接创建变长索引。但是可以创建定长索引。要创建这种不定长的需要使用全文索引。
MySQL从3.23.23版开始支持全文索引和全文检索,FULLTEXT索引仅可用于 MyISAM 表;他们可以从CHAR、VARCHAR或TEXT列中作为CREATE TABLE语句的一部分被创建,或是随后使用ALTER TABLE 或CREATE INDEX被添加。
对于较大的数据集,将你的资料输入一个没有FULLTEXT索引的表中,然后创建索引,其速度比把资料输入现有FULLTEXT索引的速度更为快。不过切记对于大容量的数据表,生成全文索引是一个非常消耗时间非常消耗硬盘空间的做法。
–直接创建索引 CREATE FULLTEXT INDEX index_content ON article(content) –修改表结构添加全文索引 ALTER TABLE article ADD FULLTEXT index_content(content) –创建表的适合添加全文索引 CREATE TABLE `article` ( `aid` int(11) NOT NULL AUTO_INCREMENT , `title` char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL , `time` int(10) NULL DEFAULT NULL , PRIMARY KEY (`aid`), FULLTEXT (content) );
2.索引的的查询
查看一个表的索引:
1.简易法
show index from table_name;
2.元数据法
其实show方法也是从元数据库information_schema直接查询返回
select * from information_schema.STATISTICS where TABLE_SCHEMA='库名' and table_name='表名';
3.索引删除
与同样查询索引一样,都有两个方法。一个是简易的一个是从元数据表中直接删除。
1.简易法
DROP INDEX index_name ON table_name
2.元数据删除法
DELETE FROM information_schema.STATISTICS where TABLE_SCHEMA='库名' and table_name='表名' and INDEX_NAME='索引名';
索引的原理
一切的一切我们都要从主键说起,比如我们建表并没有建主键,且后面不另添加索引,结果没有索引,我们添加了主键这张表就自动带有索引。
但事实上,第一个也是有索引的,InnoDB会隐式定义一个主键来作为聚簇索引。
如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。
InnoDB只聚集在同一个页面中的记录,但是包含相邻键值的页面可能会相距甚远。
聚簇索并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索实际上在同一个结构中保存了B-Tree索引和数据行。
当表有聚簇索引时,它的数据行实际上存放在索引的叶子页(leaf page)中。
表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况)。
聚簇索引中的记录是如何存放的。注意到,叶子页包含了行的全部数据,但是节点页只包含了索引列。在这个案例中,索引列包含的是整数值。
转换成平衡树结构会把复杂度从O(n)降低到O(log10(n))的复杂度从而节约时间,但是索引能让数据库查询数据的速度上升, 而使写入数据的速度下降, 因为平衡树这个结构必须一直维持在一个正确的状态, 增删改数据都会改变平衡树各节点中的索引数据内容,破坏树结构, 因此,在每次数据改变时, DBMS必须去重新梳理树(索引)的结构以确保它的正确,这会带来不小的性能开销,也就是为什么索引会给查询以外的操作带来副作用的原因。
非聚集索引和聚集索引一样, 同样是采用平衡树作为索引的数据结构。索引树结构中各节点的值来自于表中的索引字段,在数据改变时, DBMS需要一直维护索引结构的正确性。如果给表中多个字段加上索引 , 那么就会出现多个独立的索引结构,每个索引(非聚集索引)互相之间不存在关联。 这样的索引也被称之为二级索引。
- 二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
- 二级索引访问需要两次索引查找,而不是一次。因为二级索引中保存的“行指针”的实质,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。
给表按字段建一个新索引, 字段中的数据就会被复制一份出来, 用于生成索引。 因此, 给表添加索引,会增加表的体积, 占用磁盘存储空间。
非聚集索引和聚集索引的区别在于, 通过聚集索引可以查到需要查找的数据, 而通过非聚集索引可以查到记录对应的主键值 , 再使用主键的值通过聚集索引查找到需要的数据。
不管以任何方式查询表, 最终都会利用主键通过聚集索引来定位到数据, 聚集索引(主键)是通往真实数据所在的唯一路径。
覆盖索引:
如果一个索引包含所有需要查询的字段的值,我们就称之为“覆盖索引”。
不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引
前缀索引
对mysql来说其实还有一种前缀索引。前缀索引就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或者很长的VARCHAR类型的列,必须使用前缀索引,因为MySQL不允许索引这些列的完整长度。
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基数”应该接近于完整列的“基数”。
一般来说以下情况可以使用前缀索引:
-
字符串列(varchar,char,text等),需要进行全字段匹配或者前匹配。也就是=‘xxx’ 或者 like ‘xxx%’
-
字符串本身可能比较长,而且前几个字符就开始不相同。比如我们对中国人的姓名使用前缀索引就没啥意义,因为中国人名字都很短,另外对收件地址使用前缀索引也不是很实用,因为一方面收件地址一般都是以XX省开头,也就是说前几个字符都是差不多的,而且收件地址进行检索一般都是like ’%xxx%’,不会用到前匹配。相反对外国人的姓名可以使用前缀索引,因为其字符较长,而且前几个字符的选择性比较高。同样电子邮件也是一个可以使用前缀索引的字段。
-
前一半字符的索引选择性就已经接近于全字段的索引选择性。如果整个字段的长度为20,索引选择性为0.9,而我们对前10个字符建立前缀索引其选择性也只有0.5,那么我们需要继续加大前缀字符的长度,但是这个时候前缀索引的优势已经不明显,没有太大的建前缀索引的必要了。
MySQL 前缀索引能有效减小索引文件的大小,提高索引的速度。
--下面这句话存疑。我未在《高性能mysql》看到这个论据或观点,还需验证。
但是前缀索引也有它的坏处:MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引(Covering Index)。
MySQL索引的优化
上面都在说使用索引的好处,但过多的使用索引将会造成滥用。因此索引也会有它的缺点:虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。下面是一些总结以及收藏的MySQL索引的注意事项和优化方法。
1. 何时使用聚集索引或非聚集索引?
事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。
如:返回某范围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列,这时您查询2004年1月1日至2004年10月1日之间的全部数据时,这个速度就将是很快的,因为您的这本字典正文是按日期进行排序的。
聚类索引只需要找到要检索的所有数据中的开头和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码,然后再根据页码查到具体内容。
2. 索引不会包含有NULL值的列--存疑
只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。
3. 使用短索引
对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。
4. 索引列排序
MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。
5. like语句操作
一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。
6. 不要在列上进行运算
例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′,一个运算符号引发的MYSQL性能损失。
最后总结一下,MySQL只对一下操作符才使用索引:<,<=,=,>,>=,between,in,以及某些时候的like(不以通配符%或_开头的情形)。而理论上每张表里面最多可创建16个索引,不过除非是数据量真的很多,否则过多的使用索引会严重增大IO,减慢速度。
7.字符串与数字比较不使用索引;
CREATE TABLE t1 (varchar(10)); EXPLAIN SELECT * FROM t1 WHEREa="123" – 走索引 EXPLAIN SELECT * FROM t1 WHERE a=123 – 不走索引
8.正则表达式不使用索引,这也是为什么在SQL中很难看到regexp关键字的原因
索引的底层实现
mysql默认存储引擎innodb只显式支持B-Tree( 从技术上来说是B+Tree)索引,对于频繁访问的表,innodb会透明建立自适应hash索引,即在B树索引基础上建立hash索引,可以显著提高查找效率,对于客户端是透明的,不可控制的,隐式的。
Hash索引
基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且Hash索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。
B-Tree索引
B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。(MySQL使用B+Tree)
B+Tree索引
是B-Tree的改进版本,同时也是数据库索引索引所采用的存储结构。
数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。
相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高。
假设有一张学生表,id为主键
在MyISAM引擎中的实现(二级索引也是这样实现的)
在InnoDB中的实现
问:为什么索引结构默认使用B-Tree,而不是hash,二叉树,红黑树?
hash:虽然可以快速定位,但是没有顺序,IO复杂度高。
二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
红黑树:树的高度随着数据量增加而增加,IO代价高。
问:为什么官方建议使用自增长主键作为索引。
结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。
插入连续的数据:
插入非连续的数据