索引与算法
INNODB存储引擎索引概述
INNODB存储引擎支持以下几种常见的索引:
- B+树索引
- 全文索引
- 哈希索引
InnoDB存储引擎支持的哈希索引是自适应的。会根据表的情况自动添加
B+树索引就是传统意义上的索引,这是目前关系型数据库系统中查找最为常用和最为有效的索引。
B+树索引并不能找到一个给定键值的具体行。B+数索引能找到的只是被查找数据行所在的页。然后数据库通过把页读入到内存中,再在内存中查找,最后得到要查找的数据。
数据结构与算法
二分查找法
有序序列使用
二叉查找树和平衡二叉树
B+树是通过二叉查找树,再由平衡二叉树,B树演化而来。
在二叉查找树总左子树的键值总是小于根的键值,右子树的键值总是大于根的键值。所以中序遍历可以得到键值的排序输出。通过二叉查找树进行查找,性能还是可以的。但是二叉查找树的构造方式有很多,如果是下图,效率就很低
所以如果要最大性能构造一颗二叉查找树,需要这颗二叉查找树是平衡的,从而引出新定义——平衡二叉树,又称为AVL树。但是维护一颗AVL树代价很大,每次插入都需要通过左旋右旋来保证平衡。
因此AVL多用于内存结构对象中,维护的开销相对较小。
B+树
B+树是为磁盘或其他直接存取辅助设备设计的一种平衡查找树。在B+树种,所有记录节点都是按键值的大小顺序排序存放在同一层的叶子节点上,又各叶子节点指针进行联结。
B+树的插入操作
第一种情况:插入键值
第二种情况:插入键值70
第三种情况:插入键值95
为了保持平衡对于新插入的键值,可能需要做大量的拆分页操作,因为B+树结构主要用于磁盘,页的拆分意味着磁盘的操作,所以在可能的情况下尽量减少页的拆分操作。所以提供了类似平衡二叉树的旋转功能。如下面,插入键值70
这样子旋转操作使B+树减少了一次页的拆分操作。
B+树的删除操作
B+树使用填充因子来控制树的删除变化,50%是填充因子可设的最小值。
第一种情况:删除键值70
B+树索引
数据库中,B+树的高度一般在2到4层,所以查询一键值的行记录最多只需要2到4次IO。B+索引分为聚集索引和辅助索引,区别在于叶子节点存放的是否是一整行的信息。
聚集索引
聚集索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点成为数据页。聚集索引的这个特性决定了索引组织表中数据也是索引的一部分。同B+树数据结构一样,每个数据页都通过一个双向链表来进行链接。
由于实际的数据页只能按照一颗B+树进行排序,因此每张表只能拥有一个聚集索引。在多数请框下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上直接找到数据。此外,由于定义了数据的逻辑数据,聚集索引能够特别快地访问针对范围值的查询。查询优化器能够快速发现某一段范围的数据页需要扫描。
数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。
聚集索引的存储并不是物理上连续的,而是逻辑上连续的。这其中有两点,一是前面说过的页通过双向链表链接,页按照主键的顺序排序;另一点是每个页中额记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。
聚集索引的一个好处:对于主键的排序查找和范围查找速度非常快。
辅助索引
叶子节点并不包含行记录的全部数据,叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签。该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。
辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获取指向主键索引的主键,然后再通过主 键索引来找到一个完整的行记录。
B+树索引的管理
索引管理
索引的创建和删除可以通过两种方法:一种是ALTER TABLE,另一种是CREATE/DROP INDEX。通过ALTER TABLE创建索引的语法是:
ALTER TABLE tb1_name | ADD {INDEX|KEY} [index_name] [index_type] (index_col_name, ...) [index_option] ... ALTER TABLE tb1_name DROP PRIMARY KEY | DROP {INDEX|KEY} index_name
通过
CREATE/DROP INDEX
的语法同样很简单CREATE [UNIQUE] INDEX index_name [index_type] ON tab_name (index_col_name, ...) DROP INDEX index_name ON tb1_name
创建前100个字段的索引
ALTER TABLE t ADD KEY idx_b(b(100));
创建联合索引
ALTER TABLE t ADD KEY idx_a_c (a,c);
查看表中索引信息
SHOW INDEX FROM t;
SHOW INDEX展现结果中每列的含义
name value table
索引所在的表名 non_unique
非唯一的索引,可以看到primary key是0 key_name
索引的名字,可以根据这个名字来drop index sql_in_index
索引中该列的位置 column_name
索引列的名字 collation
列以什么方式存储在索引中。B+树索引总是A,使用HEAP引擎,且使用HASH索引,这里会显示NULL。 cardinality
索引中唯一值的数目的估计值。cardinality表行数应尽可能接近1,如果非常小,用户需要考虑是否可以删除此索引。 sub_part
是否是列的部分索引 packed
关键字如何被压缩 null
索引列是否含有NULL index_type
索引的类型。 comment
注释 Cardinality值非常关键,优化器会根据这个值来判断是否使用这个索引。但是这个值不是实时更新的,代价太大,所以只是一个大概值,正确是和行数一致,需要立即更新的话使用命令:
analyze table t\G;
如果Cardinality为NULL,
在某些情况下可能会发生索引建立了却没有用到的情况。
对两条基本一样的语句执行EXPLAIN,但是最终出来的结果不一样,一个使用索引,另一个使用全表扫描。
解决的最好方法,就是做一次ANALYZE TABLE的操作,建议是在一个非高峰时间。
Fast Index Creation
Mysql 5.5版本之前存在的一个普遍被人诟病的问题是MYSQL数据库对于索引的添加或者删除的这类操作,MYSQL数据库的操作过程为:
- 首先创建一张新的临时表,表结构为通过命令ALTER TABLE新定义的结构。
- 然后把原表中数据导入到临时表
- 接着删除原表
- 最后把临时表重名为原来的表名。
如果用户对于一张大表进行索引的添加和删除操作,会需要很长的时间,更关键的是,若有大量事务需要访问正在被修改的表,这意味着数据库服务不可用。
从InnoDB 1.0.X版本开始支持一种称为
fast index creation
快速索引创建的索引创建方式——简称FIC。 对于辅助索引的创建,会对创建索引的表加上一个S锁。在创建的过程过程中,不需要重建表,因此速度较之前提高很多,并且数据库的可用性也得到了提高。删除辅助索引操作就更简单了,InnoDB存储引擎只需更新内部视图,并将辅助索引的空间标记为可用,同时删除MYSQL数据库内部视图上对该表的索引定义即可。
这里需要注意的是:临时表的创建路径是通过参数tmpdir进行设置的,用户必须保证tmpdir有足够的空间可以存放临时表,否则会导致创建索引失败。由于FIC在索引创建的过程中对表机上了S锁,因此在创建的过程中只能对该表进行读操作,若有大量的事务需要对目标库进行写操作,那么数据库的服务同样不可用。
FIC方式只限定于辅助索引。
Online Schema Change
——在线架构改变 所谓“在线”是指事务的创建过程中,可以有读写事务对表进行操作,提高了原有MYSQL数据库在DDL操作时的并发性。是通过PHP脚本开发的。
实现OSC步骤如下:
init
:初始化阶段,对创建的表做一些验证工作,如检查表是否有主键,是否存在触发器或者外键等。createCopyTable
:创建和原始表结构一样的新表alterCopyTable
:对创建的新表进行ALTER TABLE操作,如添加索引或列等。createDeltasTable
:创建deltas
表,该表的作用是为下一步创建的触发器所使用。之后对元彪的所有DML操作会被记录到createDeltasTable
中。createTriggers
:对原表创建INSERT、UPDATE、DELETE操作的触发器。触发操作产生的记录被记录到的deltas
表中。startSnpshotXact
:开始OSC操作的事务。selectTableIntoOutfile
:将原表的数据写入到新表。为了减少对原表的锁定时间,这里通过分片将数据输出到多个外部文件,然后将外部文件的数据导入到copy表中。分片的大小可以指定。dropNCIndexs
:在导入到新表前,删除新表中所有的辅助索引。loadCopyTable
:将导出的分片文件导入到新表。replayChanges
:将OSC过程中原表DML操作的记录应用到新表中,这些记录被保存在deltas
表中。recreateNCIndexes
:重新创建辅助索引。replayChanges
:再次进行DML日志的回放操作,这些日志是在上述创建辅助索引中过程新产生的日志。swapTables
:将原表和新表交换名字,整个操作需要锁定2张表,不允许新的数据产生。由于改名是一个很快的操作,因此堵塞的时间非常短。
有一定局限性,要求进行修改的表一定要有主键,且表本身不能存在外键和触发器。此外,在进行OSC过程中,允许
sql_bin_log=0
,因此所做的操作不会同步到slave服务器,可能导致主从不一致的情况。Online DDL
——在线数据定义 在5.6版本开始支持,允许辅助索引创建的同时,进行INSERT、UPDATE、DELETE等DML操作。
以下操作都可以通过
Online DDL
进行操作- 辅助索引的创建与删除
- 改变自增长值
- 添加或删除外键约束
- 列的重命名
通过新的
ALTER TABLE
语法,用户可以选择索引的创建方式:ALTER TABLE tb1_name | ADD {INDEX | KEY} [index_name] [index_type] (index_col_name, ...) [index_option] ... ALGORITHM [=] {DEFAULT | INPLACE | COPY} LOCK [=] {DEFAULT|NONE|SHARED|EXCLUSIVE}
ALGORITHM
指定了创建或删除索引的算法:COPY
:按照5.1版本之前的工作模式,即创建临时表的方式。INPLACE
:表示索引创建或删除不需要创建临时表。DEFAULT
:表示根据参数old_alter_table
来判断是通过INPLACE
还是COPY
算法。该参数默认为OFF。表示采用INPLACE
的方式。
SHOW VARIABLES LIKE 'old_alter_table'\G;`
LOCK
部分为索引创建或删除时对表添加锁的情况,可有的选择为:NONE
:执行索引创建或删除操作时,对目标表不添加任何的锁,即事务仍然可以进行读写操作,不会受到阻塞。因此这种模式可以获得最大的并发度。SHARE
:和之前的FIC类似,执行索引创建或删除操作时,对目标库加上一个S锁,对于并发的读事务,依然可以执行,但是遇到写事务,就会发生等待操作。如果存储引擎不支持SHARE模式,会返回一个错误信息。EXCLUSIVE
:执行索引创建或删除操作时,对目标库加上一个X锁。读写事务都不能进行,因此会堵塞所有的线程,这和COPY方式运行得到的状态类似,但是不需要像COPY方式那样创建一张临时表。DEFAULT
:首先会判断当前操作是否可以使用NONE模式,若不能,则判断是否可以使用SHARE模式,最后判断是否可以使用EXCLUSIVE模式。
InnoDB
存储引擎实现Online DDL
的原理是在执行创建或者删除操作的同时,将INSERT\UPDATE\DELETE
这类操作写入到一个缓存中,待完成索引创建后再将重做应用在表上,一次打到数据的一致性。这个缓存的大小由参数innodb_online_alter_log_max_size
控制,默认的大小128MB。 需要特别注意的时候,在索引创建过程中,SQL优化器不会选择正在创建中的索引。
Cardinality
值
什么是Cardinality
一般经验是:在访问表中很少一部分数据时使用B+树索引才有意义。
对于低选择字段(性别、地区、类型)没有必要添加B+树索引。对于高选择性字段(几乎不重复)添加索引最为合适。
如何查看索引是否是高选择性?通过SHOW INDEX
结果的列Cardinlity
来观察。表示索引中不重复记录数量的预估值。实际应用中Cardinlity/n_rows_in_table
应该约等于1,如果非常小,那么需要考虑是否需要创建这个索引。
InnoDB
存储引擎的Cardinality
统计
数据库对于
Cardinality
的统计是通过采样的方法来完成的。Cardinality
统计信息的更新发生在两个操作中:INSERT和UPDATE
。更新策略为:
- 表中1/16的数据已经发生过变化
stat_modified_counter>2000000000
,发生变化的次数
更新
Cardinality
的方法:- 取得B+树索引中叶子节点的数量A。
- 随机取得B+树索引中的8个叶子节点。统计每个页不同记录的个数:P1、P2……P8。
- 根据采样信息给出
Cardinality
的预估值。`Cardinality=(P1+P2+……+P8)*A/8。
相关参数
innodb_stats_sample_pages
:设置采样数,默认为8innodb_stats_method
:设置对待NULL值的策略null_equal
:默认值,所有空值视为一种。null_unequal
:空值视为不同种情况nulls_ignored
:忽略空值情况
当执行SQL语句
ANALYZE TABLE
、SHOW TABLE STATUS
、SHOW INDEX
以及访问INFORMATION_SCHEMA
架构下的表TABLE
和STATISTICS
会导致InnoDB
存储引擎去重新计算索引的Cardinality
值。若表数量很大,或者存在多个辅助索引的时候,执行上述操作会很慢,所以提供了相关参数来设置,不更新Cardinality
值。参数 说明 innodb_stats_persistent
是否将命令 ANALYZE TABLE
计算得到的Cardinality
值存放到磁盘上。默认为OFFinnodb_stats_on_metadata
当执行SQL语句 SHOW TABLE STATUS
、SHOW INDEX
以及访问INFORMATION_SCHEMA
架构下的表TABLE
和STATISTICS是否
重新计算索引的Cardinality
值。默认OFFinnodb_stats_persistent_sample_pages
若 innodb_stats_persistene
设置为ON,表示采样值,默认值20innodb_stats_transient_sample_pages
替代 innodb_stats_sample_pages
,表示每次采样页数量,默认值8
B+树索引的使用
联合索引
CREATE TABLE t (
a INT,
b INT,
PRIMARY KEY (a),
KEY idx_a_b (a,b)
)ENGINE=INNODB
# 对于a b列查询可以使用联合索引(a,b)
select * from table where a = xxx and b = xxx;
# 对于单个a列的查询可以使用联合索引(a,b)
select * from table where a = xxx;
# 对于单个b列的查询则不可以使用联合索引(a,b),因为叶子节点上的b值为1,2,1,4,1,2,显然不是排序的
select * from table where b = xxx;
使用联合索引的话,已经对第二个键值进行了排序操作。例如,很多情况下应用程序都要查找某个用户的购物情况,并按照时间进行排序,然后取出最近三次的购买记录,这时候使用联合索引可以避免多一次的排序操作。
一个Demo.
CREATE TABLE buy_log (
userid INT UNSIGNED NOT NULL,
buy_date DATE
)ENGINE=InnoDB;
INSERT INTO buy_log VALUES (1, '2009-01-01');
INSERT INTO buy_log VALUES (2, '2009-01-01');
INSERT INTO buy_log VALUES (3, '2009-01-01');
INSERT INTO buy_log VALUES (1, '2009-02-01');
INSERT INTO buy_log VALUES (3, '2009-02-01');
INSERT INTO buy_log VALUES (1, '2009-03-01');
INSERT INTO buy_log VALUES (1, '2009-04-01');
ALTER TABLE buy_log ADD KEY (userid)
ALTER TABLE but_log ADD KEY (userid, buy_date);
# -------------------------------------
# 查询一个数据,使用索引KEY(userid)
SELECT * FROM buy_log WHRER userid=2;
# 查询最近3次购买记录的数据,使用索引KEY(userid, buy_date),而且无需再对buy_date做一次额外的排序操作。
SELECT * FROM buy_log WHRER userid=2;
覆盖索引
定义:从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录。
好处:辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。
查询辅助索引对应的字段,可以直接通过辅助索引查询得到,不需要查询聚集索引。
#对于InnodbDB存储引擎的辅助索引而言,由于包含了主键信息,因此其叶子节点存放的数据为(primary key1,primary key2,... , key1, key2,...),则下列语句可以使用一次辅助联合索引来完成。 SELECT KEY2 FROM table WHERE KEY1=xxx; SELECT primary key2, KEY2 FROM table WHERE KEY1=xxx; SELECT primary key1, KEY2 FROM table WHERE KEY1=xxx; SELECT primary key1, primary key2, KEY2 FROM table WHERE KEY1=xxx;
对于统计问题而言,可以减少IO操作
SELECT COUNT(*) FROM buy_log;
优化器选择不使用索引的情况
在某些情况下,当执行EXPLAIN
命令进行SQL语句的分析时,会发现优化器并没有选择索引去查找数据,而是通过扫描聚集索引,也就是直接进行全表的扫描来得到数据。这种情况多发生于范围查找,JOIN连接等情况下。
一个demo
select * from orderdetails
where orderid > 10000 and orderid < 102000;
这个表上的索引有:
但是通过EXPLAIN
命令,可以发现优化器并没有按照OrderID
上的索引来查找数据。
最后使用了主键,进行权标扫描。主要原因是,用户选择的数据是整个行信息,而OrderID
索引不能覆盖到我们要查询的信息,因此在对OrderID
索引查到指定数据后,还需要一次书签访问来查找整行数据的信息。虽然OrderID索引中数据是顺序存放的。但是再一次进行书签查找的数据则是无序的,因此变为了磁盘上的离散读操作。如果数据量小,还会使用辅助索引,但是数据量大的时候,依旧选择使用聚集索引来查找数据。
如果用户使用SSD盘,可以是用FORCE INDEX
来强制使用某个索引。
索引提示
MYSQL数据库支持索引提示INDEX HINT
,显式地告诉优化器使用哪个索引。使用的两种情况:
- MYSQL优化器错误选择了某个索引。。一般不可能现在。
- 某SQL语句的索引选择非常多,这时优化器选择执行计划时间的开销可能会大于SQL语句本身。
# 使用USE INDEX,但是不一一定生效,只是告诉优化器,可以使用这个索引
select * from t use index(a) where a = 1 and b = 2;
# 使用FROCE INDEX,可以强制使用某个索引执行
select * from t force index(a) where a = 1 and b = 2;
Multi-Range Read
优化
目的就是为了减少磁盘的随机访问,并且将随机访问转化为顺序的数据访问,这对于IO-bound类型的SQL查询语句可带来性能极大的提升。Multi-range Read
优化可适用于range,ref,eq_ref
类型的查询。
MRR优化的好处:
- 使数据访问变得较为顺序。在查询辅助索引时,首先根据得到的查询结果,按照主键进行排序,并按照主键排序的顺序进行书签的查找。
- 减少缓冲池中页被替换的次数
- 批量处理对键值的查询操作。
对于InnoDB
和MyISAM
存储引擎的范围查找和JOIN查询工作,MRR的工作方式如下:
- 将查询得到的辅助索引键值存放于一个缓冲中,这时缓冲中的数据是根据辅助索引键值排序的。
- 将缓存中的键值根据RowID进行排序
- 根据RowID的排序顺序来访问实际的数据文件。
场景一:
若Innodb
或者MyISAM
存储引擎的缓冲池不足够大,不能存放下一张表的所有数据,此时频繁的离散读操作会导致缓存中的页被替换出缓冲池,又不断地读入缓冲池。若是按照主键顺序进行访问,则可以将此重复息行为降到最低。
select * from salaries where salary>10000 AND salary<40000;
场景二:
可以将某些范围查询,拆分为键值对,以此进行批量的数据查询。
select * from t
where key_part1 >= 1000 and key_part1 < 2000
and key_part2 = 10000;
这个表上有(key_part1, key_part2)的辅助联合索引,在不采用MRR的时候,会先按照key_part1
在1000和2000的数据全部查出来,然后再按照key_part2
进行过滤。所以这个时候启用MRR,他会先把过滤条件拆解为(1000,1000),(1001,1000)……然后再进行查询。
参数设置:
optimizer_switch
控制是否启用MRR优化。
# 总是启用MRR优化
set @@optimizer_switch = 'mrr=on, mrr_cost_based=off';
read_rnd_buffer_size
控制键值的缓冲区大小。默认值256KB。当大于改值时,执行器对已经缓存的数据根据ROWID进行排序,并通过ROWID来取得行数据。
select @@read_rnd_buffer_size\G;
Index Condition Pushdown
ICP优化
在支持ICP优化后,MYSQL数据库会在取出索引的同时,判断是否可以进行where条件的过滤,也就是将WHERE的部分过滤操作放在了存储引擎层。
支持range
、ref
、eq_ref
、ref_or_null
类型的查询。当优化器选择ICP优化时,extra看到using index condition
提示。
哈希算法
哈希表
哈希表也称散列表,由直接寻址表改进而来。
直接寻址表就是用一个数组来来记录每个数据存放的位置。
但是如果数据很大,那么这个数组就要很大。相反,如果开辟了很大的数组,但是没什么数据使用,就浪费了空间,所以提出了哈希表。
在哈希的方式下,改元素处于h(k)中,利用哈希函数h,根据关键字k计算出槽的位置,函数h将关键字域U映射到哈希表T[0,...,m-1]的槽位上。
但是可能连个关键字映射到同一个槽上,这种情况称为碰撞,数据库中结局碰撞的最简单方法是使用链表法。
一般哈希函数需要能够很好的散列,最简单的除法散列法:h(k) = k mod m
INNODB存储引擎中的哈希算法
INNODB存储引擎采用哈希算法对字典进行查找,其冲突机制采用链表方式,哈希函数采用除法散列方式。
对于缓冲池页的哈希表来说,在缓冲池中的Page页都有一个chain指针,它指向相同哈希函数值的页。而对于除法散列,m的取值为略大于2倍的缓冲池页数量的质数。
查找方式是:表空间都有一个space_id,用户查询的应该是某个表空间的某个连续16KB的页,即偏移量Offset。关键字计算公式为:K = space_id << 20 + space_id + offset
,然后通过除法散列到各个槽中。
自适应哈希索引
数据库自身创建并使用,不能进行干预。自适应哈希索引景哈希函数映射到一个哈希表中,因此对字典类型的查找非常迅速。但是对范围查找就无能为力了。
select * from table where index_col = 'xxx'