连接表上的条件比引用条件更快

时间:2013-10-23 22:12:54

标签: mysql sql join

我有一个涉及两个表的查询:table A有很多行,并且包含一个名为b_id的字段,该字段引用表B中的记录,该记录有大约30个不同的表行。表格A的索引位于b_id,表格B的索引位于name列。

我的查询看起来像这样:

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

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

此查询非常慢(以2秒为单位),并使用说明,显示查询优化器以表B开头,提供约29行,然后扫描表A。执行STRAIGHT_JOIN,转动订单并立即查询。

我不是黑魔法的粉丝,所以我决定尝试别的东西:在B中提出名为dummy的记录的ID,比方说23,并且然后将查询简化为:

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

令我惊讶的是,这个查询实际上比直接连接要慢,向北移动一秒。

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

更新:在评论中发出请求后,解释输出:

直接加入:

+----+-------------+-------+--------+-----------------+---------+---------+---------------+--------+-------------+
| 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 (<all the ids except for 23>) AND <condition>;

这比无连接运行得更快,但仍然比连接慢,所以似乎不等操作负责部分性能命中,但不是全部。

5 个答案:

答案 0 :(得分:4)

如果您使用的是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过滤到基于连接所需的行,然后使用外键来获取行表格1}}。

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

A

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

基本规则:如果 | ref | rows | Extra | | schema.A.b_id | 1 | Using where | 没有说使用索引,那么您需要添加合适的索引。

看看解释输出,第一个有趣的事情是具有讽刺意味的是每一行的最后一件事;即EXPLAIN

在第一个例子中,我们看到了

Extra

这两个使用的地方都不好;理想情况下,至少有一个,最好两者都应该说使用索引

当你这样做时

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

并查看使用位置,然后您需要在进行表扫描时添加索引。

例如,如果你做了

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

你应该看到使用where;使用索引(假设此处Id是主键并具有索引)

如果您随后在最后添加条件

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

并查看使用位置,然后您需要为这两个字段添加索引。只是在字段上有索引并不意味着MySQL可以在跨多个字段的查询期间使用该索引 - 这是查询优化器将在内部决定的内容。我不完全确定内部规则;但通常添加一个额外的索引来匹配查询会有很大的帮助。

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

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

应该更改它,以便在根据这两个字段查询时有一个索引。

我在我的一个有ALTER TABLE `A` ADD INDEX `IndexIdField` (`Id`,`Field`) Transactions表的数据库上试过这个。

我将使用此查询

User

在两个字段上没有索引的情况下运行:

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`);

这次它使用索引 - 结果会更快。


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

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

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

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

由于某种原因,查询优化器决定不使用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. 上的索引 - 我在这里假设由于查询,优化器已经决定执行表扫描会更有效。

第二个解释对我有点担心,因为它没有考虑b_id上的索引;可能是因为b_id(省略了所以我猜它可能是什么)。当我在AND <condition>上使用索引进行尝试时,它确实使用了索引;但是一旦添加条件,它就不会使用索引。

所以,在做的时候

b_id

对我来说,所有表示 SELECT COUNT(A.id) FROM A INNER JOIN B ON A.b_id = B.id. 上的PRIMARY索引是速度差异的来源;我假设因为解释中的B在这个表上有一个外键;它必须是比schema.A.b_id上的索引更好的相关行集合 - 因此查询优化器可以使用此关系来定义要选择的行 - 并且因为主索引比二级索引更好,所以它会更快从B中选择行,然后使用关系链接匹配A中的行。

答案 1 :(得分:2)

我在这里看不到任何奇怪的行为。您需要了解MySQL如何使用索引的基础知识。以下是我通常建议的文章:3 ways MySQL uses indexes

观察人们编写像WHERE (B.name != 'dummy') AND <condition>这样的东西总是很有趣,因为这个AND <condition>可能是MySQL优化器选择特定索引的原因,并且没有正当理由比较其性能查询具有WHERE b_id != 23 AND <condition>的另一个查询,因为这两个查询通常需要不同的索引才能表现良好。

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

所以,让我们比较两个查询。

直接加入

对于A.id顺序中的每一行(它是主键并且是群集的,即数据按顺序存储在磁盘上)从磁盘获取行的数据以检查<condition>是否为met和b_id,然后(我为每个匹配的行重复)找到b_id的相应行,转到磁盘上,取b.name,将它与'dummy'进行比较。虽然这个计划根本没有效率,但是你的A表中只有200000行,所以它看起来相当高效。

没有直接加入

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

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

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

UPD1:您是否仍然可以将未加入的查询的效果与以下内容进行比较:

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

UPD2:您在EXPLAIN中看不到索引的事实并不意味着根本没有使用索引。索引至少用于定义读取顺序:当没有其他有用的索引时,它通常是主键,但是,如上所述,当存在一个完整条件和相应的索引时,MySQL将使用索引。因此,基本上,要了解使用哪个索引,您可以查看输出行的顺序。如果顺序与主键相同,则不使用索引(即使用主键索引),如果行的顺序是混洗的 - 与其他索引相关。

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

答案 2 :(得分:0)

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

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

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

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

所以我最接近的问题是:关于为什么连接比简化查询更快的任何想法? ...简而言之,就是第8层错误。

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

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

只需2美分。

答案 3 :(得分:0)

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

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

通过使用index in(everything other then val),您可以更快地完成SELECT,MySQL将学会使用索引。

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

答案 4 :(得分:0)

这个问题的答案实际上是算法设计的一个非常简单的结果:

  • 这两个查询之间的主要区别是合并操作。

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

以下是关键问题:

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

让我们用比喻解释一下:

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

让我们说我们决定解决这个问题的两种方法:

  1. 我们可以用一只手握住牌组,一张接一张地将卡片移到桌子上,然后再计算。
  2. 我们可以将卡片分为两组:黑色西装和红色西装。然后我们可以在其中一个组上执行步骤1,并重复使用第二组的结果。
  3. 如果我们选择选项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&lt; C2。我们将看到常数因素可以      与依赖相比,对运行时间的影响要小得多      输入大小n。让我们把插入排序的运行时间写成c1n·      n并将sort的运行时间合并为c2n·lg n。然后我们看到了哪里      insert sort在其运行时间中具有n因子,merge sort具有      系数lg n,小得多。(例如,当n = 1000时,      lg n约为10,当n等于一百万时,lg n为      大约只有20个。)尽管插入排序通常运行得更快      一旦输入大小n变为,则小输入大小的合并排序      足够大,合并排序的lg n与n的优势将超过      补偿常数因素的差异。无论多少      较小的c1比c2小,总会有超越的交叉点      哪种合并排序更快。“

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