MySQL索引道理以及优化
前言
本文是美团一位大佬写的,还不错拿出来和大家分享下,代码中嵌套在html中sql语句是java框架的写法,懂得其sql要施行的语句即可。
背景
MySQL凭借着杰出的机能、低廉的成本、丰硕的资源,已经成为绝大多数互联网公司的首选关系型数据库。虽然机能杰出,但所谓“好马配好鞍”,怎样能够更好的使用它,已经成为开发工程师的必修课,我们经常会从职位描写上看到诸如“熟知MySQL”、“SQL语句优化”、“理解数据库道理”等要求。我们知道一样的利用系统,读写比例在10:1摆布,并且插入操纵和一样的更新操纵很少显现机能问题,碰到最多的,也是最容易出问题的,还是一些复杂的查询操纵,所以查询语句的优化明显是重中之重。
本人从13年7月份起,不断在美团中心业务系统部做慢查询的优化工作,共计十余个系统,累计解决和积存了上百个慢查询案例。随着业务的复杂性晋升,碰到的问题光怪陆离,八门五花,匪夷所思。本文旨在以开发工程师的角度来说明数据库索引的道理和怎样优化慢查询。
<span class="hljs-keyword">select</span> <span class="hljs-keyword">count</span>(*) <span class="hljs-keyword">from</span> task <span class="hljs-keyword">where</span> <span class="hljs-keyword">status</span>=<span class="hljs-number">2</span> <span class="hljs-keyword">and</span> operator_id=<span class="hljs-number">20839</span> <span class="hljs-keyword">and</span> operate_time><span class="hljs-number">1371169729</span> <span class="hljs-keyword">and</span> operate_time<<span class="hljs-number">1371174603</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">type</span>=<span class="hljs-number">2</span>;
系统使用者反响有一个功效越来越慢,于是工程师寻到了上面的SQL。
并且兴趣冲冲的寻到了我,“这个SQL需要优化,给我把每个字段都加上索引”。
我很惊奇,问道:“为什么需要每个字段都加上索引?”
“把查询的字段都加上索引会更快”,工程师信念满满。
“这种状况完全可以建一个结合索引,由于是最左前缀匹配,所以operate_time需要放到最后,并且还需要把其他相关的查询都拿来,需要做一个综合评估。”
“结合索引?最左前缀匹配?综合评估?”工程师不禁堕入了深思。
多数状况下,我们知道索引能够提高查询效力,但应当怎样创立索引?索引的次序怎样?很多人却只知道大约。其实懂得这些概念并不难,并且索引的道理远没有想象的那么复杂。
索引目的
索引的目的在于提高查询效力,可以类比字典,假如要查“mysql”这个单词,我们必定需要定位到m字母,然后从下往下寻到y字母,再寻到剩下的sql。假如没有索引,那么你大概需要把所有单词看一遍才能寻到你想要的,假如我想寻到m开头的单词呢?或者ze开头的单词呢?是不是觉得假如没有索引,这个事情基本没法完成?
索引道理
除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的名目等。它们的道理都是一样的,通过不竭的缩小想要获得数据的范畴来挑选出终究想要的结果,同时把随机的事件变成次序的事件,也就是我们总是通过统一种查寻方式来锁定数据。
数据库也是一样,但明显要复杂很多,由于不仅面临着等值查询,还有范畴查询(>、<、between、in)、含糊查询(like)、并集查询(or)等等。数据库应当选中如何样的方式来应对所有的问题呢?我们回想字典的例子,能不克不及把数据分成段,然后分段查询呢?最简便的假如1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段……这样查第250条数据,只要寻第三段就可以了,一下子去除了90%的无效数据。但假如是1千万的记载呢,分成几段比力好?稍有算法根基的同学会想到搜索树,其均匀复杂度是lgN,具有不错的查询机能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次雷同的操纵成原本思考的,数据库实现比力复杂,数据留存在磁盘上,而为了提高机能,每次又可以把部分数据读入内存来运算,由于我们知道拜访磁盘的成本大约是拜访内存的十万倍摆布,所以简便的搜索树难以知足复杂的利用场景。
磁盘IO与预读
前面提到了拜访磁盘,那么这里先简便介绍一下磁盘IO和预读,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、扭转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一样在5ms以下;扭转延迟就是我们经常据说的磁盘转速,比方一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,扭转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一样在零点几毫秒,相关于前两个时间可以忽略不计。那么拜访一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms摆布,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以施行5亿条指令,由于指令依托的是电的性质,换句话说施行一次IO的时间可以施行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,明显是个劫难。下图是运算机硬件延迟的对照图,供大家参照 :
various-system-software-hardware-latencies
思考到磁盘IO是非常昂扬的操纵,运算机操纵系统做了一些优化,当一次IO时,不但把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,由于部分预读性道理告诉我们,当运算机拜访一个地址的数据的时候,与其相邻的数据也会很快被拜访到。每一次IO读取的数据我们称之为一页(page)。详细一页有多大数据跟操纵系统有关,一样为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论关于索引的数据构造设计非常有帮忙。
索引的数据构造
前面讲了生活中索引的例子,索引的根本道理,数据库的复杂性,又讲了操纵系统的相关知识,目的就是让大家理解,任何一种数据构造都不是凭空发生的,必然会有它的背景和使用处景,我们此刻总结一下,我们需要这种数据构造能够做些什么,其实很简便,那就是:每次查寻数据时把磁盘IO次数操纵在一个很小的数目级,最好是常数数目级。那么我们就想到假如一个高度可控的多路搜索树可否能知足需求呢?就这样,b+树应运而生。
详解b+树
b+树
如上图,是一颗b+树,关于b+树的定义可以拜见B+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包括几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包括数据项17和35,包括指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不储备真实的数据,只储备指引搜索标的目的的数据项,如17、35并不真实存在于数据表中。
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,明显成本非常非常高。
b+树性质
通过上面的剖析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数目是m,则有h=㏒(m+1)N,当数据量N必然的状况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是牢固的,假如数据项占的空间越小,数据项的数目越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比方int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下落,致使树增高。当数据项等于1时将会退化成线性表。
当b+树的数据项是复合的数据构造,比方(name,age,sex)的时候,b+数是依照从左到右的次序来创立搜索树的,比方当(张三,20,F)这样的数据来检索的时候,b+树会优先比力name来肯定下一步的所搜标的目的,假如name雷同再顺次比力age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,由于创立搜索树的时候name就是第一个比力因子,必需要先按照name来搜索才能知道下一步去哪里查询。比方当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索标的目的,但下一个字段age的缺失,所以只能把名字等于张三的数据都寻到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
关于MySQL索引道理是比力枯燥的东西,大家只需要有一个感性的认识,并不需要懂得得非常透彻和深入。我们回过头来看看一开端我们说的慢查询,理解完索引道理之后,大家是不是有什么设法呢?先总结一下索引的几大根本原则:
建索引的几大原则
- 最左前缀匹配原则,非常重要的原则,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的次序可以任意调整。
- =和in可以乱序,比方a = 1 and b = 2 and c = 3 创立(a,b,c)索引可以任意次序,mysql的查询优化器会帮你优化成索引可以识别的情势。
- 尽量选中区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不反复的比例,比例越大我们扫描的记载数越少,独一键的区分度是1,而一些状态、性别字段大概在大数据面前区分度就是0,那大概有人会问,这个比例有什么经历值吗?使用处景不一样,这个值也很难肯定,一样需要join的字段我们都要求是0.1以上,即均匀1条扫描10笔记录。
- 索引列不克不及参与运算,保持列“洁净”,比方from_unixtime(create_time) = ’2014-05-29’就不克不及使用到索引,缘由很简便,b+树中存的都是数据表中的字段值,但停止检索时,需要把所有元素都利用函数才能比力,明显成本太大。所以语句应当写成create_time = unix_timestamp(’2014-05-29’)。
- 尽量的扩展索引,不要创建索引。比方表中已经有a的索引,此刻要加(a,b)的索引,那么只需要修改本来的索引即可。
回到开端的慢查询
按照最左匹配原则,最开端的sql语句的索引应当是status、operator_id、type、operate_time的结合索引;其中status、operator_id、type的次序可以颠倒,所以我才会说,把这个表的所有相关查询都寻到,会综合剖析; 比方还有如下查询:
<span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> task <span class="hljs-keyword">where</span> <span class="hljs-keyword">status</span> = <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">type</span> = <span class="hljs-number">12</span> <span class="hljs-keyword">limit</span> <span class="hljs-number">10</span>;
<span class="hljs-keyword">select</span> <span class="hljs-keyword">count</span>(*) <span class="hljs-keyword">from</span> task <span class="hljs-keyword">where</span> <span class="hljs-keyword">status</span> = <span class="hljs-number">0</span> ;
那么索引创立成(status,type,operator_id,operate_time)就是非常准确的,由于可以覆盖到所有状况。这个就是利用了索引的最左匹配的原则
查询优化神器 – explain命令
关于explain命令信赖大家并不生疏,详细用途和字段含义可以参照 官网explain-output,这里需要强调rows是中心目标,绝大部分rows小的语句施行必然很快(有例外,下面会讲到)。所以优化语句根本上都是在优化rows。
慢查询优化根本步骤
- 先运转看看可否真的很慢,留意设定SQL_NO_CACHE
- where前提单表查,锁定最小返回记载表。这句话的意思是把查询语句的where都利用到表中返回的记载数最小的表开端查起,单表每个字段离别查询,看哪个字段的区分度最高
- explain查看施行方案,可否与1预测一致(从锁定记载较少的表开端查询)
- order by limit 情势的sql语句让排序的表优先查
- 理解业务方使用处景
- 加索引时参照建索引的几大原则
- 视察结果,不相符预测连续从0剖析
几个慢查询案例
下面几个例子具体说明了怎样剖析和优化慢查询。
复杂语句写法
许多状况下,我们写SQL只是为了实现功效,这只是第一步,不一样的语句书写方式关于效力往往有本质的差异,这要求我们对mysql的施行方案和索引原则有非常分明的认识,请看下面的语句:
<span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> cert.emp_id <span class="hljs-keyword">from</span> cm_log cl <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> ( <span class="hljs-keyword">select</span> emp.id <span class="hljs-keyword">as</span> emp_id, emp_cert.id <span class="hljs-keyword">as</span> cert_id <span class="hljs-keyword">from</span> employee emp <span class="hljs-keyword">left</span> <span class="hljs-keyword">join</span> emp_certificate emp_cert <span class="hljs-keyword">on</span> emp.id = emp_cert.emp_id <span class="hljs-keyword">where</span> emp.is_deleted=<span class="hljs-number">0</span> ) cert <span class="hljs-keyword">on</span> ( cl.ref_table=<span class="hljs-string">'Employee'</span> <span class="hljs-keyword">and</span> cl.ref_oid= cert.emp_id ) <span class="hljs-keyword">or</span> ( cl.ref_table=<span class="hljs-string">'EmpCertificate'</span> <span class="hljs-keyword">and</span> cl.ref_oid= cert.cert_id ) <span class="hljs-keyword">where</span> cl.last_upd_date >=<span class="hljs-string">'2013-11-07 15:03:00'</span> <span class="hljs-keyword">and</span> cl.last_upd_date<=<span class="hljs-string">'2013-11-08 16:00:00'</span>;
- 先运转一下,53笔记录 1.87秒,又没有用聚合语句,比力慢
53 rows in <span class="hljs-keyword">set</span> (<span class="hljs-number">1.87</span> sec)
- explain
+<span class="hljs-comment">----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+</span> | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +<span class="hljs-comment">----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+</span> | 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 | +<span class="hljs-comment">----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+</span>
简述一下施行方案,第一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机能。
优化过的语句如下:
<span class="hljs-keyword">select</span> emp.id <span class="hljs-keyword">from</span> cm_log cl <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> employee emp <span class="hljs-keyword">on</span> cl.ref_table = <span class="hljs-string">'Employee'</span> <span class="hljs-keyword">and</span> cl.ref_oid = emp.id <span class="hljs-keyword">where</span> cl.last_upd_date >=<span class="hljs-string">'2013-11-07 15:03:00'</span> <span class="hljs-keyword">and</span> cl.last_upd_date<=<span class="hljs-string">'2013-11-08 16:00:00'</span> <span class="hljs-keyword">and</span> emp.is_deleted = <span class="hljs-number">0</span> <span class="hljs-keyword">union</span> <span class="hljs-keyword">select</span> emp.id <span class="hljs-keyword">from</span> cm_log cl <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> emp_certificate ec <span class="hljs-keyword">on</span> cl.ref_table = <span class="hljs-string">'EmpCertificate'</span> <span class="hljs-keyword">and</span> cl.ref_oid = ec.id <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> employee emp <span class="hljs-keyword">on</span> emp.id = ec.emp_id <span class="hljs-keyword">where</span> cl.last_upd_date >=<span class="hljs-string">'2013-11-07 15:03:00'</span> <span class="hljs-keyword">and</span> cl.last_upd_date<=<span class="hljs-string">'2013-11-08 16:00:00'</span> <span class="hljs-keyword">and</span> emp.is_deleted = <span class="hljs-number">0</span>
不需要理解业务场景,只需要革新的语句和革新此前的语句保持结果一致
现有索引可以知足,不需要建索引
用革新后的语句实验一下,只需要10ms 落低了近200倍!
+<span class="hljs-comment">----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+</span> | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +<span class="hljs-comment">----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+</span> | 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 | | +<span class="hljs-comment">----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+</span> 53 rows in <span class="hljs-keyword">set</span> (<span class="hljs-number">0.01</span> sec)
明白利用场景
举这个例子的目的在于颠覆我们对列的区分度的认知,一样上我们认为区分度越高的列,越容易锁定更少的记载,但在一些非凡的状况下,这种理论是有局限性的。
<span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> stage_poi sp <span class="hljs-keyword">where</span> sp.accurate_result=<span class="hljs-number">1</span> <span class="hljs-keyword">and</span> ( sp.sync_status=<span class="hljs-number">0</span> <span class="hljs-keyword">or</span> sp.sync_status=<span class="hljs-number">2</span> <span class="hljs-keyword">or</span> sp.sync_status=<span class="hljs-number">4</span> );
先看看运转多长时间,951条数据6.22秒,真的很慢。
951 rows in <span class="hljs-keyword">set</span> (<span class="hljs-number">6.22</span> sec)
先explain,rows到达了361万,type = ALL表白是全表扫描。
+<span class="hljs-comment">----+-------------+-------+------+---------------+------+---------+------+---------+-------------+</span> | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +<span class="hljs-comment">----+-------------+-------+------+---------------+------+---------+------+---------+-------------+</span> | 1 | SIMPLE | sp | ALL | NULL | NULL | NULL | NULL | 3613155 | Using where | +<span class="hljs-comment">----+-------------+-------+------+---------------+------+---------+------+---------+-------------+</span>
所有字段都利用查询返回记载数,由于是单表查询 0已经做过了951条。
让explain的rows 尽量亲近951。
看一下accurate_result = 1的记载数:
<span class="hljs-keyword">select</span> <span class="hljs-keyword">count</span>(*),accurate_result <span class="hljs-keyword">from</span> stage_poi <span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> accurate_result; +<span class="hljs-comment">----------+-----------------+</span> | count(*) | accurate_result | +<span class="hljs-comment">----------+-----------------+</span> | 1023 | -1 | | 2114655 | 0 | | 972815 | 1 | +<span class="hljs-comment">----------+-----------------+</span>
我们看到accurate_result这个字段的区分度非常低,整个表只要-1,0,1三个值,加上索引也没法锁定特殊少量的数据。
再看一下sync_status字段的状况:
<span class="hljs-keyword">select</span> <span class="hljs-keyword">count</span>(*),sync_status <span class="hljs-keyword">from</span> stage_poi <span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> sync_status; +<span class="hljs-comment">----------+-------------+</span> | count(*) | sync_status | +<span class="hljs-comment">----------+-------------+</span> | 3080 | 0 | | 3085413 | 3 | +<span class="hljs-comment">----------+-------------+</span>
一样的区分度也很低,按照理论,也不适合创立索引。
问题剖析到这,仿佛得出了这个表没法优化的结论,两个列的区分度都很低,即使加上索引也只能顺应这种状况,很难做遍及性的优化,比方当sync_status 0、3分布的很均匀,那么锁定记载也是百万级别的。
寻业务方去沟通,看看使用处景。业务方是这么来使用这个SQL语句的,每隔五分钟会扫描相符前提的数据,处置完成后把sync_status这个字段变成1,五分钟相符前提的记载数并不会太多,1000个摆布。理解了业务方的使用处景后,优化这个SQL就变得简便了,由于业务方包管了数据的不服衡,假如加上索引可以过滤掉绝大部分不需要的数据。
按照创立索引规则,使用如下语句创立索引
<span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> stage_poi <span class="hljs-keyword">add</span> <span class="hljs-keyword">index</span> idx_acc_status(accurate_result,sync_status);
视察预测结果,发明只需要200ms,快了30多倍。
952 rows in <span class="hljs-keyword">set</span> (<span class="hljs-number">0.20</span> sec)
我们再来回忆一下剖析问题的历程,单表查询相对来说比力好优化,大部分时候只需要把where前提里面的字段遵照规则加上索引就好,假如只是这种“无脑”优化的话,明显一些区分度非常低的列,不该该加索引的列也会被加上索引,这样会对插入、更新机能造成严峻的影响,同时也有大概影响其它的查询语句。所以我们第4步调差SQL的使用处景非常关键,我们只要知道这个业务场景,才能更好地辅助我们更好的剖析和优化查询语句。
没法优化的语句
<span class="hljs-keyword">select</span> c.id, c.name, c.position, c.sex, c.phone, c.office_phone, c.feature_info, c.birthday, c.creator_id, c.is_keyperson, c.giveup_reason, c.status, c.data_source, from_unixtime(c.created_time) <span class="hljs-keyword">as</span> created_time, from_unixtime(c.last_modified) <span class="hljs-keyword">as</span> last_modified, c.last_modified_user_id <span class="hljs-keyword">from</span> contact c <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> contact_branch cb <span class="hljs-keyword">on</span> c.id = cb.contact_id <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> branch_user bu <span class="hljs-keyword">on</span> cb.branch_id = bu.branch_id <span class="hljs-keyword">and</span> bu.status <span class="hljs-keyword">in</span> ( <span class="hljs-number">1</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> org_emp_info oei <span class="hljs-keyword">on</span> oei.data_id = bu.user_id <span class="hljs-keyword">and</span> oei.node_left >= <span class="hljs-number">2875</span> <span class="hljs-keyword">and&llt;/span> oei.node_right <= <span class="hljs-number">10802</span> <span class="hljs-keyword">and</span> oei.org_category = - <span class="hljs-number">1</span> <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> c.created_time <span class="hljs-keyword">desc</span> <span class="hljs-keyword">limit</span> <span class="hljs-number">0</span> , <span class="hljs-number">10</span>;
还是几个步骤。
- 先看语句运转多长时间,10笔记录用了13秒,已经不成忍耐。
10 rows in <span class="hljs-keyword">set</span> (<span class="hljs-number">13.06</span> sec)
- explain
+<span class="hljs-comment">----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+</span> | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +<span class="hljs-comment">----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+</span> | 1 | SIMPLE | oei | ref | idx_category_left_right,idx_data_id | idx_category_left_right | 5 | const | 8849 | Using where; Using temporary; Using filesort | | 1 | SIMPLE | bu | ref | PRIMARY,idx_userid_status | idx_userid_status | 4 | meituancrm.oei.data_id | 76 | Using where; Using index | | 1 | SIMPLE | cb | ref | idx_branch_id,idx_contact_branch_id | idx_branch_id | 4 | meituancrm.bu.branch_id | 1 | | | 1 | SIMPLE | c | eq_ref | PRIMARY | PRIMARY | 108 | meituancrm.cb.contact_id | 1 | | +<span class="hljs-comment">----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+</span>
从施行方案上看,mysql先查org_emp_info表扫描8849记载,再用索引idx_userid_status关联branch_user表,再用索引idx_branch_id关联contact_branch表,最后主键关联contact表。
rows返回的都非常少,看不到有什么非常状况。我们在看一下语句,发明后面有order by + limit组合,会不会是排序量太大搞的?于是我们简化SQL,去除后面的order by 和 limit,看看到底用了多少记载来排序。
<span class="hljs-keyword">select</span> <span class="hljs-keyword">count</span>(*) <span class="hljs-keyword">from</span> contact c <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> contact_branch cb <span class="hljs-keyword">on</span> c.id = cb.contact_id <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> branch_user bu <span class="hljs-keyword">on</span> cb.branch_id = bu.branch_id <span class="hljs-keyword">and</span> bu.status <span class="hljs-keyword">in</span> ( <span class="hljs-number">1</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> org_emp_info oei <span class="hljs-keyword">on</span> oei.data_id = bu.user_id <span class="hljs-keyword">and</span> oei.node_left >= <span class="hljs-number">2875</span> <span class="hljs-keyword">and</span> oei.node_right <= <span class="hljs-number">10802</span> <span class="hljs-keyword">and</span> oei.org_category = - <span class="hljs-number">1</span> +<span class="hljs-comment">----------+</span> | <span class="hljs-keyword">count</span>(*) | +<span class="hljs-comment">----------+</span> | <span class="hljs-number">778878</span> | +<span class="hljs-comment">----------+</span> <span class="hljs-number">1</span> <span class="hljs-keyword">row</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">set</span> (<span class="hljs-number">5.19</span> sec)
发明排序此前竟然锁定了778878笔记录,假如针对70万的结果集排序,将是劫难性的,怪不得这么慢,那我们能不克不及换个思绪,先按照contact的created_time排序,再来join会不会比力快呢?
于是革新成下面的语句,也可以用straight_join来优化:
select c.id, c.name, c.position, c.sex, c.phone, c.office_phone, c.feature_info, c.birthday, c.creator_id, c.is_keyperson, c.giveup_reason, c.status, c.data_source, from_unixtime(c.created_time) as created_time, from_unixtime(c.last_modified) as last_modified, c.last_modified_user_id from contact c where exists ( select 1 from contact_branch cb inner join branch_user bu on cb.branch_id = bu.branch_id and bu.status in ( 1, 2) inner join org_emp_info oei on oei.data_id = bu.user_id and oei.node_left >= 2875 and oei.node_right <= 10802 and oei.org_category = – 1 where c.id = cb.contact_id ) order by c.created_time desc limit 0 , 10;
验证一下结果 估计在
<span class="hljs-number">1</span>ms内,晋升了<span class="hljs-number">13000</span>多倍! sql <span class="hljs-number">10</span> rows <span class="hljs-keyword">in</span> <span class="hljs-keyword">set</span> (<span class="hljs-number">0.00</span> sec)
本认为至此大工告成,但我们在前面的剖析中漏了一个细节,先排序再join和先join再排序理论上开销是一样的,为什么晋升这么多是由于有一个limit!大致施行历程是:mysql先按索引排序得到前10笔记录,然后再去join过滤,当发明不足10条的时候,再次去10条,再次join,这明显在内层join过滤的数据非常多的时候,将是劫难的,极端状况,内层一条数据都寻不到,mysql还傻乎乎的每次取10条,几乎遍历了这个数据表!
用不一样参数的SQL试验下:
<span class="hljs-keyword">select</span> sql_no_cache c.id, c.name, c.position, c.sex, c.phone, c.office_phone, c.feature_info, c.birthday, c.creator_id, c.is_keyperson, c.giveup_reason, c.status, c.data_source, from_unixtime(c.created_time) <span class="hljs-keyword">as</span> created_time, from_unixtime(c.last_modified) <span class="hljs-keyword">as</span> last_modified, c.last_modified_user_id <span class="hljs-keyword">from</span> contact c <span class="hljs-keyword">where</span> <span class="hljs-keyword">exists</span> ( <span class="hljs-keyword">select</span> <span class="hljs-number">1</span> <span class="hljs-keyword">from</span> contact_branch cb <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> branch_user bu <span class="hljs-keyword">on</span> cb.branch_id = bu.branch_id <span class="hljs-keyword">and</span> bu.status <span class="hljs-keyword">in</span> ( <span class="hljs-number">1</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> org_emp_info oei <span class="hljs-keyword">on</span> oei.data_id = bu.user_id <span class="hljs-keyword">and</span> oei.node_left >= <span class="hljs-number">2875</span> <span class="hljs-keyword">and</span> oei.node_right <= <span class="hljs-number">2875</span> <span class="hljs-keyword">and</span> oei.org_category = - <span class="hljs-number">1</span> <span class="hljs-keyword">where</span> c.id = cb.contact_id ) <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> c.created_time <span class="hljs-keyword">desc</span> <span class="hljs-keyword">limit</span> <span class="hljs-number">0</span> , <span class="hljs-number">10</span>; Empty <span class="hljs-keyword">set</span> (<span class="hljs-number">2</span> <span class="hljs-keyword">min</span> <span class="hljs-number">18.99</span> sec)
2 min 18.99 sec!比此前的状况还糟糕许多。由于mysql的nested loop机制,碰到这种状况,根本是没法优化的。这条语句终究也只能交给利用系统去优化本人的逻辑了。 通过这个例子我们可以看到,并不是所有语句都能优化,而往往我们优化时,由于SQL用例回来时落掉一些极端状况,会造成比本来还严峻的后果。所以,第一:不要盼望所有语句都能通过SQL优化,第二:不要过于自信,只针对详细case来优化,而忽略了更复杂的状况。
慢查询的案例就剖析到这儿,以上只是一些比力典型的案例。我们在优化历程中碰到过超越1000行,触及到16个表join的“垃圾SQL”,也碰到过线上线下数据库差别致使利用直接被慢查询拖死,也碰到过varchar等值比力没有写单引号,还碰到过笛卡尔积查询直接把从库搞死。再多的案例其实也只是一些经历的积存,假如我们熟知查询优化器、索引的内部道理,那么剖析这些案例就变得特殊简便了。
本文以一个慢查询案例引入了MySQL索引道理、优化慢查询的一些办法论;并针对碰到的典型案例做了具体的剖析。其实做了这么长时间的语句优化后才发明,任何数据库层面的优化都抵不上利用系统的优化,一样是MySQL,可以用来支撑Google/FaceBook/Taobao利用,但大概连你的个人网站都撑不住。套用比来比力流行的话:“查询容易,优化不易,且写且珍爱!”
参照 文献:
1.《高机能MySQL》
2.《数据构造与算法剖析》
更多MySQL相关技术文章,请拜访MySQL教程栏目停止学习!
以上就是MySQL索引道理乃至优化的具体内容,更多请关注百分百源码网其它相关文章!