SQL优化之in或range查询

前言

读过《高性能MYSQL》的读者应该还记得,建议使用in代替部分rang查询,提高查询效率,因为在一条索引里面,range字段后面的部分是不生效的。但是使用in的时候需要注意,在某些情况下存在问题。

问题

在线上巡检时发现一条慢SQL,语句如下:

SELECT *
FROM base_table
WHERE code IN (
  。。。。。。
)

其中 code 已经在表中单独创建了 普通索引 ,并且 in 条件的list 大约 2万行 。首先执行 EXPLAIN 看下具体的执行计划。

字段名
id 1
select_type SIMPLE
table base_table
partitions NULL
type ALL
possible_keys idx_code
key NULL
key_len NULL
ref NULL
rows 1420041
filtered 50.00
Extra Using where


我们可以看见MySQL没有使用我们创建的索引列而是选择了全表扫描。了解MySQL执行过程的同学都知道,在执行前MySQL会先执行优化器,不使用索引的原因大部分是由它导致的,那么全表扫描真的减少了执行时间么?
在测试过程中我们发先当减少in的list大小(2万减小到1万时),系统开始使用索引查询,执行时间是238ms(相对全表扫描的2300ms 提高了整整一个数量级!),MySQL在进行负优化!!!

解决

了解到这个数据,我们完全可以在业务层将大的查询分片,提高查询速度。

原理


为了解决这个问题我们可以参照MySQL官方8.2.1.23节给出的什么情况下 MySQL会选择全表扫描:

  • The table is so small that it is faster to perform a table scan than to bother with a key lookup. This is common for tables with fewer than 10 rows and a short row length.
  • There are no usable restrictions in the ON or WHERE clause for indexed columns.
  • You are comparing indexed columns with constant values and MySQL has calculated (based on the index tree) that the constants cover too large a part of the table and that a table scan would be faster. See Section 8.2.1.1, “WHERE Clause Optimization”.
  • You are using a key with low cardinality (many rows match the key value) through another column. In this case, MySQL assumes that by using the key it probably will do many key lookups and that a table scan would be faster.


带下滑线的说的很清楚:
您正在将索引列与常量值进行比较,MySQL(基于索引树)计算出常量覆盖了表中太大的一部分,如果使用表扫描会更快。


就是说MySQL计算了执行消耗,自认为使用全表比走索引更快。


我们接下来统计了:

  • 查询语句的最后返回行数/总行数为:2093 / 1444785
  • 查询语句code的个数/code的总个数:19329 / 1349374


根本不是一个数量级的,是MySQL的估算出现了问题。

那么MySQL怎么估算的呢?

在MySQL官方文档8.3.1 How MySQL Uses Indexes 最后给了一句这样的总结话:
Indexes are less important for queries on small tables, or big tables where report queries process most or all of the rows. When a query needs to access most of the rows, reading sequentially is faster than working through an index. Sequential reads minimize disk seeks, even if not all the rows are needed for the query. See Section 8.2.1.23, “Avoiding Full Table Scans” for details.
翻译后就是
    索引对于小表或覆盖了大部分行大表上,不那么重要。当查询需要访问大多数行时,顺序读取要比通过索引更快。即使查询不需要所有的行,顺序读取也可以最小化磁盘IO消耗。详见8.2.1.23节,避免全表扫描。


就是说 MySQl 的优化器(Optimization)估算后发现顺序读取可以减少消耗,那我们可以使用 Optimizer Trace 指令 分析优化器的决策和执行过程。具体的操作可以参考:https://segmentfault.com/a/1190000018136007


因为笔者的数据库是线上生产数据库就不再贴出我执行后的结果了,既然是优化器估算错误我们接下来去看看官方优化器是怎么做的。

MySQL官方优化器

在官方文档》优化器》SQL语句优化器》查询语句优化器 章节的 Equality Range Optimization of Many-Valued Comparisons 副标题里面清晰说明了,如何估算:

  • If there is a unique index on col_name, the row estimate for each range is 1 because at most one row can have the given value.
  • Otherwise, any index on col_name is nonunique and the optimizer can estimate the row count for each range using dives into the index or index statistics.

