首先我们来说下in()这种方式的查询 在《高性能MySQL》里面提及用in这种方式可以有效的替代一定的range查询,提升查询效率,因为在一条索引里面,range字段后面的部分是不生效的。使用in这种方式其实MySQL优化器是转化成了n*m种组合方式来进行查询,最终将返回值合并,有点类似union但是更高效。 同时它存在这一些问题:老版本的MySQL在IN()组合条件过多的时候会发生很多问题。查询优化可能需要花很多时间,并消耗大量内存。新版本MySQL在组合数超过一定的数量就不进行计划评估了,这可能导致MySQL不能很好的利用索引。

这里的“一定数量”在MySQL5.6.5以及以后的版本中是由eq_range_index_dive_limit这个参数控制 。默认设置是10,一直到5.7以后的版本默认修改为200,当然可以手动设置的。5.6手册说明如下:

The eq_range_index_dive_limit system variable enables you to configure the number of values at which the optimizer switches from one row estimation strategy to the other. To disable use of statistics and always use index dives, set eq_range_index_dive_limit to 0. To permit use of index dives for comparisons of up to N equality ranges, set eq_range_index_dive_limit to N + 1. eq_range_index_dive_limit is available as of MySQL 5.6.5. Before 5.6.5, the optimizer uses index dives, which is equivalent to eq_range_index_dive_limit=0.

换言之,

1. eq_range_index_dive_limit = 0 只能使用index dive
2. 0 < eq_range_index_dive_limit <= N 使用index statistics
3. eq_range_index_dive_limit > N 只能使用index dive

在MySQL5.7版本中将默认值从10修改成200目的是为了尽可能的保证范围等值运算(IN())执行计划尽量精准,因为IN()list的数量很多时候都是超过10的。

在MySQL的官方手册上有这么一句话:

the optimizer can estimate the row count for each range using dives into the index or index statistics.

大意: 优化器预估每个范围段--如"a IN (10, 20, 30)" 视为等值比较, 括3个范围段实则简化为3个单值,分别是10,20,30--中包括的元组数,用范围段来表示是因为MySQL的“range”扫描方式多数做的是范围扫描,此处单值可视为范围段的特例;

估计方法有2种:

  1. dive到index中即利用索引完成元组数的估算,简称index dive;
  2. 使用索引的统计数值,进行估算;

相比这2种方式

  1. index dive: 速度慢,但能得到精确的值(MySQL的实现是数索引对应的索引项个数,所以精确)
  2. index statistics: 速度快,但得到的值未必精确

简单说,选项 eq_range_index_dive_limit 的值设定了 IN列表中的条件个数上线,超过设定值时,会将执行计划从 1 变成 2。

为什么要区分这2种方式呢?

  1. 查询优化器会使用代价估算模型计算每个计划的代价,选择其中代价最小的
  2. 单表扫描时,需要计算代价;所以单表的索引扫描也需要计算代价
  3. 单表的计算公式通常是:代价 = 元组数 * IO平均值
  4. 所以不管是哪种扫描方式,都需要计算元组数
  5. 当遇到“a IN (10, 20, 30)”这样的表达式的时候,发现a列存在索引,则需要看这个索引可以扫描到的元组数由多少而计算其索引扫描代价,所以就用到了本文提到的“index dive”、“index statistics”这2种方式。

讨论主题

  1. range查询与索引使用
  2. eq_range_index_dive_limit的说明

range查询与索引使用

SQL如下:

SELECT * FROM pre_forum_post WHERE tid=7932552 AND `invisible` IN('0','-2')
ORDER BY dateline DESC LIMIT 10;

索引如下:

+----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| pre_forum_post | 0 | PRIMARY | 1 | tid | A | NULL | NULL | NULL | | BTREE | | |
| pre_forum_post | 0 | PRIMARY | 2 | position | A | 25521392 | NULL | NULL | | BTREE | | |
| pre_forum_post | 0 | pid | 1 | pid | A | 25521392 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | fid | 1 | fid | A | 1490 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | displayorder | 1 | tid | A | 880048 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | displayorder | 2 | invisible | A | 945236 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | displayorder | 3 | dateline | A | 25521392 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | first | 1 | tid | A | 880048 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | first | 2 | first | A | 1215304 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | new_auth | 1 | authorid | A | 1963184 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | new_auth | 2 | invisible | A | 1963184 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | new_auth | 3 | tid | A | 12760696 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | idx_dt | 1 | dateline | A | 25521392 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | mul_test | 1 | tid | A | 880048 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | mul_test | 2 | invisible | A | 945236 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | mul_test | 3 | dateline | A | 25521392 | NULL | NULL | | BTREE | | |
| pre_forum_post | 1 | mul_test | 4 | pid | A | 25521392 | NULL | NULL | | BTREE | | |
+----------------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

