批处理组
Batches over groups
我需要以不少于 N 行的批次处理 table 中的行。每个批次需要包含一整组行(组只是另一列),即当我 select 来自 table 的前 N 行进行处理时,我需要扩展 N 以覆盖最后一组批次而不是在批次之间拆分最后一组。
示例数据:
CREATE TABLE test01 (id INT PRIMARY KEY CLUSTERED IDENTITY(1, 1) NOT NULL
, person_name NVARCHAR(100)
, person_surname NVARCHAR(100)
, person_group_code CHAR(2) NOT NULL);
INSERT INTO
dbo.test01 (person_name
, person_surname
, person_group_code)
VALUES
('n1', 's1', 'g1')
, ('n2', 's2', 'g1')
, ('n3', 's3', 'g1')
, ('n4', 's4', 'g1')
, ('n5', 's5', 'g2')
, ('n6', 's6', 'g2')
, ('n7', 's7', 'g2')
, ('n8', 's8', 'g2')
, ('n9', 's9', 'g2')
, ('n10', 's10', 'g2')
, ('n11', 's11', 'g3')
, ('n12', 's12', 'g3')
, ('n13', 's13', 'g3')
, ('n14', 's14', 'g3');
我目前的尝试:
DECLARE @batch_start INT = 1
, @batch_size INT = 5;
DECLARE @max_id INT = (SELECT MAX(id) FROM dbo.test01);
WHILE @batch_start <= @max_id
BEGIN
SELECT *
FROM dbo.test01
WHERE id BETWEEN @batch_start AND @batch_start + @batch_size - 1;
SELECT @batch_start += @batch_size;
END;
DROP TABLE dbo.test01;
在上面的示例中,我将 14 行分成 3 批:第 1 批中有 5 行,第 2 批中有另外 5 行,最后一批中有 4 行。
第一批(id 从 1 到 5)仅涵盖 'g2' 组的一小部分,因此我需要扩展这批以涵盖第 1-10 行(我需要在单个中处理整个 g2批次)。
(顺便说一句,我不介意批量扩大 - 我需要确保每批至少涵盖一个完整的组)。
结果将是批次 #1 将涵盖组 g1 和 g2(10 行),然后批次 #2 将涵盖组 g3(4 行)并且根本没有批次 #3。
现在,table 有数十亿行,每个批处理大小约为 50K-100K,因此我需要一个性能良好的解决方案。
关于如何以最小的性能影响解决这个问题的任何提示?
我注意到的第一件事是您当前的代码假定标识列中没有间隙 - 但这是一个错误。标识列可能(并且经常)在数字中有间隙 - 因此您要做的第一件事是使用 row_number() over(order by id)
为所有记录获取连续的 运行 数字。
我添加的第二个东西是一个列,它为每个组提供一个数字 ID,按照与标识列相同的顺序排序 - 使用众所周知的技术来解决 gaps and islands 问题。
我使用了一个 table 变量来为源 table 上的每个 ID 存储此数据以用于此演示,但您可能想使用临时 table 并在相关列上添加索引以提高性能。
我还将您的 @batch_size
变量重命名为 @batch_min_size
并添加了一些其他变量。
所以这是我使用的 table 变量:
DECLARE @Helper As Table (Id int, Rn int, GroupId int)
INSERT INTO @Helper (Id, Rn, GroupId)
SELECT Id,
ROW_NUMBER() OVER(ORDER BY ID) As Rn,
ROW_NUMBER() OVER(ORDER BY ID) -
ROW_NUMBER() OVER(PARTITION BY person_group_code ORDER BY ID) As GroupId
FROM dbo.test01
这是这篇table的内容:
Id Rn GroupId
1 1 0
2 2 0
3 3 0
4 4 0
5 5 4
6 6 4
7 7 4
8 8 4
9 9 4
10 10 4
11 11 10
12 12 10
13 13 10
14 14 10
我使用了一个 while 循环来完成批处理。
在循环中,我使用 table 来计算每个批次的第一个和最后一个 id,以及该批次的最后一个行号。
然后我所要做的就是在原始 table:
的where子句中使用第一个和最后一个id
DECLARE @batch_min_size int = 10
, @batch_end int = 0
, @batch_start int
, @first_id_of_batch int
, @last_id_of_batch int
, @total_row_count int;
SELECT @total_row_count = COUNT(*) FROM @test01
WHILE @batch_end < @total_row_count
BEGIN
SELECT @batch_start = @batch_end + 1;
SELECT @batch_end = MAX(Rn)
, @first_id_of_batch = MIN(Id)
, @last_id_of_batch = MAX(Id)
FROM @Helper
WHERE Rn >= @batch_start
AND GroupId <=
(
SELECT MAX(GroupId)
FROM @Helper
WHERE Rn <= @batch_start + @batch_min_size - 1
)
SELECT id, person_name, person_surname, person_group_code
FROM dbo.test01
WHERE Id >= @first_id_of_batch
AND Id <= @last_id_of_batch
END
看看下面是否有帮助:
CREATE TABLE #Temp(g_record_count int, groupname varchar(50) )
insert into #Temp(g_record_count,groupname) SELECT MAX(id),person_group_code FROM dbo.test01 group by person_group_code
在这个临时循环之后 table :
DECLARE @rec_per_batch INT = 1
WHILE @batch_start <= @max_id
BEGIN
select min(g_record_count) into @rec_per_batch from #temp where g_record_count>=@batch_size * @batch_start;
SELECT *
FROM dbo.test01
WHERE id BETWEEN @batch_start AND @rec_per_batch;
SELECT @batch_start += @batch_size;
END;
我需要以不少于 N 行的批次处理 table 中的行。每个批次需要包含一整组行(组只是另一列),即当我 select 来自 table 的前 N 行进行处理时,我需要扩展 N 以覆盖最后一组批次而不是在批次之间拆分最后一组。
示例数据:
CREATE TABLE test01 (id INT PRIMARY KEY CLUSTERED IDENTITY(1, 1) NOT NULL
, person_name NVARCHAR(100)
, person_surname NVARCHAR(100)
, person_group_code CHAR(2) NOT NULL);
INSERT INTO
dbo.test01 (person_name
, person_surname
, person_group_code)
VALUES
('n1', 's1', 'g1')
, ('n2', 's2', 'g1')
, ('n3', 's3', 'g1')
, ('n4', 's4', 'g1')
, ('n5', 's5', 'g2')
, ('n6', 's6', 'g2')
, ('n7', 's7', 'g2')
, ('n8', 's8', 'g2')
, ('n9', 's9', 'g2')
, ('n10', 's10', 'g2')
, ('n11', 's11', 'g3')
, ('n12', 's12', 'g3')
, ('n13', 's13', 'g3')
, ('n14', 's14', 'g3');
我目前的尝试:
DECLARE @batch_start INT = 1
, @batch_size INT = 5;
DECLARE @max_id INT = (SELECT MAX(id) FROM dbo.test01);
WHILE @batch_start <= @max_id
BEGIN
SELECT *
FROM dbo.test01
WHERE id BETWEEN @batch_start AND @batch_start + @batch_size - 1;
SELECT @batch_start += @batch_size;
END;
DROP TABLE dbo.test01;
在上面的示例中,我将 14 行分成 3 批:第 1 批中有 5 行,第 2 批中有另外 5 行,最后一批中有 4 行。
第一批(id 从 1 到 5)仅涵盖 'g2' 组的一小部分,因此我需要扩展这批以涵盖第 1-10 行(我需要在单个中处理整个 g2批次)。
(顺便说一句,我不介意批量扩大 - 我需要确保每批至少涵盖一个完整的组)。
结果将是批次 #1 将涵盖组 g1 和 g2(10 行),然后批次 #2 将涵盖组 g3(4 行)并且根本没有批次 #3。
现在,table 有数十亿行,每个批处理大小约为 50K-100K,因此我需要一个性能良好的解决方案。
关于如何以最小的性能影响解决这个问题的任何提示?
我注意到的第一件事是您当前的代码假定标识列中没有间隙 - 但这是一个错误。标识列可能(并且经常)在数字中有间隙 - 因此您要做的第一件事是使用 row_number() over(order by id)
为所有记录获取连续的 运行 数字。
我添加的第二个东西是一个列,它为每个组提供一个数字 ID,按照与标识列相同的顺序排序 - 使用众所周知的技术来解决 gaps and islands 问题。
我使用了一个 table 变量来为源 table 上的每个 ID 存储此数据以用于此演示,但您可能想使用临时 table 并在相关列上添加索引以提高性能。
我还将您的 @batch_size
变量重命名为 @batch_min_size
并添加了一些其他变量。
所以这是我使用的 table 变量:
DECLARE @Helper As Table (Id int, Rn int, GroupId int)
INSERT INTO @Helper (Id, Rn, GroupId)
SELECT Id,
ROW_NUMBER() OVER(ORDER BY ID) As Rn,
ROW_NUMBER() OVER(ORDER BY ID) -
ROW_NUMBER() OVER(PARTITION BY person_group_code ORDER BY ID) As GroupId
FROM dbo.test01
这是这篇table的内容:
Id Rn GroupId
1 1 0
2 2 0
3 3 0
4 4 0
5 5 4
6 6 4
7 7 4
8 8 4
9 9 4
10 10 4
11 11 10
12 12 10
13 13 10
14 14 10
我使用了一个 while 循环来完成批处理。 在循环中,我使用 table 来计算每个批次的第一个和最后一个 id,以及该批次的最后一个行号。 然后我所要做的就是在原始 table:
的where子句中使用第一个和最后一个idDECLARE @batch_min_size int = 10
, @batch_end int = 0
, @batch_start int
, @first_id_of_batch int
, @last_id_of_batch int
, @total_row_count int;
SELECT @total_row_count = COUNT(*) FROM @test01
WHILE @batch_end < @total_row_count
BEGIN
SELECT @batch_start = @batch_end + 1;
SELECT @batch_end = MAX(Rn)
, @first_id_of_batch = MIN(Id)
, @last_id_of_batch = MAX(Id)
FROM @Helper
WHERE Rn >= @batch_start
AND GroupId <=
(
SELECT MAX(GroupId)
FROM @Helper
WHERE Rn <= @batch_start + @batch_min_size - 1
)
SELECT id, person_name, person_surname, person_group_code
FROM dbo.test01
WHERE Id >= @first_id_of_batch
AND Id <= @last_id_of_batch
END
看看下面是否有帮助:
CREATE TABLE #Temp(g_record_count int, groupname varchar(50) )
insert into #Temp(g_record_count,groupname) SELECT MAX(id),person_group_code FROM dbo.test01 group by person_group_code
在这个临时循环之后 table :
DECLARE @rec_per_batch INT = 1
WHILE @batch_start <= @max_id
BEGIN
select min(g_record_count) into @rec_per_batch from #temp where g_record_count>=@batch_size * @batch_start;
SELECT *
FROM dbo.test01
WHERE id BETWEEN @batch_start AND @rec_per_batch;
SELECT @batch_start += @batch_size;
END;