那些年我們寫過的T-SQL(中篇)


中篇的重點在於,在復雜情況下使用表表達式的查詢,尤其是公用表表達式(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/,有空可以去看看,有英文原版的學習視頻和資料。

 

參考資料:

  1. ()本咁. SQL Server 2012 T-SQL基礎教程[M]. 北京:人民郵電出版社, 2013.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM