11

这本书中,我目前正在阅读有关数据库的课程,其中给出了以下使用聚合运算符的非法查询示例:

找出最年长水手的姓名和年龄。

考虑以下尝试回答此查询:

SELECT S.sname, MAX(S.age)
FROM Sailors S

该查询的目的是不仅返回最大年龄,而且返回具有该年龄的水手的姓名。然而,这个查询在 SQL 中是非法的——如果 SELECT 子句使用聚合操作,那么它必须使用聚合操作,除非查询包含 GROUP BY 子句!

一段时间后,在使用 MySQL 进行练习时,我遇到了类似的问题,并犯了与上述类似的错误。然而,MySQL 并没有抱怨,只是吐出了一些后来证明不是我需要的表。

上面的查询在 SQL 中真的是非法的,但在 MySQL 中是合法的,如果是,为什么会这样?在什么情况下需要进行这样的查询?

进一步阐述问题:

问题不在于 SELECT 中提及的所有属性是否也应在 GROUP BY 中提及。这就是为什么上面的查询,使用属性和对属性的聚合操作,没有任何 GROUP BY 在 MySQL 中是合法的。

假设 Sailors 表如下所示:

+----------+------+
| sname    | age  |
+----------+------+
| John Doe |   30 |
| Jane Doe |   50 |
+----------+------+

然后查询将返回:

+----------+------------+
| sname    | MAX(S.age) |
+----------+------------+
| John Doe |         50 |
+----------+------------+

现在谁需要那个?John Doe 不是 50 岁,他是 30 岁!正如书中引文所述,这是第一次尝试获取最年长水手的姓名和年龄,在本例中为 50 岁的 Jane Doe。

SQL 会说这个查询是非法的,但 MySQL 只是继续执行并吐出“垃圾”。谁会需要这样的结果?为什么 MySQL 允许新手使用这个小陷阱?

4

4 回答 4

11

顺便说一句,这是默认的 MySQL 行为。但可以通过在my.ini文件或会话中设置 ONLY_FULL_GROUP_BY 服务器模式来更改 -

SET sql_mode = 'ONLY_FULL_GROUP_BY';
SELECT * FROM sakila.film_actor GROUP BY actor_id;

Error: 'sakila.film_actor.film_id' isn't in GROUP BY

ONLY_FULL_GROUP_BY - 不允许选择列表引用未在 GROUP BY 子句中命名的非聚合列的查询。

于 2012-10-11T15:46:22.000 回答
6

基于a_horse_with_no_name在评论中提供的链接,我得出了自己的答案:

似乎 MySQL 使用 GROUP BY 的方式与 SQL 方式不同,以便允许从 GROUP BY 子句中省略列,当它们在功能上依赖于其他包含的列时。

假设我们有一个显示银行帐户活动的表格。这不是一张经过深思熟虑的桌子,但它是我们唯一拥有的一张,而且必须这样做。我们想象一个帐户从“0”开始,而不是跟踪金额,而是记录到它的所有交易,因此金额是交易的总和。该表可能如下所示:

+------------+----------+-------------+
| costumerID | name     | transaction |
+------------+----------+-------------+
|       1337 | h4x0r    |         101 |
|         42 | John Doe |         500 |
|       1337 | h4x0r    |        -101 |
|         42 | John Doe |        -200 |
|         42 | John Doe |         500 |
|         42 | John Doe |        -200 |
+------------+----------+-------------+

很明显,“名称”在功能上取决于“客户 ID”。(在此示例中也可以采用其他方式。)

如果我们想知道每个客户的客户 ID、姓名和当前数量怎么办?

在这种情况下,两个非常相似的查询将返回以下正确结果:

+------------+----------+--------+
| costumerID | name     | amount |
+------------+----------+--------+
|         42 | John Doe |    600 |
|       1337 | h4x0r    |      0 |
+------------+----------+--------+

这个查询可以在 MySQL 中执行,并且根据 SQL 是合法的。

SELECT costumerID, name, SUM(transaction) AS amount
FROM Activity
GROUP BY costumerID, name

这个查询可以在 MySQL 中执行,根据 SQL是不合法的。

SELECT costumerID, name, SUM(transaction) AS amount
FROM Activity
GROUP BY costumerID

下面的行将使查询返回和错误,因为它现在必须遵循使用聚合操作和 GROUP BY 的 SQL 方式:

SET sql_mode = 'ONLY_FULL_GROUP_BY';

