【问题标题】:Help me turn a SUBQUERY into a JOIN帮我把一个 SUBQUERY 变成一个 JOIN
【发布时间】:2011-03-07 07:10:03
【问题描述】:

两张桌子。

电子邮件 id (int10) |所有权 (int10)

消息 emailid (int10) 索引 |消息(中文本)

子查询(这在 mysql 中很糟糕)。

从消息中选择 COUNT(*) WHERE 消息 LIKE '%word%' AND emailid IN (SELECT id FROM emails WHERE Ownership = 32)


这里的用法是我对电子邮件进行搜索(在上面的示例中显然简化了),生成了一个包含 3,000 个电子邮件 ID 的列表。然后我想对消息进行搜索,因为我需要进行文本匹配 - 仅从针对消息的那 3000 封电子邮件中进行。

对消息的查询很昂贵(消息没有被索引)但这很好,因为它只会检查几行。

想法:

i) 连接。到目前为止,我在这方面的尝试没有奏效,并导致对消息表进行全表扫描(即未使用 emailid 索引) ii) 临时表。我认为这可以工作。 iii) 在客户端缓存 id 并运行 2 个查询。这确实有效。不优雅。 iv) 子查询。 mySQL 子查询每次运行第二个查询,所以这不起作用。可能在 mysql 6 中修复。

好的,这就是我目前所拥有的。这些是实际的字段名称(我已经简化了一些问题)。

查询:

SELECT COUNT(*) FROM ticket LEFT JOIN ticket_subject 
ON (ticket_subject.ticketid = ticket.id) 
WHERE category IN (1) 
AND ticket_subject.subject LIKE "%about%"

结果:

1   SIMPLE  ticket  ref     PRIMARY,category    category    4   const   28874    
1   SIMPLE  ticket_subject  eq_ref  PRIMARY     PRIMARY     4   deskpro.ticket.id   1   Using where

耗时 0.41 秒,返回计数 (*) 为 113。

跑步:

SELECT COUNT (*) FROM ticket WHERE category IN (1)

花费 0.01 秒并找到 33,000 个结果。

跑步

SELECT COUNT (*) FROM ticket_subject WHERE subject LIKE "%about%"

花费 0.14 秒并找到 1,300 个结果。

ticket 表和ticket_subject 表都有 300,000 行。

ticket_subject.ticketid 和 ticket.category 上有一个索引。

我现在意识到使用 LIKE 语法是一个错误——因为它对 FULLTEXT 有点牵强。这不是问题。问题是:

1) 表 A - 非常快速的查询,在索引上运行。 0.001 秒 2) 表 B - 中等到慢速查询,无索引 - 进行全表扫描。 0.1 秒。

这两个结果都很好。问题是我必须加入他们,搜索需要 0.3 秒;这对我来说毫无意义,因为表 B 上的组合查询的慢方面应该更快,因为我们现在只搜索该表的一小部分 - 即它不应该进行全表扫描,因为正在加入的字段on 已编入索引。

【问题讨论】:

  • 所以本质上你是想在message LIKE '%word%'位发生之前强制它通过emailid进行过滤?或者这正是你想要阻止的?
  • 是的,这正是我想要发生的。也许它正在发生,而 mySQL 在获取这些结果(33,000)然后搜索它们时速度很慢。但是,从索引列中查找 ticket_subject 表中的这 33,000 个结果似乎比在该表中的非索引列上搜索 300,000 个结果要慢。

标签: mysql join subquery


【解决方案1】:

记得利用布尔值short-circuit evaluation

SELECT COUNT(*) 
FROM messages 
join emails ON emails.id = messages.emailid
WHERE ownership = 32 AND message LIKE '%word%'

这会在评估 LIKE 谓词之前按 ownership 过滤。总是把你便宜的表达放在左边。

另外,我同意 @Martin Smith 和 @MJB 的观点,您应该考虑使用 MySQL 的 FULLTEXT 索引来加快速度。


关于您的评论和其他信息,这里有一些分析:

explain SELECT COUNT(*) FROM ticket WHERE category IN (1)\G

           id: 1
  select_type: SIMPLE
        table: ticket
         type: ref
possible_keys: category
          key: category
      key_len: 4
          ref: const
         rows: 1
        Extra: Using index

注意“使用索引”是一个很好的看,因为它意味着它可以通过读取索引数据结构来满足查询,甚至不接触表的数据。这肯定会运行得非常快。

explain SELECT COUNT(*) FROM ticket_subject WHERE subject LIKE '%about%'\G

           id: 1
  select_type: SIMPLE
        table: ticket_subject
         type: ALL
