mysql基础知识点三-排序分组分页优化

mysql基础知识点三-排序分组分页优化

表结构
CREATE TABLE `employees` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int NOT NULL DEFAULT '0' COMMENT '年龄',
  `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
  `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
  PRIMARY KEY (`id`),
  KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=100007 DEFAULT CHARSET=utf8mb3 COMMENT='员工记录表';
  1. order by
order by 也遵守最左前缀,但是图片里用到了Using filesort,因为position这个和name间隔问题导致没有遵守最左前缀,索引只用到了name,position排序只能用外部排序。这种情况尽量要用到索引排序,这里还有一特殊的排序(desc)Backward index scan,其实和asc差不多,效率略差,据说是mysql8.0出现的.

1642558843947

  1. 分页查询优化

    对于一条分页 select * from employees limit 90000,5;
    表示从表 employees 中取出从 10001 行开始的 10 行记录。看似只查询了 10 条记录,实际这条 SQL 是先读取 10010 条记录,然后抛弃前 10000 条记录,然后读到后面 10 条想要的数据。因此要查询一张大表比较靠后的数据,执行效率 是非常低的。
       
    如果我们根据连续的自增主键排序呢
    select * from employees where id > 90000 limit 5;
    但是自增主键如果有断裂就会有问题
        
    如果根据非自增主键呢,比如name字段(索引列)
    select * from employees ORDER BY name limit 90000,5;
    并没有走索引,因为select *,返回的结果集不在索引列上,需要扫描多颗索引树,还不如全表扫描,索引可以考虑返回结果集仅包含索引列,如下图,注意观察索引的使用和排序的类型。
    explain select * from employees e inner join
        (
            select id from employees order by name limit 90000,10
            ) ed
    on e.id =ed.id
    
    

    1642560759145

  2. join关联查询优化

-- 示例表

CREATE TABLE `t1` (
                      `id` int NOT NULL AUTO_INCREMENT,
                      `a` int DEFAULT NULL,
                      `b` int DEFAULT NULL,
                      PRIMARY KEY (`id`),
                      KEY `idx_a` (`a`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

create table t2 like t1;

-- 插入示例数据
-- t1 插入一百条记录

drop procedure if exists  insert_t1;
delimiter ;
create procedure insert_t1()
begin
    declare i int;
    set i = 1;
    while (i<=100)
        do
        insert into t1(a,b) value (i,i);
        set i=i+1;
        end while;
end;
delimiter ;
call insert_t1();

-- 插入示例数据
-- t2 插入10000条记录
drop procedure if exists  insert_t2;
delimiter ;
create procedure insert_t2()
begin
    declare i int;
    set i = 1;
    while (i<=10000)
        do
            insert into t2(a,b) value (i,i);
            set i=i+1;
        end while;
end;
delimiter ;
call insert_t2();


流程: 
上面sql的大致流程如下: 1. 把 t2 的所有数据放入到 join_buffer 中 2. 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比 3. 返回满足 join 条件的数据

驱动表解释:
inner join(内连),优化器会选择小表作为驱动表,看下面这个sql,t2是驱动表,因为是先执行的,通过解释计划可以看t2先执行的,第二就是看rows(扫描行数),驱动表会一行一行扫描。
    
BNL:(extra列 Using join buffer)
基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法 把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。    

NLJ:   
嵌套循环连接 Nested-Loop Join(NLJ) 算法 一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动 表)里取出满足条件的行,然后取出两张表的结果合集。
    
被驱动表的关联字段没索引为什么要选择使用 BNL 算法而不使用 Nested-Loop Join 呢?:
如果上面第二条sql使用 Nested-Loop Join,那么扫描行数为 100 * 10000 = 100万次,这个是磁盘扫描。 很显然,用BNL磁盘扫描次数少很多,相比于磁盘扫描,BNL的内存计算会快得多。 因此MySQL对于被驱动表的关联字段没索引的关联查询,一般都会使用 BNL 算法。如果有索引一般选择 NLJ 算法,有 索引的情况下 NLJ 算法比 BNL算法性能更高

1642574326134

具体优化策略:

对于关联sql的优化 关联字段加索引,让mysql做join操作时尽量选择NLJ算法 小表驱动大表,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去 mysql优化器自己判断的时间

straight_join解释:straight_join功能同join类似,但能让左边的表来驱动右边的表,能改表优化器对于联表查询的执 行顺序。 比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表指定mysql选着 t2 表作为驱动表。
    straight_join只适用于inner join,并不适用于left join,right join。(因为left join,right join已经代表指 定了表的执行顺序) 
    尽可能让优化器去判断,因为大部分情况下mysql优化器是比人要聪明的。使用straight_join一定要慎重,因 为部分情况下人为指定的执行顺序并不一定会比优化引擎要靠谱。
  
对于小表定义的明确 在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据 量,数据量小的那个表,就是“小表”,应该作为驱动表。
  1. in和exists的优化

    其实就是小表驱动大表的问题
    
    in:
    结论:
        当B表的数据集小于A表的数据集时,in优于exists
        
        select * from A where id in (select id from B)
        #等价于: 
        for(select id from B)
        {  select * from A where A.id = B.id }
    
    exists:
    结论:
        当A表的数据集小于B表的数据集时,exists优于in 将主查询A的数据,放到子查询B中做条件验证,根据验证结果(true或false)来决定主查询的数据是否保留
        select * from A where exists (select 1 from B where B.id = A.id) 
        #等价于:  for(select * from A){ 
            select * from B where B.id = A.id  } 
    	#A表与B表的ID字段应建立索引
            
        关于exists有个注意事项:
            1、EXISTS (subquery)只返回TRUE或FALSE,因此子查询中的SELECT * 也可以用		SELECT 1替换,官方说法是实际执行时会 忽略SELECT清单,因此没有区别 2、EXISTS子查询的	 实际执行过程可能经过了优化而不是我们理解上的逐条对比 3、EXISTS子查询往往也可以用JOIN来	  代替,何种最优需要具体问题具体分析
    
    1. count(*)查询优化

      ‐‐ 临时关闭mysql查询缓存,为了查看sql多次执行的真实时间 
      mysql> set global query_cache_size=0; 
      mysql> set global query_cache_type=0; 
      mysql> EXPLAIN select count(1) from employees; 
      mysql> EXPLAIN select count(id) from employees; 
      mysql> EXPLAIN select count(name) from employees; 
      mysql> EXPLAIN select count(*) from employees;
      
      四个sql的执行计划一样,说明这四个sql执行效率应该差不多,图就不贴了,sql是用employees这个表
          
      结论:
          字段有索引:count(*)≈count(1)>count(字段)>count(主键 id) 
          //字段有索引,count(字段)统计走二级索引,二 级索引存储数据比主键索引少,所以count(字段)>count(主键 id) 
          
          字段无索引:count(*)≈count(1)>count(主键 id)>count(字段) //字段没有索引count(字段)统计走不了索引, count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段) count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出 字段,所以理论上count(1)比count(字段)会快一点。 count(*) 是例外,mysql并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用 count(列名)或count(常量)来替代 count(*)。 
          
          为什么对于count(id),mysql最终选择辅助索引而不是主键聚集索引?因为二级索引相对主键索引存储数据更少,检索 性能应该更高,mysql内部做了点优化(应该是在5.7版本才优化)。
          
         小知识:
          show table status 如果只需要知道表总行数的估计值可以用如下sql查询,性能很高
      
  2. 一些总结性资料

