SQL邏輯查詢解析(補充篇)


本文目錄

表操作符
  JOIN
  APPLY
  PIVOT
  UNPIVOT
OVER子句
集合操作符

在我的上一篇博客“SQL邏輯查詢解析”中,我們詳細講述了SQL邏輯查詢處理的各個步驟以及SQL語言的一些重要知識。為了SQL邏輯查詢處理的完整性,在本篇中,我們會了解到SQL邏輯查詢處理的更多內容,以作為對前一篇博客的補充。包括表操作符(JOIN,APPLY,PIVOT和UNPIVOT),OVER子句以及集合操作符(UNION,EXCEPT和INTERSECT)。

表操作符

從SQL SERVER 2008開始,SQL查詢中支持四種表操作符:JOIN,APPLY,PIVOT和UNPIVOT。其中,APPLY,PIVOT和UNPIVOT並非ANSI標准操作符,而是T-SQL中特有的擴展。

下面列出了這四個表操作符的使用格式:

(J) <left_table_expression>
    {CROSS | INNER | OUTER} JOIN <right_table_expression>
    ON <on_predicate>

(A) <left_table_expression>
    {CROSS | OUTER} APPLY <right_table_expression>

(P) <left_table_expression>
    PIVOT (<aggregate_func(<aggregation_element>)> FOR
        <spreading_element> IN(<target_col_list>))
        AS <result_table_alias>

(U) <left_table_expression>
    UNPIVOT (<target_values_col> FOR
        <target_names_col> IN(<source_col_list>))
        AS <result_table_alias>

 

JOIN

在前一篇中,我們已經對JOIN進行了比較詳細的描述,詳情請參閱:SQL邏輯查詢解析

簡單來說,它包含如下三個子步驟:(1-J1) 笛卡兒積(Cross Join), (1-J2) 應用ON條件, (1-J3) 添加外部數據行。

本篇會對另外三個表操作符進行講解。

APPLY

按類型不同,APPLY操作符包含如下一個或全部二個步驟:

  1. A1:對左表的數據行應用右表表達式
  2. A2:添加外部數據行

APPLY操作符對左表的每一行應用右表表達式,並且,右表表達式可以引用左表的列。對於左表的每一行,右表表達式都會運行一遍,以獲得一個與該行相匹配的集合並與之聯結,結果加入返回數據集。CROSS APPLY和OUTER APPLY都包含步驟A1,但只有OUTER APPLY才包含步驟A2。對於左表的輸入行,如果右表表達式返回空,那么CROSS APPLY不會返回外部行(左表當前行),而OUTER APPLY則會返回它,並且右表表達式的相關列為NULL。

比如,下面的查詢為每個customer返回兩個order ID最大的order:

SELECT C.customerid, C.city, A.orderid
FROM dbo.Customers AS C
  CROSS APPLY
    (SELECT TOP (2) O.orderid, O.customerid
    FROM dbo.Orders AS O
    WHERE O.customerid = C.customerid
    ORDER BY orderid DESC) AS A;

查詢返回如下數據:

可以看到FISSA並沒有出現在結果集中,因為表表達式A對於該數據行返回空集,如果我們希望返回那些沒有任何order的customer,則需要使用OUTER APPLY,如下所示:

SELECT C.customerid, C.city, A.orderid
FROM dbo.Customers AS C
  OUTER APPLY
    (SELECT TOP (2) O.orderid, O.customerid
    FROM dbo.Orders AS O
    WHERE O.customerid = C.customerid
    ORDER BY orderid DESC) AS A;

查詢返回如下數據:

PIVOT

PIVOT操作符允許我們對行和列中的數據進行旋轉和透視,並執行聚合計算。

示例數據

請使用如下Script創建示例數據:

CREATE TABLE dbo.OrderValues
(
orderid INT NOT NULL PRIMARY KEY,
customerid INT NOT NULL,
empid VARCHAR(20) NOT NULL,
orderdate DATETIME NOT NULL,
val NUMERIC(12,2)
);

INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1000, 100, 'John', '2006/01/12', 100)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1001, 100, 'Dick', '2006/01/12', 100)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1002, 100, 'James', '2006/01/12', 100)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1003, 100, 'John', '2006/02/12', 200)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1004, 200, 'John', '2007/03/12', 300)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1005, 200, 'John', '2008/04/12', 400)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1006, 200, 'Dick', '2006/02/12', 500)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1007, 200, 'Dick', '2007/01/12', 600)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1008, 200, 'Dick', '2008/01/12', 700)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1009, 200, 'Dick', '2008/01/12', 800)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1010, 200, 'James', '2006/01/12', 900)
INSERT INTO dbo.OrderValues(orderid, customerid, empid, orderdate, val) VALUES(1011, 200, 'James', '2007/01/12', 1000)

 選擇該表的所有數據,如下所示:

SELECT * FROM dbo.OrderValues

現在加入我們想知道每個employee在每一年完成的訂單總價。下面的PIVOT查詢能夠讓我們獲得如下的結果:每一行對應一個employee,每一列對應一個年份,並且計算出相應的訂單總價。

SELECT *
FROM (SELECT empid, YEAR(orderdate) AS orderyear, val
      FROM dbo.OrderValues) AS OV
    PIVOT(SUM(val) FOR orderyear IN([2006],[2007],[2008])) AS P;

這個查詢產生的結果如下所示:

不要被子查詢產生的派生表OV迷惑了,我們關心的是,PIVOT操作符獲得了一個表表達式OV作為它的左輸入,該表的每一行代表了一個order,包含empid, orderyear和val(訂單價格)。

PIVOT邏輯處理步驟解析

PIVOT操作符包含如下三個邏輯步驟:

  1. P1:分組
  2. P2: 擴展
  3. P3: 聚合

第一個步驟其實是一個隱藏的分組操作,它基於所有未出現在PIVOT子句中的列進行分組。上例中,在輸入表OV中有三個列empid, orderyear, val,其中只有empid沒有出現在PIVOT子句中,因此這里會按empid進行分組。

PIVOT的第二個步驟會對擴展列的值進行擴展,使其屬於相應的目標列。邏輯上,它使用如下的CASE表達式為IN子句中指定的每個目標列進行擴展:

CASE WHEN <spreading_col> = <target_col_element> THEN <expression> END

在我們的示例中,會應用下面三個表達式:

CASE WHEN orderyear = 2006 THEN val END,
CASE WHEN orderyear = 2007 THEN val END,
CASE WHEN orderyear = 2008 THEN val END

這樣,對於每個目標列,只有在數據行的orderyear與之相等時,才返回相應的值val,否則返回NULL,從而實現了數據值到相應目標列的分配和擴展。

PIVOT的第三步會使用指定的聚合函數對每一個CASE表達式進行聚合計算,生成結果列。在我們的示例中,表達式相當於:

SUM(CASE WHEN orderyear = 2006 THEN val END) AS [2006],
SUM(CASE WHEN orderyear = 2007 THEN val END) AS [2007],
SUM(CASE WHEN orderyear = 2008 THEN val END) AS [2008]

綜合上述三個步驟,我們的示例PIVOT查詢在邏輯上與下面的SQL查詢相同:

SELECT empid,
    SUM(CASE WHEN orderyear = 2006 THEN val END) AS [2006],
    SUM(CASE WHEN orderyear = 2007 THEN val END) AS [2007],
    SUM(CASE WHEN orderyear = 2008 THEN val END) AS [2008]
FROM (SELECT empid, YEAR(orderdate) AS orderyear, val
      FROM dbo.OrderValues) AS OV
GROUP BY empid

 

UNPIVOT

UNPIVOT是PIVOT的反操作,它把數據從列旋轉到行。

示例數據

在講述UNPIVOT的邏輯處理步驟之前,讓我們先運行下面的Script來創建示例數據表dbo.EmpYearValues,結果如下:

SELECT *
INTO dbo.EmpYearValues
FROM (SELECT empid, YEAR(orderdate) AS orderyear, val
      FROM dbo.OrderValues) AS OV
   PIVOT(SUM(val) FOR orderyear IN([2006],[2007],[2008])) AS P;

SELECT * FROM dbo.EmpYearValues

 

我將會使用下面的示例查詢來描述UNPIVOT操作符的邏輯處理步驟:

SELECT empid, orderyear, val
FROM dbo.EmpYearValues
UNPIVOT(val FOR orderyear IN([2006],[2007],[2008])) AS U;

這個查詢會對employee每一年(表中IN子句中的每一列)的值分割到單獨的數據行,生成如下結果:

UNPIVOT邏輯處理步驟解析

