我们有一个非常标准的数据导入过程,我们在其中加载一个
staging
表,然后将MERGE
表放入target
表。
新要求(绿色)涉及捕获导入数据的子集
进入一个单独的queue
表,进行完全不相关的处理。
(1)子集由选择的记录组成:那些记录
仅新插入target
表。
(2)子集是一些插入列的投影,但也是
至少一个仅出现在源中的列(staging
表)。
(3)MERGE
语句已使用OUTPUT..INTO
子句
严格记录$action
所采用的MERGE
,以便我们可以
PIVOT
结果和COUNT
插入次数,更新次数和
删除用于统计目的。我们真的不喜欢缓冲
像这样的整个数据集的操作,并希望聚合
动态的总和。不用说,我们不想添加更多数据
这个OUTPUT
表。
(4)我们不想做MERGE
的匹配工作
无论出于何种原因,甚至部分地进行第二次。该
target
表真的很大,我们无法索引所有内容,而且
操作通常非常昂贵(分钟,而不是秒)。
(5)我们不考虑将MERGE
的任何输出往返
客户端只是为了让客户端可以将其路由到queue
立即发回。数据必须留在服务器上。
(6)我们希望避免在临时存储中缓冲整个数据集
在staging
和queue
之间。
最好的方法是什么?
(a)仅将插入的记录排入队列的要求阻止了我们
直接在queue
子句中定位OUTPUT..INTO
表
MERGE
,因为它不允许任何WHERE
子句。我们可以使用一些
CASE
欺骗标记后续删除的不需要的记录
来自queue
没有处理,但这看起来很疯狂。
(b)因为用于queue
的某些列没有出现在。{
target
表,我们不能简单地在目标上添加插入触发器
用于加载queue
的表。 “数据流拆分”必须尽快发生。
(c)由于我们已在OUTPUT..INTO
使用了MERGE
条款,我们
无法添加第二个OUTPUT
子句和将MERGE
嵌套到一个
INSERT..SELECT
加载队列。这是一种耻辱,因为它
对于有效的东西,感觉就像一个完全随意的限制
否则很好; SELECT
仅过滤带有的记录
$action
我们想要(INSERT
)和INSERT
将queue
放在一个queue
中
声明。因此,DBMS理论上可以避免缓冲整体
数据集并简单地将其流式传输到VIEW
。 (注意:我们没有追求
它可能实际上没有以这种方式优化计划。)
我们觉得我们的选择已经用尽了,但我们决定转向这个hivemind 为了确定。我们所能想到的只有:
(S1)创建也包含可空的target
表的queue
仅适用于SELECT
的数据列,并且具有
NULL
语句将其定义为INSTEAD OF
。然后,设置target
填充queue
表和MERGE
的触发器
适当。最后,连接MERGE..OUTPUT
以定位视图。这个
工作,但我们不是建筑的粉丝 - 它绝对
看起来很棘手。
(S2)放弃,使用临时表缓冲整个数据集
另一个MERGE
。在queue
之后,立即复制数据
(再次!)从临时表到re.findall
。
答案 0 :(得分:17)
我的理解是,主要障碍是SQL Server中OUTPUT
子句的限制。它允许一个OUTPUT INTO table
和/或一个OUTPUT
将结果集返回给调用者。
您希望以两种不同的方式保存MERGE
语句的结果:
MERGE
影响的所有行,用于收集统计信息queue
我会使用你的S2解决方案。至少从一开始。它易于理解和维护,并且应该非常高效,因为资源最密集的操作(MERGE
到Target
本身只会执行一次)。下面有第二个变体,比较它们在实际数据上的表现会很有趣。
所以:
OUTPUT INTO @TempTable
MERGE
INSERT
到@TempTable
的所有行Stats
或插入前汇总。如果您只需要汇总统计信息,那么汇总此批次的结果并将其合并到最终Stats
而不是复制所有行是有意义的。INSERT
只有Queue
来自@TempTable
的“已插入”行。我将从@ i-one中获取答案的样本数据。
<强>模式强>
-- I'll return to commented lines later
CREATE TABLE [dbo].[TestTarget](
-- [ID] [int] IDENTITY(1,1) NOT NULL,
[foo] [varchar](10) NULL,
[bar] [varchar](10) NULL
);
CREATE TABLE [dbo].[TestStaging](
[foo] [varchar](10) NULL,
[bar] [varchar](10) NULL,
[baz] [varchar](10) NULL
);
CREATE TABLE [dbo].[TestStats](
[MergeAction] [nvarchar](10) NOT NULL
);
CREATE TABLE [dbo].[TestQueue](
-- [TargetID] [int] NOT NULL,
[foo] [varchar](10) NULL,
[baz] [varchar](10) NULL
);
示例数据
TRUNCATE TABLE [dbo].[TestTarget];
TRUNCATE TABLE [dbo].[TestStaging];
TRUNCATE TABLE [dbo].[TestStats];
TRUNCATE TABLE [dbo].[TestQueue];
INSERT INTO [dbo].[TestStaging]
([foo]
,[bar]
,[baz])
VALUES
('A', 'AA', 'AAA'),
('B', 'BB', 'BBB'),
('C', 'CC', 'CCC');
INSERT INTO [dbo].[TestTarget]
([foo]
,[bar])
VALUES
('A', 'A_'),
('B', 'B?');
<强>合并强>
DECLARE @TempTable TABLE (
MergeAction nvarchar(10) NOT NULL,
foo varchar(10) NULL,
baz varchar(10) NULL);
MERGE INTO TestTarget AS Dst
USING TestStaging AS Src
ON Dst.foo = Src.foo
WHEN MATCHED THEN
UPDATE SET
Dst.bar = Src.bar
WHEN NOT MATCHED BY TARGET THEN
INSERT (foo, bar)
VALUES (Src.foo, Src.bar)
OUTPUT $action AS MergeAction, inserted.foo, Src.baz
INTO @TempTable(MergeAction, foo, baz)
;
INSERT INTO [dbo].[TestStats] (MergeAction)
SELECT T.MergeAction
FROM @TempTable AS T;
INSERT INTO [dbo].[TestQueue]
([foo]
,[baz])
SELECT
T.foo
,T.baz
FROM @TempTable AS T
WHERE T.MergeAction = 'INSERT'
;
SELECT * FROM [dbo].[TestTarget];
SELECT * FROM [dbo].[TestStats];
SELECT * FROM [dbo].[TestQueue];
<强>结果强>
TestTarget
+-----+-----+
| foo | bar |
+-----+-----+
| A | AA |
| B | BB |
| C | CC |
+-----+-----+
TestStats
+-------------+
| MergeAction |
+-------------+
| INSERT |
| UPDATE |
| UPDATE |
+-------------+
TestQueue
+-----+-----+
| foo | baz |
+-----+-----+
| C | CCC |
+-----+-----+
在SQL Server 2014 Express上测试。
OUTPUT
子句可以将其结果集发送到表和调用者。因此,OUTPUT INTO
可以直接进入Stats
,如果我们将MERGE
语句包装到存储过程中,那么我们可以将INSERT ... EXEC
用于Queue
。
如果您检查执行计划,您会看到INSERT ... EXEC
无论如何都会在幕后创建一个临时表(另请参阅The Hidden Costs of INSERT EXEC
Adam Machanic),所以我希望当您明确创建临时表时,整体性能与第一个变体类似。
要解决的另一个问题是:Queue
表应该只有“插入”行,而不是所有受影响的行。为此,您可以使用Queue
表上的触发器来丢弃“已插入”以外的行。另一种可能性是使用IGNORE_DUP_KEY = ON
定义唯一索引,并以“非插入”行违反唯一索引并且不会插入表中的方式准备数据。
因此,我会在ID IDENTITY
表格中添加Target
列,并在TargetID
表格中添加Queue
列。 (在上面的脚本中取消注释它们)。
另外,我将为Queue
表添加一个索引:
CREATE UNIQUE NONCLUSTERED INDEX [IX_TargetID] ON [dbo].[TestQueue]
(
[TargetID] ASC
) WITH (
PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF,
SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = ON,
DROP_EXISTING = OFF,
ONLINE = OFF,
ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON)
重要的部分是UNIQUE
和IGNORE_DUP_KEY = ON
。
以下是MERGE
的存储过程:
CREATE PROCEDURE [dbo].[TestMerge]
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
MERGE INTO dbo.TestTarget AS Dst
USING dbo.TestStaging AS Src
ON Dst.foo = Src.foo
WHEN MATCHED THEN
UPDATE SET
Dst.bar = Src.bar
WHEN NOT MATCHED BY TARGET THEN
INSERT (foo, bar)
VALUES (Src.foo, Src.bar)
OUTPUT $action INTO dbo.TestStats(MergeAction)
OUTPUT CASE WHEN $action = 'INSERT' THEN inserted.ID ELSE 0 END AS TargetID,
inserted.foo,
Src.baz
;
END
<强>用法强>
TRUNCATE TABLE [dbo].[TestTarget];
TRUNCATE TABLE [dbo].[TestStaging];
TRUNCATE TABLE [dbo].[TestStats];
TRUNCATE TABLE [dbo].[TestQueue];
-- Make sure that `Queue` has one special row with TargetID=0 in advance.
INSERT INTO [dbo].[TestQueue]
([TargetID]
,[foo]
,[baz])
VALUES
(0
,NULL
,NULL);
INSERT INTO [dbo].[TestStaging]
([foo]
,[bar]
,[baz])
VALUES
('A', 'AA', 'AAA'),
('B', 'BB', 'BBB'),
('C', 'CC', 'CCC');
INSERT INTO [dbo].[TestTarget]
([foo]
,[bar])
VALUES
('A', 'A_'),
('B', 'B?');
INSERT INTO [dbo].[TestQueue]
EXEC [dbo].[TestMerge];
SELECT * FROM [dbo].[TestTarget];
SELECT * FROM [dbo].[TestStats];
SELECT * FROM [dbo].[TestQueue];
<强>结果强>
TestTarget
+----+-----+-----+
| ID | foo | bar |
+----+-----+-----+
| 1 | A | AA |
| 2 | B | BB |
| 3 | C | CC |
+----+-----+-----+
TestStats
+-------------+
| MergeAction |
+-------------+
| INSERT |
| UPDATE |
| UPDATE |
+-------------+
TestQueue
+----------+------+------+
| TargetID | foo | baz |
+----------+------+------+
| 0 | NULL | NULL |
| 3 | C | CCC |
+----------+------+------+
INSERT ... EXEC
期间会有额外的消息:
Duplicate key was ignored.
如果MERGE
更新了某些行。由于IGNORE_DUP_KEY = ON
,唯一索引会在INSERT
期间丢弃某些行,因此会发送此警告消息。
插入重复键值时会出现警告消息 成为一个独特的指数。只有违反唯一性约束的行 会失败。
答案 1 :(得分:7)
考虑采用以下两种方法来解决问题:
方法1 (合并数据并在触发器中收集统计信息):
示例数据设置(为简单起见省略了索引和约束):
create table staging (foo varchar(10), bar varchar(10), baz varchar(10));
create table target (foo varchar(10), bar varchar(10));
create table queue (foo varchar(10), baz varchar(10));
create table stats (batchID int, inserted bigint, updated bigint, deleted bigint);
insert into staging values
('A', 'AA', 'AAA')
,('B', 'BB', 'BBB')
,('C', 'CC', 'CCC')
;
insert into target values
('A', 'A_')
,('B', 'B?')
,('E', 'EE')
;
触发收集插入/更新/删除的统计信息:
create trigger target_onChange
on target
after delete, update, insert
as
begin
set nocount on;
if object_id('tempdb..#targetMergeBatch') is NULL
return;
declare @batchID int;
select @batchID = batchID from #targetMergeBatch;
merge into stats t
using (
select
batchID = @batchID,
cntIns = count_big(case when i.foo is not NULL and d.foo is NULL then 1 end),
cntUpd = count_big(case when i.foo is not NULL and d.foo is not NULL then 1 end),
cntDel = count_big(case when i.foo is NULL and d.foo is not NULL then 1 end)
from inserted i
full join deleted d on d.foo = i.foo
) s
on t.batchID = s.batchID
when matched then
update
set
t.inserted = t.inserted + s.cntIns,
t.updated = t.updated + s.cntUpd,
t.deleted = t.deleted + s.cntDel
when not matched then
insert (batchID, inserted, updated, deleted)
values (s.batchID, s.cntIns, s.cntUpd, cntDel);
end
合并声明:
declare @batchID int;
set @batchID = 1;-- or select @batchID = batchID from ...;
create table #targetMergeBatch (batchID int);
insert into #targetMergeBatch (batchID) values (@batchID);
insert into queue (foo, baz)
select foo, baz
from
(
merge into target t
using staging s
on t.foo = s.foo
when matched then
update
set t.bar = s.bar
when not matched then
insert (foo, bar)
values (s.foo, s.bar)
when not matched by source then
delete
output $action, inserted.foo, s.baz
) m(act, foo, baz)
where act = 'INSERT'
;
drop table #targetMergeBatch
检查结果:
select * from target;
select * from queue;
select * from stats;
目标:
foo bar
---------- ----------
A AA
B BB
C CC
队列:
foo baz
---------- ----------
C CCC
统计:
batchID inserted updated deleted
-------- ---------- --------- ---------
1 1 2 1
方法2 (使用更改跟踪功能收集统计信息):
示例数据设置与之前的情况相同(只需删除所有内容,包括触发器并从头开始重新创建表),除了在这种情况下我们需要在目标上使用PK来使示例工作:
create table target (foo varchar(10) primary key, bar varchar(10));
在数据库上启用更改跟踪:
alter database Test
set change_tracking = on
在目标表上启用更改跟踪:
alter table target
enable change_tracking
在此之后立即合并数据并获取统计信息,按更改上下文进行过滤以仅计算受合并影响的行:
begin transaction;
declare @batchID int, @chVersion bigint, @chContext varbinary(128);
set @batchID = 1;-- or select @batchID = batchID from ...;
SET @chVersion = change_tracking_current_version();
set @chContext = newid();
with change_tracking_context(@chContext)
insert into queue (foo, baz)
select foo, baz
from
(
merge into target t
using staging s
on t.foo = s.foo
when matched then
update
set t.bar = s.bar
when not matched then
insert (foo, bar)
values (s.foo, s.bar)
when not matched by source then
delete
output $action, inserted.foo, s.baz
) m(act, foo, baz)
where act = 'INSERT'
;
with ch(foo, op) as (
select foo, sys_change_operation
from changetable(changes target, @chVersion) ct
where sys_change_context = @chContext
)
insert into stats (batchID, inserted, updated, deleted)
select @batchID, [I], [U], [D]
from ch
pivot(count_big(foo) for op in ([I], [U], [D])) pvt
;
commit transaction;
检查结果:
select * from target;
select * from queue;
select * from stats;
它们与之前的样本相同。
目标:
foo bar
---------- ----------
A AA
B BB
C CC
队列:
foo baz
---------- ----------
C CCC
统计:
batchID inserted updated deleted
-------- ---------- --------- ---------
1 1 2 1
答案 2 :(得分:5)
我建议使用以下三行独立的AFTER INSERT / DELETE / UPDATE
触发器来提取统计数据。
create trigger dbo.insert_trigger_target
on [dbo].[target]
after insert
as
insert into dbo.[stats] ([action],[count])
select 'insert', count(1)
from inserted;
go
create trigger dbo.update_trigger_target
on [dbo].[target]
after update
as
insert into dbo.[stats] ([action],[count])
select 'update', count(1) from inserted -- or deleted == after / before image, count will be the same
go
create trigger dbo.delete_trigger_target
on [dbo].[target]
after delete
as
insert into dbo.[stats] ([action],[count])
select 'delete', count(1) from deleted
go
如果您需要更多上下文,请在CONTEXT_INFO
中添加内容并从触发器中删除。
现在,我要声明AFTER触发器不是 昂贵的,但你需要测试它才能确定。
处理完毕后,您可以OUTPUT
使用OUTPUT INTO
条款(不 MERGE
),然后使用 嵌套在一个select中,以便为要进入queue
表的数据进行子集化。
<强>理由强>
由于需要访问staging
和target
中的列以构建queue
的数据,因此 HAS 将使用<{1}}中的OUTPUT
选项,因为其他任何内容都无法访问“双方”。
然后,如果我们劫持了MERGE
的{{1}}条款,我们如何重新使用该功能?考虑到您所描述的统计数据的要求,我认为OUTPUT
触发器将起作用。实际上,如果需要,根据可用的图像,统计数据可能非常复杂。我断言queue
触发器“并不那么昂贵”,因为之前和之后的数据必须始终可用,以便事务可以同时提交 OR ROLLED BACK - 是的,需要对数据进行扫描(即使是为了获得计数),但这似乎并没有太大的代价。
在我自己的分析中,扫描为执行计划的基本成本增加了约5%
听起来像一个解决方案?
答案 3 :(得分:3)
你有没有考虑过放弃合并,只是做一个不存在的插入和更新?然后,您可以使用插入中的output子句填充队列表。
答案 4 :(得分:3)
通过顺序表导入可能更有效,而不是依次设置导向处理。我会考虑使用光标扫描将MERGE
重写为存储过程。然后,对于每条记录,您可以拥有任意数量的输出以及任何无枢轴的计数,总成本为staging
表扫描。
存储过程还可能提供将处理拆分为较小事务的机会,而较大数据集上的触发器可能会导致事务日志溢出。
答案 5 :(得分:2)
除非我遗漏了某些内容,否则简单的插入命令应该满足您的所有要求。
insert into queue
(foo, baz)
select staging.foo, staging.baz
from staging join target on staging.foo = target.boo
where whatever
在合并到目标之后会发生这种情况。
仅限新记录,请在合并前执行此操作
insert into queue
(foo, baz)
select staging.foo, staging.baz
from staging left join target on staging.foo = target.boo
where target.foo = null