如何使用MERGE然后将一个源记录拆分为更多目标记录?

时间:2016-09-23 08:35:57

标签: sql sql-server

我正在编写一个数据泵,它可以从source_table获取包含codesome_dataoz1,{{1}列的信息},oz2oz3,我会将其存储在oz4相同结构。与此同时,我想更新另一个具有不同结构的表格(比如target_table) - 记录oz_tablecode - 这是四个记录而不是一个记录,其中包含四个值(最大值因为oz的空值和NULL值不存储)。

我对现有的ozX使用MERGE命令(Microsoft T-SQL)(一条记录中有4盎司 - 旧方法)。使用target_table机制将INSERTed / UPDATEd记录收集到表变量OUTPUT中。 (如果源记录消失,则故意永远不会删除目标记录;因此,没有DELETE操作。)

到目前为止,我有一个这样的代码:

@info_table

到目前为止一切正常。现在我想处理CREATE PROCEDURE dbo.data_pump AS BEGIN SET NOCOUNT ON DECLARE @result int = -555 -- init (number of affected records) DECLARE @info_table TABLE ( action nvarchar(10), code int, oz1 nvarchar(40), oz2 nvarchar(40), oz3 nvarchar(40), oz4 nvarchar(40) ) BEGIN TRANSACTION tran_data_pump BEGIN TRY MERGE target_table AS target USING (SELECT code, some_data, oz1, oz2, oz3, oz4 FROM source_table) AS source ON target.code = source.code WHEN MATCHED AND (COALESCE(target.some_data, '') != COALESCE(source.some_data, '') OR COALESCE(target.oz1, '') != COALESCE(source.oz1, '') OR COALESCE(target.oz2, '') != COALESCE(source.oz2, '') OR COALESCE(target.oz3, '') != COALESCE(source.oz3, '') OR COALESCE(target.oz4, '') != COALESCE(source.oz4, '') ) THEN UPDATE SET target.some_data = source.some_data, target.oz1 = source.oz1, target.oz2 = source.oz2, target.oz3 = source.oz3, target.oz4 = source.oz4 WHEN NOT MATCHED THEN INSERT (code, some_data, oz1, oz2, oz3, oz4) VALUES (source.code, source.some_data, source.oz1, source.oz2, source.oz3, source.oz4) OUTPUT $action AS action, -- INSERT or UPDATE inserted.code AS code, inserted.oz1 AS oz1, inserted.oz2 AS oz2, inserted.oz3 AS oz3, inserted.oz4 AS oz4 INTO @info_table; SET @result = @@ROWCOUNT COMMIT TRANSACTION tran_data_pump END TRY BEGIN CATCH ROLLBACK TRANSACTION tran_data_pump SET @result = -1 -- transaction-failed indication END CATCH RETURN @result -- OK, number of the transfered records END 以插入/更新@info_table。对于操作oz_table,应首先删除包含UPDATE的记录,并插入新记录。订单并不重要,插入的记录的新数量可能不同。 oz中的code或空字符串不应产生任何记录。对于NULL操作,仅插入新记录的情况更简单。

更新:稍微修改了一下这个问题,以澄清问题的核心。数据表可以这样定义:

INSERT

