如何使用 WHERE IN 子查询优化 SQL 查询

Posted

技术标签:

【中文标题】如何使用 WHERE IN 子查询优化 SQL 查询【英文标题】:How to optimize SQL query with WHERE IN subquery 【发布时间】:2020-06-12 12:09:30 【问题描述】:

我在 mysql 5.6 中有两个表用于收集事件数据。 当事件发生时,它会在特定时间段内生成数据。 名为“事件”的父表会记住事件的最后状态。 名为“event_version”的子表会记住任何事件生成的所有数据版本。 此表的架构如下所示:

CREATE TABLE `event` (
`id` BIGINT(20) NOT NULL,
`version_id` BIGINT(20)', -- refers to last event_version   
`version_number` BIGINT(20)', -- consecutive numbers increased when new version appears 
`first_event_time` TIMESTAMP(6),  -- time when a set of event data was generated first time,
-- it is immutable after creation
`event_time` TIMESTAMP(6), -- time when a set of event data changed last time
`other_event_data` VARCHAR(30),--more other columns
PRIMARY KEY (`id`),
INDEX `event_time` (`event_time`),
INDEX `version_id` (`version_id`),
CONSTRAINT `FK_version_id` FOREIGN KEY (`version_id`) REFERENCES `event_version` (`id`)
);




CREATE TABLE `event_version` (
`id` BIGINT(20) NOT NULL,
`event_id` BIGINT(20)', -- refers to event  
`version_number` BIGINT(20)', -- consecutive numbers increased when new version appears 
`event_time` TIMESTAMP(6) NULL DEFAULT NULL, -- time when a set of event data was generated
`other_event_data` VARCHAR(30),--more other columns
PRIMARY KEY (`id`),
INDEX `event_time` (`event_time`), -- time when a set of event data changed
INDEX `event_id` (event_id),
CONSTRAINT `FK_event_id` FOREIGN KEY (`event_id`) REFERENCES `event` (`id`)
);

我想获取在选定时间段内添加了新行的所有 event_version 行。 例如:在 2019-04-28 出现了一个 event.id=21 的事件,它在以下位置生成了版本:

2019-04-28 version_number: 1, event_version.event_id=21  
2019-04-30 version_number: 2, event_version.event_id=21  
2019-05-02 version_number: 3, event_version.event_id=21  
2019-05-04 version_number: 4, event_version.event_id=21  

我希望在我从2019-05-01 to 2019-06-01 搜索句号时找到这些记录。

我们的想法是查找在选定期间创建的所有 event_version.event_id,然后从 event_version 中找到该列表中具有 event_id 的所有行。 要创建事件 ID 列表,我有一个内部 SELECT 查询: 第一个查询:

SELECT DISTINCT event_id FROM event_version WHERE event_time>='2019-05-01' AND event_time<'2019-06-01';  

大约需要 10 秒,返回大约 500 000 条记录。

但我有第二个使用父表的查询,如下所示:

SELECT id FROM event WHERE (first_event_time>='2019-05-01' AND first_event_time<'2019-06-01') OR (first_event_time<'2019-05-01' AND event_time>'2019-05-01');  

大约需要 7s 并且返回相同的一组 id。

然后我在最终查询中使用这个子查询:

SELECT * FROM event_version WHERE event_id IN (<one of prvious two queries>);  

问题是,当我使用第二个子查询时,大约需要 8 秒才能产生结果(大约 500 万条记录)。 使用第一个子查询创建相同的结果需要 3 分钟 15 秒。

我不明白为什么即使子查询产生相同的结果列表,执行时间也会有如此大的差异。 我想使用第一个示例中的子查询,因为它仅取决于 event_time,而不取决于父表中的其他数据。 我有更多类似的表,我只能依靠 event_time。

我的问题:是否有可能优化查询以仅使用 event_time 产生预期结果?

【问题讨论】:

我对你想要做的事情有点迷茫。您有多个关于您可能正在做什么的查询、时间安排和陈述。有什么具体的吗? 那么,你想要 4/12 到 7/12,而不是 5/12 到 7/12 吗? 【参考方案1】:

据我了解,您希望优化以下查询:

SELECT * 
FROM event_version
WHERE event_id IN (
  SELECT DISTINCT event_id
  FROM event_version
  WHERE event_time >= '2019-05-01'
    AND event_time <  '2019-06-01'
)

我会尝试的事情:

event_version(event_time, event_id) 上创建索引。这应该通过避免第二次查找来获得event_id 来提高子查询的性能。尽管整体性能可能会相似。原因是当子查询返回很多行时,WHERE IN (&lt;subquery&gt;) 往往很慢(至少在旧版本中)。

尝试将子查询作为派生表进行 JOIN:

SELECT * 
FROM (
  SELECT DISTINCT event_id
  FROM event_version
  WHERE event_time >= '2019-05-01'
    AND event_time <  '2019-06-01'
) s
JOIN event_version USING(event_id)

看看上面提到的索引在这里有没有帮助。

尝试 EXISTS 子查询:

SELECT v.*
FROM event e
JOIN event_version v ON v.event_id = e.id
WHERE EXISTS (
  SELECT *
  FROM event_version v1
  WHERE v1.event_id = e.id
    AND v1.event_time >= '2019-05-01'
    AND v1.event_time <  '2019-06-01'
)

这里你需要一个event_version(event_id, event_time) 的索引。虽然性能可能更差。我会赌派生表连接解决方​​案。

我的猜测 - 为什么您的第二个查询运行得更快 - 是优化器能够将 IN 条件转换为 JOIN,因为返回的列是 event 表的主键。

【讨论】:

第一次选择不使用父表就足够了。我需要不到 11 秒的时间来完成。我在两列上没有索引,但我认为没有必要。感谢您的帮助。【参考方案2】:

我猜 event_version 表比事件表大很多。子查询很容易做到,您扫描表一次以查找谓词并返回行。当您在子查询中执行此操作时,外部查询检查的每一行都会执行子查询。所以如果 event_version 有 1m 行,它执行子查询 1m 次。可能有一些更聪明的逻辑不会让它变得如此极端,但原则仍然存在。

但是,我看不到第三个查询的重点。如果您将第三个查询与第一个查询一起用作子查询,那么您将获得完全相同的行,如果您已将第一个查询作为从 event_version 中选择全部,那么为什么要使用子查询?

会不会这样:

SELECT * FROM event_version WHERE event_id IN (insert query 1);

SELECT * FROM event_version WHERE event_time>='2019-05-01' AND event_time<'2019-06-01'; 

?

【讨论】:

如示例所示。最终结果应该包含这 4 行 event_id=21,即使在开始 event_time 之前产生了两个第一行。这是因为 event_id 在子查询生成的列表中。 啊,很公平。你可以只使用一个连接。从 event_version a Join event_version b on a.eventID = b.eventid and a.id b.id where b.event_time between .. and .. 这应该进行 2 次表扫描,一次用于 a,一次用于 b,而不是每行一个 我已经测试了上述解决方案,看起来很有希望,但存在一个问题。 db-fiddle.com/f/uCBuoLUrUzyMm2i72eo6s5/2 我应该检索 4 行,但结果有一些翻倍的数据。 哦,有道理。您使用 1 个月的时间,在此期间可能有 2 个事件,所有行加倍。在与选择相同的列上使用分组依据。另外,删除 a.id b.id,否则您将排除时间范围内发生的行。

以上是关于如何使用 WHERE IN 子查询优化 SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

如何使用具有多个 GROUP BY、子查询和 WHERE IN 在大表上的查询来优化查询?

mysql where in(几千个ID)如何优化

sql面试题_SQl优化技巧_1注意通配符中like的使用,百分号放后面_2避免在where子句中对字段进行函数操作_3在子查询当中,尽量用exists代替in_4where子句中尽量不要使用(代码片

MySQL 子查询优化 - where not in(子查询)

SQL 子查询, 如何按照IN的顺序查询

Sql server not in优化