12. 索引/B+树/Explain

索引

索引的定义

索引是对记录按照一个或者多个字段进行排序的一种方式。对表中的某个字段建立索引会创建另一种数据结构,其中保存着字段的值,每个值又指向与它相关的记录。这种索引的数据结构是经过排序的,因而可以对其执行二分查找。且性能较高。

 

二叉树(Binary Tree)

二叉树的定义

  • 每个节点至多只有二棵子树;
  • 二叉树的子树有左右之分,次序不能颠倒;
  • 一棵深度为k,且有 2^k -1 个节点,称为满二叉树(Full Tree);
  • 一棵深度为k,且root到k-1层的节点树都达到最大,第k层的所有节点都 连续集中 在最左边,此时为完全二叉树(Complete Tree)

  

平衡二叉树

  • 左子树和右子树都是平衡二叉树;
  • 左子树和右子树的高度差绝对值不超过1; 

  

 

  • 平衡二叉树的遍历

  以上面平衡二叉树的图例为样本,进行遍历:

    • 前序 : 6 , 3, 2, 5, 7, 8(ROOT节点在开头, 中 -左-右 顺序)
    • 中序 :2, 3, 5, 6 , 7, 8(中序遍历即为升序,左- 中 -右 顺序)
    • 后序 :2, 5, 3, 8, 7, 6 (ROOT节点在结尾,左-右- 中 顺序)
    1. 可以通过 前序 和 中序 或者是 后序 和 中序 来推导出一个棵树
    2. 前序 或者 后序 用来得到ROOT节点, 中序 可以区分左右子树
  • 平衡二叉树的旋转

  

  需要通过旋转(左旋,右旋)来维护平衡二叉树的平衡,在添加和删除的时候需要有额外的开销。

 

B树

  • Balance Tree, 即为平衡树的意思,下图即是一颗B树

  • 图中的p节点为指向子节点的指针,二叉树和平衡二叉树其实也有,因为图的美观性被省略了。
  • 图中的每个节点称为页,页就是磁盘块,在MySQL中读取数据的基本单位都是页,所以我们这里叫做页更符合MySQL的底层数据结构
  • B树相对平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点
    • 子节点的个数一般称为阶,上述图中的B树为3阶B树,高度也会很低。基于这个特性B树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多

假如我们要查找id等于28的用户信息,那么我们在上述B树中的查找流程如下:

  1. 先找到根节点也就是页1,判断28在键值17和35之间,我们那么我们根据页1中的指针p2找到页3。
  1. 将28和页3中的键值相比较,28在26和30之间,我们根据页3中的指针p2找到页8。
  2. 将28和页8中的键值相比较,发现有匹配的键值28,键值28对应的用户信息为(28,bv)。

 

B+树

B+树是对B树的进一步优化。下图为B+树的结构图

  • 数据只存储在叶子节点上,非叶子节点只保存索引信息;
    • 非叶子节点(索引节点)存储的只是一个Flag,不保存实际数据记录;
    • 索引节点指示该节点的左子树比这个Flag小,而右子树大于等于这个Flag
  • 叶子节点本身按照数据的升序排序进行链接(串联起来);
    • 叶子节点中的数据在物理存储上是无序的,仅仅是在逻辑上有序 (通过指针串在一起);
  •  B+树含有非常高的扇出(fanout),通常超过100,在查找一个记录时,可以有效的减少IO操作;

 

B+树和B树的区别

  • B+树非叶子节点不存储数据,只存储键值(key),而B树不仅存储键值也会存储数据。之所以这么做是因为数据库中页的大小是固定的,InnoDB中页的大小默认为16KB。如果不存储数据就能存储更多的键值,相应的树的阶数(节点的子节点数)就会更大,树就会更矮更胖。如此一来我们查找数据的磁盘IO次数就会再次减少,查询数据的效率也会更快。
  • B树的阶数等于键值的数量,如果B+树一个节点可以存储1000个键值,那么3层B+树可以存储1000 * 1000 * 1000=10亿个数据。一般根节点是常驻内存的。所以一般我们查找10亿数据只需要两次磁盘的IO。
  • B+树所有数据均存在于叶子节点,而且数据是顺序排列。所以B+树使得范围查找,顺序查找,分组查找以及去重查找变得异常简单。而B树因为数据分散在各个节点,要实现这一点很不容易。
  • B+树各个页之间通过双向链表连接,叶子节点的数据通过单向链表连接。

 

