神奇的 SQL 之層級 → 爲何 GROUP BY 以後不能直接引用原表中的列

前言

  開心一刻html

感受不妙呀,弟弟舔它! 不應舔的,舔到懷疑人生了......mysql

GROUP BY 後 SELECT 列的限制

  標準 SQL 規定,在對錶進行聚合查詢的時候,只能在 SELECT 子句中寫下面 3 種內容:經過 GROUP BY 子句指定的聚合鍵、聚合函數(SUM 、AVG 等)、常量。咱們來看個例子sql

  咱們有 學生班級表(tbl_student_class) 以及 數據以下 :數據庫

DROP TABLE IF EXISTS tbl_student_class;
CREATE TABLE tbl_student_class (
  id int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  sno varchar(12) NOT NULL COMMENT '學號',
  cno varchar(5) NOT NULL COMMENT '班級號',
  cname varchar(20) NOT NULL COMMENT '班級名',
  PRIMARY KEY (id)
) COMMENT='學生班級表';

-- ----------------------------
-- Records of tbl_student_class
-- ----------------------------
INSERT INTO tbl_student_class VALUES ('1', '20190607001', '0607', '影視7班');
INSERT INTO tbl_student_class VALUES ('2', '20190607002', '0607', '影視7班');
INSERT INTO tbl_student_class VALUES ('3', '20190608003', '0608', '影視8班');
INSERT INTO tbl_student_class VALUES ('4', '20190608004', '0608', '影視8班');
INSERT INTO tbl_student_class VALUES ('5', '20190609005', '0609', '影視9班');
INSERT INTO tbl_student_class VALUES ('6', '20190609006', '0609', '影視9班');

  咱們想統計各個班(班級號、班級名)一個有多少人、以及最大的學號,咱們該怎麼寫這個查詢 SQL ? 我想你們應該都會服務器

SELECT cno,cname,count(sno),MAX(sno) 
FROM tbl_student_class
GROUP BY cno,cname;

  但是有人會想了,cno 和 cname 原本就是一對一,cno 一旦肯定,cname 也就肯定了,那 SQL 是否是能夠這麼寫 ?oracle

SELECT cno,cname,count(sno),MAX(sno) 
FROM tbl_student_class
GROUP BY cno;

  執行報錯了:ide

[Err] 1055 - Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.tbl_student_class.cname' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

  提示信息:SELECT 列表中的第二個表達式(cname)不在 GROUP BY 的子句中,同時它也不是聚合函數;這與 sql 模式:ONLY_FULL_GROUP_BY 不相容。函數

  爲何 GROUP BY 以後不能直接引用原表(不在 GROUP BY 子句)中的列 莫急,咱們慢慢往下看。this

