一. Secondary Index(二级索引)
1.1. Secondary Index 介绍

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

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)的 查找过程 ,即为 回表 。
4. 上述过程都是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('Tom');
insert into idx_username(1, 'Tom');
insert into idx_registdate(1, '1990-01-01')
commit;
• 假设要查找TOM的 email :
1. 先查找 Tom 对应的 userid ,即找的是 idx_username表 (对应之前就是在idx_username索引中找tom)
2. 得到 userid 后,再去 userinfo表 ,通过 userid 得到 email 字段的内容(对对应之前就是在 聚集索引 中找userid的记录(row data))
3. 上述两次查找就是 人工回表

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

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

1.4. 堆表和IOT表二级索引的对比

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

1.5. index with included column(含列索引)
在上面给出的 userinfo 的例子中,如果要查找某个 用户的email ,需要回表,如何不回表进行查询呢?

1. 方案一 :复合索引
-- 表结构
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是不排序的,只是带到索引中,方便查找
2. 方案二:拆表
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. 回表的代价

mysql> alter table employees add index idx_date (hire_date); -- 给 employees 增加一个索引

mysql> 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`) -- 新增的索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
-- 查询语句1
mysql> select * from employees where emp_no between 10000 and 20000; -- 主键查找1W条数据

-- 查询语句2
mysql> select * from employees where hire_date >= '1990-01-01' limit 10000; -- select * 操作,每次查找需要回表
1. 对于 查询语句1 ,假设一个页中有100条记录,则只需要100次IO;
2. 对于 查询语句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表中进行 回表 的次数
3. 在MySQL5.6之前,实际使用过程中,优化器可能会选择直接进行 扫表 ,而 不会 进行如此多的回表操作。

2.2. MRR 介绍
MRR:针对 物理访问 ,随机转顺序,空间换时间。

1. 开辟一块 内存 空间作为cache
  ◦ 默认为 32M ,注意是 线程级 的,不建议设置的很大;
mysql> show variables like "%read_rnd%";
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| read_rnd_buffer_size | 33554432 | -- 32M
+----------------------+----------+
1 row in set (0.00 sec)
2. 将 需要回表 的 主键 放入上述的 内存 空间中(空间换时间), 放满 后进行 排序 (随机转顺序);
3. 将 排序 好数据(主键)一起进行回表操作,以提高性能;
  ◦ 在 IO Bound 的SQL场景下,使用MRR比不使用MRR系能 提高 将近 10倍 (磁盘性能越低越明显);
  ◦ 如果数据都在内存中,MRR的帮助不大, 已经在内存 中了,不存在随机读的概念了(随机读主要针对物理访问)
SSD 仍然需要开启该特性,多线程下的随机读确实很快,但是我们这里的操作是一条SQL语句,是 单线程 的,所以 顺序 的访问还是比 随机 访问要 更快 。
mysql> 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_in
dex_extensions=on,condition_fanout_filter=on,derived_merge=on
1 row in set (0.00 sec)
-- 其中MRR默认是打开的 mrr=on,不建议关闭
mysql> 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 | 298124 | 50.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec) -- 虽然mrr=on打开了,但是没有使用MRR
mysql> set optimizer_switch='mrr_cost_based=off'; -- 将该值off,不让MySQL对MRR进行成本计算(强制使用MRR)
Query OK, 0 rows affected (0.00 sec)
mysql> 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 | 149062 | 100.00 | Using index condition; Using MRR |
+----+-------------+-----------+------------+-------+---------------+----------+---------+------+--------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 使用了MRR

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

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

3.4. 获取root页

mysql> use information_schema;Reading table information for completion of table and column names

You can turn off this feature to get a quicker startup with -A
Database changed
mysql> 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)
mysql> select * from INNODB_SYS_INDEXES where space<>0 limit 1\G
*************************** 1. row ***************************
INDEX_ID: 18
NAME: PRIMARY
TABLE_ID: 16
TYPE: 3
N_FIELDS: 1
PAGE_NO: 3 -- 根据官方文档,该字段就是B+树root页的PAGE_NO
SPACE: 5
MERGE_THRESHOLD: 50
1 row in set (0.01 sec)
-- 没有table的name,只有ID
mysql> 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          | 64 | 3 | 43 | 3 |
| dbt3/customer | i_c_nationkey | 65 | 0 | 43 | 4 |
| dbt3/lineitem    | PRIMARY | 66 | 3   | 44  | 3     |
| dbt3/lineitem     | i_l_shipdate | 67 | 0 | 44 | 4 |
| dbt3/lineitem     | i_l_suppkey_partkey | 68 | 0 | 44 | 5 |
| dbt3/lineitem     | i_l_partkey | 69 | 0 | 44 | 6 |
| dbt3/lineitem      | i_l_suppkey | 70 | 0 | 44 | 7 |
| dbt3/lineitem     | i_l_receiptdate | 71 | 0 | 44 | 8 |
| dbt3/lineitem     | i_l_orderkey | 72 | 0 | 44 | 9 |
| dbt3/lineitem     | i_l_orderkey_quantity | 73 | 0   | 44   | 10   |
| dbt3/lineitem     | i_l_commitdate | 74 | 0   | 44   | 11   |
| dbt3/nationq      | PRIMARY | 75 | 3   | 45   | 3    |
| dbt3/nation     | i_n_regionkey | 76 | 0   | 45   | 4   |
| dbt3/orders     | PRIMARY | 77 | 3   | 46   | 3 |
| dbt3/orders       | i_o_orderdate | 78 | 0   | 46   | 4 |
| dbt3/orders | i_o_custkey | 79 | 0   | 46   | 5 |
| dbt3/part       | PRIMARY | 80 | 3 | 47 | 3 |
| dbt3/partsupp | PRIMARY | 81 | 3 | 48    | 3 |
| dbt3/partsupp | i_ps_partkey | 82 | 0 | 48 | 4 |
| dbt3/partsupp | i_ps_suppkey | 83 | 0 | 48 | 5 |
| dbt3/region | PRIMARY | 84 | 3 | 49 | 3 |
| dbt3/supplier | PRIMARY | 85 | 3 | 50 | 3 |
| dbt3/supplier | i_s_nationkey | 86 | 0 | 50 | 4 |
| dbt3/time_statistics | GEN_CLUST_INDEX | 87 | 1 | 51 | 3 |
+----------------------+-----------------------+----------+------+-------+---------+
24 rows in set (0.00 sec)
-- 聚集索引页的root页的PAGE_NO一般就是3

3.5. 读取PAGE_LEVEL

mysql> select count(*) from dbt3.lineitem;
+----------+
| count(*) |
+----------+
| 6001215 |
+----------+
1 row in set (5.68 sec)
shell> hexdump -h
hexdump: invalid option -- 'h'
hexdump: [-bcCdovx] [-e fmt] [-f fmt_file] [-n length] [-s skip] [file ...]

shell> hexdump -s 24640 -n 2 -Cv lineitem.ibd
00006040 00 02 |..|
00006042 1. 24640 = 8192 * 3 + 64
  ◦ 其中 8192 是我的页大小
  ◦ root页 的 PAGE_NO 为 3 ,表示是 第4个页 ,则需要 跳过 前面 3个页 ,才能 定位到root页 ,所以要 *3
  ◦ 然后加上 64 个字节的偏移量,即可定位到 PAGE_LEVEL
2. -n 2 表示读取的字节数,这里读取 2个字节 ,即可以读到 PAGE_LEVEL
根据上述 hexdump 的结果,root页中的 PAGE_LEVEL 为2,表示该索引的高度为 3 (从0开始计算)

23.Secondary Index的更多相关文章

  1. Cassandra Secondary Index 介绍

    摘要 本文主要介绍cassandra中的索引,物化视图,有些知识点需要对cassandra有基本的认识才能理解.比如数据在cassandra节点中如何分布.如果有不明白的地方可以看本专栏之前文章.或者 ...

  2. [20180608]Wrong Results with IOT, Added Column and Secondary Index.txt

    [20180608]Wrong Results with IOT, Added Column and Secondary Index.txt --//链接:http://db-oriented.com ...

  3. 聚簇索引(clustered index )和非聚簇索引(secondary index)的区别

    这两个名字虽然都叫做索引,但这并不是一种单独的索引类型,而是一种数据存储方式.对于聚簇索引存储来说,行数据和主键B+树存储在一起,辅助键B+树只存储辅助键和主键,主键和非主键B+树几乎是两种类型的树. ...

  4. Differences between INDEX, PRIMARY, UNIQUE, FULLTEXT in MySQL?

    487down vote Differences KEY or INDEX refers to a normal non-unique index.  Non-distinct values for ...

  5. Oracle alter index rebuild 与 ORA-08104 说明

    在ITPUB 论坛上看到的一个帖子,很不错.根据论坛的帖子重做整理了一下. 原文链接如下: alter index rebuild online引发的血案 http://www.itpub.net/t ...

  6. MySQL: Building the best INDEX for a given SELECT

    Table of Contents The ProblemAlgorithmDigressionFirst, some examplesAlgorithm, Step 1 (WHERE "c ...

  7. Index on DB2 for z/OS: DB2 for z/OS 的索引

    可以创建在任何表上的索引: Unique Index:An index that ensures that the value in a particular column or set of col ...

  8. 【mysql】关于Index Condition Pushdown特性

    ICP简介 Index Condition Pushdown (ICP) is an optimization for the case where MySQL retrieves rows from ...

  9. adaptive hash index

    An optimization for InnoDB tables that can speed up lookups using = and IN operators, by constructin ...

随机推荐

  1. 百度地图API:自定义控件

    HTML: <!DOCTYPE html> <html> <head> <meta name="viewport" content=&qu ...

  2. 【SPOJ116】Intervals

    题目大意:有 N 个区间,在区间 [a, b] 中至少取任意互不相同的 c 个整数.求在满足 N 个区间约束的情况下,至少要取多少个正整数. 题解:差分约束系统模板题. 差分约束系统是对于 N 个变量 ...

  3. 关于json_encode转数组为json对象时里有数组格式数据的问题

    前言:这次是给一款小程序提供接口时发现的 别的不多说,下面直接看出现问题的json数据 可以看到,这是一个大的json对象,是由多维数组组成,一般api接口提供的也是这种格式. 但是仔细看红框中的内容 ...

  4. 简单ATM系统

    模拟实现一个ATM + 购物商城程序1.额度 15000或自定义2.实现购物商城,买东西加入 购物车,调用信用卡接口结账3.可以提现,手续费5%4.每月22号出账单,每月10号为还款日,过期未还,按欠 ...

  5. 给你的手机加上安全保障,请设置SIM卡PIN码

    [手机上了锁,为啥还丢钱?专家支招:设置SIM卡PIN码]智能手机一旦丢失,不仅会带来诸多不便,甚至还会造成个人隐私泄露及财产损失. 然而很多人认为,自己已经设置了手机屏锁.支付密码.指纹锁等防御措施 ...

  6. 解决access 导出 excel 字段截断错误的问题

    解决方法:这个问题通过从EXCEL中导入外部数据,也就是ACCESS数据可以解决. 1.选择导入数据 2.点击选择数据源 选择需要导入的access数据源

  7. Tomcat源码组织结构

    Tomcat 源码组织结构 目录结构 这里所介绍的目录结构,是使用CATALINA-BASE变量定义的路径,如果没有通过配置多个CATALINA-BASE目录来使用多实例,则CATALINA-BASE ...

  8. (BFS/DFS) leetcode 200. Number of Islands

    Given a 2d grid map of '1's (land) and '0's (water), count the number of islands. An island is surro ...

  9. ArrayList 实现随机点名

    package lijun.cn.demo1; import java.util.ArrayList; import java.util.Random; public class CallName { ...

  10. 关键字(8):数据库记录的增删查改insert,delete,select,update

    insert:一般只要参数个数和类型没问题,不会插入异常 INSERT INTO t_pos_dynamic_map(autoid, lt_termno, lt_merchno) VALUES(SEQ ...