优化总结:

1、MySQL支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序。index

效率高,filesort效率低。

2、order by满足两种情况会使用Using index。

  1. order by语句使用索引最左前列。

  2. 使用where子句与order by子句条件列组合满足索引最左前列。

3、尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀法则。

4、如果order by的条件不在索引列上,就会产生Using filesort。

5、能用覆盖索引尽量用覆盖索引

6、group by与order by很类似,其实质是先排序后分组,遵照索引创建顺序的最左前缀法则。对于group

by的优化如果不需要排序的可以加上order by null禁止排序。注意,where高于having,能写在where中

的限定条件就不要去having限定了。

Using filesort文件排序原理详解

filesort文件排序方式

单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可

以看到sort_mode信息里显示< sort_key, additional_fields >或者< sort_key,

packed_additional_fields >

双路排序(又叫回表排序模式):是首先根据相应的条件取出相应的排序字段可以直接定位行

数据的行 ID,然后在 sort buffer 中进行排序,排序完后需要再次取回其它需要的字段;用trace工具

可以看到sort_mode信息里显示< sort_key, rowid >

MySQL 通过比较系统变量 max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来

判断使用哪种排序模式。

如果 字段的总长度小于max_length_for_sort_data ,那么使用 单路排序模式;

如果 字段的总长度大于max_length_for_sort_data ,那么使用 双路排序模∙式

索引设计原则

1、代码先行,索引后上

不知大家一般是怎么给数据表建立索引的,是建完表马上就建立索引吗?

这其实是不对的,一般应该等到主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立

索引。

2、联合索引尽量覆盖条件

比如可以设计一个或者两三个联合索引(尽量少建单值索引),让每一个联合索引都尽量去包含sql语句里的

where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原

则。

3、不要在小基数字段上建立索引

索引基数是指这个字段在表里总共有多少个不同的值,比如一张表总共100万行记录,其中有个性别字段,

其值不是男就是女,那么该字段的基数就是2。

如果对这种小基数字段建立索引的话,还不如全表扫描了,因为你的索引树里就包含男和女两种值,根本没

法进行快速的二分查找,那用索引就没有太大的意义了。

