【问题标题】:Condition on joined table faster than condition on reference联接表上的条件比参考上的条件快
【发布时间】:2013-10-23 22:12:53
【问题描述】:

我有一个涉及两个表的查询:表A 有很多行,并包含一个名为b_id 的字段,它引用表B 中的一条记录,该表有大约30 个不同的行。表Ab_id 上有一个索引,表Bname 列上有一个索引。

我的查询如下所示:

SELECT COUNT(A.id) FROM A INNER JOIN B ON B.id = A.b_id WHERE (B.name != 'dummy') AND <condition>;

condition 是表 A 上的一些随机条件(我有很多,都表现出相同的行为)。

这个查询非常慢(耗时 2 秒以北),并且使用解释,显示查询优化器从表 B 开始,得出大约 29 行,然后扫描表 A。执行STRAIGHT_JOIN,将订单翻转,查询立即运行。

我不是黑魔法的粉丝,所以我决定尝试其他方法:为B 中名称为dummy 的记录找出id,假设为23,然后简化查询到:

SELECT COUNT(A.id) FROM A WHERE (b_id != 23) AND <condition>;

令我惊讶的是,这个查询实际上比直接连接慢,需要一秒钟。

关于为什么加入比简化查询更快的任何想法?

更新:根据 cmets 中的请求,解释的输出:

直接连接:

+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+
| id | select_type | table | type   | possible_keys   | key     | key_len | ref           | rows   | Extra       |
+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+
|  1 | SIMPLE      | A     | ALL    | b_id            | NULL    | NULL    | NULL          | 200707 | Using where |
|  1 | SIMPLE      | B     | eq_ref | PRIMARY,id_name | PRIMARY | 4       | schema.A.b_id |     1  | Using where |
+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+

没有加入:

+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | A     | ALL  | b_id          | NULL | NULL    | NULL | 200707 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

更新 2: 尝试了另一种变体:

SELECT COUNT(A.id) FROM A WHERE b_id IN (&lt;all the ids except for 23&gt;) AND &lt;condition&gt;;

这比 no join 运行得快,但仍然比 join 慢,所以看起来不等式操作是造成部分性能损失的原因,但不是全部。

【问题讨论】:

  • b.id 有索引吗?
  • 这两个中的一个EXPLAIN 可以告诉你很多,很可能是 MySQL 选择的索引在第二个中不是最优的,但被JOIN 强制考虑一个b_id 上的索引实际上更有益。我的建议是:查看EXPLAIN,查看可能的索引,在测试环境中使用FORCE INDEX 来了解可能性(避免在生产中使用它们,除非您非常确定 i>),如果您当前的单个索引不是最优的,并且您通常需要在您的条件下组合超过 1 个,则考虑组合索引。
  • 我能提供的唯一猜测是 Table A 根本没有索引或索引不佳,而 Table B 索引完美。因此,在 JOIN 之后可以使用 B 的索引。
  • @EugenRieck 是的,确实如此
  • 请原谅我的提问,但为了绝对肯定,您能否澄清一下您是如何排除查询缓存的?

标签: mysql sql join


【解决方案1】:

如果您使用的是 MySQL 5.6 或更高版本,那么您可以询问查询优化器它在做什么;

SET optimizer_trace="enabled=on";

## YOUR QUERY 
SELECT COUNT(*) FROM transactions WHERE (id < 9000) and user != 11;
##END YOUR QUERY

SELECT trace FROM information_schema.optimizer_trace;

SET optimizer_trace="enabled=off";

您几乎肯定需要参考 MySQL 参考中的以下部分 Tracing the OptimiserThe Optimizer


查看第一个解释,查询似乎更快,可能是因为优化器可以使用表B 根据连接过滤到所需的行,然后使用外键获取表中的行@ 987654327@。

在解释中,有趣的是这一点;只有一行匹配,它使用schema.A.b_id。实际上,这是从A 中预过滤行,这是我认为性能差异的来源。

   | ref           | rows   | Extra       |
   | schema.A.b_id |     1  | Using where |

