1. 程式人生 > >SQL Server溫故系列(3):SQL 子查詢 & 公用表表達式 CTE

SQL Server溫故系列(3):SQL 子查詢 & 公用表表達式 CTE

  • 1、子查詢 Subqueries
    • 1.1、單行子查詢
    • 1.2、多行子查詢
    • 1.3、相關子查詢
    • 1.4、巢狀子查詢
    • 1.5、子查詢小結及效能問題
  • 2、公用表表達式 CTE
    • 2.1、普通公用表表達式
    • 2.2、遞迴公用表表達式
  • 3、本文小結

1、子查詢 Subqueries

子查詢是一個巢狀在 SELECT、INSERT、UPDATE 或 DELETE 語句或其他子查詢中的查詢。通俗來講,子查詢就是巢狀在大“查詢”中的小查詢。子查詢也稱為內部查詢或內部選擇,而包含子查詢的語句也稱為外部查詢或外部選擇。

從概念上說,子查詢結果會代入外部查詢(儘管這不一定是 SQL Server 實際處理帶有子查詢的 T-SQL 語句的方式)。所以子查詢會在其父查詢之前執行,以便可以將內部查詢的結果傳遞給外部查詢。

比較常見的子查詢有:單行子查詢、多行子查詢、相關子查詢、巢狀子查詢等。然而並沒有一種涇渭分明的子查詢分類方法,換句話說,有可能某個子查詢既是多行子查詢,也是相關子查詢,同時還是巢狀子查詢。

1.1、單行子查詢

顧名思義,單行子查詢就是隻查詢一行資料的內部查詢。如果單行子查詢僅返回單一值,就可以稱之為標量子查詢。標量子查詢也是最常見的單行子查詢。示例如下:

-- 查詢年齡最小的學生
SELECT * FROM T_Students WHERE Birthday = (SELECT MAX(Birthday) FROM T_Students);

-- 第 1 次課程 1 考試的成績高於學生 12 的成績
SELECT StudentId,Scores FROM T_ExamResults 
WHERE Counts = 1 AND CourseId = 1 AND Scores > (
    SELECT Scores FROM T_ExamResults WHERE Counts = 1 AND CourseId = 1 AND StudentId = 12);

-- 歷次課程 1 考試的平均分高於學生 12 的成績
SELECT StudentId,AVG(Scores) AvgScore,COUNT(1) ExamCount FROM T_ExamResults 
WHERE CourseId = 1 
GROUP BY StudentId 
HAVING AVG(Scores) > (SELECT AVG(Scores) FROM T_ExamResults WHERE CourseId = 1 AND StudentId = 12);

1.2、多行子查詢

相較於單行子查詢,多行子查詢就是會返回多行的內部查詢。示例如下:

-- 查詢有女生的班級裡的學生
SELECT * FROM T_Students WHERE ClassId IN(SELECT ClassId FROM T_Students WHERE Gender = 0);

-- 查詢有女生的班級之外的所有班級的學生
SELECT * FROM T_Students WHERE ClassId NOT IN(SELECT ClassId FROM T_Students WHERE Gender = 0);

-- 查詢有 2003 年及以後出生的學生的班級
SELECT * FROM T_Classes WHERE Id IN(SELECT ClassId FROM T_Students WHERE Birthday >= '2003-01-01');

1.3、相關子查詢

相關子查詢是指查詢條件引用了外部查詢中欄位的內部查詢。相反的,如果外部查詢的欄位沒有出現在內部查詢的條件中即為非相關子查詢。相關子查詢的內部查詢得依靠外部查詢獲得值,這意味著內部查詢是重複執行的,為外部查詢選擇的每一行都要執行一次,因此相關子查詢也被稱之為重複子查詢。示例如下:

-- 查詢在三(1)班和三(2)班的學生
SELECT * FROM T_Students t1 WHERE EXISTS(
    SELECT Id FROM T_Classes t2 WHERE t2.Id = t1.ClassId AND t2.Name IN('三(1)班','三(2)班'));

-- 查詢不在三(1)班和三(2)班的學生
SELECT * FROM T_Students t1 WHERE NOT EXISTS(
    SELECT Id FROM T_Classes t2 WHERE t2.Id = t1.ClassId AND t2.Name IN('三(1)班','三(2)班'));

