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(芭芭拉) | 德里克 |
每个父亲的母亲的不同组合是:
总的来说,母亲有两种不同的组合:
我想编写的查询将返回母亲的这些不同组合的计数,无论它们与哪个父亲相关联:
| 独特的母亲团体 |
|---|
| 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):
-- 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 长度取决于您的实际数据。
为什么要建立索引?如果要列出聚合的母组,读取索引可能比读取整个表更快。
正如所说;这取决于您(和您的数据)。如果我们不需要查看聚合的母组,那么我们根本不需要此列。
对于此演示/解决方案,我们将添加用于调试目的的列,而不使用任何索引。
-- 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)