在SQL中评估相关子查询

时间:2016-04-20 02:12:53

标签: mysql sql

我在评估相关子查询时遇到了麻烦。一个例子是在SELECT中使用相关子查询,因此不需要GROUP BY:

考虑关系:

Movies : Title, Director Length
Schedule : Theatre, Title

我有以下查询

SELECT S.Theater, MAX(M.Length)
FROM Movies M JOIN Schedule S ON M.Title=S.Title
GROUP BY S.Theater

这是每个影院正在播放的最长的电影。这是不使用GROUP BY的相同查询:

SELECT DISTINCT S.theater,
    (SELECT MAX(M.Length)
    FROM Movies M
    WHERE M.Title=S.Title)
FROM Schedule S

但我不明白它是如何运作的。

如果有人能给我一个关于如何评估相关子查询的例子,我会很感激。

谢谢:)

2 个答案:

答案 0 :(得分:1)

从概念的角度来看,假设数据库在没有子查询的情况下遍历结果的每一行:

SELECT DISTINCT S.Theater, S.Title
FROM Schedule S

然后,对于每一个,为你运行子查询:

SELECT MAX(M.Length)
FROM Movies M
WHERE M.Title = (whatever S.Title was)

将其作为值放入。实际上,它(概念上)与使用函数不同:

SELECT DISTINCT S.Theater, SUBSTRING(S.Title, 1, 5)
FROM Schedule S

只是这个函数对另一个表执行查询。

但我在概念上说。数据库可能正在将相关查询优化为更像连接的东西。无论它在内部对性能有什么影响,但对于理解这个概念并不重要。

但是,它可能无法返回您期望的结果。考虑以下数据(对不起sqlfiddle似乎是错误的atm):

CREATE TABLE Movies (
  Title varchar(255),
  Length int(10) unsigned,
  PRIMARY KEY (Title)
);

CREATE TABLE Schedule (
  Title varchar(255),
  Theater varchar(255),
  PRIMARY KEY (Theater, Title)
);

INSERT INTO Movies
VALUES ('Star Wars', 121);
INSERT INTO Movies
VALUES ('Minions', 91);
INSERT INTO Movies
VALUES ('Up', 96);

INSERT INTO Schedule
VALUES ('Star Wars', 'Cinema 8');
INSERT INTO Schedule
VALUES ('Minions', 'Cinema 8');
INSERT INTO Schedule
VALUES ('Up', 'Cinema 8');
INSERT INTO Schedule
VALUES ('Star Wars', 'Cinema 6');

然后这个查询:

SELECT DISTINCT
  S.Theater,
  (
    SELECT MAX(M.Length)
    FROM Movies M
    WHERE M.Title = S.Title
  ) AS MaxLength
FROM Schedule S;

你会得到这个结果:

+----------+-----------+
| Theater  | MaxLength |
+----------+-----------+
| Cinema 6 |       121 |
| Cinema 8 |        91 |
| Cinema 8 |       121 |
| Cinema 8 |        96 |
+----------+-----------+

正如您所看到的,它不是GROUP BY的替代品(您仍然可以使用GROUP BY),它只是为每一行运行子查询。 DISTINCT只会从结果中删除重复项。它没有给出最长的"每个剧院,它只是给每个与剧院名称相关的独特电影长度。

PS:您可能会使用某种ID列来识别电影,而不是使用连接中的标题。这样一来,如果必须修改电影的名称,它只需要在一个地方改变,而不是整个时间表。另外,加入ID号而不是字符串会更快。

答案 1 :(得分:1)

概念上......

要理解这一点,首先忽略关于相关子查询的位。

考虑这样一个语句的操作顺序:

SELECT t.foo FROM mytable t

MySQL准备一个空结果集。结果集中的行将包含一列,因为SELECT列表中有一个表达式。从mytable中检索一行。 MySQL在结果集中添加一行,使用mytable行中foo列的值,将其分配给结果集中的foo列。获取下一行,重复相同的过程,直到没有更多的行要从表中获取。

非常简单的东西。但忍受我。

考虑这个陈述:

SELECT t.foo AS fi, 'bar' AS fo FROM mytable t

MySQL进程以同样的方式。准备一个空的结果集。结果集中的行此次将具有两个列。第一列的名称为fi(因为我们为其指定了名称为fi的别名)。结果集行中的第二列将命名为fo,因为(再次)我们分配了一个别名。

现在我们从mytable中蚀刻一行,并在结果集中插入一行。 foo列的值进入列名fi,文字字符串' bar'进入名为fo的列。继续获取行并在结果集中插入行,直到不再需要获取行。

