中篇的重點在於,在復雜情況下使用表表達式的查詢,尤其是公用表表達式(CTE),也就是非常方便的WITH AS XXX的應用,在SQL代碼,這種方式至少可以提高一倍的工作效率。此外開窗函數ROW_NUMBER的使用也使得數據庫分頁變得異常的容易,其他的一些特性使用相對較少,在需要時再查閱即可。
本系列包含上中下三篇,內容比較駁雜,望大家耐心閱讀:
那些年我們寫過的T-SQL(上篇):上篇介紹查詢的基礎,包括基本查詢的邏輯順序、聯接和子查詢
那些年我們寫過的T-SQL(中篇):中篇介紹表表達式、集合運算符和開窗函數
那些年我們寫過的T-SQL(下篇):下篇介紹數據修改、事務&並發和可編程對象
表表達式Table Expression是一種命名的查詢表達式,代表一個有效的關系表與其他表的使用類似。SQL Server支持4種類型的表表達式:派生表、公用表表達式、視圖等。
-
派生表
派生表也稱為子查詢表,非常的常見,之前介紹相關子查詢時那些命名了的外部表均是表表達式。表表達式並沒有任何的物理實例化,其優勢在於使得代碼邏輯清晰並可重用,但對性能並無影響。
獲取處理訂單數超過100的訂單年度及其客戶數量:SELECT * FROM (SELECT orderyear, COUNT(DISTINCT custid)) AS numcusts FROM (SELECT YEAR(orderdate) AS orderyear, custid FROM sales.[order]) AS D1 GROUP BY orderyear) AS D2 WHERE numcusts > 100 |
-
公用表表達式CTE
其是T-SQL提供的一種表表達式的增強形式,使用起來非常的便捷方便(重用性很強),z而且代碼非常的清晰,在數據庫查詢分頁等場景下和開窗函數ROW_NUMBER()配合的很好,這兒將之前介紹的派生表轉化為CTE的形式。
嵌套的CTE |
WITH D1 AS ( SELECT YEAR(orderdate) AS orderyear, custid FROM sales.[order] GROUP BY orderyear ), D2 AS( SELECT orderyear, COUNT(DISTINCT custid)) AS numcusts FROM D1 ) SELECT * FROM D2 WHERE numcusts > 70 |
遞歸的CTE |
這個比較有意思,比如想在員工表中獲取當前雇員的最大BOSS時很有效哦 WITH empsCTE AS( SELECT * FROM hr.employee WHERE empid = 6 --定位點元素 UNION ALL SELECT c.* FROM empsCTE AS p JOIN hr.employee AS c ON c.empid = p.manageid --遞歸元素 ) SELECT * FROM empsCTE WHERE manageid IS NULL |
-
視圖和內嵌表值函數(參數化視圖)
視圖 |
IF OBJECT_ID('sale.ChinaCusts') IS NOT NULL DROP VIEW sale.ChinaCusts GO CREATE VIEW sale.ChinaCusts AS SELECT * FROM sale.Customer WHERE country = 'China' |
內嵌表值函數 |
IF OBJECT_ID('dbo.GetOrderByUID') IS NOT NULL DROP FUNCTION dbo.GetOrderByUID GO CREATE FUNCTION dbo.GetOrderByUID (@uid AS INT) RETURNS TABLE AS RETURN SELECT * FROM sales.[order] WHERE uid = @uid; GO SELECT * FROM dbo.GetOrderByUID(8888) AS O; |
-
APPLY操作符
該運算符也是一個表運算符,其支持CROSS APPLY和OUTER APPLY兩種類型。其對兩個輸入表進行操作,右側表往往是是一個派生表或者內聯的TVF。其邏輯查詢處理階段將右側表應用到左側表的每一行,並生成組合的結果集。它與JOIN操作符最大的不同是右側的表可以引用左側表中的屬性,例子如下。
返回每個客戶3個最近的訂單: SELECT c.custid, a.orderid, a.orderdate FROM sales.customer as c CROSS[OUTER] APPLY (SELECT TOP(3) orderid, empid, orderdate, requiredate FROM sales.[order] AS o WHERE o.custid = c.custid ORDER BY orderdate DESC, orderid DESC) AS a |
當使用CROSS APPLY操作符時會將orderid為空列去除,而OUTER APPLY則會在第二個邏輯階段把其添加上,和外聯接操作類似。
T-SQL支持集合運算符,除了常見UNION還支持INTERSECT和EXCEPT,也就是並集、交集和差集,其優先級順序是INTERSECT > UNION = EXCEPT。需要注意的一點是,集合操作符默認認為兩個NULL值是相等的,而不是之前邏輯操作符中提到的UNKNOWN。可能你會說使用外聯接或者EXISTS運算符也可以達到相似效果,並在存在NULL比較的情況下必須添加相應處理代碼,使用集合操作符可以簡化SQL代碼。
集合操作默認都存在一個隱式去除重復(即包含DISDINCT)的行為,只有UNION ALL支持重復數據。這兒補充一個關於集合概念,集合指不包含重復數據的集合,包含重復數據的情況我們稱之為多元集合。在對兩個(或多個)查詢結果集進行集合操作時,需要注意其中的查詢並不支持ORDER BY操作,如果還是需要這樣的功能可以使用外部的ORDER BY或者是使用TOP等操作符將返回的游標轉化為結果集。
集合操作符涉及的查詢應該有相同列數,並對應列具有兼容類型(即低級別數據可以隱式的轉化為高級別數據,如int->bigint),查詢的列名稱由第一次查詢決定(在其中設置列別名)。
元數據查詢類型 |
解釋與示例 |
UNION [ALL], INTERSECT, EXCEPT |
SELECT country, region, city FROM address UNION SELECT country, region, city FROM user order by country |
復雜情況 |
對前置查詢進行復雜操作,獲取1、6號員工最近的2個訂單,使用表表達式: SELECT empid, orderid, orderdate FROM (SELECT TOP 2 empid, orderid, orderdate FROM [order] WHERE empid = 1 ORDER BY orderdate DESC) AS O1 UNION ALL SELECT empid, orderid, orderdate FROM (SELECT TOP 2 empid, orderid, orderdate FROM [order] WHERE empid = 6 ORDER BY orderdate DESC) AS O2 |
INTERSECT[EXCEPT] ALL的替代方案 |
實際SQL SERVER還不支持這種類型的操作,理解起來有點復雜,簡單來說就是如果我的子查詢A, B都有重復數據,一個是3條,一個是5條, 那么其INTERSECT ALL操作結果應該為3條,EXCEPT ALL的結果是2條。代碼如下,重點是熟悉開窗函數的使用。 SELECT row_number() OVER(PARTITION BY country, region, city ORDER BY (SELECT 0)) AS rownum, country, region, city FROM address INTERSECT SELECT row_number() OVER(PARTITION BY country, region, city ORDER BY (SELECT 0)) AS rownum, country, region, city FROM user 這兒注意的是ORDER BY (SELECT 0)的用法,表示告訴系統不用排序的意思,減少不必要的開銷。 |
這部分內容主要涉及T-SQL自身的一些新特性,例如開窗函數、透視數據等概念,相對來說比以前的內容難理解一些,不過經常幾次簡單的實踐,你會發現它的強大和有效。
-
開窗函數
其根據基礎查詢的行子集計算,為子集中每行計算一個標量結果值,行子集被稱為"窗口",通過OVER字句進行相關操作,簡單來說以前對分組查詢操作GROUP BY的粒度僅限於一個聚合函數(子查詢操作也類似),比如SUM(Amount),但現在想對分組內的行記錄進行排序,這個更小的操作粒度在過去的SQL中是難以實現的,這是開窗函數卻可以完成這部分的工作。常見的分組查詢實際在查詢中定義集合或組,因此在查詢中的所有計算都要在這些組中完成,還記得那個邏輯順序吧,GROUP BY是在SELECT之前的,因此一旦分組后,自然的就丟失了很多細節信息,但現在開窗函數是在SELECT字句階段,那么也就是說所有的信息仍然都在,可以支持各種細粒度的操作。此外,開窗函數能夠定義順序,並不會和顯示數據時的排序混淆。
計算每個雇員每月的銷售總計值:SELECT empid, ordermonth, val, SUM(val) OVER (PARTITION BY empid ORDER BY ordermonth ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS runval FROM Sales.EmpOrders |
以上的窗口函數包括三個部分:分區、排序和框架。
分區字句,PARTITION BY:限定聚合函數運算的行子集,比如這個用empid分區,那么每個窗口自會包含該empid的計算(類似一個分組子集)。
順序字句,ORDER BY:定義窗口中的排序,但不要和顯示排序混淆,窗口排序是針對之后的窗口框架的,無論如何不要忘記字句的邏輯處理順序,外部的ORDER BY字句是在SELECT字句后的。
框架字句,ROWS BETWEEN <top delimiter> AND <bottom delimiter>:進一步篩選之前的行子集(類似在子集中使用TOP操作),這兒的UNBOUNDED PRECEDING表示分區開始,CURRENT ROW表示當前行,使用UNBOUNDED FOLLOWING表示分區中的最后一行。
接下來介紹三類開窗函數,其中排序和聚合使用的場景比較多。
開窗函數類型 |
解釋與示例 |
排名開窗函數 |
其中包含4種類型的排名函數,ROW_NUMBER()、RANK()、DENSE_RANK()、NTILE(),最常用的是ROW_NUMBER,介紹一個分頁場景 WITH CTE AS( SELECT ROW_NUMBER() OVER(ORDER BY custid) AS rownum, * FROM Sales.Customers) SELECT * FROM CTE WHERE rownum > 10 AND rownum <= 20 接下來介紹一個分區內排序,要求選取每個雇員最大的3單金額及其排名 WITH CTE AS( SELECT ROW_NUMBER() OVER(PARTITION BY empid ORDER BY freight DESC) AS rownum_ingroup, * FROM Sales.Orders) SELECT empid, freight, rownum_ingroup FROM CTE WHERE rownum_ingroup >= 1 AND rownum_ingroup <= 3 |
偏移開窗函數 |
涉及LAG、LEAD、FIRST_VALUE、LAST_VALUE四個函數,這兒就介紹LEG和LEAD,表示當前記錄的前一個記錄和后一個記錄,記得在上篇的子查詢有寫過一種"小於該值的最大值"的方式,這兒使用函數更加的簡單。 SELECT orderid, freight, LAG(freight) OVER(ORDER BY orderid) AS pre_freight, LEAD(freight) OVER(ORDER BY orderid) AS next_freight FROM Sales.Orders 這兒比較奇葩的是LAG用於獲取前一條記錄,LEAD獲取后一條記錄,不得不說設計的小伙伴那天"腦袋不小心被門夾了下",哈哈 |
聚合開窗函數 |
看到之后的例子,你會感覺開窗函數和人類的自然語言很像,獲取每個訂單、所有訂單的運費總和 SELECT orderid, freight, SUM(freight) OVER() AS freightTotal FROM Sales.Orders |
-
透視和逆透視數據
透視實際上就是常說的"行轉列",而逆透視就是常說的"列轉行",由於這種操作實際上已有標准SQL的解決方案,不過很復雜和繁瑣,這兒將SQL標准的解決方案和PIVOT、UNPIVOT函數的解決方案都描述出來。
透視/逆透視解決方案 |
解釋與示例 |
標准透視 |
相信大家都很熟悉這種寫法,因為面試中經常問到 SELECT empid, SUM(CASE WHEN custid = 'A' THEN qty END) AS A, SUM(CASE WHEN custid = 'B' THEN qty END) AS B, SUM(CASE WHEN custid = 'C' THEN qty END) AS C, SUM(CASE WHEN custid = 'D' THEN qty END) AS D FROM dbo.orders GROUP BY empid; 這兒需要強調的重點是這個解決方案其實涉及3個階段:第一個階段為GROUP BY empid分組階段;第二階段為擴展階段通過在SELECT字句中使用針對目標列的CASE表達式;最后一個階段聚合階段通過對每個CASE表達式結果聚合,例如SUM。 |
PIVOT透視 |
PIVOT實際是一個表運算符,包含分組、擴展、聚合三個邏輯階段 SELECT empid, A, B, C, D FROM ( SELECT empid, custid, qty FROM dbo.Orders) AS D PIVOT(SUM(qty) FOR custid IN (A, B, C, D)) AS P 以上可以發現子查詢D中,包含empid、custid、qty三個屬性,其中custid作為分組屬性,qty作為聚合屬性,那么剩下的empid就是擴展屬性(不顯示的指出但可以推算出) |
標准逆透視 |
WITH CTE AS( SELECT empid, custid, CASE custid WHEN 'A' THEN A WHEN 'B' THEN B WHEN 'C' THEN C END AS qty FROM dbo.EmpCustOrders CROSS JOIN (VALUES('A'), ('B'), ('C'), ('D')) AS Custs(custid) ) SELECT * FROM CTE WHERE qty IS NOT NULL 逆透視包括也包括三個邏輯階段:第一階段需要通過交叉聯接生成每一列對應的一個副本;第二階段通過CASE運算符生成列(qty);最后一個階段通過去qty IS NOT NULL刪除不相關的交叉點,這一點一定不能忘了。 |
UNPIVOT逆透視 |
SELECT empid, custid, qty FROM dbo.EmpCustOrders UNPIVOT(qty FOR custid IN(A, B, C, D)) AS U ,有沒有覺得超簡單? |
-
分組集
分組集就是一個屬性集,分組GROUP BY字句只支持在一個查詢中使用一種分組方式,如果需要多種分組的結果就需要通過UNION ALL將多個分組聚合起來,為了字段對應,需要為部分列設置NULL占位符。這部分的使用場景主要是在報表分析中,分組集提供4類操作符用於增強原有的GROUP BY字句,這兒就介紹GROUPING SETS操作符,CUBE和ROLLUP是對它的簡化,可以通過語義理解,CUBE是立方即包含提供的分組屬性的所有組合,ROLLUP是歸納,按照層次對分組屬性進行組合,最后的GROUPING和GROUPING_ID是對分組的標識。
GROUPING SETS |
SELECT empid, custid, SUM(qty) AS sumqty FROM dbo.Orders GROUP BY GROUPING SETS((empid, custid), (empid), (custid), ()); |
最后推薦一個學習T-SQL的網站,http://tsql.solidq.com/,有空可以去看看,有英文原版的學習視頻和資料。
參考資料:
-
(美)本咁. SQL Server 2012 T-SQL基礎教程[M]. 北京:人民郵電出版社, 2013.