因此,与查询一样,这一切都取决于索引 - 或者更准确地说是缺少索引。仅仅因为您在各个字段上都有索引,并不一定意味着这些索引适合您正在运行的查询。

基本规则:如果EXPLAIN没有说Using Index,那么你需要添加一个合适的索引。

查看解释输出,讽刺的是,每行的最后一个有趣的东西;即Extra

在我们看到的第一个例子中

|  1 | SIMPLE      | A     | .... Using where |
|  1 | SIMPLE      | B     | ...  Using where |

这两种使用where都不好;理想情况下至少有一个,最好两者都应该说 Using index

当你这样做时

SELECT COUNT(A.id) FROM A WHERE (b_id != 23) AND <condition>;

看看Using where然后你需要在它进行表扫描时添加一个索引。

例如,如果你这样做了

EXPLAIN SELECT COUNT(A.id) FROM A WHERE (Id > 23)

你应该看到 使用 where;使用索引(这里假设Id为主键且有索引)

如果你在末尾添加了一个条件

EXPLAIN SELECT COUNT(A.id) FROM A WHERE (Id > 23) and Field > 0

并查看Using where,那么您需要为这两个字段添加索引。仅仅在一个字段上有一个索引并不意味着 MySQL 将能够在跨多个字段的查询期间使用该索引 - 这是查询优化器将在内部决定的事情。我不完全确定内部规则;但通常添加一个额外的索引来匹配查询会有很大帮助。

所以添加一个索引(在上面查询中的两个字段上):

ALTER TABLE `A` ADD INDEX `IndexIdField` (`Id`,`Field`)

应该改变它,以便在基于这两个字段进行查询时有一个索引。

我已经在我的一个具有TransactionsUser 表的数据库上尝试过这个。

我会使用这个查询

EXPLAIN SELECT COUNT(*) FROM transactions WHERE (id < 9000) and user != 11;

在两个字段上没有索引运行:

PRIMARY,user    PRIMARY 4   NULL    14334   Using where

然后添加一个索引:

ALTER TABLE `transactions` ADD INDEX `IndexIdUser` (`id`, `user`);

然后这次又是相同的查询

PRIMARY,user,Index 4    Index 4 4   NULL    12628   Using where; Using index

这一次它使用索引 - 结果会快很多。


来自@Wrikken 的 cmets - 同时请记住,我没有准确的架构/数据,因此一些调查需要对架构进行假设(这可能是错误的)

SELECT COUNT(A.id) FROM A FORCE INDEX (b_id)

would perform at least as good as 

SELECT COUNT(A.id) FROM A INNER JOIN B ON A.b_id = B.id.

如果我们查看 OP 中的第一个 EXPLAIN,我们会看到查询有两个元素。参考 *eq_ref* 的EXPLAIN 文档,我可以看到这将根据这种关系定义要考虑的行。

解释输出的顺序并不一定意味着它先做一个然后再做另一个;它只是选择执行查询的内容(至少据我所知)。

由于某种原因,查询优化器决定不使用b_id 上的索引 - 我在这里假设由于查询,优化器决定执行表扫描会更有效。

第二个解释有点让我担心,因为它没有考虑b_id 上的索引;可能是因为AND &lt;condition&gt;(它被省略了,所以我猜测它可能是什么)。当我使用b_id 上的索引尝试此操作时,它确实使用了索引;但是一旦添加了条件,它就不会使用索引。

所以,在做的时候

  SELECT COUNT(A.id) FROM A INNER JOIN B ON A.b_id = B.id.

这一切表明对我来说,B 上的 PRIMARY 索引是速度差异的来源;我假设因为 schema.A.b_id 解释中的这个表上有一个外键;这必须是比b_id 上的索引更好的相关行集合 - 因此查询优化器可以使用这种关系来定义要选择的行 - 并且因为主索引比二级索引更好,所以选择起来要快得多B 中的行,然后使用关系链接匹配 A 中的行。

