1. 程式人生 > 其它 >SqlServer-邏輯查詢-ON與WHERE的天壤之別

SqlServer-邏輯查詢-ON與WHERE的天壤之別

在日常開發中,我們經常會通過SQL對資料庫中的基礎資料元素進行查詢,通過對業務具有相關性的資料表進行關聯組合,生成新的資料模型來達到我們將資料轉換為業務資訊的目的,但不掌握查詢元素的邏輯處理次序就開始用SQL程式設計,在日常工作中很容易碰到令人費解的問題。

所以瞭解SQL執行的邏輯查詢過程可以幫助我們更加自如的應用SQL查詢,雖然SQL的邏輯查詢的處理步驟看起來很低效,但SqlServer在實際的執行過程中資料庫引擎會通過查詢優化器來生成最有效的物理處理過程,查詢的實際物理過程與邏輯處理過程有很大不同,但邏輯查詢過程中的執行的步驟是通過邏輯步驟表述的方式讓我們更容易理解SQL查詢處理中的各個階段。

閒話少說,下面我們開始一起討論SQL查詢的邏輯處理過程,假設我們正在開發一套當地的健康碼小程式,我們的後臺需要查詢目前誰的健康碼出示的次數,用來統計期間人員的出行頻次,同時我們需要將出行次數少於2次的人員資訊統計出來,那麼就通過這個示例來解析一下邏輯查詢的各個步驟。

首先,會建立一張人員表與健康碼錶,用來描述人員資訊與健康碼出示的次數,表結構如下:

我們分別為這兩張表初始化如下資料:

原諒我隨意使用名稱作為主鍵與我對錶資料型別的選擇,我知道這有些不妥,但我們只需要通過實力示例理解我所表達的業務就好,那麼建立程式碼如下:

CREATE TABLE Person
(
   Name 
NVARCHAR(5) NOT NULL PRIMARY KEY, City NVARCHAR(10) NOT NULL ); INSERT INTO Person VALUES('張三','北京'); INSERT INTO Person VALUES('李四','上海'); INSERT INTO Person VALUES('王五','西安'); CREATE TABLE HealthCode ( ShowNo INT NOT NULL PRIMARY KEY, Name NVARCHAR(5) REFERENCES Person(Name), Status
NVARCHAR(2) ); INSERT INTO HealthCode VALUES(1,'張三','綠碼'); INSERT INTO HealthCode VALUES(2,'張三','金碼'); INSERT INTO HealthCode VALUES(3,'李四','金碼'); INSERT INTO HealthCode VALUES(4,'王五','金碼');

接下來我們查詢返回所有綠碼和金碼的人員出行次數,且出行次數少於2次的人員資訊,並對返回結果按照出行次數進行由小到大的排序,查詢程式碼與查詢結果如下:

SELECT P.Name,P.City,COUNT(H.ShowNo) AS ShowCnt
          FROM Person AS P 
     LEFT JOIN HealthCode AS H
            ON P.Name = H.Name
         WHERE H.Status IN ('金碼','綠碼')
      GROUP BY P.Name,P.City
        HAVING COUNT(H.ShowNo) < 2
  ORDER BY ShowCnt

查詢結果如下:

OK,看到這裡想必我們就要開始根據該示例進行整體的邏輯查詢處理過程的分析了,在開始分析之前,我想先簡要介紹一下邏輯查詢處理的步驟,在我們使用的大多數程式語言中,程式碼是按照由上至下、從左到右順序執行,但在SQL中我們程式碼的編寫是按順序編寫,但執行往往由FROM子句第一個被執行,每個子句的步驟執行完畢後會生成一張虛擬表,用作被下一個子句執行時使用。

FROM開始,那其他關鍵字的先後執行的處理順序是什麼呢?簡單起見,我們先看看我們最常用的關鍵字在SQL語句中的執行順序,一張圖讓你瞭解邏輯查詢的處理步驟:

從該圖中我們可以看出,典型的SQL語句基本上為關鍵字+表示式的方式進行組成,我們可以從SQL中提取其中的關鍵字,並在圖中對各個關鍵字在邏輯處理中的執行順序以及過程進行了簡要描述。

初步瞭解了語句整個邏輯查詢的處理步驟後,如果沒看懂或者思路很模糊也不必太擔心,接下來我將根據我們的示例,對每一步具體的執行過程與中間產生的臨時表的結構與過程,進行逐步的詳細分解,本文的結尾,我會展示一個典型的問題用於大家思考,閒話少說,接下來我們來看看每一步的背後究竟是如何執行的。

第一步:執行FROM關鍵字

從示例SQL語句與上圖描述的過程中,我們可以看到SQL語句執行的第一步為FROM,如果FROM為單表,則直接返回該表中的資料放入臨時表VT1,用於下一步操作的資料來源,若FROM為多表關聯,會對FROM後面的表依次進行交叉聯接[笛卡爾積],生成臨時表VT1,根據我們的示例SQL為例,我們FROM關鍵字後描述了Person表與HealthCode的左外聯接,那麼第一步是將Person表的資料與HealthCode表的資料進行交叉聯接。

先拋開左外聯接的LEFT,因為他會在後面執行,為了讓更多不同水平的人能看懂,接下來我會盡可能的用圖的方式進行詳細的描述,若您對該技術點已經特別熟悉,請直接跳過執行方式,看執行結果就好,具體執行方式如下:

1、將主表中的第一行與關聯表中的所有行逐一關聯,形成結果集VR1;

2、以此類推,將主表中的第二行與關聯表中的所有行逐一關聯VR2;

3、以此類推,將主表中的第二行與關聯表中的所有行逐一關聯VR3;

4、主表有多少行就重複多少次,將VR1-VRn的結果集合並形成FROM步驟的最終結果,將結果放入虛擬表VT1中,按照我們的示例我們將得到VT1中資料如下:

經過以上將Person與HealthCode表交叉聯接後,FROM過程就執行完畢,並且生成一張虛擬表VT1,用於下一步驟的資料來源。

第二步:執行ON篩選器

在上一步返回的虛擬表VT1中,對每一行資料進行ON篩選器中描述的條件表示式進行過濾,將表示式返回為TRUE或者換句話說滿足表示式條件的資料,放入新生成的虛擬表VT2中,在我們示例中ON的表示式為:Person.Name = HealthCode.Name,詳細圖解過程如下:

通過ON篩選器設定的邏輯表示式篩選後,結果為TRUE的值,放入虛擬表VT2中,用於下一步驟的資料來源。VT2該虛擬表結構如下:

第三步:執行OUTER JOIN,補全主表缺失行

這一步操作只在當SQL中採用外聯接時執行,用於將主表缺失的資料行補全,在未關聯到的關聯表資料列中補NULL,若想深入詳細瞭解SQL表關聯的知識請參考SQLSqlServer系列關聯查詢部分的相關文章,因本示例主表與關聯表在執行ON篩選器後,VT2中的Person.Name均包含主表Person.Name中的所有名稱,所以該步驟不會補全缺失行,詳細圖解如下:

VT2包含所有主表中的行資料未進行補全,所以VT2未產生任何變化,將結果生成虛擬表VT3,用於下一步操作的資料來源。VT3結構如下:

第四步:執行WHERE篩選器

該步驟對上一步返回的虛擬表VT3中的資料內容進行WHERE篩選器的過濾,通過WHERE設定的表示式對資料進行過濾,本示例的WHERE表示式為:WHERE H.Status IN ('金碼','綠碼'),該步驟圖解與執行結果如下:

因為所有條件均滿足WHERE篩選器的條件,所以VT3內容原封不動生成到虛擬表VT4,用於下一步驟的資料來源。

第五步:執行GROUP BY分組

該步驟通過GROUP BY後面所描述的列明進行分組,在構建虛擬表的過程中,虛擬表中會包含分組列與上一步驟返回的資料兩部分組成,生成虛擬表VT5,其中在VT5虛擬表中分組列用來描述VT4表中的資料應該資料哪個組,本示例中GROUP BY是以Person表中的Name欄位進行分組,那麼詳細圖示如下:

分組列用於描述每一行資料應該資料哪一個組,加入分組列描述的資料內容,生成虛擬表VT5,用於下一步驟的資料來源。

第六步:執行HAVING篩選器

HAVING篩選器是專門與分組步驟搭配的篩選器,用於篩選分組後的結果內容,邏輯查詢過程會根據該篩選器表示式在VT5分組虛擬表中加入對應的結果資訊,用於執行實際的篩選判斷,本示例的HAVING篩選器的條件為:HAVING COUNT(H.ShowNo) < 2,那麼具體虛擬表的結構具體詳細圖示如下:

因HAVING統計的為COUNT(H.ShowNo) < 2,VT5中張三統計的數量為2,所以不滿足該條件,張三這行資料被移除,同時生成新的虛擬表VT6,用於下一步驟的資料來源。這裡需要注意的是HAVING與分組是搭配使用,HAVING中的聚合函式預設會根據分組指定的內容進行計算。

第七步:執行SELECT構建查詢列表

往往虛擬表中包含的列不一定全是我們需要看到的列,SELECT用於指定生成的虛擬表中應該有哪些列,用做下一步驟的資料來源,同時我們可以在此步驟中對現有的列明通過AS關鍵字進行重新命名,那麼後續步驟中會使用新的列明進行相關操作。本示例中SELECT指定的列明包括Person表中的Name列,同時還包括了聚合列並對聚合列進行重新命名為ShowCnt,需要注意的是該聚合列是在HAVING階段產生的,若SQL語句中沒有HAVING條件,那麼邏輯查詢會根據SELECT 階段中指定的聚合函式,計算出聚合列的值,本示例中包含HAVING階段,那麼包含該階段的SELECT步驟具體的圖示如下:

生成虛擬表VT7,用於下一步驟的資料來源。

第八步:執行ORDER BY排序並返回結果集

因為本示例最後一步為排序,同時生成結果集返回客戶端,我在此將這兩步合併到一步進行講解,需要注意的是,ORDER BY步驟返回的不是虛擬表,而是返回一個物件,SqlServer中將該物件成為遊標,該物件包含了物理表中排序的順序,而不是針對實際的表進行真實的排序,在C語言中該物件就好比一個指標陣列,指標陣列已經按照排序的規則指定了指標在陣列中的具體索引位置,而在返回客戶端的時候,是根據該指標陣列中每個指標元素所指向行的地址獲取改行資料,進行表拼裝,返回客戶端,具體圖示如下:

根據以上示例中SQL執行步驟的分解,我們瞭解了整個SQL執行過程中的順序以及每個階段資料是如何組織變化的,不管是做RDBMS還是大資料,理解SQL的用法都是非常必要,同時,這裡需要提醒一下除非必須要排序,否則在日常SQL開發中,儘量不要使用ORDER BY,畢竟對大數量排序是非常佔用資源成本的。

在本文開頭我描述了了解邏輯查詢過程的必要性,以及不了些該過程會為開發中的我們帶來一定困擾,那我想寫兩個SQL語句,大家能否在評論區,告訴我這兩個SQL語句的執行結果有什麼差別?如果有差別原因是什麼?

語句一:

        SELECT P.Name,COUNT(H.ShowNo) AS ShowCnt
          FROM Person AS P 
     LEFT JOIN HealthCode AS H
            ON P.Name = H.Name AND P.City = '西安'
      GROUP BY P.Name
        HAVING COUNT(H.ShowNo) < 2
      ORDER BY ShowCnt;

語句二:

        SELECT P.Name,COUNT(H.ShowNo) AS ShowCnt
          FROM Person AS P 
     LEFT JOIN HealthCode AS H
            ON P.Name = H.Name 
         WHERE P.City = '西安'
      GROUP BY P.Name
        HAVING COUNT(H.ShowNo) < 2
      ORDER BY ShowCnt

能否告訴我一下區別到底在哪裡?