• MySQL索引原理及慢查询优化


    建索引的几大原则

    1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。 3.尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

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

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

    慢查询

    select
       count(*) 
    from
       task 
    where
       status=2 
       and operator_id=20839 
       and operate_time>1371169729 
       and operate_time<1371174603 
       and type=2;

    根据最左匹配原则,最开始的sql语句的索引应该是status、operator_id、type、operate_time的联合索引;其中status、operator_id、type的顺序可以颠倒,所以我才会说,把这个表的所有相关查询都找到,会综合分析; 比如还有如下查询:

    select * from task where status = 0 and type = 12 limit 10;
    select count(*) from task where status = 0 ;

    那么索引建立成(status,type,operator_id,operate_time)就是非常正确的,因为可以覆盖到所有情况。这个就是利用了索引的最左匹配的原则

    查询优化神器 - explain命令

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

    慢查询优化基本步骤

    0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE 1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高 2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询) 3.order by limit 形式的sql语句让排序的表优先查 4.了解业务方使用场景 5.加索引时参照建索引的几大原则 6.观察结果,不符合预期继续从0分析

    几个慢查询案例

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

    复杂语句写法

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

    select
       distinct cert.emp_id 
    from
       cm_log cl 
    inner join
       (
          select
             emp.id as emp_id,
             emp_cert.id as cert_id 
          from
             employee emp 
          left join
             emp_certificate emp_cert 
                on emp.id = emp_cert.emp_id 
          where
             emp.is_deleted=0
       ) cert 
          on (
             cl.ref_table='Employee' 
             and cl.ref_oid= cert.emp_id
          ) 
          or (
             cl.ref_table='EmpCertificate' 
             and cl.ref_oid= cert.cert_id
          ) 
    where
       cl.last_upd_date >='2013-11-07 15:03:00' 
       and cl.last_upd_date<='2013-11-08 16:00:00';

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

    53 rows in set (1.87 sec)

    1.explain

    +----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
    | id | select_type | table      | type  | possible_keys                   | key                   | key_len | ref               | rows  | Extra                          |
    +----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
    |  1 | PRIMARY     | cl         | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date     | 8       | NULL              |   379 | Using where; Using temporary   |
    |  1 | PRIMARY     | <derived2> | ALL   | NULL                            | NULL                  | NULL    | NULL              | 63727 | Using where; Using join buffer |
    |  2 | DERIVED     | emp        | ALL   | NULL                            | NULL                  | NULL    | NULL              | 13317 | Using where                    |
    |  2 | DERIVED     | emp_cert   | ref   | emp_certificate_empid           | emp_certificate_empid | 4       | meituanorg.emp.id |     1 | Using index                    |
    +----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+

    简述一下执行计划,首先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性能。

    优化过的语句如下:

    select
       emp.id 
    from
       cm_log cl 
    inner join
       employee emp 
          on cl.ref_table = 'Employee' 
          and cl.ref_oid = emp.id  
    where
       cl.last_upd_date >='2013-11-07 15:03:00' 
       and cl.last_upd_date<='2013-11-08 16:00:00' 
       and emp.is_deleted = 0  
    union
    select
       emp.id 
    from
       cm_log cl 
    inner join
       emp_certificate ec 
          on cl.ref_table = 'EmpCertificate' 
          and cl.ref_oid = ec.id  
    inner join
       employee emp 
          on emp.id = ec.emp_id  
    where
       cl.last_upd_date >='2013-11-07 15:03:00' 
       and cl.last_upd_date<='2013-11-08 16:00:00' 
       and emp.is_deleted = 0

    4.不需要了解业务场景,只需要改造的语句和改造之前的语句保持结果一致

    5.现有索引可以满足,不需要建索引

    6.用改造后的语句实验一下,只需要10ms 降低了近200倍!

    +----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
    | id | select_type  | table      | type   | possible_keys                   | key               | key_len | ref                   | rows | Extra       |
    +----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
    |  1 | PRIMARY      | cl         | range  | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8       | NULL                  |  379 | Using where |
    |  1 | PRIMARY      | emp        | eq_ref | PRIMARY                         | PRIMARY           | 4       | meituanorg.cl.ref_oid |    1 | Using where |
    |  2 | UNION        | cl         | range  | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8       | NULL                  |  379 | Using where |
    |  2 | UNION        | ec         | eq_ref | PRIMARY,emp_certificate_empid   | PRIMARY           | 4       | meituanorg.cl.ref_oid |    1 |             |
    |  2 | UNION        | emp        | eq_ref | PRIMARY                         | PRIMARY           | 4       | meituanorg.ec.emp_id  |    1 | Using where |
    | NULL | UNION RESULT | <union1,2> | ALL    | NULL                            | NULL              | NULL    | NULL                  | NULL |             |
    +----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+
    53 rows in set (0.01 sec)

    明确应用场景

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

    select
       * 
    from
       stage_poi sp 
    where
       sp.accurate_result=1 
       and (
          sp.sync_status=0 
          or sp.sync_status=2 
          or sp.sync_status=4
       );

    0.先看看运行多长时间,951条数据6.22秒,真的很慢。

    951 rows in set (6.22 sec)

    1.先explain,rows达到了361万,type = ALL表明是全表扫描。

    +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
    | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
    +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
    |  1 | SIMPLE      | sp    | ALL  | NULL          | NULL | NULL    | NULL | 3613155 | Using where |
    +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+

    2.所有字段都应用查询返回记录数,因为是单表查询 0已经做过了951条。

    3.让explain的rows 尽量逼近951。

    看一下accurate_result = 1的记录数:

    select count(*),accurate_result from stage_poi  group by accurate_result;
    +----------+-----------------+
    | count(*) | accurate_result |
    +----------+-----------------+
    |     1023 |              -1 |
    |  2114655 |               0 |
    |   972815 |               1 |
    +----------+-----------------+

    我们看到accurate_result这个字段的区分度非常低,整个表只有-1,0,1三个值,加上索引也无法锁定特别少量的数据。

    再看一下sync_status字段的情况:

    select count(*),sync_status from stage_poi  group by sync_status;
    +----------+-------------+
    | count(*) | sync_status |
    +----------+-------------+
    |     3080 |           0 |
    |  3085413 |           3 |
    +----------+-------------+

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

    问题分析到这,好像得出了这个表无法优化的结论,两个列的区分度都很低,即便加上索引也只能适应这种情况,很难做普遍性的优化,比如当sync_status 0、3分布的很平均,那么锁定记录也是百万级别的。

    4.找业务方去沟通,看看使用场景。业务方是这么来使用这个SQL语句的,每隔五分钟会扫描符合条件的数据,处理完成后把sync_status这个字段变成1,五分钟符合条件的记录数并不会太多,1000个左右。了解了业务方的使用场景后,优化这个SQL就变得简单了,因为业务方保证了数据的不平衡,如果加上索引可以过滤掉绝大部分不需要的数据。

    5.根据建立索引规则,使用如下语句建立索引

    alter table stage_poi add index idx_acc_status(accurate_result,sync_status);

    6.观察预期结果,发现只需要200ms,快了30多倍。

    952 rows in set (0.20 sec)

    我们再来回顾一下分析问题的过程,单表查询相对来说比较好优化,大部分时候只需要把where条件里面的字段依照规则加上索引就好,如果只是这种“无脑”优化的话,显然一些区分度非常低的列,不应该加索引的列也会被加上索引,这样会对插入、更新性能造成严重的影响,同时也有可能影响其它的查询语句。所以我们第4步调差SQL的使用场景非常关键,我们只有知道这个业务场景,才能更好地辅助我们更好的分析和优化查询语句。

    转载:美团技术

  • 相关阅读:
    0508---字符串练习题
    0506--习题
    0503---练习题 punctuation isdigit() strip() upper()
    0505---练习题
    0504---习题str.swapcase() str. capitalize() str.title()
    0429---每日习题 菲薄纳西数列 正则ip匹配
    习题之---文件操作
    NOIP 模拟 $13; ext{工业题}$
    NOIP 模拟 $12; ext{简单的填数}$
    NOIP 模拟 $12; ext{简单的玄学}$
  • 原文地址:https://www.cnblogs.com/java-zzl/p/10485021.html
Copyright © 2020-2023  润新知