为啥 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_startrange_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_end8033576667466317。所有索引都不能满足这个条件,所以它必须扫描表。

我相信如果你能把它改写成这样的话它可以被优化:

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 查询?的主要内容,如果未能解决你的问题,请参考以下文章

MySQL BETWEEN 查询不使用索引

为啥目前的数据库查询优化技术不支持计算列的优化?

MySQL查询性能优化

Mysql优化

MySQL数据库篇之索引原理与慢查询优化之二

通过索引优化sql