看下执行计划:

root@localhost 16:08:27 [ultrax]> explain SELECT  * FROM pre_forum_post WHERE tid=7932552 AND `invisible` IN('0','-2')
-> ORDER BY dateline DESC LIMIT 10;
+----+-------------+----------------+-------+-------------------------------------------+--------------+---------+------+------+---------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------+-------+-------------------------------------------+--------------+---------+------+------+---------------------------------------+
| 1 | SIMPLE | pre_forum_post | range | PRIMARY,displayorder,first,mul_test,idx_1 | displayorder | 4 | NULL | 54 | Using index condition; Using filesort |
+----+-------------+----------------+-------+-------------------------------------------+--------------+---------+------+------+---------------------------------------+
1 row in set (0.00 sec)

MySQL优化器认为这是一个range查询,那么(tid,invisible,dateline)这条索引中,dateline字段肯定用不上了,也就是说这个SQL最后的排序肯定会生成一个临时结果集,然后再结果集里面完成排序,而不是直接在索引中直接完成排序动作,于是我们尝试增加了一条索引。

root@localhost 16:09:06 [ultrax]> alter table pre_forum_post add index idx_1 (tid,dateline);
Query OK, 20374596 rows affected, 0 warning (600.23 sec)
Records: 0 Duplicates: 0 Warnings: 0
root@localhost 16:20:22 [ultrax]> explain SELECT * FROM pre_forum_post force index (idx_1) WHERE tid=7932552 AND `invisible` IN('0','-2') ORDER BY dateline DESC LIMIT 10;
+----+-------------+----------------+------+---------------+-------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------+------+---------------+-------+---------+-------+--------+-------------+
| 1 | SIMPLE | pre_forum_post | ref | idx_1 | idx_1 | 3 | const | 120646 | Using where |
+----+-------------+----------------+------+---------------+-------+---------+-------+--------+-------------+
1 row in set (0.00 sec)
root@localhost 16:22:06 [ultrax]> SELECT sql_no_cache * FROM pre_forum_post WHERE tid=7932552 AND `invisible` IN('0','-2') ORDER BY dateline DESC LIMIT 10;
...
10 rows in set (0.40 sec)
root@localhost 16:23:55 [ultrax]> SELECT sql_no_cache * FROM pre_forum_post force index (idx_1) WHERE tid=7932552 AND `invisible` IN('0','-2') ORDER BY dateline DESC LIMIT 10;
...
10 rows in set (0.00 sec)

实验证明效果是极好的,其实不难理解,上面我们就说了in()在MySQL优化器里面是以多种组合方式来检索数据的,如果加了一个排序或者分组那势必只能在临时结果集上操作,也就是说索引里面即使包含了排序或者分组的字段依然是没用的。唯一不满的是MySQL优化器的选择依然不够靠谱。 总结下:在MySQL查询里面使用in(),除了要注意in()list的数量以及eq_range_index_dive_limit的值以外(具体见下),还要注意如果SQL包含排序/分组/去重等等就需要注意索引的使用。

eq_range_index_dive_limit的说明

还是上面的案例,为什么idx_1无法直接使用?需要使用hint强制只用这个索引呢?这里我们首先看下eq_range_index_dive_limit的值。

root@localhost 22:38:05 [ultrax]> show variables like 'eq_range_index_dive_limit';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| eq_range_index_dive_limit | 2 |
+---------------------------+-------+
1 row in set (0.00 sec)

根据我们上面说的这种情况0 < eq_range_index_dive_limit <= N使用index statistics,那么接下来我们用OPTIMIZER_TRACE来一看究竟。

