为啥 MySql 不自动优化 BETWEEN 查询?
Posted
技术标签:
【中文标题】为啥 MySql 不自动优化 BETWEEN 查询?【英文标题】:Why doesn't MySql automatically optimises BETWEEN query?为什么 MySql 不自动优化 BETWEEN 查询? 【发布时间】:2016-03-16 07:20:19 【问题描述】:我有两个查询相同的输出
慢查询:
SELECT
*
FROM
account_range
WHERE
is_active = 1 AND '8033576667466317' BETWEEN range_start AND range_end;
执行时间:~800 ms。
解释:
+----+-------------+---------------+------------+------+-------------------------------------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------+------------+------+-------------------------------------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | account_range | NULL | ALL | range_start,range_end,range_se_active_idx | NULL | NULL | NULL | 940712 | 2.24 | Using where |
+----+-------------+---------------+------------+------+-------------------------------------------+------+---------+------+--------+----------+-------------+
非常快速的查询: learnt from here
SELECT
*
FROM
account_range
WHERE
is_active = 1 AND
range_start = (SELECT
MAX(range_start)
FROM
account_range
WHERE
range_start <= '8033576667466317') AND
range_end = (SELECT
MIN(range_end)
FROM
account_range
WHERE
range_end >= '8033576667466317')
执行时间:~1ms
解释:
+----+-------------+---------------+------------+------+-------------------------------------------+---------------------+---------+-------------------+------+----------+------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------+------------+------+-------------------------------------------+---------------------+---------+-------------------+------+----------+------------------------------+
| 1 | PRIMARY | account_range | NULL | ref | range_start,range_end,range_se_active_idx | range_se_active_idx | 125 | const,const,const | 1 | 100.00 | NULL |
| 3 | SUBQUERY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away |
| 2 | SUBQUERY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away |
+----+-------------+---------------+------------+------+-------------------------------------------+---------------------+---------+-------------------+------+----------+------------------------------+
表结构:
CREATE TABLE account_range (
id int(11) unsigned NOT NULL AUTO_INCREMENT,
range_start varchar(20) NOT NULL,
range_end varchar(20) NOT NULL,
is_active tinyint(1) NOT NULL,
bank_name varchar(100) DEFAULT NULL,
addedon timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedon timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
description text,
PRIMARY KEY (id),
KEY range_start (range_start),
KEY range_end (range_end),
KEY range_se_active_idx (range_start , range_end , is_active)
) ENGINE=InnoDB AUTO_INCREMENT=946132 DEFAULT CHARSET=utf8;
请解释一下为什么mysql不自动优化BETWEEN查询?
更新:
从@kordirko 的回答中意识到我的错误。我的表仅包含 non-overlapping
范围,因此两个查询都返回相同的结果。
【问题讨论】:
没有引号会改善情况吗?尝试删除范围开始和范围结束索引 两个查询是否返回相同的行?因为据我所知,根据您在表格中的实际日期,它们可能会给出不同的结果...... 将索引 range_se_active_idx 中的顺序更改为 (is_active, range_start , range_end) 可能会提高第一个查询的性能 @piotrgajow:两个查询的输出相同。 简单的答案是 MySQL 不能假设 start..end 对不重叠。有关示例,请参见 @kordirko 的答案。 【参考方案1】:这样的比较没有意义,因为您将苹果与橙子进行比较。 这两个查询不是等价的,它们给出不同的结果,因此 MySql 以不同的方式优化它们,它们的计划也可能不同。 看这个简单的例子:http://sqlfiddle.com/#!9/98678/2
create table account_range(
is_active int,
range_start int,
range_end int
);
insert into account_range values
(1,-20,100), (1,10,30);
第一个查询给出 2 行:
select * from account_range
where is_active = 1 and 25 between range_start AND range_end;
| is_active | range_start | range_end |
|-----------|-------------|-----------|
| 1 | -20 | 100 |
| 1 | 10 | 30 |
第二次查询只给出 1 行:
SELECT * FROM account_range
WHERE
is_active = 1 AND
range_start = (SELECT MAX(range_start)
FROM account_range
WHERE range_start <= 25
) AND
range_end = (SELECT MIN(range_end)
FROM account_range
WHERE range_end >= 25
)
| is_active | range_start | range_end |
|-----------|-------------|-----------|
| 1 | 10 | 30 |
为了加快这个查询(第一个),两个位图索引可以与“位图和”操作一起使用 - 但 MySql 没有这样的功能。 另一种选择是空间索引(例如 PostgreSql 中的 GIN 索引:http://www.postgresql.org/docs/current/static/textsearch-indexes.html)。 另一种选择是星型转换(或星型模式)-您需要将此表“划分”为两个“维度”或“度量”表和一个“事实”表....但这太宽泛了,如果想了解更多可以从这里开始:https://en.wikipedia.org/wiki/Star_schema
【讨论】:
我敢打赌,您可以在该表中添加另一行并将 0 行添加到零! “快速”查询取决于 start...end 的某些属性。 如果我的数据只包含不重叠的范围,那么我有哪些优化这个查询的选项,我准备探索除 Mysql 之外的其他选项。【参考方案2】:第二次查询很快,因为 MySQL 能够使用可用的索引。
SELECT * FROM account_range
WHERE
is_active = 1 AND
range_start = a_constant_value_1 AND
range_end = a_constant_value_2
上面的查询速度很快,因为range_se_active_idx
索引可以满足搜索条件所以被使用了。
两个子查询也很快(参见 EXPLAIN 输出中的 Select tables optimized away
)
SELECT MAX(range_start) FROM account_range
WHERE range_start <= '8033576667466317'
SELECT MIN(range_end) FROM account_range
WHERE range_end >= '8033576667466317'
因为range_start
和range_end
都已编入索引,所以它们是有序的。
对于有序数据,对于第一个子查询,MySQL 基本上只选择一条记录,其 range_start
等于 8033576667466317
或低于它的一条记录 (MAX(range_start)
)。对于第二个子查询,MySQL 选择一条记录,其 range_end
等于 8033576667466317
或高于它的一条记录 (MIN(range_end)
)。
对于BETWEEN ... AND ..
查询,MySQL 找不到任何索引,因为这不是范围搜索。基本一样
SELECT * FROM account_range
WHERE
is_active = 1 AND
range_start >= '8033576667466317' AND
range_end <= '8033576667466317';
它必须搜索具有range_start
的记录,从8033576667466317
到最大值,也从最小的range_end
到8033576667466317
。所有索引都不能满足这个条件,所以它必须扫描表。
我相信如果你能把它改写成这样的话它可以被优化:
SELECT * FROM account_range
WHERE
is_active = 1 AND
(range_start BETWEEN a_min_value AND a_max_value) AND
(range_end BETWEEN a_min_value AND a_max_value);
【讨论】:
我的疑问在于您所说的“介于”查询与“range_start >= '803...7' AND range_end 问题是,一个查询检查值是否相等,而另一个查询检查一个是否大于另一个。这是两个完全不同的约束,我很惊讶这两个查询给了你相同的结果。根据数据,较慢的一个(带有 between 子句)可能会返回比另一个更多的行。以上是关于为啥 MySql 不自动优化 BETWEEN 查询?的主要内容,如果未能解决你的问题,请参考以下文章