zoukankan      html  css  js  c++  java
  • 021:二级索引

    一. Secondary Index(二级索引)

    1.1. Secondary Index 介绍

    • Clustered Index(聚集索引)
      • 叶子节点存储所有记录(all row data)

    • Secondary Index(二级索引)
      • 也可以称为非聚集索引
      • 叶子节点存储的是索引和主键信息
      • 在找到索引后,得到对应的主键,再回到聚集索引中找主键对应的记录(row data)
        • Bookmark Lookup (书签查找)
        • 俗称回表
        • 回表不止多一次IO
        • 而是多N次IO(N=树的高度)

    • Secondary Index 查找数据

    1.2. Secondary Index 回表

    create table userinfo (
    userid int not null auto_increment,
    username varchar(30),
    registdate datetime,
    email varchar(50),
    primary key(userid),
    unique key idx_username(username),
    key idx_registdate(registdate)
    );
    
    1. 假设查找username为Tom,先找二级索引idx_username,通过找到key 为Tom,并得到对应的primary key:userid_a
    2. 得到了userid_a后,再去找聚集索引中userid_a的记录(row data)
    3. 上述一次通过二级索引得到数据(row data)的查找过程,即为回表。整个过程都是MySQL自动帮你做的。
    • 可以将上述的userinfo 表进行人工拆分,从而进行人工回表,拆分如下:
    -- 表1 : 创建一个只有主键userid的表,将原来的二级索引人工拆分成独立的表
    create table userinfo(
    userid int not null auto_increment,
    username varchar(30),
    registdate datetime,
    email varchar(50),
    primary key(userid)
    );
    -- 表2:idx_username表,将userid和username作为表的字段,并做一个复合主键(对应原来的idx_username索引)
    create table idx_username(
    userid int not null,
    username varchar(30),
    primary key(username, userid)
    );
    -- 表3:idx_registdate表,将userid和registdate作为表的字段,并做一个复合主键(对应原来的idx_registdate 索引)
    create table idx_registdate(
    userid int not null,
    registdate datetime,
    primary key(registdate, userid)
    );
    -- 表4:一致性约束表
    create table idx_username_constraint(
    username varchar(30),
    primary key(username)
    );
    -- 插入数据,使用事物,要么全插,要么全不插入
    start transaction;
    insert into userinfo values(1, 'Tom', '1990-01-01', 'tom@123.com');
    insert into idx_username_constraint values('Tom');
    insert into idx_username values(1, 'Tom');
    insert into idx_registdate values(1, '1990-01-01');
    commit;
    
    --
    -- 执行sql
    --
    
    (root@localhost) 10:49:12 [(none)]> use mytest;
    Database changed
    (root@localhost) 09:45:19 [mytest]> create table userinfo (
        -> userid int not null auto_increment,
        -> username varchar(30),
        -> registdate datetime,
        -> email varchar(50),
        -> primary key(userid),
        -> unique key idx_username(username),
        -> key idx_registdate(registdate)
        -> );
    Query OK, 0 rows affected (0.01 sec)
    
    (root@localhost) 09:45:46 [mytest]> create table idx_username(
        -> userid int not null,
        -> username varchar(30),
        -> primary key(username, userid)
        -> );
    Query OK, 0 rows affected (0.01 sec)
    
    (root@localhost) 09:46:19 [mytest]> create table idx_registdate(
        -> userid int not null,
        -> registdate datetime,
        -> primary key(registdate, userid)
        -> );
    Query OK, 0 rows affected (0.00 sec)
    
    (root@localhost) 09:46:29 [mytest]> create table idx_username_constraint(
        -> username varchar(30),
        -> primary key(username)
        -> );
    Query OK, 0 rows affected (0.00 sec)
    
    (root@localhost) 10:14:23 [mytest]> start transaction;
    Query OK, 0 rows affected (0.00 sec)
    
    (root@localhost) 10:15:36 [mytest]> insert into userinfo values(1, 'Tom', '1990-01-01', 'tom@123.com');
    Query OK, 1 row affected (0.00 sec)
    
    (root@localhost) 10:15:36 [mytest]> insert into idx_username_constraint values('Tom');
    Query OK, 1 row affected (0.00 sec)
    
    (root@localhost) 10:15:36 [mytest]> insert into idx_username values(1, 'Tom');
    Query OK, 1 row affected (0.00 sec)
    
    (root@localhost) 10:15:36 [mytest]> insert into idx_registdate values(1, '1990-01-01');
    Query OK, 1 row affected (0.00 sec)
    
    (root@localhost) 10:15:36 [mytest]> commit;
    Query OK, 0 rows affected (0.00 sec)
    
    • 假设要查找TOM的email
      • 先查找Tom 对应的userid ,即找的是idx_username表(对应之前就是在idx_username索引中找tom)
      • 得到userid 后,再去userinfo表,通过userid 得到email 字段的内容(对应之前就是在聚集索引中找userid的记录(row data))
      • 上述两次查找就是人工回表

    拆表后,就需要开发自己去实现回表的逻辑;而开始的一张大表,则是MySQL自动实现该逻辑。

    1.3. 堆表的二级索引

    • 在堆表中,是没有聚集索引的, 所有的索引都是二级索引
    • 索引的叶子节点存放的是key指向堆中记录的指针(物理位置)

    1.4. 堆表和索引组织表(IOT)二级索引的对比

    • 堆表中的二级索引查找不需要回表,且查找速度和主键索引一致,因为两者的叶子节点存放的都是指向数据的指针;反之IOT表的的二级索引查找需要回表
    • 堆表中某条记录(row data)发生更新且无法原地更新时,该记录(row data)的物理位置将发生改变;此时, 所有索引中对该记录的指针都需要更新(代价较大);反之,IOT表中的记录更新,且主键没有更新时, 二级索引无需更新(通常来说主键是不更新的)
      • 实际数据库设计中,堆表的数据无法原地更新时,且在一个页内有剩余空间时,原来数据的空间位置不会释放,而是使用指针指向新的数据空间位置,此时该记录对应的所有索引就无需更改了;
      • 如果页内没有剩余空间,所有的索引还是要更新一遍;
    • IOT表页内是有序的,页与页之间也是有序的,做range查询很快。

    1.5. index with included column(含列索引)

    在上面给出的userinfo 的例子中,如果要查找某个用户的email ,需要回表,如何不回表进行查询呢?

    • 方案一:复合索引
    -- 表结构
    create table userinfo (
    userid int not null auto_increment,
    username varchar(30),
    registdate datetime,
    email varchar(50),
    primary key(userid),
    unique key idx_username(username, email), -- 索引中有email,可以直接查到,不用回表
    key idx_registdate(registdate)
    );
    -- 查询
    select email from userinfo where username='Tom';
    

    该方案可以做到只查一次索引就可以得到用户的email,但是复合索引中username和email都要排序
    含列索引的意思是索引中只对username 进行排序,email是不排序的,只是带到索引中,方便查找。

    • 方案二:拆表
    create table userinfo (
    userid int not null auto_increment,
    username varchar(30),
    registdate datetime,
    email varchar(50),
    primary key(userid),
    key idx_registdate(registdate)
    );
    create table idx_username_include_email (
    userid int not null,
    username varchar(30),
    email varchar(50),
    primary key(username, userid),
    unique key(username)
    );
    -- 两个表的数据一致性可以通过事物进行保证
    

    通过拆表的方式,查找idx_username_include_email 表,既可以通过username 找到email ,但是需要告诉研发,如果想要通过useranme得到email,查这张表速度更快,而不是查userinfo表

    • 对于含有多个索引的IOT表,可以将索引拆成不同的表,进而提高查询速度
    • 但是实际使用中,就这个例子而言,使用复合索引,代价也不会太大。

    二. Multi-Range Read 多范围读(MRR)

    2.1. 回表的代价

    (root@localhost) 10:53:33 [employees]>  alter table employees add index idx_date (hire_date);  -- 给 employees 增加一个普通索引
    Query OK, 0 rows affected (0.89 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    
    (root@localhost) 10:54:16 [employees]> show create table employees G;
    *************************** 1. row ***************************
           Table: employees
    Create Table: CREATE TABLE `employees` (
      `emp_no` int(11) NOT NULL,
      `birth_date` date NOT NULL,
      `first_name` varchar(14) NOT NULL,
      `last_name` varchar(16) NOT NULL,
      `gender` enum('M','F') NOT NULL,
      `hire_date` date NOT NULL,
      PRIMARY KEY (`emp_no`),
      KEY `idx_date` (`hire_date`)   --新增的索引dx_date
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    1 row in set (0.00 sec)
    
    -- 查询语句1
    select * from employees where emp_no between 10000 and 20000; -- 主键查找1W条数据
    -- 查询语句2
    select * from employees where hire_date >= '1990-01-01' limit 10000; -- select * 操作,每次查找需要回表
    
    • 对于查询语句1 ,假设一个页中有100条记录,则只需要100次IO
    • 对于查询语句2 ,此次查询中,假设聚集索引hire_date索引(二级索引)的高度都是3 ,且查找1W 条(假设不止1W条),则需要查询的IO数为(3+N)+3W
      • 3 为第一次找到hire_date>=1990-01-01 所在的页(二级索引)的IO次数
      • N 为从第一次找到的页往后读页的IO次数(注意二级索引也是连续的, 不需要从根再重新查找)
        • 所以3+N 就是在hire_date (二级索引)中读取IO的次数
      • 3W 为在IOT表中进行回表的次数
    • 在MySQL5.6之前,实际使用过程中,优化器可能会选择直接进行扫表,而不会进行如此多的回表操作。

    2.2. MRR 介绍

    MRR:它的作用针对基于辅助/第二索引的查询(磁盘的随机访问),随机顺序空间时间

    • 开辟一块内存空间作为cache
      • 配置为8M ,注意是线程级的,不建议设置的很大;
    • 参数read_rnd_buffer_size用来控制键值缓冲区大小,当大于该值,执行器对已经缓存的数据根据RowID进行排序,并通过RowID来取得行数据
    (root@localhost) 10:45:04 [employees]> show variables like "%read_rnd%";
    +----------------------+---------+
    | Variable_name        | Value   |
    +----------------------+---------+
    | read_rnd_buffer_size | 8388608 |   --8M
    +----------------------+---------+
    1 row in set (0.00 sec)
    
    (root@localhost) 10:45:12 [employees]>
    
    • 需要回表主键放入上述的内存空间中(空间换时间), 放满后进行排序(随机转顺序);
    • 排序好数据(主键)一起进行回表操作,以提高性能;
      • IO Bound 的SQL场景下,使用MRR不使用MRR系能提高将近10倍(磁盘性能越低越明显);
      • 如果数据都在内存中MRR帮助不大, 已经在内存中了,不存在随机读的概念了(随机读主要针对物理访问)

    SSD 仍然需要开启该特性,多线程下的随机读确实很快,但是我们这里的操作是一条SQL语句,是单线程的,所以顺序的访问还是比随机访问要更快

    (root@localhost) 10:15:38 [mytest]>  show variables like 'optimizer_switch'G
    *************************** 1. row ***************************
    Variable_name: optimizer_switch
            Value: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
    1 row in set (0.00 sec)
    -- 其中MRR默认是打开的 mrr=on,不建议关闭
    
    (root@localhost) 10:57:39 [employees]> (root@localhost) 10:57:39 [employees]> explain select * from employees where hire_date >= '1990-01-01';
    +----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    | id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | employees | NULL       | ALL  | idx_date      | NULL | NULL    | NULL | 299423 |    50.00 | Using where |
    +----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    (root@localhost) 10:57:44 [employees]>
    -- 虽然mrr=on打开了,但是没有使用MRR
    
    (root@localhost) 10:57:44 [employees]> set optimizer_switch='mrr_cost_based=off'; -- 将该值off,不让MySQL对MRR进行成本计算(强制使用MRR)
    Query OK, 0 rows affected (0.00 sec)
    
    (root@localhost) 10:58:47 [employees]> explain select * from employees where hire_date >= '1990-01-01';
    +----+-------------+-----------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+
    | id | select_type | table     | partitions | type  | possible_keys | key      | key_len | ref  | rows   | filtered | Extra                            |
    +----+-------------+-----------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+
    |  1 | SIMPLE      | employees | NULL       | range | idx_date      | idx_date | 3       | NULL | 149711 |   100.00 | Using index condition; Using MRR |
    +----+-------------+-----------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+
    1 row in set, 1 warning (0.00 sec)
    (root@localhost) 10:58:52 [employees]>
    -- 使用了MRR
    

    三. 求B+树的高度

    3.1 B+树

    3.1 B+树高度

    如上图所示,每个页的Page Header 中都包含一个PAGE_LEVEL 的信息,表示该页所在B+树中的层数, 叶子节点的PAGE_LEVEL为0
    所以树的高度就是root页PAGE_LEVEL + 1

    3.3. PAGE_LEVEL

    从一个页的第64字节开始读取,然后再读取2个字节,就能得到PAGE_LEVEL 的值

    3.4. 获取root页

    官方文档

    (root@localhost) 10:58:52 [employees]> use information_schema;
    Database changed
    (root@localhost) 13:45:37 [information_schema]>  desc INNODB_SYS_INDEXES;
    +-----------------+---------------------+------+-----+---------+-------+
    | Field           | Type                | Null | Key | Default | Extra |
    +-----------------+---------------------+------+-----+---------+-------+
    | INDEX_ID        | bigint(21) unsigned | NO   |     | 0       |       |
    | NAME            | varchar(193)        | NO   |     |         |       |
    | TABLE_ID        | bigint(21) unsigned | NO   |     | 0       |       |
    | TYPE            | int(11)             | NO   |     | 0       |       |
    | N_FIELDS        | int(11)             | NO   |     | 0       |       |
    | PAGE_NO         | int(11)             | NO   |     | 0       |       |
    | SPACE           | int(11)             | NO   |     | 0       |       |
    | MERGE_THRESHOLD | int(11)             | NO   |     | 0       |       |
    +-----------------+---------------------+------+-----+---------+-------+
    8 rows in set (0.00 sec)
    
    (root@localhost) 13:45:56 [information_schema]>  select * from INNODB_SYS_INDEXES where space<>0 limit 1G
    *************************** 1. row ***************************
           INDEX_ID: 32
               NAME: PRIMARY
           TABLE_ID: 27
               TYPE: 3
           N_FIELDS: 2
            PAGE_NO: 3                 -- 根据官方文档,该字段就是B+树root页的PAGE_NO
              SPACE: 16
    MERGE_THRESHOLD: 50
    1 row in set (0.01 sec)
    
    (root@localhost) 13:46:27 [information_schema]> desc INNODB_SYS_TABLES;
    +---------------+---------------------+------+-----+---------+-------+
    | Field         | Type                | Null | Key | Default | Extra |
    +---------------+---------------------+------+-----+---------+-------+
    | TABLE_ID      | bigint(21) unsigned | NO   |     | 0       |       |
    | NAME          | varchar(655)        | NO   |     |         |       |
    | FLAG          | int(11)             | NO   |     | 0       |       |
    | N_COLS        | int(11)             | NO   |     | 0       |       |
    | SPACE         | int(11)             | NO   |     | 0       |       |
    | FILE_FORMAT   | varchar(10)         | YES  |     | NULL    |       |
    | ROW_FORMAT    | varchar(12)         | YES  |     | NULL    |       |
    | ZIP_PAGE_SIZE | int(11) unsigned    | NO   |     | 0       |       |
    | SPACE_TYPE    | varchar(10)         | YES  |     | NULL    |       |
    +---------------+---------------------+------+-----+---------+-------+
    9 rows in set (0.00 sec)
    
    (root@localhost) 13:46:56 [information_schema]> SELECT
        -> b. NAME,
        -> a. NAME,
        -> index_id,
        -> type,
        -> a.space,
        -> a.PAGE_NO
        -> FROM
        -> INNODB_SYS_INDEXES AS a,
        -> INNODB_SYS_TABLES AS b
        -> WHERE
        -> a.table_id = b.table_id
        -> AND a.space <> 0
        -> AND b. NAME LIKE "dbt3/%";
    +----------------------+-----------------------+----------+------+-------+---------+
    | NAME                 | NAME                  | index_id | type | space | PAGE_NO |
    +----------------------+-----------------------+----------+------+-------+---------+
    | dbt3/customer        | PRIMARY               |     2166 |    3 |  1644 |       3 |
    | dbt3/customer        | i_c_nationkey         |     2167 |    0 |  1644 |       4 |
    | dbt3/lineitem        | PRIMARY               |     2168 |    3 |  1645 |       3 |
    | dbt3/lineitem        | i_l_shipdate          |     2169 |    0 |  1645 |       4 |
    | dbt3/lineitem        | i_l_suppkey_partkey   |     2170 |    0 |  1645 |       5 |
    | dbt3/lineitem        | i_l_suppkey           |     2172 |    0 |  1645 |       7 |
    | dbt3/lineitem        | i_l_receiptdate       |     2173 |    0 |  1645 |       8 |
    | dbt3/lineitem        | i_l_orderkey_quantity |     2175 |    0 |  1645 |      10 |
    | dbt3/lineitem        | i_l_commitdate        |     2176 |    0 |  1645 |      11 |
    | dbt3/nation          | PRIMARY               |     2177 |    3 |  1646 |       3 |
    | dbt3/nation          | i_n_regionkey         |     2178 |    0 |  1646 |       4 |
    | dbt3/orders          | PRIMARY               |     2179 |    3 |  1647 |       3 |
    | dbt3/orders          | i_o_orderdate         |     2180 |    0 |  1647 |       4 |
    | dbt3/orders          | i_o_custkey           |     2181 |    0 |  1647 |       5 |
    | dbt3/part            | PRIMARY               |     2182 |    3 |  1648 |       3 |
    | dbt3/partsupp        | PRIMARY               |     2183 |    3 |  1649 |       3 |
    | dbt3/partsupp        | i_ps_suppkey          |     2185 |    0 |  1649 |       5 |
    | dbt3/region          | PRIMARY               |     2186 |    3 |  1650 |       3 |
    | dbt3/supplier        | PRIMARY               |     2187 |    3 |  1651 |       3 |
    | dbt3/supplier        | i_s_nationkey         |     2188 |    0 |  1651 |       4 |
    | dbt3/time_statistics | GEN_CLUST_INDEX       |     2189 |    1 |  1652 |       3 |
    +----------------------+-----------------------+----------+------+-------+---------+
    21 rows in set (0.01 sec)
    
    (root@localhost) 13:47:27 [information_schema]>
    
    -- 聚集索引页的root页的PAGE_NO一般就是3
    

    3.5. 读取PAGE_LEVEL

    (root@localhost) 13:49:04 [information_schema]> select count(*) from dbt3.lineitem;
    +----------+
    | count(*) |
    +----------+
    |  6001215 |
    +----------+
    1 row in set (43.47 sec)
    
    [root@localhost-m(252) /r2/mysqldata/dbt3]# hexdump -s 24640 -n 2 -Cv lineitem.ibd
    00006040  02 22                                             |."|
    00006042
    [root@localhost-m(252) /r2/mysqldata/dbt3]#
    
    • 24640 = 8192 * 3 + 64
      • 其中8192 是页大小
      • root页PAGE_NO 为3 ,表示是第4个页,则需要跳过前面3个页,才能定位到root页,所以要*3
      • 然后加上64 个字节的偏移量,即可定位到PAGE_LEVEL
    • -n 2 表示读取的字节数,这里读取2个字节,即可以读到PAGE_LEVEL
      根据上述hexdump 的结果,root页中的PAGE_LEVEL2,表示该索引的高度为3 (从0开始计算)
  • 相关阅读:
    英雄联盟离线更新方法
    (七) Keras 绘制网络结构和cpu,gpu切换
    剑指offer | 链表中的倒数第K个结点 | 12
    剑指offer | 从尾到头打印链表 | 11
    剑指offer | 栈的压入,弹出序列 | 10
    剑指offer | 包含min函数的栈 | 09
    剑指offer | 调整数组顺序使奇数位于偶数前面 | 08
    剑指offer | 旋转数组的最小数字 | 07
    剑指offer | 两个栈实现一个队列 | 06
    剑指offer | 替换空格 | 05
  • 原文地址:https://www.cnblogs.com/gczheng/p/8250690.html
Copyright © 2011-2022 走看看