**500w**
条数据中检索出一条数据,只能依次遍历这张表的全部数据,直到找到这条数据。但是有了索引后,只需要在索引里去检索这条数据就可以了,因为它是一种专门进行数据检索特殊的数据结构,在找到数据存放的磁盘地址后就可以拿到数据。在 **InnoDB**
存储引擎中,索引有三类: **normal**
):非唯一索引,没有任何限制;**unique**
):唯一索引要求键值不能重复;主键索引是一种特殊的唯一索引,还多了一个条件限制,要求键值不能为空;**fulltext**
):主要针对比较大的数据;例存放 **n KB**
的消息内容,当要解决 **like**
查询效率低的问题时就可以创建全文索引。只有文本类型字段才可以创建全文索引(**cahr、varchar、text**
)。在 **MySQL**
中的 **InnoDB & MyISAM**
存储引擎都支持全文索引。**BST Binary Search Trees**
)的特点是左子树所有的节点都小于父节点,右子树所有的节点都大于父节点,其实就是个有序的线性表。二叉查找树能实现快速查找和插入;**O(n)**
,比如把刚刚的数据按照有序插入后会变成链表(斜树),在这种情况下不能达到快速检索的效率,与顺序查找效率是等价的。造成它倾斜的原因是左右子树深度差别太大,这个树的左子树没有节点,就就是说不够平衡。所以为了解决该问题就出现了平衡二叉树。**Balanced Binary Search Trees**
)是指左右子树的深度差绝对值不能超过 **1**
;比如左子树的深度是 **2**
,右子树的深度只能是 **1 | 3**
;在这时按照顺序插入 **1、2、3、4、5、6**
就不会变成一颗斜树;可以通过平衡二叉树测试看到在插入 **1、2**
后,如果按照二叉查找树的定义,**3**
是会插入在 **2**
的右边的。但是在平衡二叉树中的根节点 **1**
的右节点深度就会变成 **2**
,而左节点的深度是 **0**
,因为根节点 **1**
没有左子节点,所以就会违反平衡二叉树的定义。所以在操作的过程中可以看到它的右节点是右-右型的,在这时就需要把 **2**
提上去,这个操作称为左旋。同样,再以 **7、6、5**
的顺序插入就会变成左左形,就会发生右旋操作,把 **6**
提上去。所以为了保持平衡,**AVL**
树在插入和更新数据时执行了一系列的计算和调整操作。解决了平衡的问题后,就需要解决 **AVL**
树作为索引是怎么查询数据的问题。在平衡二叉树中,一个节点的大小是一个固定的单位,作为索引应该存储三块内容: **id**
上创建了一个索引,在使用 **where id = 1**
的条件进行查询时就会找到索引里面的 **id**
的这个键值;**MySQL**
的存储结构分为:表空间、段、簇、页以及行**Tablespace**
):表空间可以看做是 **InnoDB**
存储引擎逻辑结构的最高层,所有的数据都存放在表空间中;表空间又分为系统表空间、独占表空间、通用表空间、临时表空间以及 **Undo**
表空间;**Segment**
):表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念。一个 **ibd**
文件(独立表空间文件)里是由很多个段组成。创建一个索引时会创建两个段: **leaf node segment**
:主要管理非叶子节点的数据**non-leaf node segment**
:主要管理叶子节点的数据**Extent**
):一个段又分为很多个区,每个区的大小都是 **1MB**
(64
个连续的页);每一个段至少有一个簇,一个段所管理的空间大小是无限的,可以一直扩展下去,但是扩展的最小单位就是簇。**Page**
):为了高效管理物理攻击,对簇进行进一步的细分为页;每个簇都是由 **64**
个连续的页空间组成,每个页的大小是 **16 KB**
并且是 **InnoDB**
存储引擎的磁盘管理中的最小单位。这个页的大小是可以通过 **innodb_page_size**
进行设置;这些页在物理上和逻辑上都是连续的。所以说一个表空间最多拥有 **2**
个页,默认情况下一个页的大小为 **16KB**
,也就是说一个表空间最多可以存储 **64TB**
的数据。**Row**
):**InnoDB**
存储引擎是面向行的,意味着数据的存放按行进行存放。-- 查看数据和索引的大小
SELECT
CONCAT( ROUND( SUM( DATA_LENGTH / 1024 / 1024 ), 2 ), 'MB' ) AS data_len,
CONCAT( ROUND( SUM( INDEX_LENGTH / 1024 / 1024 ), 2 ), 'MB' ) AS index_len
FROM
information_schema.TABLES
WHERE
table_schema = 'mybatis'
AND table_name = 'tb_user';
**IO**
操作,**InnoDB**
操作磁盘的最小的单位是一页(磁盘块),大小是 **16KB(16384 bytes)**
;那么一个树的节点就是 **16KB**
大小。当一个节点只存储一个键值、数据和引用(比如 **int**
类型的字段,可能只是用了十几个或几十个字节,它远远达不到 **16KB**
的容量)就访问一个树节点去进行一次 **I/O**
操作时,就会浪费大量的空间。所以当每个节点存储的数据太少,从索引中找到所需要的数据就需要访问更多的节点,也就意味着与磁盘的交互次数就会增多。如果只是使用机械硬盘,每次从磁盘读取数据就需要 **10ms**
左右的寻址时间,交互次数越多的情况下,消耗的时间也就越多。如下图,当查询 **id = 6**
时就需要查询两个子节点,要和磁盘交互 **3**
次;如果存储的数据是百万级以上,那么这个查询的时间就更加难以预测。为了解决该问题就需要让每个节点存储更多的数据,而节点上的关键字数量越多的情况下,指针数也越多,意味着有更多的分叉。当分叉越来越多时,随之树的深度就会减少;这时就称之为多路平很二叉树。**Balanced Tree(B Tree)**
,它与 **AVL**
树一样,**B**
树在枝节点和叶子节点存储键值、数据地址和节点应用;其特点是分叉数量永远比关键字字数多 **1**
;**B**
树的分裂与合并演示:例如当 Max.Degree = 3
时,插入 **1、2、3**
后,在插入 **3**
时应该在第一个磁盘块,但是如果一个节点有三个关键字时,意味着有 **4**
个指针;子节点会变成 **4**
路,所以这时需要进行分裂,把中间的数字 **2**
提为父节点,把 **1、3**
变为 **2**
的子节点。在删除时会有相反的合并操作,然后再继续看 **4、5**
的分裂与合并操作。在下面图中可以看到在更新索引时会有大量的索引的结构调整,所以这就是为什么不要在频繁更新的列上建立索引的原因,或者不要更新主键;节点的分裂和合并就是 **InnoDB**
存储引擎中的页的分裂与合并。**B+ Tree**
是 **B Tree**
的改良版,比 **B Tree**
解决了更加全面的问题;它的特点是它的关键字的数量与路数相等,还有 **B+ Tree**
的根节点和枝节点中都不会存储数据,只有在叶子节点才存储数据。搜索到关键字不会直接返回,会到最后一层的叶子节点;例如搜索 **id = 29**
时,虽然在第一层直接命中,但是全部的数据还是在叶子节点上,所以还需要继续往下搜索,一直到叶子节点。**bitint**
类型,长度为 **8**
个字节,指针的大小在 **InnoDB**
源码中设置为 **6**
个字节,加起来就一共有 **14**
个字节;假如一条记录是 **1KB**
,一个叶子节点(一页) 可以存储 **16**
条记录,而非叶子叶节点可以存储 **16384 / 14 = 1170**
个单元,也代表着有 **1170**
个指针。当树的深度为 **2**
时,有 **1170**
个叶子节点,可以存储 **1170 * 1170 * 16 = 21902400**
条数据。在查找数据时一次页的查找代表一次 **I/O**
,也就是说一张 **2000w**
条记录的表,查询数据只需要与 **I/O**
进行 **3**
次交互。在 **InnoDB**
的 **B+ Tree**
的深度一般为 **1 - 3**
层就能满足千万级的数据存储。**B+ Tree**
的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指向下一个叶子节点的第一个数据,形成了一个有序链表的结构;
**[)**
来检索数据的。
**B+ Tree**
总结:
**29**
,在根节点就找到了对应的键值,但因为它不是叶子节点,所以会继续往下寻找。 **29**
是 **[29, 73)**
的左闭右开的区间的临界值,所以会走中间的子节点,然后继续搜索;它又是 **[29, 45)**
的左闭右开的区间的临界值,所以会走左边的子节点,最后在叶子节点上找到了需要的数据。**35 - 86**
的数据,那么在找到 **35**
之后,只需要顺着节点和指针顺序遍历就可以一次性访问所有的数据节点,这样就极大地提高了区间查询效率(不需要返回上层父节点重新遍历查找)。**B+ Tree**
是 **B Tree**
的加强版,**B Tree**
能解决的问题,它都能解决;**B Tree**
解决的两大问题是每个节点存储更多的关键字和路数更多;**B+ Tree**
** **拿到所有的数据;**B+ Tree**
的磁盘读写能力比 **B Tree**
更强,因为根节点和枝节点不保存数据区,所以一个节点可以保存更多的关键字,一次磁盘加载的关键字也就更多;**B+ Tree**
永远是在叶子节点拿到数据,所以 **I/O**
次数是稳定的。BST
,但不是严格的平衡二叉树,它必须满足 **5**
个约束:
**NULL**
节点;**5**
个规则可以得出从根节点到叶子节点的最长路径(红黑相间的路径)不大于最短路径(全部是黑色节点)的 **2**
倍。
**InnoDB**
存储引擎中不使用红黑树的原因主要是只有两条路与不够平衡;红黑树一般情况下只是放在内存中使用。
**60、56、68、45、64、58、72、43、49**
**name | gender**
建立索引,根据字段 **name**
查询很快就能查询出来,但是根据 **gender**
查询,由于该字段的重复率太高,扫描的行数就越多。ALTER TABLE user_innodb DROP INDEX comidx_name_address;
-- 建立 name 和 address 的联合索引
ALTER TABLE user_innodb add INDEX comidx_name_address (name, address);
**B+ Tree**
中是复合的数据结构,是按照从左到右的顺序来建立搜索树的(name
在左边,**address**
在右边)。这时使用 **select * from person where name = 'John' and address = '广州'**
语句查询时,**B+ Tree**
会先比较 **name**
来确定应该搜索左子节点还是右子节点,当 **name**
相同时再比较 **address**
;但是如果查询条件没有 **name**
就不确定查询哪个子节点,因为建立搜索树的时候 **name**
是第一个比较因子,所以用不到索引而进行全文检索。所以在建立联合索引时需要把最常用的列放在最左边-- 建立 name 和 address 的联合索引,这里相当于建立了两个索引 (name)、(name, address)
CREATE INDEX idx_name_address on person(name, address);
-- 使用两个字段可以用到联合索引
EXPLAIN SELECT * FROM person WHERE name= 'John' AND address = '广州';
-- 使用左边的 name 字段可以用到联合索引
EXPLAIN SELECT * FROM person WHERE name= 'John';
-- 使用右边的 address 字段,无法使用到索引而导致全表扫描
EXPLAIN SELECT * FROM person WHERE address = '广州';
**select**
的数据列只用了从索引中就能取得,不用从数据区中读取,这个时候就叫做索引覆盖,这样就避免了回表。在使用查询计划查看 **Extra = Using index**
就表示使用了覆盖索引。而 **select ***
使用不到覆盖索引。-- 删除联合索引
ALTER TABLE person DROP INDEX comixd_name_address;
-- 创建联合索引
ALTER TABLE person add INDEX `comixd_name_address` (`name`,`address`);
-- 下面三个查询都使用到了覆盖索引
EXPLAIN SELECT name, address FROM person WHERE name = 'John' AND address = '广州';
EXPLAIN SELECT name FROM person WHERE name = 'John' AND address = '广州';
EXPLAIN SELECT phone FROM person WHERE name = 'John' AND address = '广州';
-- 创建员工表
CREATE TABLE `employees` (
`emp_no` INT ( 11 ) NOT NULL,
`birth_date` date NULL,
`first_name` VARCHAR ( 14 ) NOT NULL,
`last_name` VARCHAR ( 16 ) NOT NULL,
`gender` enum ( 'M', 'F' ) NOT NULL,
`hire_date` date NULL,
PRIMARY KEY ( `emp_no` )
) ENGINE = INNODB DEFAULT CHARSET = latin1;
-- 建立 first_name 和 last_name 的联合索引
ALTER TABLE employees ADD INDEX idx_lastname_firstname ( last_name, first_name );
-- 插入数据
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 1, NULL, '698', 'liu', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 2, NULL, 'd99', 'zheng', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 3, NULL, 'e08', 'huang', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 4, NULL, '59d', 'lu', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 5, NULL, '0dc', 'yu', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 6, NULL, '989', 'wang', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 7, NULL, 'e38', 'wang', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 8, NULL, '0zi', 'wang', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 9, NULL, 'dc9', 'xie', 'F', NULL );
INSERT INTO `employees` ( `emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date` )
VALUES ( 10, NULL, '5ba', 'zhou', 'F', NULL );
-- 关闭 ICP
set optimizer_switch='index_condition_pushdown=off';
-- 查看参数
show variables like 'optimizer_switch';
-- 查询所有姓王并且最后是以 zi 结尾的数据
select * from employees where last_name='wang' and first_name LIKE '%zi';
-- 使用执行计划查看 Extra 为 Using Where
explain select * from employees where last_name='wang' and first_name LIKE '%zi' ;
-- 开启 ICP
set optimizer_switch='index_condition_pushdown=on';
-- 再次使用执行计划查看 Extra 为 Using index condition
explain select * from employees where last_name='wang' and first_name LIKE '%zi' ;
**SQL**
语句执行有两种方式 **wang**
的二级索引数据,然后回表到主键索引上去查询全部符合条件的数据;接着返回给 **Server**
层去过滤出名字以 **zi**
结尾的员工;**wang**
的二级索引数据(**3**
个索引),然后从二级索引中筛选出 **first_name**
以 **zi**
结尾的索引(**1**
个索引),然后再回表到主键索引上查询符合条件的数据后再返回给 **Server**
层**Server**
层进行的。当 **first_name**
的条件不能用于索引过滤时,**Server**
层不会把 **first_name**
的条件传递给存储引擎,所以读取了两条没有必要的记录。所以这时当满足 **last_name = 'wang'**
时的记录有 **10w**
条数据,那么就会有 **99999**
条没有必要读取的记录。**Using Where & Using index condition**
: **Using Where**
:代表从 **InnoDB**
存储引擎取回的数据不是全部满足条件,需要在 **Server**
层进行过滤;先用 **last_name**
条件进行索引范围扫描,读取数据表记录,然后进行比较;检查是否符合 **first_name Like '%zi'**
的条件,此时 **3**
条中只有一条符合条件。**ICP**
后把 **first_name Like '%zi'**
条件下推给存储引擎后只会返回读取所需的 **1**
条记录,该功能是 **MySQL 5.6**
后完善的功能,只是适用于二级所用,**ICP**
的目标是减少访问表的完整行的读数据从而减少 **I/O**
操作。**where**
判断 **order**
排序和 **join**
的 **on**
字段上创建索引;**B+ Tree**
会变化导致性能差;**like**
条件前不要带 %
;**NOT LIKE**
;**!= 、<>、NOT IN**
**SQL**
语句是否使用索引是根据数据库版本、数据量以及数据选择度有关;是否使用索引是优化器决定的,因为它是基于 **cost**
开销来进行优化的,哪种方式开销小就选择哪种。