Thu thập tất cả những người tương tự với một nhóm


7

Tôi có một người với vài Id. Một số trong số họ trong Cột Id1 và Một số trong Id2. Tôi muốn thu thập tất cả những người cùng Id vào một nhóm.

Nếu id1 = 10, nằm trong cùng hàng với id2 = 20. vậy có nghĩa là người có id1 = 10 anh ta là người giống như id2 = 20.

Ví dụ đầu vào và đầu ra:

Đầu vào

Id1     Id2
---     ---
10      20
10      30
30      30
10      40

50      70
60      50
70      70 

Đầu ra

NewId   OldId
-----   -----
1       10
1       20
1       30
1       40

2       50
2       60
2       70

Câu trả lời:


3

Sẽ tốt hơn để thực hiện các tính toán như vậy theo cách lặp.

" Vấn đề " của giải pháp đó là, trong trường hợp bảng nguồn lớn, bảng đầu ra sẽ chứa tất cả các hàng. Nhưng, trên thực tế, đây cũng là lợi ích: trong trường hợp bảng nguồn lớn, mã như vậy sẽ hoạt động nhanh hơn nhiều, đó là sự thu hồi.


if object_id('tempdb..#src') is not null drop table #src;
create table #src (id1 int not null, id2 int)

if object_id('tempdb..#rez') is not null drop table #rez;
create table #rez (id int not null, chainid int not null);

declare @chainId int;

insert #src
values (10, 20), (10, 30), (30, 30), (10, 40), (50, 70), (60, 50), (70, 70)
--values (4457745,255714),(4457745,2540222),(2540222,4457745),(255714,4457745)

set @chainId = 1