【讨论】:

  • 来自 cmets:b_id 上的 FORCE INDEX 在没有 JOIN 的查询上仍然比 with JOIN 慢(它选择 @ 987654357@索引通常)...
  • 我从未使用过 FORCE INDEX;虽然显然有时(mysqldiary.com/…)优化器选择了错误的索引,但我相当确定获取正确的索引策略,特别是对于这种情况,与某些查询相比,这似乎相当简单(优化器可能需要一个提示)。
  • 嗯,这个问题的重点是:我们有一个给定的查询。我们不是为了优化那个查询,我们是为了解释为什么添加JOIN可以让它运行得更快。它很好地解释了如何确定可能需要哪些索引(尽管 OP 声明有很多“随机”条件要添加,所以除非我们得到或多或少完整的 CREATE TABLE 语句和所有/最常用的列表条件,我们可以说得很少)。然而,它并没有解释核心问题:为什么JOIN 比没有JOIN 更快? (这就是我添加赏金的原因;))
  • 我以为我已经回答了 -) 这两种情况是不同的,因为没有足够的索引。没有模式和数据就很难确定;但我相当确定为什么 JOIN 更快的答案是因为没有足够的索引 - 所以使用连接有助于查询优化器减少要扫描的行数。表扫描是性能的敌人,如果幸运的话,您执行的查询看起来应该更慢,但不是因为这两个查询都是次优的,并且需要索引。
  • 如果是这样的话,SELECT COUNT(A.id) FROM A FORCE INDEX (b_id) 的表现至少会和SELECT COUNT(A.id) FROM A INNER JOIN B ON A.b_id = B.id 一样好。
【解决方案2】:

我在这里没有看到任何奇怪的行为。您需要了解 MySQL 如何使用索引的基础知识。这是我通常推荐的一篇文章:3 ways MySQL uses indexes

观察人们写WHERE (B.name != 'dummy') AND &lt;condition&gt;之类的东西总是很有趣,因为AND &lt;condition&gt;可能是MySQL优化器选择特定索引的原因,没有正当理由将查询的性能与另一个是WHERE b_id != 23 AND &lt;condition&gt;,因为这两个查询通常需要不同的索引才能表现良好。

你应该明白的一点是,MySQL 喜欢相等比较,而不喜欢范围条件和不等比较。指定正确的值通常比使用范围条件或指定!= 值更好。

那么,让我们比较一下这两个查询。

直接连接

对于 A.id 顺序中的每一行(这是主键并且是集群的,即数据按其顺序存储在磁盘上)从磁盘中获取该行的数据以检查您的 &lt;condition&gt; 是否满足并且b_id,然后(我对每个匹配的行重复)为 b_id 找到适当的行,在磁盘上,取 b.name,将其与“dummy”进行比较。尽管这个计划根本没有效率,但您的 A 表中只有 200000 行,因此它看起来相当高效。

没有直接连接

对于表 B 中的每一行比较 name 是否匹配,查看 A.b_id 索引(显然按 b_id 排序,因为它是一个索引,因此包含随机顺序的 A.ids),并且对于每个 A .id 为给定的 A.b_id 在磁盘上找到对应的 A 行以检查 &lt;condition&gt;,如果匹配计数 id,否则丢弃该行。

如您所见,第二个查询花费了这么长时间,这并不奇怪,您基本上强制 MySQL 随机访问 A 表中的几乎每一行,在第一个查询中您按顺序读取 A 表存储在磁盘上。

没有连接的查询根本不使用任何索引。它实际上应该与使用直接连接的查询大致相同。我的猜测是b_id!=23&lt;condition&gt; 的顺序很重要。

UPD1:您能否将不使用连接的查询的性能与以下内容进行比较:

SELECT COUNT(A.id)
FROM A
WHERE IF(b_id!=23, <condition>, 0);

