Oracle/SQL 分組/分析/聚合比較通俗深入的解析

目錄

引言

爲什麼會出現這個異常?

什麼是SQL行源(SQL Row Source)生成?

SQL解析

SQL優化器

行源樹的生成

什麼是分析(Analysis)函數?

什麼是聚合(Aggregation)函數?

分組到底分的是什麼組?

以上問題會如何解決最初的問題?


 

引言

       在SQL實踐的過程中,經常會出現一個異常。

       “不是單組分組函數”,從字面意思理解起來似乎我們使用了一個無法對單個組進行分組的函數,而函數似乎就是我們經常使用的count、sum之類的封裝接口。但其實,這樣理解是100%不正確的,爲何這樣講,如果讀者真的迫切有問題想要解決,想了解這個事情,就請耐心的往下讀,通俗,但需要仔細閱讀,如果讀者希望深入淺出,那再合適不過了。

 

爲什麼會出現這個異常?

       我們拿到一個問題,不妨先給它一個明確的終極目標-----“爲什麼會出現這個異常?”,我們如果能清晰的把這個問題解決,也就自然明白如何修復此類異常。對於Oracle或者SQL,區別與Mysql最顯著的特徵就是 不開源。不開源,就意味着我們無法Debug它的源碼,從根本上梳理該異常的判斷條件,執行邏輯。我們在求學的過程中一定聽說過這樣兩句話:

於是,我們想搞清楚此類問題,就不得不讀一下官方文檔(點擊這個超鏈接傳送門直達)。

       這裏要提到一點,我們選擇數據庫,選擇版本,進入文檔後,會發現,是一個全英文的官方文檔,即使擁有一定英語基礎的讀者,讀起來也很難做到事半功倍。所以這篇文章作者利用時間閱讀了相關的內容,並在資源站上找到了一份十分友好的中英對照Oracle 11g官方文檔(文檔將會放置在附件中)。

我們可以從官方文檔中得知兩件事情:

  1. 文檔中說明了任何ORA異常,即被Oracle虛擬機捕捉到的異常,都來源於SQL行源生成的過程;
  2. 文檔中並未提及分組函數這樣的說法,只有group by關鍵詞,以及分析函數與聚合函數。

也就是說,我們i想解決最初的問題,需要先搞清楚1、2點中提到的內容,比如:

  • 什麼是SQL行源(SQL Row Source)生成?
  • 什麼是分析(Analysis)函數?
  • 什麼是聚合(Aggregation)函數?
  • 分組到底分的是什麼組?
  • 以上問題會如何解決最初的問題?

 

什麼是SQL行源(SQL Row Source)生成?

       這個是一種官方的說法,行源生成,其實行源生成,需要從一段SQL如何被編譯器解析來定位。

SQL提交到執行中間具體發生的事情

我們平時寫一段SQL,寫好,執行,提交,似乎存儲過程就這樣被完成了。其實,它底層所作的事情遠遠超出我們的想象。上述過程翻譯過來就是:

SQL解析

如圖所示,我們的SQL提交後,首先會被編譯器解析,其中包括:語法、語義、共享池。官方的說法其實也十分通俗易懂:

  • 語法解析:檢查關鍵詞結構是否符合狀態機的規範;

這個意思其實就是,我們使用的select、from、where這些關鍵詞,是否存在,這個過程是去與數據庫中系統關鍵字表進行比對,其次,還會確定是否滿足一個子查詢下select、from、where等關鍵詞的邏輯順序,比如 select * from dual;這樣就是一個正確語法邏輯,而 The * selected from dual; 這樣就是一個不符合語法規範的DDL語句,雖然兩句話表達的意思相同。

  • 語義解析:檢查語法解析後的文本語義是否有意義;

這個意思其實就是,在語法解析通過後,會對關鍵詞的前後名詞(即非關鍵詞外的名詞,比如表名啦、字段名等等)是否有意義,有意義的意思就是說,是否存在,是否屬於某個名詞,最清晰的解釋就是是否物理的存在。比如:select * from dual;這個語義解析就會通過,因爲 * 代表着all,dual代表着實際存在的系統表。而select * from inexistentTable;這個表假設我們沒有建過,也不是系統表,表物理的不存在,所以在語義解析時就會不通過,返回一個異常。

  • 共享池檢查:檢查通過語義解析的名詞資源,是否正在被佔用;