不太难。

接下来,请考虑这个声明,看起来有点棘手:

SELECT t.foo AS fi, (SELECT 'bar') AS fo FROM mytable t

同样的事情再次发生。空结果集。行有两列,名称为fi和fo。

从mytable中获取一行,并在结果集中插入一行。 foo的值进入列fi(就像之前一样。)这就是它变得棘手的地方......对于结果集中的第二列,MySQL 在parens中执行查询。在这种情况下,它是一个非常简单的查询,我们可以很容易地测试它,看看它返回什么。从该查询中获取结果并将其分配给fo列,并将该行插入结果集。

还在我身边吗?

SELECT t.foo AS fi, (SELECT q.tip FROM bartab q LIMIT 1) AS fo FROM mytable 

这开始看起来更复杂。但它并没有那么多不同。同样的事情再次发生。准备空结果集。行将有两列,一个名称为fi,另一列名为fo。从mytable中获取一行。从foo列获取值,并将其分配给结果行中的fi列。对于fo列,执行查询,并将查询结果分配给fo列。将结果行插入结果集。从mytable中获取另一行,重复该过程。

我们应该停下来注意一些事情。 MySQL在SELECT列表中对该查询很挑剔。真的很挑剔。 MySQL对此有限制。查询必须只返回一列。并且它不能返回多行。

在最后一个示例中,对于插入结果集的行,MySQL正在寻找 single 值以分配给fo列。当我们以这种方式思考时,有意义的是查询不能返回多个列...... MySQL会对第二列的值做什么?而且我们不希望返回多行是有意义的...... MySQL会对多行做什么?

MySQL将允许查询返回零行。当发生这种情况时,MySQL会为fo列分配一个NULL。

如果您对此有所了解,那么95%的方式可以理解相关的子查询。

让我们看另一个例子。我们的单行SQL有点不合适,所以我们只需添加一些换行符和空格,以便我们更轻松地使用它。额外的空格和换行不会改变我们陈述的含义。

SELECT t.foo AS fi
     , ( SELECT q.tip
           FROM bartab q
          WHERE q.col = t.foo
          ORDER BY q.tip DESC
          LIMIT 1
        ) AS fo
   FROM mytable t

好的,这看起来要复杂得多。但它真的吗?它又是相同的事物。准备一个空的结果集。行将有两列,fi和fo。从mytable中获取一行,并准备好一行插入结果集。复制foo列中的值,将其指定给fi列。对于fo列,执行查询,将查询返回的单个值带到fo列,然后将行推入结果集。从mytable中获取下一行,然后重复。

解释(finall!)关于"相关"。

的部分

我们将运行该查询以获取fo列的结果。它包含来自外部表的列的引用t.foo。在此示例中出现在WHERE子句中;它没有必要,它可以出现在声明的任何地方。

MySQL使用它做什么,当它运行该子查询时, foo列的值传入查询。如果我们刚从mytable获取的行在foo列中的值为42 ...该子查询等效于

         SELECT q.tip
           FROM bartab q
          WHERE q.col =   42
          ORDER BY q.tip DESC
          LIMIT 1

但是由于我们没有传入42的字面值,我们传入的是来自外部查询中的行的值,我们的子查询返回的结果是"相关&## 34;到我们在外部查询中处理的行。

我们的子查询中可能要复杂得多,只要我们记住SELECT列表中有关子查询的规则...它必须返回一列,最多一行。它最多返回一个值。

相关子查询可以出现在SELECT列表以外的语句部分中,例如WHERE子句。同样的一般概念适用。对于由外部查询处理的每一行,该行中的列的值传递到到子查询。从子查询返回的结果是相关到外部查询中正在处理的行。

讨论省略了实际执行之前的所有步骤...将statament解析为标记,执行语法检查(关键字和标识符在正确的位置)。然后执行语义检查(mytable是否存在,用户是否具有select权限,mytable中是否存在列foo)。然后确定访问计划。并在执行中,获取所需的锁,等等。我们执行的每个语句都会发生这种情况。)

我们不会讨论我们可以使用相关子查询创建的各种可怕的性能问题。虽然前面的讨论应该提供一个线索。由于子查询是为每个行执行的,我们将其放入结果集中(如果它在我们的外部查询的SELECT列表中),或者正在为每个执行外部查询访问的行...如果外部查询返回40,000行,则意味着我们的相关子查询将被执行40,000次。所以我们最好确保子查询快速执行。即使它执行得很快,我们仍然会执行它40,000次。