B+树的操作

  • B+树的插入

  B+树的插入必须保证插入后叶子节点中的记录依然有序

  

B+树总是会保持平衡。但是为了保持平衡对于新插入的键值可能需要做大量的拆分页(split)操作;部分情况下可以通过B+树的旋转来替代拆分页操作,进而达到平衡效果。

 

  • B+树的删除

  B+树使用填充因子(fill factor)来控制树的删除变化,50%是填充因子可设的最小值。B+树的删除操作同样必须保证删除后叶子节点中的记录依然排序。与插入不同的是,删除根据填充因子的变化来衡量。 

  

 

B+树的扇出

  • B+树图例

  

    • 该 B+ 树高度为 2
    • 每叶子页(LeafPage)4条记录
    • 扇出数为5
    • 叶子节点(LeafPage)由小到大(有序)串联在一起
  1. 扇出:每个索引节点(Non-LeafPage)指向每个叶子节点(LeafPage)的指针
  2. 扇出数 = 索引节点(Non-LeafPage)可存储的最大关键字个数 + 1
  3. 图例中的索引节点(Non-LeafPage)最大可以存放4个关键字,但实际使用了3个; 

 

MySQL索引

索引类型(待补充)

创建索引

alter table test_index_1 add index idx_a (a); -- 给字段a添加索引。索引名为idx_a

 

查看索引

  1. desc table_name
  2. show create table table_name\G
  3. show index from table_name\G

  

root@mysqldb 13:59:  [dbt3_s1]> show index from orders\G
*************************** 1. row ***************************
        Table: orders
   Non_unique: 0    -- 表示唯一的
     Key_name: PRIMARY    -- key的name是primary
 Seq_in_index: 1
  Column_name: o_orderkey
    Collation: A
  Cardinality: 1486533    -- 基数,这个列上不同值的记录数
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE    -- 索引类型是BTree
      Comment: 
Index_comment: 
      Visible: YES
   Expression: NULL
*************************** 2. row ***************************
        Table: orders
   Non_unique: 1    -- Non_unique为True,表示不唯一
     Key_name: i_o_orderdate
 Seq_in_index: 1
  Column_name: o_orderDATE
    Collation: A
  Cardinality: 2462
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE
      Comment: 
Index_comment: 
      Visible: YES
   Expression: NULL
*************************** 3. row ***************************
        Table: orders
   Non_unique: 1
     Key_name: i_o_custkey
 Seq_in_index: 1
  Column_name: o_custkey
    Collation: A
  Cardinality: 100436
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE
      Comment: 
Index_comment: 
      Visible: YES
   Expression: NULL
3 rows in set (0.01 sec)

root@mysqldb 13:59:  [dbt3_s1]> select count(*) from orders;
+----------+
| count(*) |
+----------+
|  1500000 |    -- orders中有150W条记录,和Cardinality 是不一致的
+----------+
1 row in set (0.42 sec)

 

Cardinality(基数)

Cardinality 表示该索引列上有多少不同的记录,这个是一个预估的值,是采样得到的(由InnoDB触发,采样20个页,进行预估),该值越大越好,即当Cardinality / RowNumber越接近1越好。表示该列是高选择性的 。

  •  高选择性:身份证 、手机号码、姓名、订单号等
  •  低选择性:性别、年龄等

即该列是否适合创建索引,就看该字段是否具有高选择性

root@mysqldb 14:02:  [dbt3_s1]> show create table lineitem\G
*************************** 1. row ***************************
       Table: lineitem