possible_keys: NULL        <---- no possible keys
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
        Extra: Using where

这表明没有可能的键可以使通配符LIKE 谓词受益。它使用 WHERE 子句中的条件,但必须通过运行表扫描来评估它。

explain SELECT COUNT(*) FROM ticket LEFT JOIN ticket_subject 
ON (ticket_subject.ticketid = ticket.id) 
WHERE category IN (1) 
AND ticket_subject.subject LIKE '%about%'\G

           id: 1
  select_type: SIMPLE
        table: ticket
         type: ref
possible_keys: PRIMARY,category
          key: category
      key_len: 4
          ref: const
         rows: 1
        Extra: Using index

           id: 1
  select_type: SIMPLE
        table: ticket_subject
         type: ref
possible_keys: ticketid
          key: ticketid
      key_len: 4
          ref: test.ticket.id
         rows: 1
        Extra: Using where

同样,访问票证表很快,但被LIKE 条件引起的表扫描破坏了。

ALTER TABLE ticket_subject ENGINE=MyISAM;

CREATE FULLTEXT INDEX ticket_subject_fulltext ON ticket_subject(subject);

explain SELECT COUNT(*) FROM ticket JOIN ticket_subject  
ON (ticket_subject.ticketid = ticket.id)  
WHERE category IN (1)  AND MATCH(ticket_subject.subject) AGAINST('about')

           id: 1
  select_type: SIMPLE
        table: ticket
         type: ref
possible_keys: PRIMARY,category
          key: category
      key_len: 4
          ref: const
         rows: 1
        Extra: Using index

           id: 1
  select_type: SIMPLE
        table: ticket_subject
         type: fulltext
possible_keys: ticketid,ticket_subject_fulltext
          key: ticket_subject_fulltext          <---- now it uses an index
      key_len: 0
          ref: 
         rows: 1
        Extra: Using where

你永远不会让LIKE 表现良好。请参阅我的演示文稿Practical Full-Text Search in MySQL


关于您的评论:好的,我已经对类似大小的数据集(堆栈溢出数据转储中的用户和徽章表 :-) 进行了一些实验。这是我发现的:

select count(*) from users
where reputation > 50000

+----------+
| count(*) |
+----------+
|       37 |
+----------+
1 row in set (0.00 sec)

这真的很快,因为我在信誉列上有一个索引。

           id: 1
  select_type: SIMPLE
        table: users
         type: range
possible_keys: users_reputation_userid_displayname
          key: users_reputation_userid_displayname
      key_len: 4
          ref: NULL
         rows: 37
        Extra: Using where; Using index

select count(*) from badges
where badges.creationdate like '%06-24%'

+----------+
| count(*) |
+----------+
|     1319 |
+----------+
1 row in set, 1 warning (0.63 sec)

正如预期的那样,因为该表有 700k 行,并且它必须进行表扫描。现在让我们进行连接:

select count(*) from users join badges using (userid)
where users.reputation > 50000 and badges.creationdate like '%06-24%'

+----------+
| count(*) |
+----------+
|       19 |
+----------+
1 row in set, 1 warning (0.03 sec)

这似乎并没有那么糟糕。这是解释报告:

           id: 1
  select_type: SIMPLE
        table: users
         type: range
possible_keys: PRIMARY,users_reputation_userid_displayname
          key: users_reputation_userid_displayname
      key_len: 4
          ref: NULL
         rows: 37
        Extra: Using where; Using index

           id: 1
  select_type: SIMPLE
        table: badges
         type: ref
possible_keys: badges_userid
          key: badges_userid
      key_len: 8
          ref: testpattern.users.UserId
         rows: 1
        Extra: Using where

这似乎是智能地使用索引来进行连接,并且它有助于我拥有一个包括用户 ID 和声誉的复合索引。请记住,MySQL 每个表只能使用一个索引,因此为您需要执行的查询定义正确的复合索引非常重要。


您的评论:好的,我已经尝试过声誉 > 5000、声誉 > 500 和声誉 > 50 的情况。这些应该匹配更大的用户集。

select count(*) from users join badges using (userid)
where users.reputation > 5000 and badges.creationdate like '%06-24%'

+----------+
| count(*) |
+----------+
|      194 |
+----------+
1 row in set, 1 warning (0.27 sec)

select count(*) from users join badges using (userid)
where users.reputation > 500 and badges.creationdate like '%06-24%'

+----------+
| count(*) |
+----------+
|      624 |
+----------+
1 row in set, 1 warning (0.93 sec)

select count(*) from users join badges using (userid)
where users.reputation > 50 and badges.creationdate like '%06-24%'
--------------