這個很好理解,就是說,我們的資源如果存在臨界情況,那麼編譯器在編譯的時候會去查一下,這個資源允不允許被訪問,允許訪問的情況下,有無排他鎖,是否正在被佔用,如果被佔用,我們可以選擇等待或是放棄,一般默認是等待排他鎖的釋放。如果該資源沒有被物理的訪問,那麼我們就把鎖加上了,避免髒數據產生。

SQL優化器

什麼是SQL優化器,可以對於許多讀者,這是一個全新的名詞。所謂SQL優化器,顧名思義就是SQL執行前,從時間代價、空間代價考量,生成執行方案的優化軟件。許多SQL虛擬機中,內置的便是CBO(Cost-Based Optimizer),基於成本代價的優化器。

從Oracle 6開始,優化器就支持下面4種表連接方式:

— 嵌套循環連接(Nested Loop Join)

— 羣集連接(Cluster Join)

— 排序合併連接(Sort-Merge Join)

— 笛卡爾連接(Cartesian Join)

 

這裏就不詳細展開,有興趣的讀者可以閱讀:https://blog.csdn.net/xstardust/article/details/81188972 這篇博文,十分的詳細。我們在這裏只需要瞭解,我們的SQL經過CBO後,會生成一個成本代價理論最小的執行樹。

