SQL优化之in或range查询

前言

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

问题

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

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

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

字段名
id1
select_typeSIMPLE
tablebase_table
partitionsNULL
typeALL
possible_keysidx_code
keyNULL
key_lenNULL
refNULL
rows1420041
filtered50.00
ExtraUsing 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 Variableeq_range_index_dive_limit
ScopeGlobal, Session
DynamicYes
TypeInteger
Default Value200
Minimum Value0
Maximum Value4294967295


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 Variablerange_optimizer_max_mem_size
ScopeGlobal, Session
DynamicYes
TypeInteger
Default Value (≥ 5.7.12)8388608
Default Value (≤ 5.7.11)1536000
Minimum Value0
Maximum Value18446744073709551615

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的作用


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用以下 SQL 语句进行分页查询,每次查询 5000 条记录: ``` SELECT * FROM table_name LIMIT 5000 OFFSET 0; SELECT * FROM table_name LIMIT 5000 OFFSET 5000; SELECT * FROM table_name LIMIT 5000 OFFSET 10000; ... ``` 其中,OFFSET 的值需要根据当前查询的页数和每页显示的记录数计算得出。 如果每次查询的记录数不足 5000 条,可以在最后一次查询时修改 LIMIT 子句的值,如: ``` SELECT * FROM table_name LIMIT 20000 OFFSET 0; ``` 这个语句将查询前 20000 条记录。 在代码中可以使用循环来实现分页查询。比如使用 Python 的 pymysql 库,可以写出如下的代码: ``` import pymysql # 连接数据库 conn = pymysql.connect(host='localhost', user='root', password='123456', database='db_name') # 创建游标 cur = conn.cursor() # 每页显示的记录数 page_size = 5000 # 查询总记录数 cur.execute('SELECT COUNT(*) FROM table_name') total_records = cur.fetchone()[0] # 计算总页数 total_pages = (total_records + page_size - 1) // page_size # 分页查询 for page in range(total_pages): offset = page * page_size limit = page_size if page < total_pages - 1 else total_records - page * page_size cur.execute(f'SELECT * FROM table_name LIMIT {limit} OFFSET {offset}') results = cur.fetchall() # 处理查询结果 for row in results: # 处理每一行记录 pass # 关闭游标和连接 cur.close() conn.close() ``` 以上代码做了如下操作: 1. 连接数据库; 2. 计算总记录数和总页数; 3. 循环查询每一页的数据; 4. 处理查询结果; 5. 关闭游标和连接。 注意,以上代码仅供参考,需要根据实际情况进行修改和优化。同时,为了避免 SQL 注入攻击,建议使用参数化查询

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值