优化使用between子句的SQL

时间:2009-02-17 15:41:25

标签: sql mysql query-optimization

考虑以下两个表:

Table A:
id
event_time

Table B
id
start_time
end_time

表A中的每条记录都映射到表B中的1条记录。这意味着表B没有重叠的句点。表A中的许多记录可以映射到表B中的相同记录。

我需要一个返回所有A.id,B.id对的查询。类似的东西:

SELECT A.id, B.id 
FROM A, B 
WHERE A.event_time BETWEEN B.start_time AND B.end_time

我正在使用MySQL而我无法优化此查询。表A中有约980条记录,表B中有130.000条,这需要永远。我知道这必须执行980个查询,但是在一台强壮的机器上花费超过15分钟是很奇怪的。有什么建议吗?

P.S。我无法更改数据库架构,但我可以添加索引。但是,时间字段上的索引(带有1或2个字段)无济于事。

19 个答案:

答案 0 :(得分:4)

你可能想尝试这样的事情

Select A.ID,
(SELECT B.ID FROM B
WHERE A.EventTime BETWEEN B.start_time AND B.end_time LIMIT 1) AS B_ID
FROM A

如果你有B的Start_Time,End_Time字段的索引,那么这应该工作得很好。

答案 1 :(得分:3)

我不确定这是否可以完全优化。我在MySQL 5.1.30上尝试过它。我还根据其他人的建议在{B.start_time, B.end_time}添加了一个索引。然后我收到了EXPLAIN的报告,但我能得到的最好的是Range Access Method

EXPLAIN SELECT A.id, B.id FROM A JOIN B 
ON A.event_time BETWEEN B.start_time AND B.end_time;

+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                                          |
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------------------------+
|  1 | SIMPLE      | A     | ALL  | event_time    | NULL | NULL    | NULL |    8 |                                                | 
|  1 | SIMPLE      | B     | ALL  | start_time    | NULL | NULL    | NULL |   96 | Range checked for each record (index map: 0x4) | 
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------------------------+

请参阅最右侧的说明。优化器认为可能能够使用{B.start_time, B.end_time}上的索引,但它最终决定不使用该索引。您的结果可能会有所不同,因为您的数据分布更具代表性。

如果将A.event_time与常量范围进行比较,则与索引用法进行比较:

EXPLAIN SELECT A.id FROM A
WHERE A.event_time BETWEEN '2009-02-17 09:00' and '2009-02-17 10:00';

+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key        | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+
|  1 | SIMPLE      | A     | range | event_time    | event_time | 8       | NULL |    1 | Using where | 
+----+-------------+-------+-------+---------------+------------+---------+------+------+-------------+

与@Luke和@Kibbee给出的依赖子查询形式进行比较,它似乎更有效地利用了索引:

EXPLAIN SELECT A.id AS id_from_a,
    (
        SELECT B.id
        FROM B
        WHERE A.id BETWEEN B.start_time AND B.end_time
        LIMIT 0, 1
    ) AS id_from_b
FROM A;

+----+--------------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type        | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+--------------------+-------+-------+---------------+---------+---------+------+------+-------------+
|  1 | PRIMARY            | A     | index | NULL          | PRIMARY | 8       | NULL |    8 | Using index | 
|  2 | DEPENDENT SUBQUERY | B     | ALL   | start_time    | NULL    | NULL    | NULL |  384 | Using where | 
+----+--------------------+-------+-------+---------------+---------+---------+------+------+-------------+

奇怪的是,EXPLAIN将possible_keys列为NULL(即不能使用索引),但毕竟决定使用主键。可能是MySQL的EXPLAIN报告的特质吗?

答案 2 :(得分:2)

我已针对类似问题进行了一些测试 - 根据IP地址计算国家/地区(以数字形式给出)。以下是我的数据和结果:

  • 表A(包含用户和IP地址)包含大约20条记录。
  • 表B(包含每个国家/地区的IP范围)包含大约100000条记录。

使用“between”的JOIN查询大约需要10秒; SELECT查询中的SELECT使用“between”,大约需要5.5秒; SELECT查询中的SELECT使用空间索引大约需要6.3秒。 使用空间索引的JOIN查询需要0秒!

答案 3 :(得分:2)

我通常不推荐像这样的查询,但是......

由于您已指定表A只有大约980行,并且每行映射到表B中的一行,因此您可以执行以下操作,它很可能比笛卡尔联接快得多:< / p>

SELECT A.id AS id_from_a,
    (
        SELECT B.id
        FROM B
        WHERE A.event_time BETWEEN B.start_time AND B.end_time
        LIMIT 0, 1
    ) AS id_from_b