執行樹被樹物理的存儲,指導查詢器先查詢哪個結果到內存上並且以什麼樣的查詢方式,並且,根據執行樹計劃執行過後的每個子樹,都會產生一個行源組。(這裏終於引出了我們的主角--SQL行源

 

行源樹的生成

行源樹就是SQL根據執行樹計劃地執行後生成的行集的組合,也就是說一個行源樹有多個行源組。

舉例:

select * from t1,t2 
 where t1.no = t2.no and
       t1.no < 100000;

如果我們自己去計算成本的話,那麼我們肯定會先執行t1.no<100000,再去進行Hash關聯,從而得到最終結果。也就是這樣一個執行樹:

由於我們是前序生成的,所以我們執行時,就用前序遍歷。首先我們執行:

根據No索引在t1表中查詢No小於100000的所有記錄,並記錄下每行對應的HashID。

假設,我們該執行查詢出來的結果是:

我們這時候要注意了!一個查詢結果始終對應一個行源組,也就是說,即使我們這裏看着是18條記錄,他們在物理上是屬於一個行源組的,可以這麼理解:

接下來它會根據執行樹執行hash_join,所謂hash_join,簡單的對於兩個表來講,hash-join就算講兩表中的小表(稱S)作爲hash表,然後去掃描另一個表(稱M)的每一行數據,用得出來的行數據根據連接條件去映射建立的hash表,hash表是放在內存中的,這樣可以很快的得到對應的S表與M表相匹配的行。

所以我們左子葉的節點執行過後產生一個行源組,作爲關聯查詢的一個表,與另一個表關聯,查詢過後產生的結果根據“一個查詢結果始終對應一個行源組”,就會產生一個具有1個或多個行記錄的行集(行源組)。

由於我們沒有進行分組操作,目前而言,即使有6條記錄,它也是一個行源組。並且執行樹遍歷完成,此時返回查詢結果,這個行源組便成爲了查詢結果。任何複雜的操作都可以通過這樣的過程去單步它的執行過程以及每一步的行源組

我們探究了SQL行源生成,明白了一段SQL在編譯器中經歷了怎麼樣的過程,這些基礎知識將作爲我們解決問題的關鍵。

 

什麼是分析(Analysis)函數?

       按官方的解釋,所謂分析函數就是對行源組的每一行進行數學統計處理的過程。

其實有一種十分通俗的理解,就是對行維度進行處理的函數

首先,我們可以明確一個點,partition by並不是函數,而是分組關鍵字之一,它會根據字段去分組,所以會把一個含有n個行記錄的1個行源組,分解爲含有y個行記錄的z個行源組(如果分組列相同,將會歸爲一個行源組,不會去除)。這個概念與國內目前很多人的理解有很大的出入,即很多人都在錯誤的使用分析函數的稱呼。其實像sum、count並不是一直所屬聚合函數,相反它們也具有分析函數的特性。

許多分析函數同時也是聚合函數,比如sum()函數,這樣使用就是聚合函數。

 SQL> select deptno,sum(sal) sum_sal fromemp  group by deptno;

 而這樣使用就是分析函數。

 SQL> select distinct deptno,sum(sal)over(partition by deptno) sum_sal from emp;

它們得出的結果是相同的,但要注意,將sum函數作爲分析函數時,使用了distinct關鍵詞(去重)從而導致結果相同,否則會在查詢結果上加上“每一”,這個概念,比如上述sql去除distinct關鍵詞,寓意就變爲:對每個僱員計算他/她所在的部門的薪金總數。

 

 

什麼是聚合(Aggregation)函數?

       按官方的解釋,所謂聚合函數就是對行源組的每一列進行數學統計處理的過程。

其實與分析函數相對的,也有一種十分通俗的理解,就是對列維度進行處理的函數

我們可以看到,一個行源組的記錄被數學統計聚合爲一個綜合的結果,也可以明白,不論聚合函數還是分析函數,其API可以接收的對象始終是一個行源組

分析函數、聚合函數也只是我們邏輯上使用時,一種區別操作的稱呼而已。像sum、count、max底層等並沒有對分析和聚合進行區分,像Oracle時使用關鍵詞partition by/group by 來進行行源組拆解。所以接下來我們就要明白,分組到底分的是什麼組?

 

分組到底分的是什麼組?

       其實根據我們上文提到的點,我們可以明白分組其實分的就是行源組,不是單單的結果,分組後的行源組使用得當也可以作爲子查詢,供上層使用,所以看到這裏,是不是對分組有了一個更爲清晰的認識了呢?

我們常見的分組關鍵字有兩個 group by,partition by。

他們的區別其實在於:group by以所選字段去重的進行分組,而partition by以所選字段進行分組,並對相同列值的分爲1組,賦予同樣的組HashID。

對於分組,我特別要在這裏提到一個大家經常會忽視的現象。就是 select * /select 某個字段,我們查詢的結果是1個行源組還是多個行源組呢?我們不妨做個實驗。

我們可以看到,返回了兩個行記錄,那麼是1個行源組還是2個行源組呢?我們可以用函數去實驗一下。

我們可以發現,這個sql在語義分析的時候被攔截下來,爲什麼呢,因爲*在SQL中也是一個關鍵字,我們語義分析是不允許關鍵字作爲語義的,只允許名詞。所以,我們可以換成兩個名詞來嘗試。

出現了參數個數無效的異常,我們說過,函數不論作爲分析函數還是聚合函數去使用,API只允許接收一個行源組。那我們只用一個字段進行嘗試。

我們可以發現聚合出了一個最大值,我們關鍵的問題來了:這就說明了,select * /select 字段返回的是一個行源組嗎? NONONO大錯特錯。我們要注意,這個sql 的執行樹,我們首先會根據索引遍歷t1的id,返回一個t1所有行的行源組,然後對這個行源組進行聚合。所以我們一定要清楚,結果和行源組並不是一回事!作者建議讀者閱讀sql的時候養成一個好習慣,就說從from開始閱讀,因爲select後的過程往往是最終執行的過程。

那麼我們的問題依舊存在,select * /select 字段返回的是一個行源組嗎?

我們可以這樣寫一種形式,就比如,我們想知道這個最大值的fname是誰。

終於我們遇到最初的問題,這個異常爲什麼出現呢?根據前文的閱讀,讀者應該似乎明白了一點。導致這個的原因是:max作爲聚合函數時,接收的對象並不是1個行源組而是多個。怎麼驗證呢?請觀察如下查詢:

 

我們可以發現,這兩種寫法查詢結果相同。其實後者的寫法纔是一個查詢語句最完整的寫法,當我們查詢單字段時,SQL允許省略group by組合,但會在虛擬機內執行全字段分組,並返回所需查詢的結果。也就是說,我們平時的select * /select 字段省略group by關鍵字,返回的結果也是一個多行源組結果。所以

select t.fanme,max(t.fmoney) from t1 t;

實際上是 select t.fanme,max(t.fmoney) from t1 t group by t.fanme,t.money;

在聚合前對檢索t1的1個行源組進行了全字段分組,產生了2個行源組,再統一聚合,max表示吃不下,行源組太多,便反饋了一個這樣的異常。

以上問題會如何解決最初的問題?

       作者最近在閱讀http://cs231n.stanford.edu/syllabus.html課程筆記時,發現斯坦福大學的思考方式往往習慣將一個問題的大的框架寫出來,然後把這個問題分解爲若干子問題,直到尋找到合適的解決方法變不再分解,從而最終將大問題解決。本文也是嘗試用這樣的一種思考方式解決最初遇到的問題,以及對沿子問題所用到的知識梳理進行總結。

 

                                 十分感謝本文中涉及到的引用文章作者及譯者,如有疏漏錯誤請讀者指出。

 

附件:Oracle11g官方文檔中英對照版.pdf  https://download.csdn.net/download/u011433684/10001623

發佈了15 篇原創文章 · 獲贊 58 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章