UNPIVOT操作符包含如下三個邏輯處理步驟:

  1. U1: 生成數據副本
  2. U2: 抽取數據
  3. U3: 刪除帶NULL值的行

第一步會生成UNPIVOT輸入表的數據行的副本(在我們的示例中為dbo.EmpYearValues)。它會為UNPIVOT中IN子句定義的每一列生成一個數據行。因為我們在IN子句中有三列,所以會為每一行生成三個副本。新生成的虛表會包含一個新數據列,該列的列名為IN子句前面指定的名字,列值為IN子句中指定的列表的名字。對於我們的示例,該虛表如下所示:

第二步會為UNPIVOT的當前行從原始數據列中(列名與當前orderyear的值關聯)抽取數據,用於存放抽取數據的列名是在FOR子句之前定義的(我們的示例中為val)。這一步返回的虛表如下:

第三步會消除結果列(val)中值為NULL的數據行,結果如下:

OVER子句

OVER子句用於支持基於窗口(window-based)的計算。我們可以隨聚合函數一起使用OVER子句,它同時也是四個分析排名函數(ROW_NUMBER、RANK、DENSE_RANK和NTILE)的必要元素。OVER子句定義了數據行的一個窗口,而我們可以在這個窗口上執行聚合或排名函數的計算。

在我們的SQL查詢中,OVER子句可以用於兩個邏輯階段:SELECT階段和ORDER BY階段。這個子句可以訪問為相應邏輯階段提供的輸入虛表。

在下面的示例中,我們在SELECT子句中使用了帶COUNT聚合函數的OVER子句:

SELECT orderid, customerid, COUNT(*) OVER(PARTITION BY customerid) AS numorders
FROM dbo.Orders

PARTITION BY子句定義了執行聚合計算的窗口,COUNT(*)匯總了SELECT輸入虛表中customerid的值等於當前customerid的行數。

我們還可以在ORDER BY子句中使用OVER子句,如下:

SELECT orderid, customerid,
    COUNT(*) OVER(PARTITION BY customerid) AS numorders
FROM dbo.Orders
ORDER BY COUNT(*) OVER(PARTITION BY customerid) DESC

 

關於OVER子句,篇幅所限,我在這里不准備詳細的討論它的工作方式,只是簡單的介紹了它的使用方式。如有機會,我會在后續博客中對它進行詳細的解析。

集合運算符

SQL Server 2008支持四種集合運算符:UNION ALL,UNION,EXCEPT和INTERSECT。這些SQL運算符對應了數學領域中相應的集合概念。

通常,一個包含集合運算符的查詢結構如下所示,每一行前面的數字是指該元素運行的邏輯順序:

(1) query1
(2) <set_operator>
(1) query2
(3) [ORDER BY <order_by_list>]

集合運算符會比較兩個輸入表中的所有行。UNION ALL返回的結果集包含了所有兩個輸入表中的行。UNION返回的結果集中包含了兩個輸入表中的不同的數據行(沒有重復行)。EXCEPT返回在第一個輸入中出現,但沒有在第二個輸入中出現的數據行。INTERSECT返回在兩個輸入中都出現過的數據行。

在涉及集合運算的單個查詢中不允許使用ORDER BY 子句,因為查詢期望返回的是(無序的)集合。但我們可以在查詢的最后指定ORDER BY子句,對集合運算的結果進行排序。

從邏輯處理角度來看,每個輸入查詢都會根據自己的相應階段進行處理,然后處理集合運算符。如果指定了ORDER BY子句,它作用於集合運算符產生的結果集。

比如,下面的查詢:

SELECT region, city
FROM Sales.Customers
WHERE country = N'USA'

INTERSECT

SELECT region, city
FROM HR.Employees
WHERE country = N'USA'

ORDER BY region, city;

首先,每個輸入查詢都會單獨處理。第一個查詢返回來自USA的客戶位置,第二個查詢返回來自USA的員工位置。INTERSECT返回同時出現在兩個輸入查詢中的記錄,即同時屬於客戶和員工的位置。最后,按照位置信息進行排序。

理解邏輯查詢處理的各個階段和SQL的一些特性,對於理解SQL編程的特殊性和樹立正確的思維方式是非常重要的。我們的目的是真正的掌握這些必要的基礎知識,這樣我們就可以寫出優雅的查詢,制定出高效的解決方案,並且了解其中的原理。


免責聲明!

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



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