Ở đây tôi điều chỉnh câu trả lời cũ của mình từ một câu hỏi SO tương tự Làm thế nào để tìm tất cả các sơ đồ con được kết nối của một đồ thị không xác định .
Biến thể này không sử dụng con trỏ hoặc vòng lặp rõ ràng, nhưng sử dụng một truy vấn đệ quy duy nhất.
Về cơ bản, nó xử lý dữ liệu dưới dạng các cạnh trong biểu đồ và đi qua đệ quy tất cả các cạnh của biểu đồ, dừng lại khi phát hiện vòng lặp. Sau đó, nó đặt tất cả các vòng tìm thấy trong các nhóm và cung cấp cho mỗi nhóm một số.
Xem các giải thích chi tiết về cách thức hoạt động dưới đây. Tôi khuyên bạn nên chạy truy vấn CTE-by-CTE và kiểm tra từng kết quả trung gian để hiểu những gì nó làm.
Dữ liệu mẫu
DECLARE @T TABLE (ID1 int NOT NULL, ID2 int NOT NULL);
INSERT INTO @T (ID1, ID2) VALUES
(10, 20),
(10, 30),
(30, 30),
(10, 40),
(50, 70),
(60, 50),
(70, 70),
(4457745, 255714),
(4457745,2540222),
(2540222,4457745),
( 255714,4457745);
Truy vấn
WITH
CTE_Ids
AS
(
SELECT ID1 AS ID
FROM @T
UNION
SELECT ID2 AS ID
FROM @T
)
,CTE_Pairs
AS
(
SELECT ID1, ID2
FROM @T
WHERE ID1 <> ID2
UNION
SELECT ID2 AS ID1, ID1 AS ID2
FROM @T
WHERE ID1 <> ID2
)
,CTE_Recursive
AS
(
SELECT
CAST(CTE_Ids.ID AS varchar(8000)) AS AnchorID
,ID1
,ID2
,CAST(
',' + CAST(ID1 AS varchar(8000)) +
',' + CAST(ID2 AS varchar(8000)) +
',' AS varchar(8000)) AS IdPath
,1 AS Lvl
FROM
CTE_Pairs
INNER JOIN CTE_Ids ON CTE_Ids.ID = CTE_Pairs.ID1
UNION ALL
SELECT
CTE_Recursive.AnchorID
,CTE_Pairs.ID1
,CTE_Pairs.ID2
,CAST(
CTE_Recursive.IdPath +
CAST(CTE_Pairs.ID2 AS varchar(8000)) +
',' AS varchar(8000)) AS IdPath
,CTE_Recursive.Lvl + 1 AS Lvl
FROM
CTE_Pairs
INNER JOIN CTE_Recursive ON CTE_Recursive.ID2 = CTE_Pairs.ID1
WHERE
CTE_Recursive.IdPath NOT LIKE
CAST('%,' + CAST(CTE_Pairs.ID2 AS varchar(8000)) + ',%' AS varchar(8000))
)
,CTE_RecursionResult
AS
(
SELECT AnchorID, ID1, ID2
FROM CTE_Recursive
)
,CTE_CleanResult
AS
(
SELECT AnchorID, ID1 AS ID
FROM CTE_RecursionResult
UNION
SELECT AnchorID, ID2 AS ID
FROM CTE_RecursionResult
)
SELECT
DENSE_RANK() OVER (ORDER BY CA_Data.XML_Value) AS [NewID]
,CTE_Ids.ID AS [OldID]
,CA_Data.XML_Value AS GroupMembers
FROM
CTE_Ids
CROSS APPLY
(
SELECT CAST(CTE_CleanResult.ID AS nvarchar(max)) + ','
FROM CTE_CleanResult
WHERE CTE_CleanResult.AnchorID = CTE_Ids.ID
ORDER BY CTE_CleanResult.ID FOR XML PATH(''), TYPE
) AS CA_XML(XML_Value)
CROSS APPLY
(
SELECT CA_XML.XML_Value.value('.', 'NVARCHAR(MAX)')
) AS CA_Data(XML_Value)
ORDER BY [NewID], [OldID];
Kết quả
+-------+---------+-------------------------+
| NewID | OldID | GroupMembers |
+-------+---------+-------------------------+
| 1 | 10 | 10,20,30,40, |
| 1 | 20 | 10,20,30,40, |
| 1 | 30 | 10,20,30,40, |
| 1 | 40 | 10,20,30,40, |
| 2 | 255714 | 255714,2540222,4457745, |
| 2 | 2540222 | 255714,2540222,4457745, |
| 2 | 4457745 | 255714,2540222,4457745, |
| 3 | 50 | 50,60,70, |
| 3 | 60 | 50,60,70, |
| 3 | 70 | 50,60,70, |
+-------+---------+-------------------------+
Làm thế nào nó hoạt động
CTE_Ids
CTE_Ids
đưa ra danh sách tất cả các Định danh xuất hiện trong cả hai ID1
và ID2
cột. Vì chúng có thể xuất hiện theo bất kỳ thứ tự nào, UNION
cả hai cột cùng nhau. UNION
cũng loại bỏ bất kỳ bản sao.
+---------+
| ID |
+---------+
| 10 |
| 20 |
| 30 |
| 40 |
| 50 |
| 60 |
| 70 |
| 255714 |
| 2540222 |
| 4457745 |
+---------+
CTE_Pairs
CTE_Pairs
đưa ra danh sách tất cả các cạnh của đồ thị theo cả hai hướng. Một lần nữa, UNION
được sử dụng để loại bỏ bất kỳ bản sao.
+---------+---------+
| ID1 | ID2 |
+---------+---------+
| 10 | 20 |
| 10 | 30 |
| 10 | 40 |
| 20 | 10 |
| 30 | 10 |
| 40 | 10 |
| 50 | 60 |
| 50 | 70 |
| 60 | 50 |
| 70 | 50 |
| 255714 | 4457745 |
| 2540222 | 4457745 |
| 4457745 | 255714 |
| 4457745 | 2540222 |
+---------+---------+
CTE_Recursive
CTE_Recursive
là phần chính của truy vấn đi qua đệ quy biểu đồ bắt đầu từ mỗi Mã định danh duy nhất. Những hàng bắt đầu được sản xuất bởi phần đầu tiên của UNION ALL
. Phần thứ hai của UNION ALL
đệ quy tham gia vào chính nó liên kết ID2
đến ID1
. Vì chúng tôi đã tạo sẵn CTE_Pairs
với tất cả các cạnh được viết theo cả hai hướng, chúng tôi luôn có thể chỉ liên kết ID2
đến ID1
và chúng tôi sẽ nhận được tất cả các đường dẫn trong biểu đồ. Đồng thời, truy vấn được xây dựng IdPath
- một chuỗi các Định danh được phân cách bằng dấu phẩy đã được duyệt qua cho đến nay. Nó được sử dụng trong WHERE
bộ lọc:
CTE_Recursive.IdPath NOT LIKE
CAST('%,' + CAST(CTE_Pairs.ID2 AS varchar(8000)) + ',%' AS varchar(8000))
Ngay khi chúng ta bắt gặp Mã định danh đã được đưa vào Đường dẫn trước đó, đệ quy dừng lại khi danh sách các nút được kết nối đã hết.
AnchorID
là Định danh bắt đầu cho đệ quy, nó sẽ được sử dụng sau để nhóm kết quả.
Lvl
không thực sự được sử dụng, tôi bao gồm nó để hiểu rõ hơn về những gì đang xảy ra.
+----------+---------+---------+--------------------------+-----+
| AnchorID | ID1 | ID2 | IdPath | Lvl |
+----------+---------+---------+--------------------------+-----+
| 10 | 10 | 20 | ,10,20, | 1 |
| 10 | 10 | 30 | ,10,30, | 1 |
| 10 | 10 | 40 | ,10,40, | 1 |
| 20 | 20 | 10 | ,20,10, | 1 |
| 30 | 30 | 10 | ,30,10, | 1 |
| 40 | 40 | 10 | ,40,10, | 1 |
| 50 | 50 | 60 | ,50,60, | 1 |
| 50 | 50 | 70 | ,50,70, | 1 |
| 60 | 60 | 50 | ,60,50, | 1 |
| 70 | 70 | 50 | ,70,50, | 1 |
| 255714 | 255714 | 4457745 | ,255714,4457745, | 1 |
| 2540222 | 2540222 | 4457745 | ,2540222,4457745, | 1 |
| 4457745 | 4457745 | 255714 | ,4457745,255714, | 1 |
| 4457745 | 4457745 | 2540222 | ,4457745,2540222, | 1 |
| 2540222 | 4457745 | 255714 | ,2540222,4457745,255714, | 2 |
| 255714 | 4457745 | 2540222 | ,255714,4457745,2540222, | 2 |
| 70 | 50 | 60 | ,70,50,60, | 2 |
| 60 | 50 | 70 | ,60,50,70, | 2 |
| 40 | 10 | 20 | ,40,10,20, | 2 |
| 40 | 10 | 30 | ,40,10,30, | 2 |
| 30 | 10 | 20 | ,30,10,20, | 2 |
| 30 | 10 | 40 | ,30,10,40, | 2 |
| 20 | 10 | 30 | ,20,10,30, | 2 |
| 20 | 10 | 40 | ,20,10,40, | 2 |
+----------+---------+---------+--------------------------+-----+
CTE_CleanResult
CTE_CleanResult
chỉ để lại các phần có liên quan từ CTE_Recursive
và một lần nữa hợp nhất cả hai ID1
và ID2
sử dụng UNION
.
+----------+---------+
| AnchorID | ID |
+----------+---------+
| 10 | 10 |
| 10 | 20 |
| 10 | 30 |
| 10 | 40 |
| 20 | 10 |
| 20 | 20 |
| 20 | 30 |
| 20 | 40 |
| 2540222 | 255714 |
| 2540222 | 2540222 |
| 2540222 | 4457745 |
| 255714 | 255714 |
| 255714 | 2540222 |
| 255714 | 4457745 |
| 30 | 10 |
| 30 | 20 |
| 30 | 30 |
| 30 | 40 |
| 40 | 10 |
| 40 | 20 |
| 40 | 30 |
| 40 | 40 |
| 4457745 | 255714 |
| 4457745 | 2540222 |
| 4457745 | 4457745 |
| 50 | 50 |
| 50 | 60 |
| 50 | 70 |
| 60 | 50 |
| 60 | 60 |
| 60 | 70 |
| 70 | 50 |
| 70 | 60 |
| 70 | 70 |
+----------+---------+
CHỌN cuối cùng
Bây giờ chúng ta cần xây dựng một chuỗi các ID
giá trị được phân tách bằng dấu phẩy cho mỗi giá trị AnchorID
.
CROSS APPLY
với FOR XML
nó
DENSE_RANK()
tính toán các NewID
con số cho mỗi AnchorID
.
Truy vấn này xây dựng một đường dẫn đầy đủ cho mỗi định danh, điều này không thực sự cần thiết.
Có thể làm cho giải pháp tổng thể hiệu quả hơn bằng cách chạy truy vấn này một lần cho mỗi nhóm / sơ đồ con, trong một vòng lặp.
Chọn một Mã định danh bắt đầu (thêm WHERE ID = 10
vào CTE_Ids
và CTE_Pairs
), sau đó xóa khỏi bảng nguồn tất cả các mã định danh được gắn vào nhóm này. Lặp lại cho đến khi bàn lớn trống rỗng.