查看完整的测试脚本(创建数据库,表格,在http://pastebin.com/wBz3Tzwn调用CREATE TABLE dbo.source_table ( ID int IDENTITY PRIMARY KEY NOT NULL, code int, some_data nvarchar(50), oz1 nvarchar(40), oz2 nvarchar(40), oz3 nvarchar(40), oz4 nvarchar(40) ) CREATE TABLE dbo.target_table ( ID int IDENTITY PRIMARY KEY NOT NULL, code int, some_data nvarchar(50), oz2 nvarchar(40), oz3 nvarchar(40), oz1 nvarchar(40), oz4 nvarchar(40) ) CREATE TABLE dbo.oz_table ( ID int IDENTITY PRIMARY KEY NOT NULL, code int, oz nvarchar(40) NOT NULL )

怎么做得很好?我需要有效的解决方案,因为日期量可能很大,操作应该尽可能快。

4 个答案:

答案 0 :(得分:4)

如果我正确理解了你的问题陈述,那么下面的方法可能是一种解决方法 -

    -- declare the temp tables
    DECLARE @info_table TABLE (
        action nvarchar(10),
        ID int,
        oz1 nvarchar(40),
        oz2 nvarchar(40),
        oz3 nvarchar(40),
        oz4 nvarchar(40)
    )
    --create intermediate table to store the results
    CREATE TABLE #temp_alternative_table (ID int,oz nvarchar(40))
    -- insert some dummy values
    INSERT INTO @info_table (action,ID,oz1,oz2,oz3,oz4)
    VALUES 
        ('INSERT',1, '85', '94', '78', '90'),
        ('UPDATE',2, '75', '88', '91', '78')
    --SELECT * FROM @info_table
    -- doing unpivot and transforming one row many columns to many rows one column and inserting into intermediate temp table
    INSERT INTO #temp_alternative_table
    SELECT *
    FROM (
        SELECT 
               Action   
            ,  ID
            , [Oz]
        FROM @info_table
        UNPIVOT 
        (
            [Oz] FOR tt IN (oz1, oz2, oz3, oz4)
        ) unpvt
    ) t
    -- delete from main table all the records for which the action is UPDATE (stored in intermediate temp table for the same ID as of main table)
    DELETE at
    FROM alternative_table at
    INNER JOIN #temp_alternative_table tat
      ON at.ID = tat.ID
    WHERE tat.action = 'UPDATE'
    -- now insert all the records in main table
    INSERT INTO alternative_table (ID,Oz)
    SELECT ID,Oz
    FROM #temp_alternative_table

如果您正在寻找,请告诉我。希望这会有所帮助。

答案 1 :(得分:2)

既然你在谈论效率,首先应该有适当的索引。

source_tabletarget_table应在code上拥有唯一索引。 它应该是唯一的,否则主MERGE在尝试多次更新同一行时会失败。

oz_table应在code上具有非唯一索引。

@info_table应将code作为主键。 对于来自一个code的相同MERGE,不可能有两个不同的操作,因此code应该是唯一的:

DECLARE @info_table TABLE 
(
    action nvarchar(10),
    code int PRIMARY KEY,
    oz1 nvarchar(40),
    oz2 nvarchar(40),
    oz3 nvarchar(40),
    oz4 nvarchar(40)
);

不需要额外的临时表。我们已经有一个 - @info_table

MERGE之后,当填充@info_table时,我们需要执行两个步骤:1)从oz_table删除一些行,2)向oz_table添加一些行。

首先从oz_table中删除MERGE更新的那些行。 事实上,MERGE插入的那些行在oz_table中不会以任何方式存在,因此我们可以使用简单的DELETE语句。 无需按UPDATE操作进行明确过滤。这样的过滤器不会删除任何行。

如果在此数据泵过程之外有可能更改oz_table,则需要额外的过滤器。

DELETE FROM dbo.oz_table
WHERE dbo.oz_table.code IN
    (
        SELECT code FROM @info_table
    )
;

两个表上code的索引都有助于加入它们。

然后,只需插入更新和插入的行。

INSERT INTO dbo.oz_table(code, oz)
SELECT
    T.code
    ,CA.oz
FROM
    @info_table AS T
    CROSS APPLY
    (
        VALUES
            (T.oz1),
            (T.oz2),
            (T.oz3),
            (T.oz4)
    ) AS CA (oz)
WHERE
    CA.oz IS NOT NULL
    AND CA.oz <> ''
    -- The NULL or empty strings should not produce any record
;   

我更喜欢使用CROSS APPLY and VALUES instead of UNPIVOT。我们想删除NULL和空值,所以我认为最好明确地编写过滤器,这样每个阅读代码的人都会看到它。使用UNPIVOT,您需要知道它会隐式删除NULL。您仍然需要添加过滤器以删除空值。在这种情况下,性能可能大致相同,但您最好检查实际数据。

