如何获取与另一外部参照列中的任何值相关的一个外部参照列的不同组合

Eri*_*ham 6 sql t-sql sql-server

我需要选择 XRef 表中 B 列的唯一值组合的计数,该表按 A 列分组。

考虑以下架构和数据,它表示一个简单的家庭结构。每个孩子都有父亲和母亲:

表父亲

父亲ID 姓名
1 亚历克斯
2 鲍勃

表妈妈

母亲ID 姓名
1 爱丽丝
2 芭芭拉

表儿童

儿童ID 父亲ID 母亲ID 姓名
1 1(亚历克斯) 1(爱丽丝) 亚当
2 1(亚历克斯) 1(爱丽丝) 比利
3 1(亚历克斯) 2(芭芭拉) 席琳
4 2(鲍勃) 2(芭芭拉) 德里克

每个父亲的母亲的不同组合是:

  • 亚历克斯(爱丽丝、芭芭拉)
  • 鲍勃(芭芭拉饰)

总的来说,母亲有两种不同的组合

  1. 爱丽丝、芭芭拉
  2. 芭芭拉

我想编写的查询将返回母亲的这些不同组合的计数,无论它们与哪个父亲相关联

独特的母亲团体
2

我能够使用 STRING_AGG 函数成功完成此操作,但感觉很笨拙。它还需要操作数百万行,并且目前速度相当慢。有没有更惯用的方法来代替集合操作来做到这一点?

这是我的工作示例:

-- Drop pre-existing tables

DROP TABLE IF EXISTS dbo.Child;

DROP TABLE IF EXISTS dbo.Father;

DROP TABLE IF EXISTS dbo.Mother;

-- Create family tables.

CREATE TABLE dbo.Father
(
    FatherID INT NOT NULL
  , Name VARCHAR(50) NOT NULL
);

ALTER TABLE dbo.Father
ADD CONSTRAINT PK_Father
    PRIMARY KEY CLUSTERED (FatherID);

ALTER TABLE dbo.Father SET (LOCK_ESCALATION = TABLE);

CREATE TABLE dbo.Mother
(
    MotherID INT NOT NULL
  , Name VARCHAR(50) NOT NULL
);

ALTER TABLE dbo.Mother
ADD CONSTRAINT PK_Mother
    PRIMARY KEY CLUSTERED (MotherID);

ALTER TABLE dbo.Mother SET (LOCK_ESCALATION = TABLE);

CREATE TABLE dbo.Child
(
    ChildID INT NOT NULL
  , FatherID INT NOT NULL
  , MotherID INT NOT NULL
  , Name VARCHAR(50) NOT NULL
);

ALTER TABLE dbo.Child
ADD CONSTRAINT PK_Child
    PRIMARY KEY CLUSTERED (ChildID);

CREATE NONCLUSTERED INDEX IX_Parents ON dbo.Child (FatherID, MotherID);

ALTER TABLE dbo.Child
ADD CONSTRAINT FK_Child_Father
    FOREIGN KEY (FatherID)
    REFERENCES dbo.Father (FatherID);

ALTER TABLE dbo.Child
ADD CONSTRAINT FK_Child_Mother
    FOREIGN KEY (MotherID)
    REFERENCES dbo.Mother (MotherID);

-- Insert two children with the same parents

INSERT INTO dbo.Father
(
    FatherID
  , Name
)
VALUES
(1, 'Alex')
, (2, 'Bob')
, (3, 'Charlie')

INSERT INTO dbo.Mother
(
    MotherID
  , Name
)
VALUES
(1, 'Alice')
, (2, 'Barbara');

INSERT INTO dbo.Child
(
    ChildID
  , FatherID
  , MotherID
  , Name
)
VALUES
(1, 1, 1, 'Adam')
, (2, 1, 1, 'Billy')
, (3, 1, 2, 'Celine')
, (4, 2, 2, 'Derek')
, (5, 3, 1, 'Eric');

-- CTE Gets distinct combinations of parents
WITH distinctParentCombinations (FatherID, MotherID)
AS (SELECT children.FatherID
         , children.MotherID
    FROM dbo.Child as children
    GROUP BY children.FatherID
           , children.MotherID
   )
   -- CTE Gets uses STRING_AGG to get unique combinations of mothers.
   , motherGroups (Mothers)
AS (SELECT STRING_AGG(CONVERT(VARCHAR(MAX), distinctParentCombinations.MotherID), '-') WITHIN GROUP (ORDER BY distinctParentCombinations.MotherID) AS Mothers
    FROM distinctParentCombinations
    GROUP BY distinctParentCombinations.FatherID
   )

-- Remove the COUNT function to see the actual combinations
SELECT COUNT(motherGroups.Mothers) AS UniqueMotherGroups
FROM motherGroups


-- Clean up the example

DROP TABLE IF EXISTS dbo.Child;

DROP TABLE IF EXISTS dbo.Father;

DROP TABLE IF EXISTS dbo.Mother;
Run Code Online (Sandbox Code Playgroud)

小智 2

您对“问题案例”有很好的解释和设置。您的设置在(例如)tempdb 中运行良好。

您已经以一种很好的方式解决了问题,并且如果您每次运行查询时都要计算母组,我认为您无法进一步优化它。
但有一个小错误;您必须在最终计数中执行 COUNT(DISTINCT motherGroups.Mothers) 操作。

既然你提到了数百万行,我建议采用稍微不同的方法。如果您在子表中发生更改后立即聚合母组,则您的查询每次都可以快速运行 - 即使有数百万行。您想要运行的查询类型很少只运行一次,因此如果繁重的工作已经完成,那就太好了。