我们是普通索引,所以优化器是按照元祖的平均值去估算的整体,会存在误差。


那我们如何让MySQL不再使用误差较大的估算数据呢?官方说了两个办法:

  • 使用ANALYZE TABLE指令,重新优化表可提高估算的精确度
  • 配置eq_range_index_dive_limit系统参数可以控制使用 index dive(精确估算) 还是 index statistics(按统计值估算)

eq-range-index-dive-limit 系统参数

终于找到了这个问题的最终解法,我们先来看看介绍:

Command-Line Format --eq-range-index-dive-limit=#
System Variable eq_range_index_dive_limit
Scope Global, Session
Dynamic Yes
Type Integer
Default Value 200
Minimum Value 0
Maximum Value 4294967295


The optimizer can make row estimates using index dives or index statistics. If eq_range_index_dive_limit is greater than 0, the optimizer uses existing index statistics instead of index dives if there are eq_range_index_dive_limit or more equality ranges. Thus, to permit use of index dives for up to N equality ranges, set eq_range_index_dive_limit to N + 1. To disable use of index statistics and always use index dives regardless of N, set eq_range_index_dive_limit to 0.


翻译:

  • 如果设置为0 ,则一直使用 index dive 精确计算
  • 如果设置参数为 N > 0
    • 传入参数个数为 0 - N  时使用 index dive
    • 传入参数个数 > N  时,使用 index statistic 估算

由于参数默认为200,导致系统使用的估算值(偏高),认为当条数到2W时 使用索引不如全表扫描快,所以索引失效了。我们可以将参数设置为0,解决这个问题。

思考:

什么情况下,统计数据容易出现较大偏差?








答案是: 当数不同code的行数分布不均匀时,容易导致估算数据不准确。

实践

在实际实践时,由于更改数据库原始参数影响返回过大, 想通过 force index  指令强制使用 code  索引,结果 指令执行后仍然是未使用任何索引。 force index  失效了!
联系DBA在线上数据库直接执行语句,结果返回:
![image.png](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbnRyYW5ldHByb3h5LmFsaXBheS5jb20vc2t5bGFyay9sYXJrLzAvMjAyMC9wbmcvMjE4MDQyLzE1OTk0ODE1ODYyMTUtNmI2MzJlYjEtYzJkZS00NWFkLTlmZmItOTFiZmZhODVhY2NjLnBuZw?x-oss-process=image/format,png#align=left&display=inline&height=78&margin=[object Object]&name=image.png&originHeight=156&originWidth=1645&size=14707&status=done&style=none&width=822.5)
官方关于这个错误给出了解决方案:    
For individual queries that exceed the available range optimization memory and for which the optimizer falls back to less optimal plans, increasing the range_optimizer_max_mem_size value may improve performance.
对于超出可用范围优化内存的单个查询,增加范围优化器的最大mem大小值可以提高性能。详见官方说明


range-optimizer-max-mem-size参数

Command-Line Format --range-optimizer-max-mem-size=#
System Variable range_optimizer_max_mem_size
Scope Global, Session
Dynamic Yes
Type Integer
Default Value (≥ 5.7.12) 8388608
Default Value (≤ 5.7.11) 1536000
Minimum Value 0
Maximum Value 18446744073709551615

The limit on memory consumption for the range optimizer. A value of 0 means “no limit.” If an execution plan considered by the optimizer uses the range access method but the optimizer estimates that the amount of memory needed for this method would exceed the limit, it abandons the plan and considers other plans. For more information, see Limiting Memory Use for Range Optimization.
范围优化器的内存消耗限制。值为0意味着没有限制。如果优化器考虑的执行计划使用范围访问方法,但优化器估计该方法所需的内存量将超过限制,它将放弃该计划并考虑其他计划。

附录

参考

MySQL not using indexes with WHERE IN clause?
eq_range_index_dive_limit的作用


你可能感兴趣的:(数据库+php,mysql)