14.19.3 MySQL 对 GROUP BY 的处理
SQL-92 和之前的版本不允许查询,其中选择列表、HAVING
条件或ORDER BY
列表引用非聚合列,但这些列不在GROUP BY
子句中。例如,这个查询在标准 SQL-92 中是非法的,因为选择列表中的非聚合name
列不在GROUP BY
子句中:
SELECT o.custid, c.name, MAX(o.payment)
FROM orders AS o, customers AS c
WHERE o.custid = c.custid
GROUP BY o.custid;
为了使查询在 SQL-92 中合法,需要将name
列从选择列表中删除或添加到GROUP BY
子句中。
SQL:1999 及更高版本允许这些非聚合的可选特性 T301,如果它们是GROUP BY
列的函数依赖关系:如果存在name
和custid
之间的关系,这个查询是合法的。这将是一个示例,例如,如果custid
是customers
的主键。
MySQL 实现了函数依赖关系的检测。如果启用了ONLY_FULL_GROUP_BY
SQL 模式(默认情况下是启用的),MySQL 将拒绝查询,其中选择列表、HAVING
条件或ORDER BY
列表引用非聚合列,但这些列既不在GROUP BY
子句中也不函数依赖于它们。
MySQL 还允许在启用 SQL ONLY_FULL_GROUP_BY
模式时,未在 GROUP BY
子句中命名的非聚合列,但该列只能包含单个值,如以下示例所示:
mysql> CREATE TABLE mytable (
-> id INT UNSIGNED NOT NULL PRIMARY KEY,
-> a VARCHAR(10),
-> b INT
-> );
mysql> INSERT INTO mytable
-> VALUES (1, 'abc', 1000),
-> (2, 'abc', 2000),
-> (3, 'def', 4000);
mysql> SET SESSION sql_mode = sys.list_add(@@session.sql_mode, 'ONLY_FULL_GROUP_BY');
mysql> SELECT a, SUM(b) FROM mytable WHERE a = 'abc';
+------+--------+
| a | SUM(b) |
+------+--------+
| abc | 3000 |
+------+--------+
使用 ONLY_FULL_GROUP_BY
时,也可以在 SELECT
列表中包含多个非聚合列。在这种情况下,每个这样的列都必须在 WHERE
子句中限制到单个值,并且所有这些限制条件都必须使用逻辑 AND
连接,如以下所示:
mysql> DROP TABLE IF EXISTS mytable;
mysql> CREATE TABLE mytable (
-> id INT UNSIGNED NOT NULL PRIMARY KEY,
-> a VARCHAR(10),
-> b VARCHAR(10),
-> c INT
-> );
mysql> INSERT INTO mytable
-> VALUES (1, 'abc', 'qrs', 1000),
-> (2, 'abc', 'tuv', 2000),
-> (3, 'def', 'qrs', 4000),
-> (4, 'def', 'tuv', 8000),
-> (5, 'abc', 'qrs', 16000),
-> (6, 'def', 'tuv', 32000);
mysql> SELECT @@session.sql_mode;
+---------------------------------------------------------------+
| @@session.sql_mode |
+---------------------------------------------------------------+
| ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION |
+---------------------------------------------------------------+
mysql> SELECT a, b, SUM(c) FROM mytable
-> WHERE a = 'abc' AND b = 'qrs';
+------+------+--------+
| a | b | SUM(c) |
+------+------+--------+
| abc | qrs | 17000 |
+------+------+--------+
如果ONLY_FULL_GROUP_BY
被禁用,MySQL对标准SQL中的GROUP BY
的扩展允许选择列表、HAVING
条件或ORDER BY
列表引用非聚合列,即使这些列不依赖于GROUP BY
列。这使得MySQL接受前面的查询。在这种情况下,服务器可以自由选择每组中的任何值,因此除非它们相同,选择的值都是非确定性的,这可能不是你想要的。此外,对于每组的值选择不能被ORDER BY
子句影响。结果集排序发生在值已经被选择后,并且ORDER BY
不影响服务器选择每组中的哪个值。禁用ONLY_FULL_GROUP_BY
主要有助于在你知道数据的某些性质使得每组中非聚合列的所有值都相同时。
您可以使用ANY_VALUE()
来引用非聚合列,达到同样的效果,而不需要禁用ONLY_FULL_GROUP_BY
。
以下讨论演示了函数依赖性、MySQL在缺乏函数依赖性的情况下产生的错误消息,以及使得MySQL接受查询的方法。
这条查询可能在启用ONLY_FULL_GROUP_BY
的情况下无效,因为select列表中的非聚合address
列未在GROUP BY
子句中命名:
SELECT name, address, MAX(age) FROM t GROUP BY name;
如果name
是t
的主键或唯一非空列,则查询有效。在这种情况下,MySQL识别出选择的列函数依赖于分组列。例如,如果name
是主键,它的值确定了address
的值,因为每个组只有一个主键值和唯一的一行。因此,在组中没有 randomness 在选择address
值的选择中,没有必要拒绝查询。
如果name
不是t
的主键或唯一非空列,则查询无效。在这种情况下,无法推断函数依赖关系,并出现错误:
mysql> SELECT name, address, MAX(age) FROM t GROUP BY name;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP
BY clause and contains nonaggregated column 'mydb.t.address' which
is not functionally dependent on columns in GROUP BY clause; this
is incompatible with sql_mode=only_full_group_by
如果你知道,对于给定的数据集,每个name
值实际上唯一地确定了address
值,那么address
对name
实际上是函数依赖关系。要让MySQL接受查询,你可以使用ANY_VALUE()
函数:
SELECT name, ANY_VALUE(address), MAX(age) FROM t GROUP BY name;
或者禁用ONLY_FULL_GROUP_BY
。
前面的示例非常简单,但是实际上很少会对单个主键列进行分组,因为每个组都只包含一行。对于更复杂的查询中函数依赖关系的示例,请见第14.19.4节,“检测函数依赖关系”。
如果查询包含聚合函数且没有GROUP BY
子句,它不能在选择列表、HAVING
条件或ORDER BY
列表中包含非聚合列,除非启用ONLY_FULL_GROUP_BY
:
mysql> SELECT name, MAX(age) FROM t;
ERROR 1140 (42000): In aggregated query without GROUP BY, expression
#1 of SELECT list contains nonaggregated column 'mydb.t.name'; this
is incompatible with sql_mode=only_full_group_by
没有GROUP BY
时,只有一个组,它是非确定性的选择哪个name
值。这里,也可以使用ANY_VALUE()
,如果MySQL选择的name
值无关紧要:
SELECT ANY_VALUE(name), MAX(age) FROM t;
ONLY_FULL_GROUP_BY
还影响处理使用DISTINCT
和ORDER BY
的查询。考虑一个包含三个列c1
、c2
和c3
的表t
,该表包含以下行:
c1 c2 c3
1 2 A
3 4 B
1 2 C
假设我们执行以下查询,期望结果按c3
排序:
SELECT DISTINCT c1, c2 FROM t ORDER BY c3;
要对结果进行排序,首先需要删除重复项。但是,在删除重复项时,我们是否应该保留第一个行或第三个行?这种任意的选择将影响c3
的保留值,从而使排序也变得任意。为了解决这个问题,一条包含DISTINCT
和ORDER BY
的查询语句将被拒绝,如果其中任何一个ORDER BY
表达式不满足以下条件之一:
-
该表达式等于select列表中的一个
-
该表达式引用了查询的selected表中的所有列
MySQL对标准SQL进行了一些扩展,允许在HAVING
子句中引用select列表中的别名。例如,以下查询返回name
值,该值只出现一次于表orders
:
SELECT name, COUNT(name) FROM orders
GROUP BY name
HAVING COUNT(name) = 1;
MySQL扩展允许在HAVING
子句中使用聚合列的别名:
SELECT name, COUNT(name) AS c FROM orders
GROUP BY name
HAVING c = 1;
标准SQL不允许在GROUP BY
子句中使用非列表达式,因此语句这样是无效的,因为FLOOR(value/100)
是一个非列表达式:
SELECT id, FLOOR(value/100)
FROM tbl_name
GROUP BY id, FLOOR(value/100);
MySQL扩展了标准SQL,允许在GROUP BY
子句中使用非列表达式,因此语句这样是有效的。
标准SQL也不允许在GROUP BY
子句中使用别名。MySQL扩展了标准SQL,允许使用别名,因此可以将查询写成以下形式:
SELECT id, FLOOR(value/100) AS val
FROM tbl_name
GROUP BY id, val;
别名val
被认为是一个列表达式在GROUP BY
子句中。
在 GROUP BY
子句中存在非列表达式时,MySQL 会识别该表达式与 select 列表中的表达式之间的等价关系。这意味着,在启用了 ONLY_FULL_GROUP_BY
SQL 模式的情况下,包含 GROUP BY id, FLOOR(value/100)
的查询是有效的,因为该相同的FLOOR()
表达式也出现在 select 列表中。然而,MySQL 不会尝试识别 GROUP BY
非列表达式的函数依赖性,因此启用了 ONLY_FULL_GROUP_BY
的情况下,以下查询仍然无效,即使第三个选择的表达式是 id
列和 FLOOR()
表达式在 GROUP BY
子句中的简单公式:
SELECT id, FLOOR(value/100), id+FLOOR(value/100)
FROM tbl_name
GROUP BY id, FLOOR(value/100);
一种解决方法是使用派生表:
SELECT id, F, id+F
FROM
(SELECT id, FLOOR(value/100) AS F
FROM tbl_name
GROUP BY id, FLOOR(value/100)) AS dt;