{
"index": "displayorder",
"ranges": [
"7932552 <= tid <= 7932552 AND -2 <= invisible <= -2",
"7932552 <= tid <= 7932552 AND 0 <= invisible <= 0"
],
"index_dives_for_eq_ranges": false,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 54,
"cost": 66.81,
"chosen": true
}
// index dive为false,最终chosen是true
...
{
"index": "idx_1",
"ranges": [
"7932552 <= tid <= 7932552"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 120646,
"cost": 144776,
"chosen": false,
"cause": "cost"
}

我们可以看到displayorder索引的cost是66.81,而idx_1的cost是120646,而最终MySQL优化器选择了displayorder这条索引。那么如果我们把eq_range_index_dive_limit设置>N是不是应该就会使用index dive计算方式,得到更准确的执行计划呢?

root@localhost 22:52:52 [ultrax]> set  eq_range_index_dive_limit = 3;
Query OK, 0 rows affected (0.00 sec)
root@localhost 22:55:38 [ultrax]> explain SELECT * FROM pre_forum_post WHERE tid=7932552 AND `invisible` IN('0','-2') ORDER BY dateline DESC LIMIT 10;
+----+-------------+----------------+------+-------------------------------------------+-------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------+------+-------------------------------------------+-------+---------+-------+--------+-------------+
| 1 | SIMPLE | pre_forum_post | ref | PRIMARY,displayorder,first,mul_test,idx_1 | idx_1 | 3 | const | 120646 | Using where |
+----+-------------+----------------+------+-------------------------------------------+-------+---------+-------+--------+-------------+
1 row in set (0.00 sec)

optimize_trace结果如下

{
"index": "displayorder",
"ranges": [
"7932552 <= tid <= 7932552 AND -2 <= invisible <= -2",
"7932552 <= tid <= 7932552 AND 0 <= invisible <= 0"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 188193,
"cost": 225834,
"chosen": true
}
...
{
"index": "idx_1",
"ranges": [
"7932552 <= tid <= 7932552"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 120646,
"cost": 144776,
"chosen": true
}
...
"cost_for_plan": 144775,
"rows_for_plan": 120646,
"chosen": true

在备选索引选择中两条索引都被选择,在最后的逻辑优化中选在了代价最小的索引也就是idx_1 以上就是在等值范围查询中eq_range_index_dive_limit的值怎么影响MySQL优化器计算开销,从而影响索引的选择。另外我们可以通过profiling来看看优化器的统计耗时:

  • index dive
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000048 |
| checking permissions | 0.000004 |
| Opening tables | 0.000015 |
| init | 0.000044 |
| System lock | 0.000009 |
| optimizing | 0.000014 |
| statistics | 0.032089 |
| preparing | 0.000022 |
| Sorting result | 0.000003 |
| executing | 0.000003 |
| Sending data | 0.000101 |
| end | 0.000004 |
| query end | 0.000002 |
| closing tables | 0.000009 |
| freeing items | 0.000013 |
| cleaning up | 0.000012 |
+----------------------+----------+
  • index statistics
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000045 |
| checking permissions | 0.000003 |
| Opening tables | 0.000014 |
| init | 0.000040 |
| System lock | 0.000008 |
| optimizing | 0.000014 |
| statistics | 0.000086 |
| preparing | 0.000016 |
| Sorting result | 0.000002 |
| executing | 0.000002 |
| Sending data | 0.000016 |
| Creating sort index | 0.412123 |
| end | 0.000012 |
| query end | 0.000004 |
| closing tables | 0.000013 |
| freeing items | 0.000023 |
| cleaning up | 0.000015 |
+----------------------+----------+

可以看到当eq_range_index_dive_limit加大使用index dive时,优化器统计耗时明显比ndex statistics方式来的长,但最终它使用了作出了更合理的执行计划。统计耗时0.032089s vs .000086s,但是SQL执行耗时却是约0.03s vs 0.41s。

附:

如何使用optimize_trace
set optimizer_trace='enabled=on';
select * from information_schema.optimizer_trace\G
// 注:optimizer_trace建议只在session模式下开启调试即可 如何使用profile
set profiling=ON;
执行sql;
show profiles;
show profile for query 2;
show profile block io,cpu for query 2;
另外还可以看到memory,swaps,context switches,source 等信息
参考资料
  1. MySQL SQL优化系列之 in与range 查询
  2. MySQL物理查询优化技术---index dive辨析
  3. SHOW PROFILE Syntax

【MySQL】SQL优化系列之 in与range 查询的更多相关文章

  1. Mysql SQL优化系列之——执行计划连接方式浅释

    关系库SQL调优中,虽然思路都是一样的,具体方法和步骤也是大同小异,但细节却不容忽视,尤其是执行计划的具体细节的解读中,各关系库确实有区别,特别是mysql数据库,与其他关系库的差别更大些,下面,我们 ...

  2. [MySQL性能优化系列]提高缓存命中率

    1. 背景 通常情况下,能用一条sql语句完成的查询,我们尽量不用多次查询完成.因为,查询次数越多,通信开销越大.但是,分多次查询,有可能提高缓存命中率.到底使用一个复合查询还是多个独立查询,需要根据 ...

  3. [MySQL性能优化系列]巧用索引

    1. 普通青年的索引使用方式 假设我们有一个用户表 tb_user,内容如下: name age sex jack 22 男 rose 21 女 tom 20 男 ... ... ... 执行SQL语 ...

  4. [MySQL性能优化系列]LIMIT语句优化

    1. 背景 假设有如下SQL语句: SELECT * FROM table1 LIMIT offset, rows 这是一条典型的LIMIT语句,常见的使用场景是,某些查询返回的内容特别多,而客户端处 ...

  5. mysql sql优化实例

    mysql sql优化实例 优化前: pt-query-degist分析结果: # Query 3: 0.00 QPS, 0.00x concurrency, ID 0xDC6E62FA021C85B ...

  6. MySQL高性能优化系列-目录

    MySQL高性能优化系列-目录 (1)Mysql高性能优化规范建议 (2)电商数据库表设计 (3)MySQL分区表使用方法 (4)MySQL执行计划分析 (5)电商场景下的常见业务SQL处理 (6)M ...

  7. MySQL 性能优化系列之一 单表预处理

    MySQL 性能优化系列之一 单表预处理 背景介绍 我们经常在写多表关联的SQL时,会想到 left jion(左关联),right jion(右关联),inner jion(内关联)等. 但是,当表 ...

  8. SQL优化系列(二)- 优化Top SQL

    优化最耗资源的N条SQL语句 如何从SGA或者AWR中找出最消耗资源的SQL, 例如最慢的20条SQL, 然后逐条优化? SQL自动优化工具SQL Tuning Expert Pro for Orac ...

  9. SQL优化系列(三)- 用最少的索引获得最大的性能提升

    从全局出发优化索引 对于高负载的数据库,如何创建最少的索引,让数据库的整体性能提高呢?例如,对于100 条SQL语句,如何创建最佳的5条索引? SQL自动优化工具SQL Tuning Expert P ...

随机推荐

  1. Maven 标签详解

    <span style="padding:0px; margin:0px"><project xmlns="http://maven.apache.or ...

  2. javascript之小积累-匿名函数表达式的最佳实践

    在写js的时候,还是经常会用的匿名函数表达式,比如 setTimeout(function() { console.log(110); }, 1000); 上面那个function()就是匿名函数表达 ...

  3. android view : 绘制

    说到绘制,其实就是如何把一个view的对象,变成手机上可视的图形.很多人总结3个过程:测量,布局,绘制.这也是所有的要显示图形的程序所应该抽象的3个步骤,测量就是测量出你view的大小,布局就是要显示 ...

  4. 8个超实用的jQuery技巧攻略

    1)禁用右键单击功能 如果你想为用户节省网站信息,那么开发者可以使用这段代码——禁用右键单击功能. <font><font>$(document).ready(function ...

  5. update the UI property cross thread

    this.Invoke((MethodInvoker)delegate { txtResult.Text = sbd.ToString(); // runs on UI thread });

  6. Objective-C 代码块(block)的使用

    代码块本质上是和其他变量类似.不同的是,代码块存储的数据是一个函数体.使用代码块是,你可以像调用其他标准函数一样,传入参数数,并得到返回值. 脱字符(^)是块的语法标记.按照我们熟悉的参数语法规约所定 ...

  7. Swift 编程语言自己实践 -自己在Xcode6 动手写20140603

    Swift 是什么,大家都回去百度或者Google,有的甚至认为是Taylor Swift(她是我的偶像),但是如果今天在百度百科里搜索绝对没有说是Apple最新推出的编程语言,因为是在2014年6月 ...

  8. django中间件

    django的中间件就是一个对整体请求过程的装饰器,可以对请求到达view之前,view处理和响应后,通过定义process_request,process_view,process_response ...

  9. JS 设计模式

    1.单例模式:产生一个类的唯一实例 例如:我们在页面中添加遮罩层,每次只能有一个遮罩层存在,因此为单例模式. 在创建遮罩层之前判断是否已经存在,若没有存在,则创建. 这里使用闭包,将是mask变量封装 ...

  10. TOJ3136

                                                          3136: Ubiquitous Religions 时间限制(普通/Java):2000M ...