SQL 模式

  MySQL 服務器能夠在不一樣的 SQL 模式下運行,而且能夠針對不一樣的客戶端以不一樣的方式應用這些模式,具體取決於 sql_mode 系統變量的值。 DBA 能夠設置全局SQL模式以匹配站點服務器操做要求,而且每一個應用程序能夠將其會話 SQL 模式設置爲其本身的要求。模式會影響 MySQL 支持的 SQL 語法以及它執行的 數據驗證檢查,這使得在不一樣環境中使用MySQL以及將MySQL與其餘數據庫服務器一塊兒使用變得更加容易。更多詳情請查閱官網:Server SQL Modes。MySQL 版本不一樣,內容會略有不一樣(包括默認值),查閱的時候注意與自身的 MySQL 版本保持一致。spa

  SQL 模式主要分兩類:語法支持類和數據檢查類,經常使用的以下

  語法支持類    

    ONLY_FULL_GROUP_BY

      對於 GROUP BY 聚合操做,若是在 SELECT 中的列、HAVING 或者 ORDER BY 子句的列,沒有在GROUP BY中出現,那麼這個SQL是不合法的

    ANSI_QUOTES

      啓用 ANSI_QUOTES 後,不能用雙引號來引用字符串,由於它被解釋爲識別符,做用與 ` 同樣。設置它之後,update t set f1="" ...,會報 Unknown column ‘’ in field list 這樣的語法錯誤

    PIPES_AS_CONCAT

      將 || 視爲字符串的鏈接操做符而非 或 運算符,這和Oracle數據庫是同樣的,也和字符串的拼接函數 CONCAT() 相相似

    NO_TABLE_OPTIONS

      使用 SHOW CREATE TABLE 時不會輸出MySQL特有的語法部分,如 ENGINE ,這個在使用 mysqldump 跨DB種類遷移的時候須要考慮

    NO_AUTO_CREATE_USER

      字面意思不自動建立用戶。在給MySQL用戶受權時,咱們習慣使用 GRANT ... ON ... TO dbuser 順道一塊兒建立用戶。設置該選項後就與oracle操做相似,受權以前必須先創建用戶

  數據檢查類   

    NO_ZERO_DATE

      認爲日期 ‘0000-00-00’ 非法,與是否設置後面的嚴格模式有關

      一、若是設置了嚴格模式,則 NO_ZERO_DATE 天然知足。但若是是 INSERT IGNORE 或 UPDATE IGNORE,’0000-00-00’依然容許且只顯示warning;

      二、若是在非嚴格模式下,設置了NO_ZERO_DATE,效果與上面同樣,’0000-00-00’ 容許但顯示warning;若是沒有設置NO_ZERO_DATE,no warning,當作徹底合法的值;

      三、NO_ZERO_IN_DATE狀況與上面相似,不一樣的是控制日期和天,是否可爲 0 ,即 2010-01-00 是否合法;

    NO_ENGINE_SUBSTITUTION

      使用 ALTER TABLE 或 CREATE TABLE 指定 ENGINE 時, 須要的存儲引擎被禁用或未編譯,該如何處理。啓用 NO_ENGINE_SUBSTITUTION 時,那麼直接拋出錯誤;不設置此值時,CREATE用默認的存儲引擎替代,ATLER不進行更改,並拋出一個 warning

    STRICT_TRANS_TABLES

      設置它,表示啓用嚴格模式。注意 STRICT_TRANS_TABLES 不是幾種策略的組合,單獨指 INSERT、UPDATE 出現少值或無效值該如何處理:

      一、前面提到的把 ‘’ 傳給int,嚴格模式下非法,若啓用非嚴格模式則變成 0,產生一個warning;

      二、Out Of Range,變成插入最大邊界值;

      三、當要插入的新行中,不包含其定義中沒有顯式DEFAULT子句的非NULL列的值時,該列缺乏值;

  默認模式

    當咱們沒有修改配置文件的狀況下,MySQL 是有本身的默認模式的;版本不一樣,默認模式也不一樣

-- 查看 MySQL 版本
SELECT VERSION();

-- 查看 sql_mode
SELECT @@sql_mode;

     咱們能夠看到,5.7.21 的默認模式包含:

ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

    而第一個:ONLY_FULL_GROUP_BY 就會約束:當咱們進行聚合查詢的時候,SELECT 的列不能直接包含非 GROUP BY 子句中的列。那若是咱們去掉該模式(從「嚴格模式」到「寬鬆模式」)呢 ?

    咱們發現,上述報錯的 SQL

-- 寬鬆模式下 能夠執行
SELECT cno,cname,count(sno),MAX(sno) 
FROM tbl_student_class
GROUP BY cno;
View Code

    能正常執行了,可是通常狀況下不推薦這樣配置,線上環境每每是「嚴格模式」,而不是「寬鬆模式」;雖然案例中,不管是「嚴格模式」,仍是「寬鬆模式」,結果都是對的,那是由於 cno 與 cname 惟一對應的,若是 cno 與 cname 不是惟一對應,那麼在「寬鬆模式下」 cname 的值是隨機的,這就會形成難以排查的問題,有興趣的能夠去試試。那爲何會有 ONLY_FULL_GROUP_BY 模式呢 咱們繼續往下看

  階(order)是用來區分集合或謂詞的階數的概念。謂詞邏輯中,根據輸入值的階數對謂詞進行分類。= 或者 BETWEEEN 等輸入值爲一行的謂詞叫做"一階謂詞",而像 EXISTS 這樣輸入值爲行的集合的謂詞叫做"二階謂詞"(HAVING 的輸入值也是集合,但它不是謂詞)。以此類推,三階謂詞=輸入值爲"集合的集合"的謂詞,四階謂詞=輸入值爲"集合的集合的集合"的謂詞,可是 SQL 裏並不會出現三階以上的狀況,因此不用太在乎。簡單點以下圖

  談到了階,就不得不談下集合論;集合論是 SQL 語言的根基,由於它的這個特性,SQL 也被稱爲面向集合語言。只有從集合的角度來思考,才能明白 SQL 的強大威力。經過上圖,相信你們也都能看到,這裏不作更深刻的講解了,有興趣的能夠去查相關資料。

爲何聚合後不能再引用原表中的列

  不少人都知道聚合查詢的限制,可是不多有人能正確地理解爲何會有這樣的約束。表 tbl_student_class 中的 cname 存儲的是每位學生的班級信息,但須要注意的是,這裏的 cname 只是每一個學生的屬性,並非小組的屬性,而 GROUP BY 又是聚合操做,操做的對象就是由多個學生組成的小組,所以,小組的屬性只能是平均或者總和等統計性質的屬性,以下圖

  詢問每一個學生的 cname 是能夠的,可是詢問由多個學生組成的小組的 cname 就沒有意義了。對於小組來講,只有"一共多少學生"或者"最大學號是多少?"這樣的問法纔是有意義的。強行將適用於個體的屬性套用於團體之上,純粹是一種分類錯誤;而 GROUP BY 的做用是將一個個元素劃分紅若干個子集,使用 GROUP BY 聚合以後,SQL 的操做對象便由 0 階的"行"變爲了 1 階的"行的集合",此時,行的屬性便不能使用了。SQL 的世界實際上是層級分明的等級社會,將低階概念的屬性用在高階概念上會致使秩序的混亂,這是不容許的。此時我相信你們都明白:爲何聚合後不能再引用原表中的列 。

單元素集合也是集合

  如今的集合論認爲單元素集合是一種正常的集合。單元素集合和空集同樣,主要是爲了保持理論的完整性而定義的。所以對於以集合論爲基礎的 SQL 來講,固然也須要嚴格地區分元素和單元素集合。所以,元素 a 和集合 {a} 之間存在着很是醒目的層級差異。

a ≠ {a}

  這兩個層級的區別分別對應着 SQL 中的 WHERE 子句和 HAVING 子句的區別。WHERE 子句用於處理"行"這種 0 階的對象,而 HAVING 子句用來處理"集合"這種 1 階的對象。

總結

  一、SQL 嚴格區分層級,包括謂詞邏輯中的層級(EXISTS),也包括集合論中的層級(GROUP BY);

  二、有了層級區分,那麼適用於個體上的屬性就不適用於團體了,這也就是爲何聚合查詢的 SELECT 子句中不能直接引用原表中的列的緣由;

  三、通常來講,單元素集合的屬性和其惟一元素的屬性是同樣的。這種只包含一個元素的集合讓人以爲彷佛沒有必要特地地當成集合來看待,可是爲了保持理論的完整性,咱們仍是要嚴格區分元素和單元素集合;

參考

  《SQL基礎教程》

  《SQL進階教程》

相關文章
相關標籤/搜索