while 1 = 1
begin
    insert #rez(id, chainid)
    select top 1 id1, @chainId
    from #src
    where
        Id1 NOT IN (select Id from #rez)
        and id2 NOT IN (select Id from #rez)

    if @@rowcount = 0 break

    while 1 = 1
    begin
        insert #rez(id, chainid)

        select id1, @chainid
        from #src
        where
            id2 in (select id from #rez where chainid = @chainId)
            and id1 not in (select id from #rez where chainid = @chainId)

        union

        select id2, @chainId
        from #src
        where
            id1 in (select id from #rez where chainid = @chainId)
            and id2 not in (select id from #rez where chainid = @chainId)

        if @@rowcount = 0 break
    end

    select @chainId = @chainId + 1
end

select [NewId] = chainid, OldID = id
from #rez 
order by 1, 2;

Trong một số chi tiết, "logic" của tập lệnh là như sau:

  • đặt giá trị ban đầu ID mới (chainid) (ví dụ 1)

    • có được mục đầu tiên chưa được tìm thấy (chu kỳ bên ngoài)

      • tìm và lưu trữ tất cả những gì liên quan đến nó (trong cả hai cột) (chu trình nội bộ)
    • tăng ID mới

    • tìm mục tiếp theo không có trong bảng kết quả

    • tiếp tục cho đến khi có dữ liệu để làm việc

  • trả về kết quả đã sắp xếp


BTW, được kiểm tra với cả hai mẫu dữ liệu của bạn: hoạt động như mong đợi.
Sandr

3

Ở đâ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 ID1ID2cột. Vì chúng có thể xuất hiện theo bất kỳ thứ tự nào, UNIONcả hai cột cùng nhau. UNIONcũ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_Recursivelà 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_Pairsvớ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 ID1và 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 WHEREbộ 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. AnchorIDlà Định danh bắt đầu cho đệ quy, nó sẽ được sử dụng sau để nhóm kết quả. Lvlkhô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_CleanResultchỉ để lại các phần có liên quan từ CTE_Recursivevà một lần nữa hợp nhất cả hai ID1ID2sử 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 IDgiá trị được phân tách bằng dấu phẩy cho mỗi giá trị AnchorID. CROSS APPLYvới FOR XMLDENSE_RANK()tính toán các NewIDcon 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 = 10vào CTE_IdsCTE_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.


1

Tôi chắc chắn có nhiều cách hiệu quả hơn để làm điều này, nhưng có lẽ đây sẽ là một sự khởi đầu.

Tôi đã sử dụng CTE để chuyển đổi đầu tiên quanh Id, vì vậy cột bên trái nhỏ hơn cột bên phải (Đã lọc).

Sau đó, tôi tạo một bảng chứa tất cả các giá trị thấp hơn (từ cột bên trái) cùng với bất kỳ giá trị nào cao hơn chúng (từ cột bên phải) (FilteredUp).

FilteredDown loại bỏ mọi trường hợp tồn tại một ví dụ thấp hơn (ví dụ: bạn có 30,30 là một hàng, nhưng điều này được loại bỏ khi tồn tại một hàng 10,20) và cũng đảm bảo rằng chúng ta có tất cả các giá trị bên trái trong cột bên phải (ví dụ: 10,10 và 50,50).

Cuối cùng tôi đã tạo Id mới bằng ROW_NUMBER ():

IF OBJECT_ID('tempdb..#Temp') IS NULL
BEGIN
CREATE TABLE #Temp (Id1 INT, Id2 INT)

INSERT INTO #Temp 
    VALUES(10,20),(10,30),(30,30),(10,40),(50,70),(60,50),(70,70)
END

 SELECT * FROM #Temp

;With Filtered AS (
    SELECT CASE WHEN Id1 <= Id2 THEN Id1 ELSE Id2 END AS Id1, CASE WHEN Id1 >= Id2 THEN Id1 ELSE Id2 END AS Id2 FROM #Temp
), 
FilteredUp AS(
    SELECT id1, Id2 AS Up FROM Filtered 
    UNION ALL
    SELECT f.id1, fu.Id2 FROM FilteredUp f
        JOIN Filtered fu ON f.Up = fu.Id1 AND f.Up < fu.Id2  
),
FilteredDown AS (
    SELECT f.Id1, f.Id2 FROM Filtered f
        LEFT JOIN FilteredUp fu ON f.Id1 = fu.Up
        WHERE fu.Id1 IS NULL
    UNION
    SELECT f.Id1, f.Id1 AS Id2 FROM Filtered f
        LEFT JOIN FilteredUp fu ON f.Id1 = fu.Up
        WHERE fu.Id1 IS NULL 
),
RowNumber AS (
SELECT ROW_NUMBER() OVER(ORDER BY Id1) AS NewId, Id1 FROM FilteredDown
    GROUP BY Id1
)
SELECT rn.NewId, fd.Id2 AS OldId FROM RowNumber rn
    JOIN FilteredDown fd ON rn.Id1 = fd.Id1 

có một vấn đề đối với đầu vào: (4457745.255714), (4457745,2540222), (2540222,4457745), (255714,4457745). Nó sẽ trả lại cho tất cả các id của cùng một NewId
Dima Ha

Nhưng nó rất kỳ lạ, nếu tôi thay đổi 4457745 thành 10 (hoặc một số nhỏ) thì nó sẽ hoạt động. bạn có biết tại sao?
Dima Ha

Sam? bạn có thể cho tôi biết lý do tại sao cho chèn này nó không làm việc :( 4457745,255714), (4457745,2540222), (2540222,4457745), (255.714, 4.457.745)?
Dima Hà

sử dụng một bigint
McNets

Câu trả lời của Sandr đầy đủ hơn (và tốt hơn nhiều), tôi đã không xem xét khả năng của một chuỗi như trong ví dụ của bạn. bởi vì 255714 & 2540222 đều nhỏ hơn 4457745 và không có gì liên kết trực tiếp chúng với nhau, chúng bị bỏ lỡ bởi tôi. Trong khi đó giải pháp của Sandr sẽ kiểm tra đối với chuỗi này.
Sam
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.