-- 查詢第 1 次考試的課程及參加了的學生
SELECT (SELECT t2.Name FROM T_Courses t2 WHERE t2.Id=t1.CourseId) CourseName,
       (SELECT t3.Name FROM T_Students t3 WHERE t3.Id=t1.StudentId) StudentName 
FROM T_ExamResults t1 WHERE t1.Counts = 1;

1.4、巢狀子查詢

巢狀子查詢是指查詢內部巢狀一個或多個子查詢的內部查詢。一個 T-SQL 語句中可以巢狀任意數量的子查詢,儘管通常來說沒有這種必要。示例如下:

-- 查詢參加了第 1 次課程 1 考試的學生
SELECT * FROM T_Students t3 WHERE t3.Id IN(
    SELECT t2.StudentId FROM T_ExamResults t2 WHERE t2.Counts = 1 AND t2.CourseId = (
        SELECT t1.Id FROM T_Courses t1 WHERE t1.Name = '英語'));

-- 查詢西湖區所在的城市的所有學生
SELECT t3.* FROM T_Students t3 WHERE SUBSTRING(t3.Code,2,6) IN(
    SELECT t2.Code FROM T_Districts t2 WHERE t2.ParentId = (
        SELECT t1.ParentId FROM T_Districts t1 WHERE t1.Name = '西湖區'));

1.5、子查詢小結及效能問題

上文主要講述了查詢語句中的子查詢,其實在增刪改語句中也一樣能夠使用子查詢。任何能使用表示式的地方都可以使用子查詢,只要它返回的是單個值即可。很多包含子查詢的語句都可以改寫成連線查詢。示例如下:

-- 更新語句(子查詢寫法)
UPDATE T_Students SET Remark='考過滿分' 
WHERE Id IN(SELECT t.StudentId FROM T_ExamResults t WHERE t.Scores = 100);

-- 更新語句(連線寫法)
UPDATE T_Students SET Remark='考過滿分' 
FROM T_Students t1 JOIN T_ExamResults t2 ON t1.Id = t2.StudentId AND t2.Scores = 100;

-- 刪除語句(子查詢寫法)
DELETE T_ExamResults WHERE Counts = 10 AND StudentId = (
    SELECT t.Id FROM T_Students t WHERE t.Code = 'S330104010');

-- 刪除語句(連線寫法)
DELETE T_ExamResults FROM T_ExamResults t1 
JOIN T_Students t2 ON t1.StudentId = t2.Id AND t1.Counts = 10 AND t2.Code = 'S330104010';

在 T-SQL 中,包含子查詢的語句和語義上等效的不包含子查詢的語句在效能上通常是沒有差別的。但在一些需要為外部查詢的每個結果都執行內部查詢的情況下,使用連線寫法會產生更好的效能(如果資料很少,這種差別也很難體現出來),如某些非必須的相關子查詢。示例如下:

-- 查詢所有學生第 1 次課程 2 考試的成績(子查詢寫法)
SELECT (SELECT t2.Name FROM T_Students t2 WHERE t2.Id = t1.StudentId) StudentName,Scores 
FROM T_ExamResults t1 
WHERE t1.Counts = 1 AND t1.CourseId = 2;

-- 查詢所有學生第 1 次課程 2 考試的成績(連線寫法)
SELECT t2.Name StudentName,Scores 
FROM T_ExamResults t1 
JOIN T_Students t2 ON t1.StudentId=t2.Id 
WHERE t1.Counts = 1 AND t1.CourseId = 2;

2、公用表表達式 CTE

在 T-SQL 中,WITH 語句用於指定臨時命名的結果集,這些結果集被稱為公用表表達式(Common Table Expression,簡稱 CTE)。基本語法如下:

WITH cte-name (column-names) AS (cte-query) [,...]

引數釋義如下:

  • cte-name 代表公用表表達式的有效識別符號。類似於子查詢的別名,在一個語句中不能出現重複的 cte-name,但可以與 CTE 引用的基表名稱相同。引用 CTE 中的任何欄位都得用 cte-name 來限定,而不能使用欄位原本所屬的基表來限定。
  • column-names 代表公用表表達式的欄位名列表,只要 column-name 的個數與 cte-query 中定義欄位數相同即可。如果為 cte-query 中的所有欄位都提供了不同的名稱,那麼 column-names 就是可選的了(一般大家都這麼幹,畢竟有誰會喜歡沒必要的繁瑣呢?)。
  • cte-query 代表一個公用表表達式的查詢語句,可以是任意合法的 SELECT 語句。

