SQL标准知道一个有趣的特性,你可以投射在GROUP BY
子句中列出的主键(或唯一键)的任何功能依赖,而不需要明确地将该功能依赖添加到GROUP BY
子句中。
这意味着什么呢?考虑一下这个简单的模式:
CREATE TABLE author (
id INT NOT NULL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE book (
id INT NOT NULL PRIMARY KEY,
author_id INT NOT NULL REFERENCES author,
title TEXT NOT NULL
);
为了按作者计算书籍的数量,我们倾向于写:
SELECT a.name, count(b.id)
FROM author a
LEFT JOIN book b ON a.id = b.author_id
GROUP BY
a.id, -- Required, because names aren't unique
a.name -- Required in some dialects, but not in others
在这种情况下,我们必须通过一些独特的东西来分组,因为如果两个作者都叫John Doe,我们仍然希望他们能产生不同的组。因此,GROUP BY a.id
是一个必然的结果。
我们习惯于同时使用GROUP BY a.name
,特别是在这些需要这样的方言中,因为我们在SELECT
子句中列出了a.name
:
- Db2
- Derby
- Exasol
- Firebird
- HANA
- Informix
- Oracle
- SQL Server
但这真的是必须的吗?根据SQL标准,它不是,因为在author.id
和author.name
之间存在着功能依赖。换句话说,对于author.id
的每一个值,恰好有一个author.name
的可能值,或者说author.name
是一个函数。author.id
这意味着,如果我们GROUP BY
两列,或者仅是主键,这并不重要。两种情况下的结果必须是相同的,因此这是有可能的:
SELECT a.name, count(b.id)
FROM author a
LEFT JOIN book b ON a.id = b.author_id
GROUP BY a.id
哪些SQL方言支持这个?
至少有以下SQL方言支持这个语言特性:
- CockroachDB
- H2
- HSQLDB
- MariaDB
- MySQL
- PostgreSQL
- SQLite
- Yugabyte
值得注意的是,在有GROUP BY
的情况下,MySQL曾经简单地忽略了一个列是否可以被明确地预测。虽然下面的查询在大多数方言中被拒绝,但在MySQL中,在引入ONLY_FULL_GROUP_BY模式之前,它没有被拒绝:
SELECT author_id, title, count(*)
FROM author
GROUP BY author_id
如果一个作者写了不止一本书,我们应该为author.title
,显示什么?这没有意义,但MySQL仍然曾经允许它,并且会从组中投射任何任意的值。
今天,MySQL只允许投射与GROUP BY
子句有功能关系的列,这是SQL标准所允许的。
优点和缺点
虽然避免额外列的较短语法可能更容易维护(如果需要的话,很容易投射额外的列),但在生产中存在一些查询中断的风险,即当基础约束被禁用时,例如为了迁移。虽然不太可能在一个实时系统中禁用主键,但仍有可能出现这种情况,如果没有主键,以前有效的查询将不再有效,原因与MySQL的旧解释无效相同。不再有功能依赖性的保证。
其他语法
从jOOQ 3.16和#11834开始,将有可能在GROUP BY
子句中直接引用表,而不是单个列。比如说:
SELECT a.name, count(b.id)
FROM author a
LEFT JOIN book b ON a.id = b.author_id
GROUP BY a
语义将是:
- 如果表有一个主键(无论是否复合),在
GROUP BY
子句中使用该主键。 - 如果表没有主键,则列出该表的所有列。
由于jOOQ支持的RDBMS目前都不支持这种语法,所以它是一个纯粹的 合成jOOQ功能。