FROM A

答案 4 :(得分:1)

如果您无法更改架构 - 特别是如果您无法在a.event_time上添加索引,我认为在SQL级别上没有太大的改进空间。

我更倾向于在代码中这样做。

  • 将所有B开始/结束/ id元组读入列表,按开始时间排序
  • 阅读所有A事件
  • 每个A事件
    • 找到最大的开始时间&lt; =事件时间(二分搜索会很好)
    • 如果事件时间是&lt; =结束时间,则将A添加到此B的事件列表
    • 否则这个B没有回家

答案 5 :(得分:1)

通过不更改架构意味着您无法添加索引?在start_time和end_time上尝试多列索引。

答案 6 :(得分:1)

请注意,在运行此查询时,实际上在应用条件之前在内存中创建了980x130000条记录。不推荐这样的JOIN,我可以理解为什么它会给你性能问题。

答案 7 :(得分:0)

B上是否有索引(start_time,end_time)?如果没有,也许添加一个可能会加快B行与A行的匹配?

请注意,如果您无法更改架构,也许您无法创建新索引?

答案 8 :(得分:0)

您必须加快执行此查询的唯一方法是使用索引。

注意将A.event_time放入索引,然后放入另一个索引B.start_timeB.end_time

如果你说这是将两个实体联系在一起的唯一条件,我认为这是你可以采取的唯一解决方案。

Fede

答案 9 :(得分:0)

Daremon,这个答案基于你的一条评论,其中你说表A中的每条记录只映射到表B中的一条记录,

您可以在架构中添加其他表吗?如果是,您可以预先计算此查询的结果并将其存储在另一个表中。您还必须使此预先计算的表与表A和B的更改保持同步

答案 10 :(得分:0)

我看到你正在进行两个表的交叉连接。这不是很好,DBMS将花费大量时间来执行该操作。交叉连接是SQL中最昂贵的操作。 执行这么长时间的原因可能是这个。

这样做,它可以解决......

SELECT A.id,B.id 从A,B 在哪里A.id = B.id和A.event_time BETWEEN B.start_time和B.end_time

我希望这可以帮助你:)。

答案 11 :(得分:0)

根据你的评论,A中的每个条目恰好对应于B中的一个条目,最简单的解决方案是从B的id列中删除AUTOINCREMENT,然后用来自A的ID替换所有B的id。

答案 12 :(得分:0)

MySQL不允许您在派生查询中使用INDEX ORDER BY WITH RANGE

这就是为什么你需要创建一个用户定义的函数。

请注意,如果您的范围重叠,则查询将只选择一个(最后开始)。

CREATE UNIQUE INDEX ux_b_start ON b (start_date);

CREATE FUNCTION `fn_get_last_b`(event_date TIMESTAMP) RETURNS int(11)
BEGIN
  DECLARE id INT;
  SELECT b.id
  INTO id
  FROM b
  FORCE INDEX (ux_b_start)
  WHERE b.start_time <= event_date
  ORDER BY
    b.start_time DESC
  LIMIT 1;
  RETURN id;
END;

SELECT COUNT(*) FROM a;

1000


SELECT COUNT(*) FROM b;

200000

SELECT *
FROM (
  SELECT fn_get_last_b(a.event_time) AS bid,
         a.*
  FROM a
) ao, b FORCE INDEX (PRIMARY)
WHERE b.id = ao.bid
  AND b.end_time >= ao.event_time

1000 rows fetched in 0,0143s (0,1279s)

答案 13 :(得分:0)

在B.start_time降序上放一个索引,然后使用此查询:

 SELECT A.id AS idA,
 (SELECT B.id FROM B WHERE A.event_time > B.start_time LIMIT 0, 1
 ORDER BY B.start_time DESC) AS idB
 FROM A

由于B中的时间段是不相交的,因此这将为您提供第一个匹配的时间段,并且您可以摆脱其间,但仍然在那里进行子查询。也许包括索引中的B.id会给你一些额外的小性能提升。 (免责声明:不确定MySQL语法)

答案 14 :(得分:0)

我想不出你有一个130.000行的时间间隔表的原因。 无论如何,这样的设计必须有充分的理由,如果是这样,你必须避免每次尝试计算这样的连接。所以这是我的建议。 我将在表A(A.B_ID)中添加对B.id的引用,并使用触发器来保持一致性。无论何时添加新记录(插入触发器)或even_time列更改(更新触发器),您都将重新计算此时对应的B引用。 您的select语句将从A。

缩减为单个select *

答案 15 :(得分:0)