2.1、普通公用表表達式

CTE 可在單條 INSERT、DELETE、UPDATE 或 SELECT 語句的執行範圍內定義。

CTE & INSERT 如要把 2000 年之前出生的女生資訊插入到好學生表中,用 CTE 定義女生資料,示例如下:

WITH temp AS(
    SELECT t.Id,t.Name,t.Gender,t.Birthday FROM T_Students t WHERE t.Gender = 0
)
INSERT INTO T_GoodStudents(Id,Name,Gender,Birthday) 
SELECT * FROM temp WHERE Birthday < '2000-01-01';

CTE & DELETE 如要把姓名和性別都是空的學生資訊刪除,用 CTE 定義姓名為空的資料,示例如下:

WITH t AS(
    SELECT t.* FROM T_GoodStudents t WHERE t.Name IS NULL
)
DELETE FROM t WHERE t.Gender IS NULL;

CTE & UPDATE 如要把歷次語文成績的平均分更新到學生備註中,用 CTE 定義學生平均分資料,示例如下:

WITH temp AS(
    SELECT t.StudentId,t.CourseId,AVG(t.Scores) AvgScore 
    FROM T_ExamResults t 
    GROUP BY t.StudentId,t.CourseId
)
UPDATE T_Students SET Remark = t1.AvgScore 
FROM temp t1 
JOIN T_Courses t2 ON t1.CourseId = t2.Id 
WHERE T_Students.Id = t1.StudentId AND t2.Name = '語文';

CTE & SELECT(多次引用同一個 CTE)如要查詢前 3 次考試的總成績及平均成績,用 CTE 定義各次的成績資料,示例如下:

WITH temp AS(
    SELECT t.StudentId,t.Counts,SUM(t.Scores) SumScore 
    FROM T_ExamResults t 
    WHERE t.Counts IN(1,2,3) 
    GROUP BY t.StudentId,t.Counts
)
SELECT t1.Code,t1.Name,
    t2.SumScore FirstSumScore,t3.SumScore SecondSumScore,t4.SumScore ThirdSumScore,
    (t2.SumScore + t3.SumScore + t4.SumScore)/3 AvgSumScore 
FROM T_Students t1 
JOIN temp t2 ON t1.Id = t2.StudentId AND t2.Counts = 1 
JOIN temp t3 ON t1.Id = t3.StudentId AND t3.Counts = 2 
JOIN temp t4 ON t1.Id = t4.StudentId AND t4.Counts = 3;

CTE & SELECT(一個 WITH 定義多個 CTE)如要查詢男生們前 3 次課程 1 的考試成績,用 CTE 定義各次的成績資料,示例如下:

WITH t1 AS(
    SELECT t.StudentId,t.Scores FROM T_ExamResults t WHERE t.CourseId = 1 AND t.Counts = 1
),
t2 AS(
    SELECT t.StudentId,t.Scores FROM T_ExamResults t WHERE t.CourseId = 1 AND t.Counts = 2
),
t3 AS(
    SELECT t.StudentId,t.Scores FROM T_ExamResults t WHERE t.CourseId = 1 AND t.Counts = 3
)
SELECT t4.Code,t4.Name,t1.Scores FirstScore,t2.Scores SecondScore,t3.Scores ThirdScore 
FROM T_Students t4 
JOIN t1 ON t4.Id = t1.StudentId 
JOIN t2 ON t4.Id = t2.StudentId 
JOIN t3 ON t4.Id = t3.StudentId 
WHERE t4.Gender = 1;

2.2、遞迴公用表表達式

CTE 可以包含對自身的引用,這種表示式被稱為遞迴公用表表達式。一個遞迴 CTE 中至少要包含兩個查詢定義,一個定位點成員和一個遞迴成員,遞迴成員的 FROM 子句只能引用一次 CTE。另外,定位點成員和遞迴成員二者的欄位數必須相同,欄位的資料型別也需要保持一致。

