Quantcast
Channel: Darkthread
Viewing all articles
Browse latest Browse all 428

CTE應用-將多筆查詢結果合併成逗號分隔字串

$
0
0

一對多關聯是常見的資料庫應用情境,有時我會遇到將多筆關聯資料特定欄立合併成逗號分隔字串的需求。聽起來有點抽象,用個實例說明。

假設有兩個資料表,應用系統清單及負責該系統的工程師名字,用以下指令建立模擬資料:

CREATETABLE AppSystem (
  Name VARCHAR(16),
  CodeName VARCHAR(16)
)
INSERTINTO AppSystem VALUES ('HR', 'Mars');
INSERTINTO AppSystem VALUES ('ERP', 'Jupiter');
INSERTINTO AppSystem VALUES ('POS', 'Venus');
CREATETable AppSupport (
   AppName VARCHAR(16),
   Engineer VARCHAR(16)
)
INSERTINTO AppSupport VALUES ('HR', 'Jeffrey');
INSERTINTO AppSupport VALUES ('ERP', 'Jeffrey');
INSERTINTO AppSupport VALUES ('POS', 'Jeffrey');
INSERTINTO AppSupport VALUES ('ERP', 'Darkthread');
INSERTINTO AppSupport VALUES ('POS', 'Darkthread');
INSERTINTO AppSupport VALUES ('HR', 'SecrectSound');
INSERTINTO AppSupport VALUES ('ERP', 'MouthCannon');

如下圖所示,資料顯示有HR、ERP、POS三個系統,而工程師共四位,每個人各負責1到3個系統。

用簡單的JOIN查詢就能列出每個系統負責的工程師,但有個缺點,每個工程師配一個系統會出現一次,故系統資料會重複,例如:HR、POS各出現兩次,ERP出現三次。

若目的在產生報表,理想的呈現方式是將同一系統的工程師名字合併成以逗號分隔的字串,像這樣:

要轉換成上述格式,我試過幾種做法:

  1. 建Temp Table跑Cursor統整資料
    我用T-SQL寫Cursor的功力很鳥,有馬拉松跑者被抓去游泳的fu,而建Temp Table跑Cursor迴圈的程序感覺有點繁瑣沒效率。
  2. 將JOIN結果拉回.NET端整理
    寫C#彙整資料這類雕蟲小技是我的強項,幾乎是信手拈來,開發跟執行效率都不會太差。但拉到.NET端處理有個缺點,JOIN完的重複資料需透過網路傳輸傳到網站伺服器後才能加工,若主資料的欄位又多又長,同一筆重覆次數又高,浪費的頻寬很可觀。

爬文找到更多做法,包含FOR XML PATH、自訂函式、SQLCLR函式、CTE… 等。之前體驗過CTE(Common Table Expresion)的特異功能,讓我頗為驚喜,當發現CTE也可解決這類需求,便決定動手試試。

CodeProject上有一篇好文章,照方煎藥,這個案例的合併欄位CTE寫法如下:

;WITH SupportCTE (RowNum, Name, CodeName, Engineer,Engineers) AS
(
SELECT 1, A.Name, A.CodeName, MIN(B.Engineer), 
CAST(MIN(B.Engineer) ASVARCHAR(MAX)) AS Engineers
FROM AppSystem A JOIN AppSupport B ON A.Name = B.AppName
GROUPBY A.Name, A.CodeName
 
UNIONALL
 
SELECT A.RowNum + 1, A.Name, A.CodeName, B.Engineer, 
CAST(A.Engineers + ', ' + B.Engineer ASVARCHAR(MAX)) As Supports
FROM SupportCTE A JOIN AppSupport B 
ON A.Name = B.AppName AND B.Engineer > A.Engineer 
)
 
SELECT * FROM SupportCTE

上次一樣,我們要善用 CTE 主查詢 UNION ALL 遞迴查詢的原理解決問題。程式有幾個重點,第一段為主查詢,JOIN AppSystem及AppSupport兩個資料表,除了Name及CodeName、Engineer外,又多加RowNum及Engineers兩個欄位。Engineers存放工程師名字串接結果,RowNum統計Engineers包含的工程師資料筆數。一個系統可能有多位工程師,Engineer要用GROUP BY及MIN()找出名字排序最前者(稍後遞迴時要靠它排除處理過的項目,防止陷入無窮遞迴,這個環節很重要)。UNION ALL接的第二段查詢將被遞迴呼叫(故一定要設計跳出遞迴的邏輯),原理是利用現有結果反覆JOIN AppSupport找出同一系統的其他工程師,將其串接於Engineers欄位後方,同時將RowNum+1,Engineer則換成本次串接的工程師名稱。JOIN條件限定工程師名字排序要在既有資料之後,可排除已串接過的項目,避免無窮迴圈。

直接SELECT SupportCTE可以看到遞迴查詢的結果,比較能體會其運作原理。

如上圖所示,每個系統都有多筆資料,Engineers分別由1-3位工程師名字串接而成,RowNum即為串接資料的個數。如此,我們只需找出每個系統RowNum最大的一筆,即為最終結果。這應該難不倒冰雪聰明的你,用以下寫法就可輕鬆搞定:

SELECT A.Name, A.CodeName, A.Engineers FROM SupportCTE A
JOIN (SELECT Name, Max(RowNum) AS MaxRowNum FROM SupportCTE GROUPBY Name) B
ON A.Name = B.Name AND A.RowNum = B.MaxRowNum

補充:針對找出RowNum最大的一筆,路人乙提供一個更棒的解法,不用JOIN就可以搞定,特此感謝!

SELECT Name,CodeName,Engineers
FROM (
SELECT *,ROW_NUMBER() OVER (PARTITION BY Name ORDERBY RowNum Desc) AS Pos
FROM SupportCTE
) T
WHERE Pos = 1 
ORDERBY Name

薑!薑!薑!薑~

實做一趟的感想:用CTE實現多筆資料串接需要動用遞迴,設計起來要費點腦筋,有點呼應那句「遞迴只應天上有,凡人應當用迴圈」的俗諺,但只要悟透原理,也不到難以駕御的程度,而且試出來的那一刻,還有種自己通過智力測驗的成就感(笑),我想我會開始在專案上試用這種寫法

2016-01-21更新:經過實測,若只是要合併欄位,FOR XML PATH比CTE簡便有效率


Viewing all articles
Browse latest Browse all 428

Trending Articles