好久沒有分享 SQL 題目讓大家玩了,最近剛好遇到兩個問題 排列組合
和 階層目錄
,有興趣的大大可以挑戰看看。
原始資料 甲乙丙丁戊
,請展開原始資料的所有組合,並需符合下列兩個條件。
甲乙
和 乙甲
算是同一個組合。甲乙
和 乙甲
需先展開 甲乙
。結果不必排序,列出所有組合即可。
結果如下:
乙
乙丁
乙丁戊
乙丙
乙丙丁
乙丙丁戊
乙丙戊
乙戊
丁
丁戊
丙
丙丁
丙丁戊
丙戊
戊
甲
甲乙
甲乙丁
甲乙丁戊
甲乙丙
甲乙丙丁
甲乙丙丁戊
甲乙丙戊
甲乙戊
甲丁
甲丁戊
甲丙
甲丙丁
甲丙丁戊
甲丙戊
甲戊
DECLARE @Data TABLE
(
Id INT,
Txt NVARCHAR(10)
)
INSERT INTO @Data VALUES
(1,N'甲'),
(2,N'乙'),
(3,N'丙'),
(4,N'丁'),
(5,N'戊')
原始資料是具有階層性的目錄,結構如下:
DECLARE @Data TABLE
(
Id INT,
PatentId INT,
Txt NVARCHAR(1000)
)
原始資料如下:
最上層目錄的 PatentId 為 0。
Id PatentId Txt
----|----------|----------
1 | 0 | 1
2 | 0 | 2
3 | 1 | 1-1
4 | 1 | 1-2
5 | 2 | 2-1
6 | 3 | 1-1-1
現在需要用 -->
串出該目錄到 根目錄
的所有階層,結果如下:
Id PatentId Txt R
----|----------|----------|-----------------------
1 | 0 | 1 1
2 | 0 | 2 2
3 | 1 | 1-1 1 --> 1-1
4 | 1 | 1-2 1 --> 1-2
5 | 2 | 2-1 2 --> 2-1
6 | 3 | 1-1-1 1 --> 1-1 --> 1-1-1
DECLARE @Data TABLE
(
Id INT,
PatentId INT,
Txt NVARCHAR(1000)
)
INSERT INTO @Data VALUES
(1, 0, '1'),
(2, 0, '2'),
(3, 1, '1-1'),
(4, 1, '1-2'),
(5, 2, '2-1'),
(6, 3, '1-1-1')
DECLARE @Data TABLE
(
Id INT,
Txt NVARCHAR(10)
)
INSERT INTO @Data VALUES
(1,N'甲'),
(2,N'乙'),
(3,N'丙'),
(4,N'丁'),
(5,N'戊')
;WITH DLOOP (Id, Txt, R) AS
(
SELECT Id, Txt, Txt AS R FROM @Data
UNION ALL
SELECT B.Id,
B.Txt,
CAST (A.R+B.Txt AS NVARCHAR(10)) AS R
FROM DLOOP AS A
INNER JOIN @Data AS B ON B.Id>A.Id
)
SELECT R FROM DLOOP
ORDER BY R
DECLARE @Data TABLE
(
Id INT,
PatentId INT,
Txt NVARCHAR(1000)
)
INSERT INTO @Data VALUES
(1, 0, '1'),
(2, 0, '2'),
(3, 1, '1-1'),
(4, 1, '1-2'),
(5, 2, '2-1'),
(6, 3, '1-1-1')
;WITH DLOOP AS
(
SELECT Id, PatentId, Txt,
Txt AS R,
PatentId AS PrevId,
1 AS Dp
FROM @Data
UNION ALL
SELECT A.Id, A.PatentId, A.Txt,
CAST(B.Txt + ' --> ' + A.R AS NVARCHAR(1000)) AS R,
B.PatentId AS PrevId,
A.Dp + 1 AS Dp
FROM DLOOP AS A
INNER JOIN @Data AS B ON B.Id=A.PrevId
), DResult AS
(
SELECT Id, PatentId, Txt, R
FROM
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY Id ORDER BY Dp DESC) AS G
FROM DLOOP
) AS A
WHERE G=1
)
SELECT * FROM DResult
這兩題都用到了 SQL 的 CTE 遞迴,這邊稍微簡單介紹一下。
CTE 遞迴結構
;WITH Recursive
(
--初始資料
SELECT * FROM XXX
UNION ALL
--遞迴查詢
SELECT * FROM Recursive
INNER JOIN YYY
)
遞迴結構分為兩個部分
上半部為初始資料的查詢
下半部為遞迴查詢,中間透過 UNION ALL 連接
程式運行時會先執行初始查詢的部分,這個部分只會進行一次
接著透過這些資料進行遞迴查詢
每次遞迴會將上一次下半部的查詢,當成下一次 CTE 的結果
而當下半部 查詢為空
時結束遞迴
這時會將之前每次遞迴的結果 UNION ALL
並回傳 (包含第一次上半部的結果)
執行過程蠻抽象的,不知道這樣表達,大家懂不懂
第一題是個排列組合問題,觀察結果後可以發現
每個項目的結果都是順向的,例如不可能出現 乙甲
因此我以 甲乙丙丁戊
為基礎
每次遞迴時將每個項目 JOIN 小於自己編號的所有項目
最後在將所有結果加在一起就是答案
甲的遞迴
第一次遞迴 第二次遞迴 第三次遞迴 第四次遞迴
-----------|-----------|-----------|-----------
甲乙 甲乙丙 甲乙丙丁 甲乙丙丁戊
甲丙 甲乙丁 甲乙丙戊
甲丁 甲乙戊 甲乙丁戊
甲戊 甲丙丁 甲丙丁戊
甲丙戊
甲丁戊
乙的遞迴
第一次遞迴 第二次遞迴 第三次遞迴
-----------|-----------|-----------
乙丙 乙丙丁 乙丙丁戊
乙丁 乙丙戊
乙戊 乙丁戊
程式說明
;WITH DLOOP (Id, Txt, R) AS
(
-- 初始化資料 甲乙丙丁戊
SELECT Id, Txt, Txt AS R FROM @Data
UNION ALL
SELECT B.Id, -- 每次回傳最後一位的Id
B.Txt, -- 每次回傳最後一位的Txt (其實不必這個欄位)
-- R為結果
-- A.R (上一次遞迴的結果) + B.Txt (這次 Join 的項目)
CAST (A.R+B.Txt AS NVARCHAR(10)) AS R
FROM DLOOP AS A
-- Join 編號小於自己的項目
INNER JOIN @Data AS B ON B.Id>A.Id
)
第二題如各位大大說的,是個標準的遞迴題
邏輯很單純每次透過 PatentId 找到上一層的目錄
不過有一點要注意,因為在遞迴過程會產生一些中間資料
因此最後要將這些資料過濾掉
例如以 1-1-1
這個目錄來看
第一次遞迴 第二次遞迴 第三次遞迴
-----------|---------------|---------------------
1-1-1 1-1 --> 1-1-1 1 --> 1-1 --> 1-1-1
最後結果會產生三筆資料,但其實我們只要最後一筆
1-1-1
1-1 --> 1-1-1
1 --> 1-1 --> 1-1-1 <-只需要這筆資料
因此在遞迴中我新增了一個 Dp
欄位,紀錄了目錄的層數,最後就可以利用 Dp 找出最長的那筆資料。
程式說明
;WITH DLOOP AS
(
-- 原始目錄
SELECT Id, PatentId, Txt,
Txt AS R,
PatentId AS PrevId,
1 AS Dp -- 設定第一層 Dp 為 1
FROM @Data
UNION ALL
SELECT A.Id, A.PatentId, A.Txt, -- 原始目錄的欄位
-- R為結果
-- B.Txt (為這次找到的上層目錄) + A.R (上一次遞迴的結果)
CAST(B.Txt + ' --> ' + A.R AS NVARCHAR(1000)) AS R,
-- PrevId 提供下一次遞迴要去 Join 的 PatentId
B.PatentId AS PrevId,
-- 目錄層數,最後用來找出最長的那個結果
A.Dp + 1 AS Dp
FROM DLOOP AS A
-- 利用 PatentId 找出上層目錄
INNER JOIN @Data AS B ON B.Id=A.PrevId
), DResult AS
(
SELECT Id, PatentId, Txt, R
FROM
(
-- 利用原始目錄 Id 進行分組,並以 Dp 排序
SELECT *, ROW_NUMBER() OVER(PARTITION BY Id ORDER BY Dp DESC) AS G
FROM DLOOP
) AS A
WHERE G=1 -- 找出最多層的那筆資料
)
解答拖了好多天,今天才有空發文,不知道大家有沒有玩的愉快。
我用暴力解法,目前想不到用遞迴解決,另外要做成動態的可以改成拼接sql + exec
--查詢
with cte as (
-- 1.1 先要藉由row_number()定義出甲跟乙誰最小得出RANK欄位
select *,row_number() over (
order by (
case val when N'甲' then 1
when N'乙' then 2
when N'丙' then 3
when N'丁' then 4
when N'戊' then 5
else val end
)
) rnk
from T
)
,CTE2 as (
--1.2 藉由CrossJoin + 前者RNAK小於後表RANK欄位得出"結果需由小的順位開始展開"需求,也順便排除同一個組合。
select T1.Val as result
from CTE T1
union all
select T1.Val+T2.Val as result
from CTE T1,CTE T2
where T2.rnk > T1.rnk
union all
select T1.Val+T2.Val+T3.Val as result
from CTE T1,CTE T2,CTE T3
where T2.rnk > T1.rnk and T3.rnk > T2.rnk
union all
select T1.Val+T2.Val+T3.Val+T4.Val as result
from CTE T1,CTE T2,CTE T3,CTE T4
where T2.rnk > T1.rnk and T3.rnk > T2.rnk and T4.rnk > T3.rnk
union all
select T1.Val+T2.Val+T3.Val+T4.Val+T5.Val as result
from CTE T1,CTE T2,CTE T3,CTE T4,CTE T5
where T2.rnk > T1.rnk and T3.rnk > T2.rnk and T4.rnk > T3.rnk and T5.rnk > T4.rnk
)
select * from CTE2
order by substring(result,1,1) desc,len(result) asc /*單純優化結果展示*/
結果:
result
甲
甲乙
甲丙
甲丁
甲戊
甲乙丙
甲乙丁
甲乙戊
甲丙丁
甲丙戊
甲丁戊
甲乙丙丁
甲乙丙戊
甲乙丁戊
甲丙丁戊
甲乙丙丁戊
戊
乙
乙丙
乙丁
乙戊
乙丙丁
乙丙戊
乙丁戊
乙丙丁戊
丙
丙丁
丙戊
丙丁戊
丁
丁戊
這一題可以使用標準遞迴解決
--測試資料
if(OBJECT_ID('#tempdb.#T') is not null) Drop table #T;
CREATE TABLE #T ([Id] int, [PatentId] int, [Txt] varchar(6));
INSERT INTO #T ([Id], [PatentId], [Txt])
VALUES (1, 0, '1'),(2, 0, '2'),(3, 1, '1-1'),(4, 1, '1-2'),(5, 2, '2-1'),(6, 3, '1-1-1');
--查詢
with CTE as (
select T1.ID,T1.Patentid,T1.Txt,convert(nvarchar(200),T1.Txt) as R
from #T T1
where patentid = 0
union all
select
T1.ID,T1.Patentid,T1.Txt,convert(nvarchar(200),T2.Txt + ' --> ' + T1.Txt ) as R
from #T T1
join CTE T2 on T1.patentid = T2.id
)
select * from cte
order by ID
結果
ID | Patentid | Txt | R |
---|---|---|---|
1 | 0 | 1 | 1 |
2 | 0 | 2 | 2 |
3 | 1 | 1-1 | 1 --> 1-1 |
4 | 1 | 1-2 | 1 --> 1-2 |
5 | 2 | 2-1 | 2 --> 2-1 |
6 | 3 | 1-1-1 | 1-1 --> 1-1-1 |
第6階 1-1-1
要往回找到 1-1 和 1
R = 1 --> 1-1 --> 1-1-1
大大排列組合的思路和我很像
每次 join 都只取 rnk 大於自己的項目
CREATE OR REPLACE FUNCTION f_combos(_arr anyarray)
RETURNS TABLE (combo anyarray) LANGUAGE plpgsql AS
$BODY$
BEGIN
IF array_upper(_arr, 1) IS NULL THEN
combo := _arr; RETURN NEXT; RETURN;
END IF;
CASE array_upper(_arr, 1)
-- WHEN 0 THEN -- does not exist
WHEN 1 THEN
RETURN QUERY VALUES ('{}'), (_arr);
WHEN 2 THEN
RETURN QUERY VALUES ('{}'), (_arr[1:1]), (_arr), (_arr[2:2]);
ELSE
RETURN QUERY
WITH x AS (
SELECT f.combo FROM f_combos(_arr[1:array_upper(_arr, 1)-1]) f
)
SELECT x.combo FROM x
UNION ALL
SELECT x.combo || _arr[array_upper(_arr, 1)] FROM x;
END CASE;
END
$BODY$;
--
SELECT * FROM f_combos('{1,2,3,4,5}'::int[]) ORDER BY 1;
+-------------+
| combo |
+-------------+
| {} |
| {1} |
| {1,2} |
| {1,2,3} |
| {1,2,3,4} |
| {1,2,3,4,5} |
| {1,2,3,5} |
| {1,2,4} |
| {1,2,4,5} |
| {1,2,5} |
| {1,3} |
| {1,3,4} |
| {1,3,4,5} |
| {1,3,5} |
| {1,4} |
| {1,4,5} |
| {1,5} |
| {2} |
| {2,3} |
| {2,3,4} |
| {2,3,4,5} |
| {2,3,5} |
| {2,4} |
| {2,4,5} |
| {2,5} |
| {3} |
| {3,4} |
| {3,4,5} |
| {3,5} |
| {4} |
| {4,5} |
| {5} |
+-------------+
(32 rows)
Time: 0.543 ms
好強 !
我記得plpgsql...在裡面有寫CASE WHEN的狀況下,好像(非常神奇地)可以不用控制項,WHEN 2那邊感覺多寫了,ELSE已經寫得很漂亮。我測一下晚點貼上來。
大大的遞迴用的真巧妙
CASE1 和 CASE2 定義了遞迴的最小單位
之後的每一層就按照回傳加入新項目
最後 UNION ALL 的地方看了好久才明白
以 {1,2,3} 為例
SELECT x.combo FROM x
-- 結果為
1
1,2
2
UNION ALL
SELECT x.combo || _arr[array_upper(_arr, 1)] FROM x;
-- 結果為
1,3
1,2,3
2,3
3
如果再多一層 {1,2,3,4} 就是
SELECT x.combo FROM x
-- 結果為
1
1,2
2
1,3
1,2,3
2,3
3
UNION ALL
SELECT x.combo || _arr[array_upper(_arr, 1)] FROM x;
-- 結果為
1,4
1,2,4
2,4
1,3,4
1,2,3,4
2,3,4
3,4
4
上半部為上一層的回傳,下半部為上一層加新項目的結果,UNION ALL 後就是答案,這裡很漂亮。
(刪除)
DROP FUNCTION IF EXISTS fun1(r anyarray);
CREATE OR REPLACE FUNCTION fun1(r anyarray)
RETURNS TABLE(category anyarray) LANGUAGE plpgsql AS
$BODY$
BEGIN
CASE array_upper(r,1)
WHEN 1 THEN RETURN QUERY VALUES ('{}'),(r);
ELSE RETURN QUERY
SELECT b.category
FROM(SELECT a.category FROM fun1(r[1:(array_upper(r,1)-1)])a)b
UNION ALL
SELECT b.category || r[array_upper(r,1)]
FROM(SELECT a.category FROM fun1(r[1:(array_upper(r,1)-1)])a)b;
END CASE;
END
$BODY$;
--測試結果
SELECT * FROM fun1('{甲,乙,丙,丁,戊}'::text[]);
製作原理和一級屠豬士相同,只是拿掉了控制項、移除when 2 (ELSE有蓋到when2了)、還原x(這邊一級屠豬士寫得比較漂亮精簡,只是我個人沒這習慣)。
話說回來,雖說之前有經驗,知道這樣寫就跑得起來,但完全不懂為什麼可以不用寫控制項(while or IF&ENDIF or for迴圈),系統是怎麼判斷怎樣叫跑完、怎樣要繼續跑,我蠻好奇的,版上有沒有大師可以開導指教一下?
functional programming....
{} / NULL 遞迴傳遞回去
第三種寫法出現了!!! 也是使用 plpgsql,小弟沒有寫過所以對語法好陌生,臨時 Google 惡補了好多語法。
我嘗試回答大大最後的問題。
以 {甲,乙,丙} 為例
我畫了一張圖,應該會比較清楚
進入遞迴函數後,因為遇到 ELSE 所以會一直往下呼叫
直到最後一層遇到 WHEN 1
,這時有了第一個回傳 {}和{甲}
接著回傳的過程結果會不斷疊加,直到第一層離開函數
這時的 RETRUN 就是結果
可以發現遞迴函數和 while 等迴圈不太一樣
是有深度的循環,函數一層一層的呼叫,再一層一層的回傳
直到 退出第一層函數
就是遞迴的結束
表達不太好,不知道您能不能理解