原文來自MySQL 5.7 官方手冊:12.20.3 MySQL Handling of GROUP BY
SQL-92和更早版本不允許SELECT列表,HAVING條件或ORDER BY列表引用未在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-1999以及更高版本允許將這種查詢作為一個可選項,前提是這些列在功能上依賴GROUP BY列(if they are functionally dependent on GROUP BY columns)——如果name和custid之間存在這種關系,則查詢是合法的,例如custid是customer的一個主鍵。
MySQL 5.7.5及更高版本實現了對功能依賴的檢測。如果啟用了ONLY_FULL_GROUP_BY SQL模式(默認情況下是這樣),MySQL會拒絕在Select列別、Having條件或者ORDER BY列表中有引用既未在GROUP BY子句中命名也未在功能上依賴於它們的非聚合列。
在5.7.5之前,MySQL不檢測功能依賴性,默認情況下不啟用ONLY_FULL_GROUP_BY。(難怪,我的是5.7.21,默認不開啟。)
那當不啟用ONLY_FULL_GROUP_BY時,MySQL就不得不接受前面這種查詢。在這種情況下,服務器可以自由選擇每個組中的任何值,因此除非它們相同,否則所選的值是不確定的,這可能不是您想要的。
此外,添加ORDER BY子句不會影響每個組中值的選擇。結果集的排序發生在值被選擇之后,所以ORDER BY並不會影響服務器如何選擇每個組中的值。
當你知道,由於數據的某些屬性,每個未在GROUP BY中命名的非聚合列中的所有值對於每個組都是相同的。此時禁止ONLY_FULL_GROUP_BY可能是有用的。
以下的討論展示功能性依賴、以及當功能性依賴缺失時MySQL產生的錯誤信息,以及讓MySQL在功能性依賴缺失時接受查詢的方式。
在ONLY_FULL_GROUP_BY模式下,下面的查詢可能是非法的:
SELECT name, address, MAX(age) FROM t GROUP BY name;
但,如果name是t的一個主鍵,又或者name是一個unique、NOT NULL字段,這個查詢會變成合法的。在這種情況下,MySQL會識別出查詢列address功能性依賴與group列。例如,若name是一個主鍵,則其值確定address的值,因為每個組只有一個主鍵值,因此只有一行。因此,MySQL對組中address值的選擇並不會有隨機性,也不需要拒絕查詢。
反過來,如果name是並不是t的一個主鍵,又或者name也不是一個unique、NOT NULL字段,這個查詢就是非法的了,因為在這種情況下,MySQL不能推斷出功能依賴性並發生錯誤:
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
那如果非要MySQL接受這個查詢,就可以使用ANY_VALUE()函數:
SELECT name, ANY_VALUE(address), MAX(age) FROM t GROUP BY name;
當然,也可以放大招,禁止ONLY_FULL_GROUP_BY模式。
然而,這里的例子非常簡單。 特別是,我們不太可能在單個主鍵列上進行分組,因為每個組只包含一行。其它對於在更復雜的查詢中演示“功能性依賴”的示例,參考12.20.4。
如果一個select查詢中包含了聚合函數,卻沒有GROUP BY子句。那么在ONLY_FULL_GROUP_BY模式下,它不能在select子句的列表中、HAVING條件中、ORDER BY列表中包含非聚合列。如下所示:
/*sql_mode=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值。這種情況下,如果MySQL選擇的name值是無關緊要的,ANY_VALUE()就可以派上用場了:
/*不會報錯*/ SELECT ANY_VALUE(name), MAX(age) FROM t;
在MySQL 5.7.5及更高版本中,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的保留值,而反過來c3的保留值又會影響排序,使得排序也任意了。
為了防止這個問題,如果任何ORDER BY表達式不滿足以下條件中的至少一個,則有DISTINCT和ORDER BY的查詢將被拒絕為無效:
- 表達式與select列表中的某個相等;
- 所有被該表達式引用、並且屬於查詢所選表的列,都是select列表中國的元素
MySQL相對於標准SQL的另一個擴展是:允許在Having子句中引用在SELECT從句中命名的別名。
例如,以下查詢返回name值出現一次的行:
SELECT name, COUNT(name) FROM orders GROUP BY name HAVING COUNT(name) = 1;
但是MySQL擴展后可以如下使用:
SELECT name, COUNT(name) AS c FROM orders GROUP BY name HAVING c = 1;
NOTE:在MySQL 5.7.5之前,啟用ONLY_FULL_GROUP_BY會禁用此擴展,因此需要使用非別名表達式來編寫HAVING子句。
按前面的我的筆記,Having子句是在Select子句前被執行的,看起來似乎是錯的?試驗了一下,在我的版本(5.7.21)中,這樣做沒問題(猜想一下,和編譯順序相關?):
select SID,count(SId) as n from sc group by SId having n=3; /* +------+---+ | SID | n | +------+---+ | 01 | 3 | | 02 | 3 | | 03 | 3 | | 04 | 3 | +------+---+ */
還可以提一下,標准SQL在GROUP BY子句中僅允許有列表達式(column expressions),因此諸如此類的語句無效,因為FLOOR(value / 100)是非列表達式( noncolumn expression):
SELECT id, FLOOR(value/100) FROM tbl_name GROUP BY id, FLOOR(value/100);
而MySQL對此進行了擴展,上述語句有效。
標准SQL也不允許GROUP BY子句中出現別名,MySQL則允許。所以上述查詢也可以更改為:
SELECT id, FLOOR(value/100) AS val FROM tbl_name GROUP BY id, val;
這個val被視為列表達式。
當GROUP BY中出現非列表達式時,MySQL會識別該表達式與Select子句列表中的表達式之間的相等性。這意味着啟用了ONLY_FULL_GROUP_BY SQL模式后,包含GROUP BY id,FLOOR(value/100)的查詢是有效的,因為Select列表中出現了相同的FLOOR()表達式。
但是,MySQL不會嘗試識別GROUP BY非列表達式的功能依賴(functional dependence),因此以下查詢在啟用ONLY_FULL_GROUP_BY時無效,即使Select列表中的第三個表達式是一個關於id列的簡單公式:id與GROUP BY中的FLOOR()相加。(即id+FLOOR(value/100)與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;