通常我不喜欢使用触发器,因为您会在难以查找和调试的地方获得额外的逻辑。但有时触发器是很好的,特别是当您无法更改客户端上运行的源代码时。

因此,我的解决方案是向父亲表添加一个新列,并创建一个触发器,每次子表发生更改时,该触发器都会(重新)生成母亲组。这样,一旦发生更改,每个父亲的硬聚合工作就会完成,并且您不必在运行查询时进行聚合。由于您已经拥有数百万行,因此我们还必须更新这些现有行。
我使用 SQL Server 2019 来实现此解决方案。

*** 解决方案***
向Father 表添加1 或2 个新列。如果您应该添加 1 或 2,这取决于您的偏好:“我是否想查看聚合的母组以用于调试目的,或者我是否只信任哈希值?”

第 1 列:每个父亲行的聚合母亲组的哈希值。
哈希值是 VARBINARY 并且至少为 32 个字节,但我们将使用 VARBINARY(1600):

  1. 1600 小于最大非聚集索引大小 1700,因此我们对该列建立索引不会有任何问题。
  2. 由于哈希值以 32 字节为单位,因此值 1600 将覆盖一个非常非常长的聚合母组。
-- Column 1: Hashed value of the aggregated mother group for each Father row.
alter table Father add MotherHash varbinary(1600)
create index IX_MotherHash on Father(MotherHash) 
Run Code Online (Sandbox Code Playgroud)

第 2 栏:此栏是可选的,取决于您的喜好。如果对结果有任何疑问,该列可以用于调试目的。
您应该使用哪种 VARCHAR 长度取决于您的实际数据。

  • 最大限度?那么存储母组就没有问题,但索引它可能会遇到问题,因为 1700 是非聚集索引的最大值。但也许你不需要索引它?
  • 1700?然后您可以对该列进行索引,但是根据您的实际数据,这会覆盖最大的母组吗?

为什么要建立索引?如果要列出聚合的母组,读取索引可能比读取整个表更快。
正如所说;这取决于您(和您的数据)。如果我们不需要查看聚合的母组,那么我们根本不需要此列。
对于此演示/解决方案,我们将添加用于调试目的的列,而不使用任何索引。

-- Column 2: This column is more optional, and depends on your preferences.
alter table Father add MotherGroup varchar(MAX)
go
Run Code Online (Sandbox Code Playgroud)

在子表上创建触发器。
它将处理子表中的所有插入、更新和删除。

create or alter trigger trIUD_Child on Child
after insert, update, delete
as
begin
    set nocount on
    -- Get all FatherIDs from the Inserted and Deleted table.
    -- An ordinary Temp table is created with a clustered index to get SEEK performance later.
    -- The table might also have more than 100 rows, where table variables are not recommended.
    declare @numRowsInInsertedDeleted int
    create table #rowsInInsertedDeleted(rowId int identity(1, 1), FatherID int)
    create unique clustered index ix on #rowsInInsertedDeleted(rowId)
    insert #rowsInInsertedDeleted(FatherID)
    select  distinct f.FatherID
    from
        (
            select i.FatherID from inserted i
            union all
            select i.FatherID from deleted i
        ) f
    select @numRowsInInsertedDeleted = max(rowId) from #rowsInInsertedDeleted

    -- We have to loop each of the FatherIDs, since we might have several rows in the Inserted and Deleted tables.
    declare @rowId int = 0
    while (@rowId < @numRowsInInsertedDeleted)
    begin
        -- Get the father for the next row.
        select @rowId += 1
        declare @fatherId int
        select  @fatherId = r.FatherID
        from    #rowsInInsertedDeleted r
        where   r.rowId = @rowId
        
        -- Aggregate the mothers for this father.
        declare @motherGroup varchar(max) = ''
        select  @motherGroup += ',' + cast(c.MotherID as varchar)
        from    Child c
        where   c.FatherID = @fatherId
        group by c.MotherID 
        order by c.MotherID

        -- Update the father record.
        -- Any empty strings are handled automatically, skip the leading ','.
        update  Father
        set     MotherGroup = substring(@motherGroup, 2, 2147483647),
                MotherHash = HASHBYTES('SHA2_256', @motherGroup)
        where   FatherID = @fatherId
    end
end
go
Run Code Online (Sandbox Code Playgroud)

更新现有行
由于您已经拥有数百万行,因此我们必须聚合这些现有行的母组。
如果您没有磁盘空间来记录整个表的更新,也许您应该将数据库从 AG 中取出并切换到简单恢复模型来完成此任务?
在这种情况下,您还应该使用 WHERE 子句修改更新,以仅更新表的部分内容,并对每个部分运行更新,直到更新整个表。
示例: update Child set FatherID = FatherID where FatherID 介于 1 和 1000000 之间
注意:此更新语句可能会阻止其他用户访问 Child 表。

-- Aggregate the mother groups for the existing rows.
-- This could takes minutes to complete, depending on the number of rows.
-- NOTE: This update statement could block access to the Child table for other users.
update Child set FatherID = FatherID
Run Code Online (Sandbox Code Playgroud)

就是这样!
现在,您应该能够快速获取现有行上的母组,并且在子表中将来发生更改后也可以快速获取母组。

-- Voila - now you can get the unique mother groups any time at a fast speed.
select count(distinct MotherHash) from Father
Run Code Online (Sandbox Code Playgroud)