一般建立索引,尽量使用那些基数比较大的字段,就是值比较多的字段,那么才能发挥出B+树快速二分查

找的优势来。

4、长字符串我们可以采用前缀索引

尽量对字段类型较小的列设计索引,比如说什么tinyint之类的,因为字段类型较小的话,占用磁盘空间也会

比较小,此时你在搜索的时候性能也会比较好一点。

当然,这个所谓的字段类型小一点的列,也不是绝对的,很多时候你就是要针对varchar(255)这种字段建立

索引,哪怕多占用一些磁盘空间也是有必要的。

对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对这个字段的前20个

字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于 KEY

index(name(20),age,position)。

此时你在where条件里搜索的时候,如果是根据name字段来搜索,那么此时就会先到索引树里根据name

字段的前20个字符去搜索,定位到之后前20个字符的前缀匹配的部分数据之后,再回到聚簇索引提取出来

完整的name字段值进行比对。

但是假如你要是order by name,那么此时你的name因为在索引树里仅仅包含了前20个字符,所以这个排

序是没法用上索引的, group by也是同理。所以这里大家要对前缀索引有一个了解。

5、where与order by冲突时优先where

在where和order by出现索引设计冲突时,到底是针对where去设计索引,还是针对order by设计索引?到

底是让where去用上索引,还是让order by用上索引?一般这种时候往往都是让where条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。

因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可

能会小很多。

  1. 这里有一个索引优化的例子,适合背题
索引设计实战 以社交场景APP来举例,我们一般会去搜索一些好友,这里面就涉及到对用户信息的筛选,这里肯定就是对 用户user表搜索了,这个表一般来说数据量会比较大,我们先不考虑分库分表的情况,比如,我们一般会筛 选地区(省市),性别,年龄,身高,爱好之类的,有的APP可能用户还有评分,比如用户的受欢迎程度评 分,我们可能还会根据评分来排序等等。 对于后台程序来说除了过滤用户的各种条件,还需要分页之类的处理,可能会生成类似sql语句执行: select xx from user where xx=xx and xx=xx order by xx limit xx,xx 对于这种情况如何合理设计索引了,比如用户可能经常会根据省市优先筛选同城的用户,还有根据性别去筛 选,那我们是否应该设计一个联合索引 (province,city,sex) 了?这些字段好像基数都不大,其实是应该的, 因为这些字段查询太频繁了。 假设又有用户根据年龄范围去筛选了,比如 where province=xx and city=xx and age>=xx and age<=xx,我们尝试着把age字段加入联合索引 (province,city,sex,age),注意,一般这种范围查找的条件 都要放在最后,之前讲过联合索引范围之后条件的是不能用索引的,但是对于当前这种情况依然用不到age 这个索引字段,因为用户没有筛选sex字段,那怎么优化了?其实我们可以这么来优化下sql的写法:where province=xx and city=xx and sex in ('female','male') and age>=xx and age<=xx 对于爱好之类的字段也可以类似sex字段处理,所以可以把爱好字段也加入索引 (province,city,sex,hobby,age) 假设可能还有一个筛选条件,比如要筛选最近一周登录过的用户,一般大家肯定希望跟活跃用户交友了,这 样能尽快收到反馈,对应后台sql可能是这样: where province=xx and city=xx and sex in ('female','male') and age>=xx and age<=xx and latest_login_time>= xx 那我们是否能把 latest_login_time 字段也加入索引了?比如 (province,city,sex,hobby,age,latest_login_time) ,显然是不行的,那怎么来优化这种情况了?其实我们 可以试着再设计一个字段is_login_in_latest_7_days,用户如果一周内有登录值就为1,否则为0,那么我们 就可以把索引设计成 (province,city,sex,hobby,is_login_in_latest_7_days,age) 来满足上面那种场景了! 一般来说,通过这么一个多字段的索引是能够过滤掉绝大部分数据的,就保留小部分数据下来基于磁盘文件 进行order by语句的排序,最后基于limit进行分页,那么一般性能还是比较高的。 不过有时可能用户会这么来查询,就查下受欢迎度较高的女性,比如sql:where sex = 'female' order by score limit xx,xx,那么上面那个索引是很难用上的,不能把太多的字段以及太多的值都用 in 语句拼接 到sql里的,那怎么办了?其实我们可以再设计一个辅助的联合索引,比如 (sex,score),这样就能满足查询 要求了。 以上就是给大家讲的一些索引设计的思路了,核心思想就是,尽量利用一两个复杂的多字段联合索引,抗下 你80%以上的查询,然后用一两个辅助索引尽量抗下剩余的一些非典型查询,保证这种大数据量表的查询尽 可能多的都能充分利用索引,这样就能保证你的查询速度和性能了!
posted @ 2022-01-19 15:57  小傻孩丶儿  阅读(992)  评论(0编辑  收藏  举报