索引是对记录集的多个字段进行排序的方法。在一张表中为一个字段建立一个索引,将建立另一个数据结构,包含字段数值以及指向相关记录的指针,而后对这个索引结构进行排序,容许在该数据上进行二分法排序。所谓索引,就是以某个字段为关键字的B树文件。
反作用是索引须要额外的磁盘空间,对于MyISAM引擎而言,这些索引是被统一保存在一张表中的,这个文件将很快到达底层文件系统所可以支持的大小限制,若是不少字段都创建了索引的话。html
很是重要的原则,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的顺序能够任意调整。mysql
好比a = 1 and b = 2 and c = 3 创建(a,b,c)索引能够任意顺序,mysql的查询优化器会帮你优化成索引能够识别的形式sql
区分度的公式是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的顺序能够颠倒,因此我才会说,把这个表的全部相关查询都找到,会综合分析;大数据
好比还有以下查询优化
select * from task where status = 0 and type = 12 limit 10; select count(*) from task where status = 0 ;
那么索引创建成(status,type,operator_id,operate_time)就是很是正确的,由于能够覆盖到全部状况。这个就是利用了索引的最左匹配的原则spa
关于explain命令相信你们并不陌生,具体用法和字段含义能够参考官网 explain-output ,这里须要强调rows是核心指标,绝大部分rows小的语句执行必定很快(有例外,下面会讲到)。因此优化语句基本上都是在优化rows。unix
(1)先运行看看是否真的很慢,注意设置SQL_NO_CACHE
(2)where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每一个字段分别查询,看哪一个字段的区分度最高
(3)explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
(4)order by limit 形式的sql语句让排序的表优先查
(5)了解业务方使用场景
(6)加索引时参照建索引的几大原则
(7)观察结果,不符合预期继续从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';
(1)先运行一下,53条记录 1.87秒,又没有用聚合语句,比较慢
53 rows in set (1.87 sec)
(2)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
(3)不须要了解业务场景,只须要改造的语句和改造以前的语句保持结果一致
(4)现有索引能够知足,不须要建索引
(5)用改造后的语句实验一下,只须要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 );
(1)先看看运行多长时间,951条数据6.22秒,真的很慢
951 rows in set (6.22 sec)
(2)先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 | +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
(3)全部字段都应用查询返回记录数,由于是单表查询 0已经作过了951条
(4)让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分布的很平均,那么锁定记录也是百万级别的
(5)找业务方去沟通,看看使用场景。业务方是这么来使用这个SQL语句的,每隔五分钟会扫描符合条件的数据,处理完成后把sync_status这个字段变成1,五分钟符合条件的记录数并不会太多,1000个左右。了解了业务方的使用场景后,优化这个SQL就变得简单了,由于业务方保证了数据的不平衡,若是加上索引能够过滤掉绝大部分不须要的数据
(6)根据创建索引规则,使用以下语句创建索引
alter table stage_poi add index idx_acc_status(accurate_result,sync_status);
(7)观察预期结果,发现只须要200ms,快了30多倍。
952 rows in set (0.20 sec)
咱们再来回顾一下分析问题的过程,单表查询相对来讲比较好优化,大部分时候只须要把where条件里面的字段依照规则加上索引就好,若是只是这种“无脑”优化的话,显然一些区分度很是低的列,不该该加索引的列也会被加上索引,这样会对插入、更新性能形成严重的影响,同时也有可能影响其它的查询语句。因此咱们第4步调差SQL的使用场景很是关键,咱们只有知道这个业务场景,才能更好地辅助咱们更好的分析和优化查询语句。