1、b+树的查找过程

    如果要查找数据项 29,那么首先会把磁盘块 1 由磁盘加载到内存, 此时发生一次 IO,在内存中用二分查找确定 29 在 17 和 35 之间,锁定磁盘块 1 的 P2 指针,内存时间因为非常短(相比磁盘的 IO)可以忽略不计,通过磁盘 块 1 的 P2 指针的磁盘地址把磁盘块 3 由磁盘加载到内存,发生第二次 IO,29 在 26 和 30 之间,锁定磁盘块 3 的 P2 指针,通过指针加载磁盘块 8 到内存, 发生第三次 IO,同时内存中做二分查找找到 29,结束查询,总计三次 IO。真 实的情况是,3 层的 b+树可以表示上百万的数据,如果上百万的数据查找只需 要三次 IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次 IO, 那么总共需要百万次的 IO,显然成本非常非常高。

2、b+树性质

2.1. 通过上面的分析,我们知道 IO 次数取决于 b+数的高度 h,假设当前数据表的 数据为 N,每个磁盘块的数据项的数量是 m,则有 h=㏒(m+1)N,当数据量 N 一定的情况下,m 越大,h 越小;而 m = 磁盘块的大小 / 数据项的大小,磁 盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数 据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如 int 占 4 字节,要比 bigint8 字节少一半。这也是为什么 b+树要求 把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据 项会大幅度下降,导致树增高。当数据项等于 1 时将会退化成线性表。

2.2. 当 b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按 照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时 候,b+树会优先比较 name 来确定下一步的所搜方向,如果 name 相同再依次 比较 age 和 sex,最后得到检索的数据;但当(20,F)这样的没有 name 的数据来 的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候 name 就 是第一个比较因子,必须要先根据 name 来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用 name 来指定搜索方向,但下 一个字段 age 的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性 别是 F 的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

3、慢查询优化

关于 MySQL 索引原理是比较枯燥的东西,大家只需要有一个感性的认识,并不 需要理解得非常透彻和深入。我们回头来看看一开始我们说的慢查询,了解完索 引原理之后,大家是不是有什么想法呢?先总结一下索引的几大基本原则:

3.1. 最左前缀匹配原则,非常重要的原则,mysql 会一直向右匹配直到遇到范围查 询(>、 3 and d= 4 如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索 引则都可以用到,a,b,d 的顺序可以任意调整。

3.2. =和 in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意 顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式。

3.3. 尽量选择区分度高的列作为索引,区分度的公式是 count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一 键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就是 0,那可 能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一 般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条记录。

3.4.索引列不能参与计算,保持列“干净”,比如 from_unixtime(create_time) = ’ 2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字 段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所 以语句应该写成 create_time = unix_timestamp(’2014-05-29’)。

3.5、尽量的扩展索引,不要新建索引。比如表中已经有 a 的索引,现在要加(a,b) 的索引,那么只需要修改原来的索引即可。

4、查询优化神器 - explain 命令

关于 explain 命令相信大家并不陌生,具体用法和字段含义可以参考官网 explain-output,这里需要强调 rows 是核心指标,绝大部分 rows 小的语句执 行一定很快(有例外,下面会讲到)。所以优化语句基本上都是在优化 rows。

4.1 慢查询优化基本步骤

4.1.1 先运行看看是否真的很慢,注意设置 SQL_NO_CACHE

4.1.2 where 条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的 where 都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询, 看哪个字段的区分度最高

4.1.3 explain 查看执行计划

4.1.4  order by limit 形式的 sql 语句让排序的表优先查

4.1.5 了解业务方使用场景

4.1.6 加索引时参照建索引的几大原则 

4.1.7 观察结果,不符合预期继续从 4.1.1 开始分析

5、几个慢查询案例

5.1 下面几个例子详细解释了如何分析和优化慢查询。

复杂语句写法

很多情况下,我们写 SQL 只是为了实现功能,这只是第一步,不同的语句书写 方式对于效率往往有本质的差别,这要求我们对 mysql 的执行计划和索引原则 有非常清楚的认识,请看下面的语句:

mysql 去重后排序 mysql列去重_mysql 去重后排序

mysql 去重后排序 mysql列去重_mysql 去重后排序_02

先运行一下,53 条记录 1.87 秒,又没有用聚合语句,比较慢

explain

mysql 去重后排序 mysql列去重_mysql 去重后排序_03

简述一下执行计划,首先 mysql 根据 idx_last_upd_date 索引扫描 cm_log 表 获得 379 条记录;然后查表扫描了 63727 条记录,分为两部分,derived 表示 构造表,也就是不存在的表,可以简单理解成是一个语句形成的结果集,后面的 数字表示语句的 ID。derived2 表示的是 ID = 2 的查询构造了虚拟表,并且返 回了 63727 条记录。我们再来看看 ID = 2 的语句究竟做了写什么返回了这么大 量的数据,首先全表扫描 employee 表 13317 条记录,然后根据索引 emp_certificate_empid 关联 emp_certificate 表,rows = 1 表示,每个关联 都只锁定了一条记录,效率比较高。获得后,再和 cm_log 的 379 条记录根据规则关联。从执行过程上可以看出返回了太多的数据,返回的数据绝大部分 cm_log 都用不到,因为 cm_log 只锁定了 379 条记录。如何优化呢?可以看到我们在运行完后还是要和 cm_log 做 join,那么我们能不 能之前和 cm_log 做 join 呢?仔细分析语句不难发现,其基本思想是如果 cm_log 的 ref_table 是 EmpCertificate 就关联 emp_certificate 表,如果 ref_table 是 Employee 就关联 employee 表,我们完全可以拆成两部分,并用 union连接起来,注意这里用union,而不用union all是因为原语句有“distinct” 来得到唯一的记录,而 union 恰好具备了这种功能。如果原语句中没有 distinct 不需要去重,我们就可以直接使用 union all 了,因为使用 union 需要去重的动 作,会影响 SQL 性能。

5.2 优化过的语句如下:

mysql 去重后排序 mysql列去重_union all 索引_04

不需要了解业务场景,只需要改造的语句和改造之前的语句保持结果一致 ,现有索引可以满足,不需要建索引,用改造后的语句实验一下,只需要 10ms 降低了近 200 倍。

mysql 去重后排序 mysql列去重_union all 索引_05

5.3 明确应用场景

举这个例子的目的在于颠覆我们对列的区分度的认知,一般上我们认为区分度越 高的列,越容易锁定更少的记录,但在一些特殊的情况下,这种理论是有局限性 的。

mysql 去重后排序 mysql列去重_mysql 根据两个字段去重_06

mysql 去重后排序 mysql列去重_mysql 语句块_07

先看看运行多长时间,951 条数据 6.22 秒,真的很慢。.先 explain,rows 达到了 361 万,type = ALL 表明是全表扫描。

mysql 去重后排序 mysql列去重_mysql 去重后排序_08

.所有字段都应用查询返回记录数,因为是单表查询 0 已经做过了 951 条。让 explain 的 rows 尽量逼近 951。

看一下 accurate_result = 1 的记录数:

mysql 去重后排序 mysql列去重_mysql 根据两个字段去重_09

我们看到 accurate_result 这个字段的区分度非常低,整个表只有-1,0,1 三个值,

加上索引也无法锁定特别少量的数据。

再看一下 sync_status 字段的情况:

mysql 去重后排序 mysql列去重_mysql 去重后排序_10

同样的区分度也很低,根据理论,也不适合建立索引。

问题分析到这,好像得出了这个表无法优化的结论,两个列的区分度都很低,即 便加上索引也只能适应这种情况,很难做普遍性的优化,比如当 sync_status 0、

分布的很平均,那么锁定记录也是百万级别的。