如果有可能同时调用此数据泵过程多次,则必须采取额外步骤以防止可能的并发问题(不正确的结果或死锁)。我更喜欢使用sp_getapplock来保证只有一个存储过程实例可以随时运行。

答案 2 :(得分:1)

稍微不同的方法是在target_table上定义更新/插入触发器。使用此方法,您的数据泵只需要考虑您的初始目标表。触发器将转换并将合并的数据插入alternative_table表。

如果您愿意承担将原始oz列名称存储为备用表中的搜索键所需的额外数据存储成本,则可以使用另一个合并语句,该语句将提高触发器的整体性能,如下所示: / p>

-- Create example table
CREATE TABLE [dbo].[alternative_table](
    [ID] [int] NOT NULL,
    [ColumnKey] [nvarchar](5) NOT NULL,
    [oz] [nvarchar](100) NULL,
 CONSTRAINT [PK_alternative_table] PRIMARY KEY CLUSTERED 
(
    [ID] ASC,
    [ColumnKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

-- Create trigger responsible for populating Alternative table.
CREATE TRIGGER dbo.MergeWithAlternative
   ON  dbo.target_table
   AFTER INSERT, UPDATE
AS 
BEGIN

    SET NOCOUNT ON;


    MERGE [alternative_table] AS [target]
    USING   (
                SELECT 
                      [ID]
                    , [ColumnKey]
                    , [Oz]
                FROM inserted
                UNPIVOT 
                (
                    [Oz] FOR [ColumnKey] IN (oz1, oz2, oz3, oz4)
                ) unpvt
            ) AS [source]
    ON [target].ID = [source].ID AND [target].[ColumnKey] = [source].[ColumnKey]
    WHEN MATCHED THEN
        UPDATE
        SET [target].oz = [source].[Oz]
    WHEN NOT MATCHED THEN
        INSERT 
        (
            ID
            ,[ColumnKey] 
            ,[oz]
        )
        VALUES 
        (
            source.ID
            ,source.[ColumnKey]
            ,source.[Oz]
        );

END

如果您不愿意将列名存储为键查找,那么简单的删除/插入操作也将起作用:

-- Create example table
CREATE TABLE [dbo].[alternative_table](
    [ID] [int] NOT NULL,
    [oz] [nvarchar](100) NULL
) ON [PRIMARY]

GO

-- Create trigger responsible for populating Alternative table.
CREATE TRIGGER dbo.MergeWithAlternative
   ON  dbo.target_table
   AFTER INSERT, UPDATE
AS 
BEGIN

    SET NOCOUNT ON;


    DELETE [dbo].[alternative_table]
    WHERE   [ID] IN (SELECT ID FROM deleted)

    INSERT INTO [dbo].[alternative_table]
    (
        [ID]
        ,[oz]
    )
    SELECT  [ID]
            ,[Oz]
    FROM inserted
    UNPIVOT 
    (
        [Oz] FOR [ColumnKey] IN (oz1, oz2, oz3, oz4)
    ) unpvt

END

使用此方法不再需要表变量和填充它的输出子句。

答案 3 :(得分:0)

由于我们需要通过替换旧记录并添加新记录来构建oz_table,我更喜欢先截断它,然后通过INSERTING所有记录从头开始重建它。 我将使用两个cte,第一个用于读取@info_table,而第二个用于构建UNION所有四个盎司列的所有行。然后只需将UNION插入oz_Table。

Truncate table dbo.oz_table

with cte as(
    Select Code, oz1, oz2, oz3, oz4 from @info_table
), cte2 as(
            Select Code, oz1 as oz From cte
  UNION ALL Select Code, oz2 as oz From cte
  UNION ALL Select Code, oz3 as oz From cte
  UNION ALL Select Code, oz4 as oz From cte
)
Insert into dbo.oz_table(Code, oz)
select Code, oz from cte2
Where oz is not null and oz<>''