允许在 MySQL 中进行第二个查询的论点似乎是假设 SELECT 中提到但在 GROUP BY 中未提及的所有列要么在聚合操作中使用(“事务”的情况),要么是在功能上依赖于其他包含的列,(“名称”的情况)。在“name”的情况下,我们可以确保为所有组条目选择了正确的“name”,因为它在功能上依赖于“costumerID”,因此每组costumerID 只有一个可能的名称。

这种使用 GROUP BY 的方式似乎有缺陷,因为它不会对 GROUP BY 子句中遗漏的内容进行任何进一步的检查。人们可以在他们认为合适的时候从他们的 SELECT 语句中挑选列来放入他们的 GROUP BY 子句,即使包含或省略任何特定列是没有意义的。

Sailor 的例子很好地说明了这个缺陷。使用聚合运算符时(可能与 GROUP BY 结合使用),返回集中的每个组条目的每一列只有一个值。在 Sailors 的情况下,由于省略了 GROUP BY 子句,因此将整个表放入一个单独的组条目中。此条目需要名称和最大年龄。为这个条目选择一个最大年龄是很容易的,因为 MAX(S.age) 只返回一个值。但是,在 S.sname 的情况下,仅在 SELECT 中提到,现在有与整个 Sailor 表中唯一的 sname 一样多的选择(在本例中为两个,John 和 Jane Doe)。MySQL没有任何线索选择,我们没有给它任何,它也没有及时刹车,所以它只能选择先出现的东西,(Jane Doe)。如果两行互换,实际上会意外给出“正确答案”。在 MySQL 中允许这样的事情似乎很愚蠢,如果 GROUP BY 子句中遗漏了某些内容,使用 GROUP BY 的查询结果可能潜在地取决于表的顺序。显然,这就是 MySQL 的运行方式。但是,当它因为“有缺陷的”查询而不知道自己在做什么时,它至少不能礼貌地警告我们吗?我的意思是,当然,如果你给一个程序错误的指令,它可能不会(或不应该)按照你的意愿去做,但如果你给出的指令不明确,我当然不会

于 2012-10-11T20:16:34.630 回答
6

上面的查询在 SQL 中真的是非法的,但在 MySQL 中是合法的吗?

是的

如果是这样,那为什么

我不知道在 MySQL 中做出设计决策的原因,但考虑到您可以从与聚合来自(例如,或)相同的行中获取实际相关数据,只需稍加工作,我不知道从任意行返回额外的列数据没有任何优势。MAXMIN

我非常不喜欢 MySQL 中的这个“特性”,它让很多人在 MySQL 上学习聚合然后转移到不同的 dbms,然后突然意识到他们从来都不知道自己在做什么。

于 2012-10-11T15:38:45.600 回答
2

MySQL 允许这种非标准的 SQL 语法,因为至少在一种特定情况下,它使 SQL 名义上更容易编写。这种情况是当您连接两个具有 PRIMARY / FOREIGN KEY 关系的表(无论是否由数据库强制执行)并且您希望 FOREIGN KEY 端的聚合值和 PRIMARY KEY 端的多个列时。

考虑一个带有CustomerOrders表的系统。想象一下,您想要客户表中的所有Amount字段以及表中字段的总数Orders。在标准 SQL 中,您将编写:

 SELECT C.CustomerID, C.FirstName, C.LastName, C.Address, C.City, C.State, C.Zip, SUM(O.Amount)
    FROM Customer C INNER JOIN Orders O ON C.CustomerID = O.CustomerID
    GROUP BY C.CustomerID, C.FirstName, C.LastName, C.Address, C.City, C.State, C.Zip

注意笨拙的 GROUP BY 子句,想象一下如果您希望客户提供更多列会是什么样子。

在 MySQL 中,您可以编写:

 SELECT C.CustomerID, C.FirstName, C.LastName, C.Address, C.City, C.State, C.Zip, SUM(O.Amount)
    FROM Customer C INNER JOIN Orders O ON C.CustomerID = O.CustomerID
    GROUP BY C.CustomerID

甚至(我想,我还没有尝试过):

 SELECT C.*, SUM(O.Amount)
    FROM Customer C INNER JOIN Orders O ON C.CustomerID = O.CustomerID
    GROUP BY C.CustomerID

写起来容易多了。在这种特殊情况下,它也是安全的,因为您知道表中只有一行Customer将贡献给每个组(假设 CustomerID 是 PRIMARY 或 UNIQUE KEY)。

就个人而言,我不是标准 SQL 语法的这个例外的忠实拥护者(因为在很多情况下使用这种语法并依赖于从组中的任何特定行获取值是不安全的),但我可以看到它在哪里使某些类型的查询更容易并且(在我的第二个 MySQL 示例中)成为可能。

于 2012-10-11T16:11:13.120 回答