如何在非常大的表中查找行之间的配对关系

时间:2019-01-08 21:27:53

标签: sql-server

我有一个很大的表(60m行),其中包含两列的行:set_id和object_id。 set_id用于标识object_id的组。就我而言,这些object_id可以出现在多个集合中。

示例:

set_id | object_id
1 | 100
1 | 101
1 | 102
2 | 100
2 | 201
3 | 300
4 | 102
4 | 300
5 | 500

我需要检索一组至少共享一个object_id的set_id对的列表。每个set_id也将与其自身配对。配对仅显示一次(即:(1,2),但不显示(2,1))。对于上面的示例:

set_id_A | set_id_B
1 | 1
1 | 2
1 | 4
2 | 2
3 | 3
3 | 4
4 | 4
5 | 5

编写查询以实现此目标非常简单。问题是我的解决方案无法很好地扩展。这是我的代码:

-- #original_sets table created

CREATE TABLE #original_sets
    (
        [set_id] INT,
        [object_id]       BIGINT
    );

-- #original_sets populated here from other data
-- removed

-- index created on table:

CREATE CLUSTERED INDEX cx_original_sets
    ON #original_sets ([object_id], [set_id]);

-- code to create the pairs:

            SELECT
                    ck1.[set_id] AS set_id_A,
                    ck2.[set_id] AS set_id_B
            FROM
                    #original_sets ck1
                INNER JOIN
                    #original_sets ck2
                        ON ck1.[object_id] = ck2.[object_id]
                           AND ck1.[set_id] <= ck2.[set_id]
            GROUP BY
                    ck1.[set_id],
                    ck2.[set_id];

如果original_sets表很小甚至是中等大小,那将是非常快的,但是一旦我到达6000万行,它的速度真的很慢。我最终在10小时后取消了它,所以我不确定它是否会结束。

在如此大的桌子上进行自我联接只是在寻找麻烦,我知道。还有另一种方法可以更好地扩展吗?谢谢!