就个人而言,如果你有一对多关系,并且表中的每条记录只与表b中的一条记录有关,我会将表b id存储在表a中,然后进行常规连接以获取数据。你现在拥有的是一个永远不会真正有效的糟糕设计。

答案 16 :(得分:0)

我的解决方案有两点需要注意:

1)你说你可以添加索引但不能更改架构所以我不确定这是否适合你,因为你不能在MySQL中拥有基于函数的索引,你需要创建一个额外的表B列。 2)此解决方案的另一个警告是您必须使用表B的MyISAM引擎。如果您不能使用MyISAM,那么此解决方案将无法工作,因为空间索引仅支持MyISAM。

因此,假设以上两个对您来说不是问题,以下情况应该起作用并为您提供良好的表现:

此解决方案利用MySQL对空间数据的支持(请参阅documentation here)。虽然空间数据类型可以添加到各种存储引擎,但只有MyISAM支持空间R树索引(请参阅documentation here),以获得所需的性能。另一个限制是空间数据类型仅适用于数字数据,因此您不能将此技术用于基于字符串的范围查询。

我不会详细讨论空间类型如何工作以及空间索引如何有用的理论细节,但您应该查看有关如何使用空间数据类型和索引进行GeoIP查找的Jeremy Cole's explanation here。还要看一下评论,因为如果您需要原始性能并且可以放弃一些准确性,它们会提出一些有用的点和备选方案。

基本前提是我们可以采用开始/结束并使用它们中的两个来创建四个不同的点,一个用于xy网格上以0,0为中心的矩形的每个角,然后进行快速查找进入空间索引以确定我们关心的特定时间点是否在矩形内。如前所述,请参阅Jeremy Cole的解释,以更全面地了解其工作原理。

在您的特定情况下,我们需要执行以下操作:

1)将表更改为MyISAM表(请注意,除非您完全了解此类更改的后果,例如缺少事务和与MyISAM关联的表锁定行为,否则不应执行此操作)。

alter table B engine = MyISAM;

2)接下来,我们添加将保存空间数据的新列。我们将使用多边形数据类型,因为我们需要能够保持一个完整的矩形。

alter table B add column time_poly polygon NOT NULL;

3)接下来,我们使用数据填充新列(请记住,更新或插入表B的任何进程都需要进行修改,以确保它们也填充新列)。由于起始和结束范围是时间,我们需要使用unix_timestamp函数将它们转换为数字(有关其工作原理,请参阅documentation here。)

update B set time_poly := LINESTRINGFROMWKB(LINESTRING(
    POINT(unix_timestamp(start_time), -1),
    POINT(unix_timestamp(end_time), -1),
    POINT(unix_timestamp(end_time), 1),
    POINT(unix_timestamp(start_time), 1),
    POINT(unix_timestamp(start_time), -1)
  ));

4)接下来,我们将空间索引添加到表中(如前所述,这仅适用于MyISAM表并将产生错误“ERROR 1464(HY000):使用的表类型不支持SPATIAL索引” )。

alter table B add SPATIAL KEY `IXs_time_poly` (`time_poly`);

5)接下来,您需要使用以下选择,以便在查询数据时使用空间索引。

SELECT A.id, B.id 
FROM A inner join B force index (IXs_time_poly)
ON MBRCONTAINS(B.time_poly, POINTFROMWKB(POINT(unix_timestamp(A.event_time), 0)));

强制索引可以100%确定MySQL将使用索引进行查找。如果一切顺利,上面的选择说明应该显示类似于以下内容:

mysql> explain SELECT A.id, B.id
    -> FROM A inner join B force index (IXs_time_poly)
    -> on MBRCONTAINS(B.time_poly, POINTFROMWKB(POINT(unix_timestamp(A.event_time), 0)));
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra                                           |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------------------------------------------+
|  1 | SIMPLE      | A     | ALL  | NULL          | NULL | NULL    | NULL |    1065 |                                                 | 
|  1 | SIMPLE      | B     | ALL  | IXs_time_poly | NULL | NULL    | NULL | 7969897 | Range checked for each record (index map: 0x10) | 
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------------------------------------------+
2 rows in set (0.00 sec)

请参阅Jeremy Cole的分析,了解有关此方法的性能优势的详细信息,与之间的条款相比较。

如果您有任何问题,请与我们联系。

谢谢,

-Dipin

答案 17 :(得分:0)

尝试使用标准比较运算符(&lt;和&gt;)。

答案 18 :(得分:-1)

这样的事情?

SELECT A.id, B.id 
FROM A
JOIN B ON A.id =  B.id 
WHERE A.event_time BETWEEN B.start_time AND B.end_time