DBMS级别的管道和过滤器:拆分MERGE输出流

时间:2015-12-30 16:22:47

标签: sql sql-server tsql azure-sql-database sql-merge

方案

我们有一个非常标准的数据导入过程,我们在其中加载一个 staging表,然后将MERGE表放入target表。

新要求(绿色)涉及捕获导入数据的子集 进入一个单独的queue表,进行完全不相关的处理。

Scenario schema

“挑战”

(1)子集由选择的记录组成:那些记录 仅新插入target表。

(2)子集是一些插入列的投影,但也是 至少一个仅出现在源中的列(staging 表)。

(3)MERGE语句已使用OUTPUT..INTO子句 严格记录$action所采用的MERGE,以便我们可以 PIVOT结果和COUNT插入次数,更新次数和 删除用于统计目的。我们真的不喜欢缓冲 像这样的整个数据集的操作,并希望聚合 动态的总和。不用说,我们不想添加更多数据 这个OUTPUT表。

(4)我们不想做MERGE的匹配工作 无论出于何种原因,甚至部分地进行第二次。该 target表真的很大,我们无法索引所有内容,而且 操作通常非常昂贵(分钟,而不是秒)。

(5)我们不考虑将MERGE的任何输出往返 客户端只是为了让客户端可以将其路由到queue 立即发回。数据必须留在服务器上。

(6)我们希望避免在临时存储中缓冲整个数据集 在stagingqueue之间。

最好的方法是什么?

故障

(a)仅将插入的记录排入队列的要求阻止了我们 直接在queue子句中定位OUTPUT..INTOMERGE,因为它不允许任何WHERE子句。我们可以使用一些 CASE欺骗标记后续删除的不需要的记录 来自queue没有处理,但这看起来很疯狂。

(b)因为用于queue的某些列没有出现在。{ target表,我们不能简单地在目标上添加插入触发器 用于加载queue的表。 “数据流拆分”必须尽快发生。

(c)由于我们已在OUTPUT..INTO使用了MERGE条款,我们 无法添加第二个OUTPUT子句MERGE嵌套到一个 INSERT..SELECT加载队列。这是一种耻辱,因为它 对于有效的东西,感觉就像一个完全随意的限制 否则很好; SELECT仅过滤带有的记录 $action我们想要(INSERT)和INSERTqueue放在一个queue中 声明。因此,DBMS理论上可以避免缓冲整体 数据集并简单地将其流式传输到VIEW。 (注意:我们没有追求 它可能实际上没有以这种方式优化计划。)

情况

我们觉得我们的选择已经用尽了,但我们决定转向这个hivemind 为了确定。我们所能想到的只有:

(S1)创建也包含可空的target表的queue 仅适用于SELECT的数据列,并且具有 NULL语句将其定义为INSTEAD OF。然后,设置target 填充queue表和MERGE的触发器 适当。最后,连接MERGE..OUTPUT以定位视图。这个 工作,但我们不是建筑的粉丝 - 它绝对 看起来很棘手。

(S2)放弃,使用临时表缓冲整个数据集 另一个MERGE。在queue之后,立即复制数据 (再次!)从临时表到re.findall

6 个答案:

答案 0 :(得分:17)

我的理解是,主要障碍是SQL Server中OUTPUT子句的限制。它允许一个OUTPUT INTO table和/或一个OUTPUT将结果集返回给调用者。

您希望以两种不同的方式保存MERGE语句的结果:

  • MERGE影响的所有行,用于收集统计信息
  • 仅插入queue
  • 的行

简单变体

我会使用你的S2解决方案。至少从一开始。它易于理解和维护,并且应该非常高效,因为资源最密集的操作(MERGETarget本身只会执行一次)。下面有第二个变体,比较它们在实际数据上的表现会很有趣。

所以:

  • 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)

重要的部分是UNIQUEIGNORE_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表的数据进行子集化。

<强>理由

由于需要访问stagingtarget中的列以构建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