UPD2:您在 EXPLAIN 中没有看到索引这一事实并不意味着根本没有使用任何索引。一个索引至少是用来定义读取顺序的:当没有其他有用的索引时,通常是主键,但是,正如我上面所说,当有相等条件和对应的索引时,MySQL 会使用该索引.因此,基本上,要了解使用了哪个索引,您可以查看输出行的顺序。如果顺序与主键相同,则没有使用索引(即使用了主键索引),如果行的顺序被打乱 - 则涉及其他一些索引。

在您的情况下,对于大多数行来说,第二个条件似乎是正确的,但仍然使用索引,即获取 b_id MySQL 以随机顺序进入磁盘,这就是它慢的原因。这里没有黑魔法,第二个条件确实会影响性能。

【讨论】:

  • 关于有无直接连接的好答案(+1)。然而,主要问题是为什么直接连接比不连接快。顺序似乎并不重要。
  • @OnFreund,您能否对您应用的其他条件(以及其他字段的数据类型)有所了解,因为我无法重现您在我拥有的类似数据上描述的行为.一切看起来都和我描述的一样。
  • 似乎其他条件包含LIKE,或MATCH,或长字符串值上的其他字符串函数。
  • 其他条件各不相同,但通常是布尔字段等于 true 和某个日期范围的某种组合
  • @OnFreund,你能检查我更新中的查询吗?
【解决方案3】:

可能这应该是评论而不是答案,但会有点长。

首先,很难相信具有(几乎)完全相同的解释的两个查询以不同的速度运行。此外,如果解释中带有额外行的行运行得更快,则这种可能性较小。我猜更快这个词是这里的关键。

您已经比较了速度(完成查询所需的时间),这是一种极具经验的测试方式。例如,您可能不正确地禁用了缓存,这使得比较无用。更不用说您的&lt;insert your preferred software application here&gt; 在您运行测试时可能发生了页面错误或任何其他可能导致查询速度下降的操作。

衡量查询性能的正确方法是基于解释(这就是它存在的原因)

所以我要回答这个问题的最接近的事情是:关于为什么连接会比简化查询更快的任何想法?...简而言之,第 8 层错误。 p>

不过,我确实有一些其他的 cmets,应该考虑这些以加快速度。如果A.id 是主键(名称闻起来像),根据您的解释,为什么count(A.id) 必须扫描所有行?它应该能够直接从索引中获取数据,但我在额外的标志中看不到Using index。似乎您甚至没有唯一索引,并且它不是不可为空的字段。这也闻起来很奇怪。确保该字段不为空并且上面有一个唯一索引,再次运行解释,确认额外的标志包含Using index,然后(正确)为查询计时。它应该运行得更快。

另请注意,与我上面提到的相同的性能改进方法是将count(A.id) 替换为count(*)

只要我的 2 美分。

【讨论】:

  • 谢谢。 A.id 确实是主键并且定义正确。 count(*)count(id) 运行时间相同
【解决方案4】:

因为 MySQL 不会在 where 中使用 index!=val 的索引。

优化器将通过猜测来决定使用索引。由于“!=”更有可能获取所有内容,因此它会跳过并阻止使用索引来减少开销。 (是的,mysql很笨,不统计索引列)

您可以通过使用index in(everything other then val) 进行更快的 SELECT,这样 MySQL 将学会使用索引。

Example here showing query optimizer will choose to not use index by value

【讨论】:

  • MySQL will not use index for index!=val in where => 您似乎混淆了扫描所有记录并使用索引的事实。正如您在fiddle 中看到的那样,您可以同时执行这两项操作。这就是我在回答中建议做的事情:不扫描行本身,只扫描索引
  • 见这个小提琴sqlfiddle.com/#!9/485ea/1。对于更大的表和不平衡的键,SQL 优化器会做不同的事情。即使“POSSIBLE_KEYS”不是,“索引”仍然是“null”。
  • 我在 cmets 中提到了这一点,大多数记录实际上会将这部分 (B.id != 23) 评估为 true,因此在其上使用索引应该不会产生重大影响