Create Table: CREATE TABLE `lineitem` (
  `l_orderkey` int NOT NULL,
  `l_partkey` int DEFAULT NULL,
  `l_suppkey` int DEFAULT NULL,
  `l_linenumber` int NOT NULL,
  `l_quantity` double DEFAULT NULL,
  `l_extendedprice` double DEFAULT NULL,
  `l_discount` double DEFAULT NULL,
  `l_tax` double DEFAULT NULL,
  `l_returnflag` char(1) DEFAULT NULL,
  `l_linestatus` char(1) DEFAULT NULL,
  `l_shipDATE` date DEFAULT NULL,
  `l_commitDATE` date DEFAULT NULL,
  `l_receiptDATE` date DEFAULT NULL,
  `l_shipinstruct` char(25) DEFAULT NULL,
  `l_shipmode` char(10) DEFAULT NULL,
  `l_comment` varchar(44) DEFAULT NULL,
  PRIMARY KEY (`l_orderkey`,`l_linenumber`),    -- 两个列作为primary
  KEY `i_l_shipdate` (`l_shipDATE`),
  KEY `i_l_suppkey_partkey` (`l_partkey`,`l_suppkey`),
  KEY `i_l_partkey` (`l_partkey`),
  KEY `i_l_suppkey` (`l_suppkey`),
  KEY `i_l_receiptdate` (`l_receiptDATE`),
  KEY `i_l_orderkey` (`l_orderkey`),
  KEY `i_l_orderkey_quantity` (`l_orderkey`,`l_quantity`),
  KEY `i_l_commitdate` (`l_commitDATE`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

root@mysqldb 14:02:  [dbt3_s1]> show index from lineitem\G    -- 省略其他输出,只看PRIMARY
*************************** 1. row ***************************
        Table: lineitem
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1    -- 索引中的顺序,该列的顺序为1
  Column_name: l_orderkey
    Collation: A
  Cardinality: 1338364    -- 约130W
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
      Comment: 
Index_comment: 
      Visible: YES
   Expression: NULL
*************************** 2. row ***************************
        Table: lineitem
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 2    -- 索引中的顺序,该列的顺序为2
  Column_name: l_linenumber
    Collation: A
  Cardinality: 5406960    -- 约540W
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
      Comment: 
Index_comment: 
      Visible: YES
   Expression: NULL
  1. 第一个索引(Seq_in_index = 1)的 Cardinality 的值表示 当前列(l_orderkey) 的不重复的值
  2. 第二个索引(Seq_in_index = 2)的 Cardinality 的值表示 前两列(l_orderkey)和(l_linenumber) 不重复的值
--
-- SQL-1
--
root@mysqldb 14:08:  [dbt3_s1]> select * from lineitem limit 10;
+------------+-----------+-----------+--------------+------------+-----------------+------------+-------+--------------+--------------+------------+--------------+---------------+-------------------+------------+----------------------------------------+
| l_orderkey | l_partkey | l_suppkey | l_linenumber | l_quantity | l_extendedprice | l_discount | l_tax | l_returnflag | l_linestatus | l_shipDATE | l_commitDATE | l_receiptDATE | l_shipinstruct    | l_shipmode | l_comment                              |
+------------+-----------+-----------+--------------+------------+-----------------+------------+-------+--------------+--------------+------------+--------------+---------------+-------------------+------------+----------------------------------------+
|          1 |    155190 |      7706 |            1 |         17 |        21168.23 |       0.04 |  0.02 | N            | O            | 1996-03-13 | 1996-02-12   | 1996-03-22    | DELIVER IN PERSON | TRUCK      | blithely regular ideas caj             |
|          1 |     67310 |      7311 |            2 |         36 |        45983.16 |       0.09 |  0.06 | N            | O            | 1996-04-12 | 1996-02-28   | 1996-04-20    | TAKE BACK RETURN  | MAIL       | slyly bold pinto beans detect s        |
|          1 |     63700 |      3701 |            3 |          8 |         13309.6 |        0.1 |  0.02 | N            | O            | 1996-01-29 | 1996-03-05   | 1996-01-31    | TAKE BACK RETURN  | REG AIR    | deposits wake furiously dogged,        |
|          1 |      2132 |      4633 |            4 |         28 |        28955.64 |       0.09 |  0.06 | N            | O            | 1996-04-21 | 1996-03-30   | 1996-05-16    | NONE              | AIR        | even ideas haggle. even, bold reque    |
|          1 |     24027 |      1534 |            5 |         24 |        22824.48 |        0.1 |  0.04 | N            | O            | 1996-03-30 | 1996-03-14   | 1996-04-01    | NONE              | FOB        | carefully final gr                     |
|          1 |     15635 |       638 |            6 |         32 |        49620.16 |       0.07 |  0.02 | N            | O            | 1996-01-30 | 1996-02-07   | 1996-02-03    | DELIVER IN PERSON | MAIL       | furiously regular accounts haggle bl   |
|          2 |    106170 |      1191 |            1 |         38 |        44694.46 |          0 |  0.05 | N            | O            | 1997-01-28 | 1997-01-14   | 1997-02-02    | TAKE BACK RETURN  | RAIL       | carefully ironic platelets against t   |
|          3 |      4297 |      1798 |            1 |         45 |        54058.05 |       0.06 |     0 | R            | F            | 1994-02-02 | 1994-01-04   | 1994-02-23    | NONE              | AIR        | blithely s                             |
|          3 |     19036 |      6540 |            2 |         49 |        46796.47 |        0.1 |     0 | R            | F            | 1993-11-09 | 1993-12-20   | 1993-11-24    | TAKE BACK RETURN  | RAIL       | final, regular pinto                   |
|          3 |    128449 |      3474 |            3 |         27 |        39890.88 |       0.06 |  0.07 | A            | F            | 1994-01-16 | 1993-11-22   | 1994-01-23    | DELIVER IN PERSON | SHIP       | carefully silent pinto beans boost fur |
+------------+-----------+-----------+--------------+------------+-----------------+------------+-------+--------------+--------------+------------+--------------+---------------+-------------------+------------+----------------------------------------+
10 rows in set (0.00 sec)

--
-- SQL-2
--
root@mysqldb 14:08:  [dbt3_s1]> select l_orderkey, l_linenumber from lineitem limit 10;
+------------+--------------+
| l_orderkey | l_linenumber |
+------------+--------------+
|     721220 |            2 |
|     842980 |            4 |
|     904677 |            1 |
|     990147 |            1 |
|    1054181 |            1 |
|    1111877 |            3 |
|    1332613 |            1 |
|    1552449 |            2 |
|    2167527 |            3 |
|    2184032 |            5 |
+------------+--------------+
10 rows in set (0.00 sec)
--- SQL-1和SQL-2其实都是在没有排序的情况下,取出前10条数据。但是结果不一样

--
-- SQL-3
--
root@mysqldb 14:08:  [dbt3_s1]> select l_orderkey, l_linenumber from lineitem order by l_orderkey limit 10;    -- 和上面的sql相比,多了一个order by的操作
+------------+--------------+
| l_orderkey | l_linenumber |
+------------+--------------+
|          1 |            1 |     -- 看orderkey为1,对应的linenumber有6条,这就是orderkey的Cardinality仅为130W,而(orderkey + linenumber)就有540W
|          1 |            2 |
|          1 |            3 |
|          1 |            4 |
|          1 |            5 |
|          1 |            6 |
|          2 |            1 |
|          3 |            1 |
|          3 |            2 |
|          3 |            3 |
+------------+--------------+
10 rows in set (0.01 sec)

--- SQL-3 和SQL-2 不同的原因是 他们走了不同的索引
root@mysqldb 14:25:  [dbt3_s1]> explain select l_orderkey, l_linenumber from lineitem\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: lineitem
   partitions: NULL
         type: index
possible_keys: NULL
          key: i_l_shipdate    -- 使用了shipdate进行了索引
      key_len: 4
          ref: NULL
         rows: 5409258
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

root@mysqldb 14:26:  [dbt3_s1]> explain select l_orderkey, l_linenumber from lineitem order by l_orderkey limit 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: lineitem
   partitions: NULL
         type: index
possible_keys: NULL
          key: i_l_orderkey    -- 使用了orderkey进行了查询
      key_len: 4
          ref: NULL
         rows: 10
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

root@mysqldb 14:25:  [dbt3_s1]> explain select * from lineitem limit 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: lineitem
   partitions: NULL
         type: ALL    -- SQL-1进行了全表扫描
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5409258
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
-- 所以,不使用order by取出的结果,可以理解为不是根据主键排序的结果。

 innodb_on_state = off

在MySQL5.5之前,执行 show create table 操作会触发采样,而5.5之后将该参数off后,需要主动执行 analyze table 才会去采样。采样不会锁表或者锁记录。

 

复合索引

root@mysqldb 13:12:  [dbt3_s1]> show create table lineitem\G
*************************** 1. row ***************************
       Table: lineitem
Create Table: CREATE TABLE `lineitem` (
  `l_orderkey` int NOT NULL,
  `l_partkey` int DEFAULT NULL,
  `l_suppkey` int DEFAULT NULL,
  `l_linenumber` int NOT NULL,
  `l_quantity` double DEFAULT NULL,
  `l_extendedprice` double DEFAULT NULL,
  `l_discount` double DEFAULT NULL,
  `l_tax` double DEFAULT NULL,
  `l_returnflag` char(1) DEFAULT NULL,
  `l_linestatus` char(1) DEFAULT NULL,
  `l_shipDATE` date DEFAULT NULL,
  `l_commitDATE` date DEFAULT NULL,
  `l_receiptDATE` date DEFAULT NULL,
  `l_shipinstruct` char(25) DEFAULT NULL,
  `l_shipmode` char(10) DEFAULT NULL,
  `l_comment` varchar(44) DEFAULT NULL,
  PRIMARY KEY (`l_orderkey`,`l_linenumber`),    -- 两个列作为primary,这个就是复合索引
  KEY `i_l_shipdate` (`l_shipDATE`),
  KEY `i_l_suppkey_partkey` (`l_partkey`,`l_suppkey`),
  KEY `i_l_partkey` (`l_partkey`),
  KEY `i_l_suppkey` (`l_suppkey`),
  KEY `i_l_receiptdate` (`l_receiptDATE`),
  KEY `i_l_orderkey` (`l_orderkey`),
  KEY `i_l_orderkey_quantity` (`l_orderkey`,`l_quantity`),
  KEY `i_l_commitdate` (`l_commitDATE`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.01 sec)
  • 例子
root@mysqldb 13:55:  [gavin]> explain select * from test_index_2 where a = 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_index_2
   partitions: NULL
         type: ref
possible_keys: idx_mul_ab
          key: idx_mul_ab    -- 符合最左前缀,走了索引
      key_len: 5
          ref: const
         rows: 2
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.01 sec)

root@mysqldb 14:13:  [gavin]>  explain select * from test_index_2 where a = 1 and b = 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_index_2
   partitions: NULL
         type: ref
possible_keys: idx_mul_ab
          key: idx_mul_ab    -- 符合最左前缀,走了索引
      key_len: 10
          ref: const,const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

root@mysqldb 14:14:  [gavin]> explain select * from test_index_2 where b = 2\G -- 只查询b
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_index_2
   partitions: NULL
         type: ALL    -- 不符合最左前缀,不走索引
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 6
     filtered: 16.67
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

root@mysqldb 14:20:  [gavin]> explain select * from test_index_2 where a=1 or b = 2\G -- 使用or,要求结果集是并集
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_index_2
   partitions: NULL
         type: ALL    -- 全表扫描,因为b上没有索引,所以需要全表扫描才能知道哪些行的值b = 2。
                      -- 既然已经全表扫描,a的值就一起过滤了,就没必要再查一次a的索引
possible_keys: idx_mul_ab
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 6
     filtered: 44.44
        Extra: Using where
1 row in set, 1 warning (0.00 sec)


---- 还是只使用b列去做范围查询,走了索引,注意查询的是 count(*)
---- 
root@mysqldb 14:21:  [gavin]> explain select count(*) from test_index_2 where b >1 and b < 3\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_index_2
   partitions: NULL
         type: index
possible_keys: idx_mul_ab
          key: idx_mul_ab
      key_len: 10
          ref: NULL
         rows: 6
     filtered: 16.67
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
-- 因为要求的是count(*), 要求所有的记录的和,那索引a是包含了全部的记录的,即扫描(a,b)的索引也是可以得到count(*)
-- 先根据索引找到所有的行,再根据where条件筛选出符合条件的行。所以Extra是: Using where; Using index

root@mysqldb 14:23:  [gavin]> explain select * from test_index_2 where a = 1 and c = 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test_index_2
   partitions: NULL
         type: ref
possible_keys: idx_mul_ab
          key: idx_mul_ab    -- 如上同理。先根据联合索引筛选出所有的行,再根据c的条件判断
      key_len: 5
          ref: const
         rows: 2
     filtered: 16.67
        Extra: Using where
1 row in set, 1 warning (0.00 sec)
posted @ 2024-03-11 23:06  bingo-HF  阅读(8)  评论(0编辑  收藏  举报