MySQL技术内幕读书笔记(五)——索引与算法

索引与算法

INNODB存储引擎索引概述

​ INNODB存储引擎支持如下几种常见的索引:mysql

  • B+树索引
  • 全文索引
  • 哈希索引

​ InnoDB存储引擎支持的哈希索引是自适应的。会根据表的状况自动添加算法

​ B+树索引就是传统意义上的索引,这是目前关系型数据库系统中查找最为经常使用和最为有效的索引。sql

​ B+树索引并不能找到一个给定键值的具体行。B+数索引能找到的只是被查找数据行所在的页。而后数据库经过把页读入到内存中,再在内存中查找,最后获得要查找的数据。数据库

数据结构与算法

二分查找法

​ 有序序列使用数组

二叉查找树和平衡二叉树

​ B+树是经过二叉查找树,再由平衡二叉树,B树演化而来。缓存

​ 在二叉查找树总左子树的键值老是小于根的键值,右子树的键值老是大于根的键值。因此中序遍历能够获得键值的排序输出。经过二叉查找树进行查找,性能仍是能够的。可是二叉查找树的构造方式有不少,若是是下图,效率就很低服务器

​ 因此若是要最大性能构造一颗二叉查找树,须要这颗二叉查找树是平衡的,从而引出新定义——平衡二叉树,又称为AVL树。可是维护一颗AVL树代价很大,每次插入都须要经过左旋右旋来保证平衡。数据结构

​ 所以AVL多用于内存结构对象中,维护的开销相对较小。架构

B+树

​ B+树是为磁盘或其余直接存取辅助设备设计的一种平衡查找树。在B+树种,全部记录节点都是按键值的大小顺序排序存放在同一层的叶子节点上,又各叶子节点指针进行联结。并发

B+树的插入操做

​ 第一种状况:插入键值


​ 第二种状况:插入键值70

​ 第三种状况:插入键值95

​ 为了保持平衡对于新插入的键值,可能须要作大量的拆分页操做,由于B+树结构主要用于磁盘,页的拆分意味着磁盘的操做,因此在可能的状况下尽可能减小页的拆分操做。因此提供了相似平衡二叉树的旋转功能。以下面,插入键值70

​ 这样子旋转操做使B+树减小了一次页的拆分操做。

B+树的删除操做

​ B+树使用填充因子来控制树的删除变化,50%是填充因子可设的最小值。

​ 第一种状况:删除键值70


​ 再删除键值25,仍是属于第一种状况

​ 删除键值60,属于第三种状况

B+树索引

​ 数据库中,B+树的高度通常在2到4层,因此查询一键值的行记录最多只须要2到4次IO。B+索引分为汇集索引和辅助索引,区别在于叶子节点存放的是不是一整行的信息。

汇集索引

​ 汇集索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的即为整张表的行记录数据,也将汇集索引的叶子节点成为数据页。汇集索引的这个特性决定了索引组织表中数据也是索引的一部分。同B+树数据结构同样,每一个数据页都经过一个双向链表来进行连接。

​ 因为实际的数据页只能按照一颗B+树进行排序,所以每张表只能拥有一个汇集索引。在多数请框下,查询优化器倾向于采用汇集索引。由于汇集索引可以在B+树索引的叶子节点上直接找到数据。此外,因为定义了数据的逻辑数据,汇集索引可以特别快地访问针对范围值的查询。查询优化器可以快速发现某一段范围的数据页须要扫描。

​ 数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。

​ 汇集索引的存储并非物理上连续的,而是逻辑上连续的。这其中有两点,一是前面说过的页经过双向链表连接,页按照主键的顺序排序;另外一点是每一个页中额记录也是经过双向链表进行维护的,物理存储上能够一样不按照主键存储。

​ 汇集索引的一个好处:对于主键的排序查找和范围查找速度很是快。

辅助索引

​ 叶子节点并不包含行记录的所有数据,叶子节点除了包含键值之外,每一个叶子节点中的索引行中还包含了一个书签。该书签用来告诉InnoDB存储引擎哪里能够找到与索引相对应的行数据。因为InnoDB存储引擎表是索引组织表,所以InnoDB存储引擎的辅助索引的书签就是相应行数据的汇集索引键。

​ 辅助索引的存在并不影响数据在汇集索引中的组织,所以每张表上能够有多个辅助索引。当经过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并经过叶级别的指针获取指向主键索引的主键,而后再经过主 键索引来找到一个完整的行记录。

B+树索引的管理

  1. 索引管理

    ​ 索引的建立和删除能够经过两种方法:一种是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的操做,建议是在一个非高峰时间。

  1. 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方式只限定于辅助索引。

  2. 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服务器,可能致使主从不一致的状况。

  3. 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个叶子节点。统计每一个页不一样记录的个数:P一、P2……P8。
    • 根据采样信息给出Cardinality的预估值。`Cardinality=(P1+P2+……+P8)*A/8。
  • 相关参数

    • innodb_stats_sample_pages:设置采样数,默认为8
    • innodb_stats_method:设置对待NULL值的策略
      • null_equal:默认值,全部空值视为一种。
      • null_unequal:空值视为不一样种状况
      • nulls_ignored:忽略空值状况
  • 当执行SQL语句ANALYZE TABLESHOW TABLE STATUSSHOW INDEX以及访问INFORMATION_SCHEMA架构下的表TABLESTATISTICS会致使InnoDB存储引擎去从新计算索引的Cardinality值。若表数量很大,或者存在多个辅助索引的时候,执行上述操做会很慢,因此提供了相关参数来设置,不更新Cardinality值。

    参数 说明
    innodb_stats_persistent 是否将命令ANALYZE TABLE计算获得的Cardinality值存放到磁盘上。默认为OFF
    innodb_stats_on_metadata 当执行SQL语句SHOW TABLE STATUSSHOW INDEX以及访问INFORMATION_SCHEMA架构下的表TABLESTATISTICS是否从新计算索引的Cardinality值。默认OFF
    innodb_stats_persistent_sample_pages innodb_stats_persistene设置为ON,表示采样值,默认值20
    innodb_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

合索引的B+

# 对于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优化的好处:

  • 使数据访问变得较为顺序。在查询辅助索引时,首先根据获得的查询结果,按照主键进行排序,并按照主键排序的顺序进行书签的查找。
  • 减小缓冲池中页被替换的次数
  • 批量处理对键值的查询操做。

​ 对于InnoDBMyISAM存储引擎的范围查找和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 PushdownICP优化

​ 在支持ICP优化后,MYSQL数据库会在取出索引的同时,判断是否能够进行where条件的过滤,也就是将WHERE的部分过滤操做放在了存储引擎层。

​ 支持rangerefeq_refref_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'
相关文章
相关标签/搜索