【解决方案5】:

这个问题的答案其实是算法设计的一个很简单的结果:

  • 这两个查询的主要区别在于 merge 操作。

在我上算法课之前,我先提一下合并操作提高性能的原因。合并提高了性能,因为它减少了聚合的整体负载。这是一个迭代与递归的问题。在迭代类比中,我们只是循环遍历整个索引并计算匹配项。在递归的类比中,我们正在分而治之(可以这么说);或者换句话说,我们正在过滤我们需要计算的结果,从而减少我们实际需要计算的数字量。

以下是关键问题:

  • 为什么合并排序比插入排序快?
  • 合并排序总是比插入排序快吗?

让我们用一个比喻来解释:

假设我们有一副扑克牌,我们需要将数字为 7、8 和 9 的扑克牌的数量相加(假设我们事先不知道答案)。

假设我们决定采用两种方法来解决这个问题:

  1. 我们可以一只手拿着一副牌,将牌一张一张地移到桌子上,边走边算。
  2. 我们可以将卡片分为两组:黑色套装和红色套装。然后我们可以对其中一组执行第 1 步,并将结果重复用于第二组。

如果我们选择选项 2,那么我们将问题分成了两半。因此,我们可以计算匹配的黑卡并将数字乘以 2。换句话说,我们正在重用查询执行计划中需要计数的部分。这种推理特别适用当我们提前知道卡片是如何排序的(又名“聚集索引”)时。数一半的牌显然比数一整副牌要少得多。

如果我们想再次提高性能,根据我们数据库的大小,我们甚至可以进一步考虑将其分为四组(而不是两组):梅花、方块、红心和黑桃。我们是否要执行此进一步步骤取决于将卡片分类到其他组的开销是否可以通过性能增益来证明。在少量卡片中,性能提升可能不值得为分类到不同组所需的额外开销。随着卡数量的增加,性能提升开始超过间接成本。

这是“算法简介,第 3 版”(Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest、Clifford Stein)的摘录: (注意:如果有人能告诉我如何格式化子符号,我会编辑它以提高可读性。)

(另外,请记住,“n”是我们正在处理的对象的数量。)

“例如,在第 2 章中,我们将看到两种排序算法。 第一种称为插入排序,所用时间大致等于 c1n2 对 n 个项目进行排序,其中 c1 是一个不依赖于 n 的常数。 即,所花费的时间大致与n2成正比。二、合并 排序,耗时大致等于 c2n lg n,其中 lg n 代表 log2 n 和 c2 是另一个不依赖于 n 的常数。 插入排序通常具有比合并更小的常数因子 排序,使得 c1 我们看到在哪里 插入排序的运行时间为 n 倍,归并排序 lg n 的因子,要小得多。(例如,当 n = 1000 时, lg n 约为 10,当 n 等于一百万时, lg n 为 大约只有 20 个。)虽然插入排序通常运行得更快 与小输入大小的合并排序相比,一旦输入大小 n 变为 足够大,归并排序的 lg n vs. n 的优势将超过 补偿常数因子的差异。 不管多少 c1 比 c2 小,总会有一个交叉点超出 哪种归并排序更快。”

为什么这是相关的?让我们看看这两个查询的查询执行计划。我们会看到有一个由内连接引起的合并操作。

【讨论】:

  • EXPLAIN 中没有任何关于合并的内容,我看不出如何在这里应用索引合并。
  • 在Microsoft SQL Server 2008 R2中查看查询执行计划时可以看到合并操作。周末我会尝试在 MySQL 中运行查询。
  • 有人可以发布 EXPLAIN EXTENDED 和随后的 SHOW WARNINGS\G 的输出吗?
  • (对于 MySQL 中的两个查询。)
猜你喜欢
  • 2019-02-23
  • 2016-12-19
  • 2012-08-10
  • 2019-08-10
  • 1970-01-01
  • 2017-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多