從上到下遞迴,如要查詢浙江省及以下各級別的行政區,示例如下:

WITH temp AS(
    SELECT t1.Id,t1.Name FROM T_Districts t1 WHERE t1.Code = '330000' 
    UNION ALL 
    SELECT t2.Id,t2.Name FROM T_Districts t2,temp t1 WHERE t2.ParentId = t1.Id 
)
SELECT temp.Name FROM temp;

從下到上遞迴,如要查詢西湖區及其所有上級行政區,示例如下:

WITH temp AS(
    SELECT t1.ParentId,t1.Name FROM T_Districts t1 WHERE t1.Code = '330106' 
    UNION ALL 
    SELECT t2.ParentId,t2.Name FROM T_Districts t2,temp t1 WHERE t2.Id = t1.ParentId 
)
SELECT temp.Name FROM temp;

可以定義多個定位點成員和遞迴成員,但必須將所有定位點成員查詢定義置於第一個遞迴成員定義之前。在起點成員之間可以用任意集合運算子,而在最後一個定位點成員和第一個遞迴成員之間,以及多個遞迴成員之間,必須用 UNION ALL 來連線。示例如下(查詢盧小妹的所有祖先):

WITH temp(Id) AS(
    SELECT t1.Father FROM T_Persons t1 WHERE t1.Name = '盧小妹' 
    UNION 
    SELECT t2.Mother FROM T_Persons t2 WHERE t2.Name = '盧小妹' 
    UNION ALL 
    SELECT t3.Father FROM T_Persons t3,temp WHERE t3.Id = temp.Id 
    UNION ALL 
    SELECT t4.Mother FROM T_Persons t4,temp WHERE t4.Id = temp.Id 
)
SELECT t1.Id,t1.Name,t1.Father,t1.Mother 
FROM T_Persons t1,temp 
WHERE t1.Id=temp.Id;

遞迴運算一定要有出口,否則就是死迴圈了!SQL Server 提供了一個 MAXRECURSION 提示來限制遞迴級數,以防止出現無限迴圈。但我個人覺得應該儘可能的通過 WHERE 條件或業務邏輯來定義更合理的出口。例如要顯示的限定只返回某一遞迴級別的資料,示例如下(查詢浙江省下所有縣一級的行政區):

WITH temp AS(
    SELECT t1.Id,t1.Name,t1.Code,t1.Level 
    FROM T_Districts t1 
    WHERE t1.Code = '330000' 
    UNION ALL 
    SELECT t2.Id,t2.Name,t2.Code,t2.Level 
    FROM T_Districts t2,temp 
    WHERE t2.ParentId = temp.Id
)
SELECT temp.Code,temp.Name,temp.Level FROM temp WHERE temp.Level = 3;

儘管看上去很簡單,但在實際開發中很可能並沒有類似 Level 這種標識級別的欄位可用。如果是這樣,那我們還可以通過遞迴成員的遞迴次數來實現同樣的過濾效果。示例如下:

WITH temp AS(
    SELECT t1.Id,t1.Name,t1.Code,t1.Level,0 Step 
    FROM T_Districts t1 
    WHERE t1.Code = '330000' 
    UNION ALL 
    SELECT t2.Id,t2.Name,t2.Code,t2.Level,temp.Step + 1 
    FROM T_Districts t2,temp 
    WHERE t2.ParentId = temp.Id
)
SELECT temp.Code,temp.Name,temp.Level FROM temp WHERE temp.Step = 2;

3、本文小結

本文主要介紹了 T-SQL 中最常見的幾種子查詢以及公用表表達式 CTE。本文還專門說明了遞迴 CTE,它可以實現類似於 PL/SQL 中的 CONNECT BY 層次查詢。

本文參考連結:

  • 1、SQL Server 2016 Subqueries
  • 2、SQL Server 2016 WITH
  • 3、.Net程式設計師學用Oracle系列之子查詢

本文連結:http://www.cnblogs.com/hanzongze/p/tsql-subquery.html
版權宣告:本文為部落格園博主 韓宗澤 原創,作者保留署名權!歡迎通過轉載、演繹或其它傳播方式來使用本文,但必須在明顯位置給出作者署名和本文連結!個人部落格,能力有限,若有不當之處,敬請批評指正,謝謝!