修改1: 可能有助于提高性能的另一件事:获得集对之后,我还有另一个过程,该过程然后创建包含与原始集相关的所有对象ID的超级集(请参阅:传递闭包聚类http://sqlblog.com/blogs/davide_mauri/archive/2017/11/12/lateral-thinking-transitive-closure-clustering-with-sql-server-uda-and-json.aspx顶部显示得很好)

因为在此之后我要这样做,所以我并不真正在乎set_ids本身,而只是在乎如何将object_ids组合在一起。因此可以安全地消除重复集。也许首先这样做会是减小表的整体大小的好方法。

编辑2:

尝试减小原始表大小的新版本

-- #original_sets table created

CREATE TABLE #original_sets
    (
        [set_id] INT,
        [object_id]       BIGINT
    );

-- #original_sets populated here from other data
-- removed

-- index created on table:

CREATE CLUSTERED INDEX cx_original_sets
    ON #original_sets ([object_id], [set_id]);

--added this index:
CREATE CLUSTERED INDEX IDX_original_sets
    ON #original_sets ([set_id], [object_id]);

-- added this part to identify sets with only one object_id 
CREATE TABLE #lonely_sets
(
    [set_id] INT PRIMARY KEY
);

INSERT INTO #lonely_sets
SELECT  
        [set_id]
    FROM 
        #original_sets
    GROUP BY 
        [set_id]
    HAVING 
        COUNT(*) = 1

--then use that data to eliminate duplicate single object sets (see edit 1 for why)

DELETE FROM #original_sets
WHERE set_id IN 
(

SELECT
   [set_id ]
FROM
   #lonely_sets lonely_sets
WHERE
   [set_id ] NOT IN
   (
   SELECT
      MIN(original_sets.[set_id ])
   FROM
      #original_sets original_sets
      INNER join #lonely_sets lonely_sets
        ON original_sets.set_id  =  lonely_sets.set_id 
   GROUP BY
      original_sets.[object_id]
   )

)

-- then run this 
-- code to create the pairs as before:

            SELECT
                    ck1.[set_id] AS set_id_A,
                    ck2.[set_id] AS set_id_B
            FROM
                    #original_sets ck1
                INNER JOIN
                    #original_sets ck2
                        ON ck1.[object_id] = ck2.[object_id]
                           AND ck1.[set_id] <= ck2.[set_id]
            GROUP BY
                    ck1.[set_id],
                    ck2.[set_id];

额外的工作将原始集减少到约1600万行。 使用〜1m个唯一的object_ids和〜7m个唯一的set_ids。

以下是每组对象的细分:

object_count_per_set | sets_with_that_count
67  32
49  8
42  197
41  120
38  1
37  101
35  16
30  23
29  18
28  109
27  1643
26  382
25  43
24  35
23  8
22  492
21  703
20  339
19  1548
18  2176
17  358
16  1156
15  852
14  1755
13  1845
12  2452
11  3073
10  4570
9   4723
8   9726
7   16178
6   35493
5   81091
4   211305
3   724627
2   5360781
1   789573

因此,总体来说,要处理的表要小得多,但是完成了一个多小时(影响了1,035,212,815行),运行起来仍然很慢。

我知道有很多重复集可以安全地消除,我只需要一个好的方法即可。

2 个答案:

答案 0 :(得分:0)

您说表中有6000万行,大约50m个唯一的set_id和100k个唯一的object_id。

因此,平均每个object_id具有600行。平均而言,ck1.[object_id] = ck2.[object_id] AND ck1.[set_id] <= ck2.[set_id]会为每条外部行匹配300行,因此当前您的查询正在生成和聚合大约180亿行的内容

5000万套ID和6000万行意味着大多数套只能与自己配对,

首先,我会想到只使用简单的GROUP BY ... COUNT找到这些保证的非配对集,然后通过三角自连接将它们排除在较昂贵的部分中。

如果该查询仍然太慢,请提供有关#paired_sets的特征的信息,包括行数和不同的object_idset_id的数量以及最大的大小object_id在那里(行数)

CREATE TABLE #lonely_sets
    (
        [set_id] INT PRIMARY KEY
    );

INSERT INTO #lonely_sets
SELECT  [set_id]
FROM #original_sets
GROUP BY [set_id]
HAVING COUNT(*) = 1;


CREATE TABLE #paired_sets
(
    [set_id] INT,
    [object_id] INT,
    PRIMARY KEY  ([object_id], [set_id])
);

INSERT INTO #paired_sets
SELECT [set_id], [object_id]
FROM #original_sets
WHERE [set_id] NOT IN (SELECT ls.set_id FROM #lonely_sets ls);

--Final Select
SELECT [set_id] AS set_id_A, [set_id] AS set_id_B
FROM #lonely_sets
UNION ALL
SELECT
        ck1.[set_id] AS set_id_A,
        ck2.[set_id] AS set_id_B
FROM
        #paired_sets ck1
    INNER JOIN
        #paired_sets ck2
            ON ck1.[object_id] = ck2.[object_id]
                AND ck1.[set_id] <= ck2.[set_id]
GROUP BY
        ck1.[set_id],
        ck2.[set_id];

答案 1 :(得分:0)

因此,根据Martin的建议,我被指示减少连接表的大小,这就是我要结束的地方:

我决定尝试消除重复的集合(请参见上面我的原始帖子中的Edit 1)。 就我而言,这应该做两件事:减少以后运行自连接的表的大小,并帮助逐步扩展(每周都会引入新集合,但它们通常是以前集合的重复)。

我使用了旧的XML PATH行串联技巧(我没有运行2017,否则STRING_AGG可能会更快?)创建一个用分号分隔的列表,其中列出了每个set_id中的所有object_id。

然后用于标识包含相同object_id组的set_id,因此可以安全地将其删除。这样行数从60m减少到1m。就我而言,这大约需要50分钟。有没有更快的方法来识别相同的集合?我不确定。

然后创建一个过滤的集合表,并基于自连接来创建关系表。有了新的过滤后的数据,运行查询的那部分时间现在只有几分钟。

该过程中最慢的部分是不到一个小时的XML PATH行concat查询。这并不理想,但是由于此过程是紧急维护程序的一部分,因此我愿意接受运行所花费的时间。

代码:

-- #original_sets table created

CREATE TABLE #original_sets
    (
        [set_id] INT,
        [object_id]       BIGINT
    );

-- #original_sets populated here from other data
-- removed

-- index created on table:

CREATE CLUSTERED INDEX cx_original_sets
    ON #original_sets ([object_id], [set_id]);

CREATE CLUSTERED INDEX IDX_original_sets
    ON #original_sets ([set_id], [object_id]);

----------------------------------------------------------

CREATE TABLE #filtered_sets
    (
        [set_id] INT,
        [object_id]       BIGINT
    );

    INSERT INTO #filtered_sets
    SELECT
         original_sets.set_id,
         original_sets.[object_id]
    FROM
        #original_sets original_sets
        INNER JOIN
        (
            SELECT
                MIN(set_id) AS set_id
            FROM
            (
                    SELECT DISTINCT
                            set_id,
                            STUFF(
                                (
                                    SELECT
                                        '; ' +  CAST(original_sets.object_id AS VARCHAR(20))
                                    FROM
                                        #original_sets original_sets 
                                    WHERE
                                        original_sets.set_id = s2.set_id
                                    ORDER BY
                                        original_sets.object_id
                                    FOR XML PATH('')
                                ), 1, 2, ''
                                )                                            AS object_id_list

                    FROM
                            #original_sets s2

                    GROUP BY
                            set_id
            ) a

            GROUP BY
            object_id_list
        ) unique_sets
            ON original_clusters.cluster_id = unique_sets.cluster_id


CREATE CLUSTERED INDEX cx_filtered_sets
    ON #filtered_sets ([object_id], [set_id]);

CREATE NONCLUSTERED INDEX IDX_filtered_sets
    ON #filtered_sets ([set_id],[object_id]);

----------------------------------------------------------
-- then run this 
-- code to create the pairs as before:

            SELECT
                    ck1.[set_id] AS set_id_A,
                    ck2.[set_id] AS set_id_B
            FROM
                    #filtered_sets ck1
                INNER JOIN
                    #filtered_sets  ck2
                        ON ck1.[object_id] = ck2.[object_id]
                           AND ck1.[set_id] <= ck2.[set_id]
            GROUP BY
                    ck1.[set_id],
                    ck2.[set_id];

我接受了马丁的回答,因为它对于指出我需要去的地方很有用。谢谢!