SQL优化INNER JOIN子查询代替EXISTS

需求是将未分配的记录,按照指定排序选一条分配给一个人。
最原始的实现是,查询更新,先加锁,select出一条,再update,然后释放锁,由于select很慢,还提前生成了缓存,两分钟生成一次。

经过梳理,得到以下SQL,用的exists语法,直接update,利用MySQL的锁机制保证不会重复分配。

UPDATE com_info c SET infoOperatorName='wy-test', infoOperatorFlag=1 
WHERE deleteFlag=0 AND infoType='F道路封闭' AND infoOperatorFlag=0 AND infoOperatorName='' 
AND EXISTS (SELECT 1 FROM ext_info e WHERE e.infoCode=c.infoCode AND e.infoCheckResult='有效') 
ORDER BY DATE(infoCreateTime) DESC, catagory_priority, city_priority, infoLevel DESC, infoCreateTime DESC LIMIT 1 ;

实测耗时4s左右,很稳定。

一番优化后,舍弃了exists,改用inner join + 子查询,如下:

UPDATE com_info t1 INNER JOIN (
    SELECT c.infoCode FROM com_info c INNER JOIN ext_info e ON c.infoCode=e.infoCode  
    WHERE deleteFlag=0 AND infoType='F道路封闭' AND infoOperatorFlag=0 AND infoOperatorName='' 
    AND e.infoCheckResult='有效'
    ORDER BY DATE(infoCreateTime) DESC, catagory_priority, city_priority, infoLevel DESC, infoCreateTime DESC LIMIT 1
) t2 ON t1.infoCode=t2.infoCode SET infoOperatorName='wy-test', infoOperatorFlag=1;

实测耗时0.28s,很稳定。

以下是执行情况和执行计划:


执行耗时

执行计划

这里INNER JOINEXISTS快的原因,其实也看场景。
由于两张表的规模是一样的,并且主键相同,所以JOIN会走主键索引,非常快。EXISTS反而会去遍历了。

扫描方式 type,从慢到快:

  1. all : 全表扫描
  2. index : 全索引扫描,和全表扫描一样。只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大。如在Extra列看到Using index,说明正在使用覆盖索引,只扫描索引的数据,它比按索引次序全表扫描的开销要小很多。这个比all效率要好一点,主要有几种情况,一是当前的查询是覆盖索引的,即我们需要的数据在索引中就可以获取(Extra中有Using Index),或者是使用了索引进行排序,这样就避免数据的重排序(Extra中无 Using Index)。如果Extra中Using Index与Using Where同时出现的话,则是利用索引查找键值的意思
  3. range : 范围查询,避免了全索引扫描,限制的范围越小,效率越高
  4. index_subquery : 在 某 些 IN 查 询 中 使 用 此 种 类 型 , 与 unique_subquery 类似,但是查询的是非唯一 性索引
  5. unique_subquery : 在某些 IN 查询中使用此种类型,而不是常规的 ref
  6. index_merge : 说明索引合并优化被使用了
  7. ref_or_null : 如同 ref, 但是 MySQL 必须在初次查找的结果 里找出 null 条目,然后进行二次查找。
  8. ref : 使用了非唯一性索引进行数据的查找
  9. eq_ref : 使用的唯一性索引进行数据查找,例如主键索引之类的
  10. const : 通常情况下,将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量,如何转化以及何时转化,这个取决于优化器。这个比eq_ref效率高一点。
  11. system : 表只有一行。不过这种情况下就没意义了。
  12. NULL : MySQL不用访问表或者索引就直接能到结果。

(关于覆盖索引:MySQL系列-优化之覆盖索引

测试时发现,由于com_info表的查询需求很多,因此针对不同的场景,对不同的字段建了各种索引,在测试数据库和线上数据库上测试发现,不同的数据分布下,MySQL对索引的选取差别很大,线上库采用了专门为这个场景建的联合索引(包含了WHERE中需要的字段),而测试库中则采用了index_merge的方式,挨个索引走一遍,取交集,实测index_merge还是要比ref慢一两个数量级。

另外index_merge在这个场景下会引发死锁。
因为这里用到了

UPDATE ... WHERE ... ORDER BY ... LIMIT ...;

由于有ORDER BY LIMIT存在,UPDARE的时候,会锁住命中的所有行。而index_merge在高并发的时候,如果索引行数有重叠,因为加锁的顺序可能不同,互相等待可能会导致死锁。加锁顺序的原因是,MySQL会先用索引1进行扫表,再用索引2进行扫表,然后求交集形成一个合并索引。这个使用索引扫表的过程和我们本身SQL使用索引的顺序可能存在互斥,造成了死锁。

这里有个场景可能引发死锁:
请求的WHERE条件不完全一样,但包含了相同的某两个字段,可能造成上述的问题。

index_merge引发的死锁排查

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,805评论 18 399
  • 写在开头 这里所说的劣质SQL限定在数据量未到分库分表和使用分布式缓存程度,指那些执行较差的查询、插入、更新、删除...
    alivs阅读 755评论 0 0
  • 有了孩子的人都会有这样一个感触,“不是你教育孩子,而是孩子在教导你“,他们是天生的哲学家,生来就通晓一切的真理。 ...
    果蛋皮阅读 792评论 3 2
  • 大脑如同肌肉,如果长期不用的话,真的是会退化的。人类一旦停止了稍微有深度的思考,面对问题的时候,就完全是凭着原始大...
    原同学阅读 16,856评论 4 3
  • 我一直以为自己心静如水 直到遇见他―― 一个逆着光行走的男孩 他蓦然转头 笑了 有那么一瞬间我看呆了 但我并没有以...
    仙女味的我阅读 163评论 0 0