+----------+
| count(*) |
+----------+
|     1067 |
+----------+
1 row in set, 1 warning (1.72 sec)

解释报告在所有情况下都是相同的,但如果查询在 Users 表中找到更多匹配行,那么它自然必须根据 Badges 表中更多匹配行来评估 LIKE 谓词。

确实,进行连接需要一些成本。令人惊讶的是,它如此昂贵。但是,如果您使用索引,则可以减轻这种情况。

我知道您说过您有一个不能使用索引的查询,但也许是时候考虑使用原始列数据的一些转换版本创建一个冗余列,以便您可以 索引它。在上面的示例中,我可能会创建一个列 creationdate_day 并从 DAYOFYEAR(creationdate) 填充它。


这就是我的意思:

ALTER TABLE Badges ADD COLUMN creationdate_day SMALLINT;
UPDATE Badges SET creationdate_day = DAYOFYEAR(creationdate);
CREATE INDEX badge_creationdate_day ON Badges(creationdate_day);

select count(*) from users join badges using (userid)
where users.reputation > 50 and badges.creationdate_day = dayofyear('2010-06-24')

+----------+
| count(*) |
+----------+
|     1067 |
+----------+
1 row in set, 1 warning (0.01 sec)  <---- not too shabby!

这是解释报告:

          id: 1
  select_type: SIMPLE
        table: badges
         type: ref
possible_keys: badges_userid,badge_creationdate_day
          key: badge_creationdate_day    <---- here is our new index
      key_len: 3
          ref: const
         rows: 1318
        Extra: Using where

           id: 1
  select_type: SIMPLE
        table: users
         type: eq_ref
possible_keys: PRIMARY,users_reputation_userid_displayname
          key: PRIMARY
      key_len: 8
          ref: testpattern.badges.UserId
         rows: 1
        Extra: Using where

【讨论】:

  • 我不熟悉 MySQL,但你确定它在创建执行计划时没有重新排序 where 谓词?
  • @Mike:是的,我确定。任何支持短路求值的编程语言都不应该重新排序布尔表达式!
  • 我刚刚对此进行了一些测试 - 该命令对我的试验没有影响。奇怪的是,如果您单独运行 SQL 查询 - 单词 one 需要 0.12 秒,而所有权则需要 0.01 秒。然而,JOIN 需要 0.37 秒。
  • 我上面说的是,运行 JOIN 所花费的时间是运行慢查询作为全表扫描所花费的 3 倍。
  • @Chris Padfield:那么你需要用 EXPLAIN 进行分析,并可能创建一个索引。
【解决方案2】:
SELECT COUNT(*) 
FROM messages 
join emails ON emails.id = messages.emailid
WHERE message LIKE '%word%' 
AND ownership = 32

问题在于'%word%' 这始终需要扫描邮件。如果您使用的是MyISAM,您可能需要查看full text search

【讨论】:

  • 我真的想说明一种将快速搜索的结果与慢速搜索的结果结合起来的情况。但在这种情况下,对 %word% 的搜索应该非常快,因为它只搜索索引选择的几百或几千行。
  • @Chris - 你能用迄今为止你尝试过的最好的问题更新你的问题吗?这是解释计划?
【解决方案3】:

我想这就是你要找的东西:

select count(*)
from messages m
  inner join emails e
    on e.id = m.emailid
where m.message like '%word%'
  and e.ownership = 32

很难确定它的表现如何。如果 FTS 是因为 WORD 上的起始通配符,那么这样做并不能解决问题。但好消息是,连接可能会限制您必须查看的消息表中的记录。

【讨论】:

  • 谢谢,这在 Martin 的回答中以相同的速度执行。它比运行慢查询慢 3 倍(针对消息的 %word%)。
  • @Chris - 我认为一个问题是你正在对非索引列进行连接 - emails.id - 所以除非你索引该列。您也在对该表强制执行全表扫描 (FTS)。
  • 此列已编入索引。我在上面放了一个解释。
【解决方案4】:

您是否可以将加入反过来?似乎第二个查询成本较低,并且由于整个事情是一个简单的联接,因此您希望执行成本较低的查询以尽可能缩小数据集,然后对更昂贵的查询进行联接。

【讨论】:

  • 好吧,我相信子查询会很快,因为它会使用索引来获取需要检查的消息列表,然后只处理这些消息。问题是我无法创建似乎适用于该逻辑的连接;我拥有的所有连接都比对整个表运行昂贵的查询慢 3 倍。
猜你喜欢
  • 2011-01-18
  • 2013-09-16
  • 2023-02-16
  • 1970-01-01
  • 2017-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多