SQL 合并父子数据列表
SQL Merge a List of Parent Child data
我有一个数据列表,要作为 XML 传递给存储过程。数据是 Widget
的列表,小部件包含 WidgetItem
的列表(父子数据)。我想做一个 MERGE
基于 Widget
的一个子集基于 ParentID
。 ParentID
的一些数据已更新,一些已被删除(因此从 xml 中丢失)并且一些数据是新的。
更新的数据永远不需要更新子数据,因为Widget
记录只能调整,而不是其中的项目(子数据)。 Insert 总会有一个或多个子记录(WidgetItems)。
我似乎无法弄清楚如何在 MERGE
中执行此操作,因为与在数据层中处理合并相比,这似乎是最好的方法。
这是我目前所知道的...我在卡住的地方发表了评论:
CREATE PROCEDURE dbo.pWidgetsMerge
@Widgets XML
AS
/*
Assumed XML input @Widgets xml:
<Widgets>
<Widget>
<WidgetID>
<ParentID>
<StartDate>
<EndDate>
<Details>
<WidgetDetailItem>
<WidgetDetailItemID>
<WidgetID>
<SomeID>
<SomeData>
*/
MERGE
[dbo].[Widget] as w
USING
(
SELECT
'WidgetID' = P.value('WidgetID[1]', 'INT'),
'ParentID' = P.value('ParentID[1]', 'INT'),
'StartDate' = P.value('EffectiveStartDate[1]', 'DATETIME'),
'EndDate' = P.value('EffectiveEndDate[1]', 'DATETIME')
FROM
@Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P)
)
AS xmlIn
(
[WidgetID],
[StartDate],
[EndDate]
)
ON
w.[WidgetID] = xmlIn.[WidgetID]
WHEN
NOT MATCHED
THEN
INSERT
(
[ParentID],
[StartDate],
[EndDate]
)
VALUES
(
xmlIn.[ParentID],
xmlIn.[StartDate],
xmlIn.[EndDate]
)
/*STUCK HERE: After the insert, need to put in the child
records into a new table [WidgetItems]. Maybe it's another
operation outside of the merge?*/
WHEN
MATCHED AND (
(w.[StartDate] <> xmlIn.[StartDate]) OR
(w.[EndDate] <> xmlIn.[EndDate]))
THEN
UPDATE SET
w.[StartDate] = xmlIn.[StartDate],
w.[EndDate] = xmlIn.[EndDate]
WHEN
NOT MATCHED BY SOURCE AND w.[ParentID] = xmlIn.[ParentID]
THEN
UPDATE SET
w.[DeletedDate] = GETDATE()
此外,如果我处理这个错误,我们将不胜感激,或者我可能确实需要在数据层处理这个问题。
我已经将传入的 XML 反序列化为 table。这让我们有机会验证 XML 字符串中的数据。
项目 1:新的反序列化 table 将提供一种简单的方法来过滤哪些数据包含在 MERGE 中。
项目 2:将数据插入子项 table 必须单独调用。 MERGE 只能处理一个 table 上的 CRUD(创建更新删除)操作。
注意:MERGE 将使用 DESTINATION table 中的所有记录。因此,当您与 Source 不匹配时,这将对未包含在 table.
中的所有记录起作用
下面是更新后的代码,应该可以回答您的问题。我添加了评论来解释发生了什么。希望它有意义。
如您所述,ParentID 对于传入的所有 Widget 都是相同的,因此我将其视为参数而不是 XML
的元素
DECLARE @ParentID INT = 1
DECLARE @Widgets AS XML =
N'<Widgets>
<Widget>
<WidgetID />
<StartDate />
<EndDate />
<Details>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>4</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>323</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>1</SomeID>
<SomeData/>
</WidgetDetailItem>
</Details>
</Widget>
<Widget>
<WidgetID>10</WidgetID>
<StartDate>January 1, 2015</StartDate>
<EndDate />
<Details>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>4</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>99</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>6</SomeID>
<SomeData/>
</WidgetDetailItem>
</Details>
</Widget>
</Widgets>';
--Used to hold the pseudoID -> WidgetID relationship for inserting the details
DECLARE @WidgetIds AS TABLE ([Action] varchar(10), PseudoID INT, WidgetID INT);
;
--Use a CTE of the subset of data to be more performant. If we just went straight to the
--merge we'd be operating on the entire table and that can have some major performance hits
WITH T AS (
SELECT
w.*
FROM
[dbo].[Widget] as w
WHERE
w.[ParentID] = @ParentID
)
MERGE INTO T
USING (
SELECT
--Generate a pseudoid based on the order of the Widget elements so that we have some way of
--linking the detail records to the master
row_number() OVER(ORDER BY PROPERTYFEED.P) PseudoID,
'WidgetID' = P.value('WidgetID[1]', 'INT'),
'ParentID' = @ParentID,
'StartDate' = P.value('StartDate[1]', 'DATETIME'),
'EndDate' = P.value('EndDate[1]', 'DATETIME')
FROM
@Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P)
)
AS xmlIn
(
[PseudoID],
[WidgetID],
[ParentID],
[StartDate],
[EndDate]
)
ON
T.[WidgetID] = xmlIn.[WidgetID]
WHEN
NOT MATCHED
THEN
INSERT
(
[ParentID],
[StartDate],
[EndDate]
)
VALUES
(
xmlIn.[ParentID],
xmlIn.[StartDate],
xmlIn.[EndDate]
)
WHEN
MATCHED AND (
(T.[StartDate] <> xmlIn.[StartDate]) OR
(T.[EndDate] <> xmlIn.[EndDate]))
THEN
UPDATE SET
T.[StartDate] = xmlIn.[StartDate],
T.[EndDate] = xmlIn.[EndDate]
WHEN
NOT MATCHED BY SOURCE AND T.[DeletedDate] IS NULL
THEN
UPDATE SET
T.[DeletedDate] = GETDATE()
OUTPUT $action, xmlIn.PseudoID, INSERTED.WidgetID INTO @WidgetIds
;
--This is some magic to generate a temp table of numbers from 1 to COUNT(Widget)
--This is so we can reference the parent Widget row in the same order as the pseudoid generated above
--
;WITH Total(TotalWidgets) AS (SELECT COUNT(1) TotalWidgets FROM @Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P))
, Numbers(Num) as (
SELECT 1 AS Num
UNION ALL
SELECT Num+1
FROM Numbers
JOIN Total t ON 1 = 1
WHERE Num < t.TotalWidgets )
INSERT INTO WidgetDetailItem (WidgetID,SomeID,SomeData)
SELECT
w.WidgetID
,Details.SomeID
,Details.SomeData
FROM
(SELECT
P.value('WidgetDetailItemID[1]','int') WidgetDetailItemID
, P.value('SomeID[1]','int') SomeID
, P.value('SomeData[1]','varchar(5)') SomeData
, n.Num AS PsuedoID
FROM Numbers n
--This is what gives us our pseudo ID to link to the row_number() function from the first merge statement
CROSS APPLY @Widgets.nodes('/Widgets/Widget[sql:column("n.Num")]/Details/WidgetDetailItem') AS M(P)
) Details
JOIN @WidgetIds w on Details.PsuedoID = w.PseudoID
WHERE w.Action = 'INSERT' --We only want inserts by your spec
SELECT * FROM Widget;
SELECT * FROM WidgetDetailItem;
我有一个数据列表,要作为 XML 传递给存储过程。数据是 Widget
的列表,小部件包含 WidgetItem
的列表(父子数据)。我想做一个 MERGE
基于 Widget
的一个子集基于 ParentID
。 ParentID
的一些数据已更新,一些已被删除(因此从 xml 中丢失)并且一些数据是新的。
更新的数据永远不需要更新子数据,因为Widget
记录只能调整,而不是其中的项目(子数据)。 Insert 总会有一个或多个子记录(WidgetItems)。
我似乎无法弄清楚如何在 MERGE
中执行此操作,因为与在数据层中处理合并相比,这似乎是最好的方法。
这是我目前所知道的...我在卡住的地方发表了评论:
CREATE PROCEDURE dbo.pWidgetsMerge
@Widgets XML
AS
/*
Assumed XML input @Widgets xml:
<Widgets>
<Widget>
<WidgetID>
<ParentID>
<StartDate>
<EndDate>
<Details>
<WidgetDetailItem>
<WidgetDetailItemID>
<WidgetID>
<SomeID>
<SomeData>
*/
MERGE
[dbo].[Widget] as w
USING
(
SELECT
'WidgetID' = P.value('WidgetID[1]', 'INT'),
'ParentID' = P.value('ParentID[1]', 'INT'),
'StartDate' = P.value('EffectiveStartDate[1]', 'DATETIME'),
'EndDate' = P.value('EffectiveEndDate[1]', 'DATETIME')
FROM
@Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P)
)
AS xmlIn
(
[WidgetID],
[StartDate],
[EndDate]
)
ON
w.[WidgetID] = xmlIn.[WidgetID]
WHEN
NOT MATCHED
THEN
INSERT
(
[ParentID],
[StartDate],
[EndDate]
)
VALUES
(
xmlIn.[ParentID],
xmlIn.[StartDate],
xmlIn.[EndDate]
)
/*STUCK HERE: After the insert, need to put in the child
records into a new table [WidgetItems]. Maybe it's another
operation outside of the merge?*/
WHEN
MATCHED AND (
(w.[StartDate] <> xmlIn.[StartDate]) OR
(w.[EndDate] <> xmlIn.[EndDate]))
THEN
UPDATE SET
w.[StartDate] = xmlIn.[StartDate],
w.[EndDate] = xmlIn.[EndDate]
WHEN
NOT MATCHED BY SOURCE AND w.[ParentID] = xmlIn.[ParentID]
THEN
UPDATE SET
w.[DeletedDate] = GETDATE()
此外,如果我处理这个错误,我们将不胜感激,或者我可能确实需要在数据层处理这个问题。
我已经将传入的 XML 反序列化为 table。这让我们有机会验证 XML 字符串中的数据。
项目 1:新的反序列化 table 将提供一种简单的方法来过滤哪些数据包含在 MERGE 中。
项目 2:将数据插入子项 table 必须单独调用。 MERGE 只能处理一个 table 上的 CRUD(创建更新删除)操作。
注意:MERGE 将使用 DESTINATION table 中的所有记录。因此,当您与 Source 不匹配时,这将对未包含在 table.
中的所有记录起作用下面是更新后的代码,应该可以回答您的问题。我添加了评论来解释发生了什么。希望它有意义。
如您所述,ParentID 对于传入的所有 Widget 都是相同的,因此我将其视为参数而不是 XML
的元素DECLARE @ParentID INT = 1
DECLARE @Widgets AS XML =
N'<Widgets>
<Widget>
<WidgetID />
<StartDate />
<EndDate />
<Details>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>4</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>323</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>1</SomeID>
<SomeData/>
</WidgetDetailItem>
</Details>
</Widget>
<Widget>
<WidgetID>10</WidgetID>
<StartDate>January 1, 2015</StartDate>
<EndDate />
<Details>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>4</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>99</SomeID>
<SomeData/>
</WidgetDetailItem>
<WidgetDetailItem>
<WidgetDetailItemID></WidgetDetailItemID>
<WidgetID/>
<SomeID>6</SomeID>
<SomeData/>
</WidgetDetailItem>
</Details>
</Widget>
</Widgets>';
--Used to hold the pseudoID -> WidgetID relationship for inserting the details
DECLARE @WidgetIds AS TABLE ([Action] varchar(10), PseudoID INT, WidgetID INT);
;
--Use a CTE of the subset of data to be more performant. If we just went straight to the
--merge we'd be operating on the entire table and that can have some major performance hits
WITH T AS (
SELECT
w.*
FROM
[dbo].[Widget] as w
WHERE
w.[ParentID] = @ParentID
)
MERGE INTO T
USING (
SELECT
--Generate a pseudoid based on the order of the Widget elements so that we have some way of
--linking the detail records to the master
row_number() OVER(ORDER BY PROPERTYFEED.P) PseudoID,
'WidgetID' = P.value('WidgetID[1]', 'INT'),
'ParentID' = @ParentID,
'StartDate' = P.value('StartDate[1]', 'DATETIME'),
'EndDate' = P.value('EndDate[1]', 'DATETIME')
FROM
@Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P)
)
AS xmlIn
(
[PseudoID],
[WidgetID],
[ParentID],
[StartDate],
[EndDate]
)
ON
T.[WidgetID] = xmlIn.[WidgetID]
WHEN
NOT MATCHED
THEN
INSERT
(
[ParentID],
[StartDate],
[EndDate]
)
VALUES
(
xmlIn.[ParentID],
xmlIn.[StartDate],
xmlIn.[EndDate]
)
WHEN
MATCHED AND (
(T.[StartDate] <> xmlIn.[StartDate]) OR
(T.[EndDate] <> xmlIn.[EndDate]))
THEN
UPDATE SET
T.[StartDate] = xmlIn.[StartDate],
T.[EndDate] = xmlIn.[EndDate]
WHEN
NOT MATCHED BY SOURCE AND T.[DeletedDate] IS NULL
THEN
UPDATE SET
T.[DeletedDate] = GETDATE()
OUTPUT $action, xmlIn.PseudoID, INSERTED.WidgetID INTO @WidgetIds
;
--This is some magic to generate a temp table of numbers from 1 to COUNT(Widget)
--This is so we can reference the parent Widget row in the same order as the pseudoid generated above
--
;WITH Total(TotalWidgets) AS (SELECT COUNT(1) TotalWidgets FROM @Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P))
, Numbers(Num) as (
SELECT 1 AS Num
UNION ALL
SELECT Num+1
FROM Numbers
JOIN Total t ON 1 = 1
WHERE Num < t.TotalWidgets )
INSERT INTO WidgetDetailItem (WidgetID,SomeID,SomeData)
SELECT
w.WidgetID
,Details.SomeID
,Details.SomeData
FROM
(SELECT
P.value('WidgetDetailItemID[1]','int') WidgetDetailItemID
, P.value('SomeID[1]','int') SomeID
, P.value('SomeData[1]','varchar(5)') SomeData
, n.Num AS PsuedoID
FROM Numbers n
--This is what gives us our pseudo ID to link to the row_number() function from the first merge statement
CROSS APPLY @Widgets.nodes('/Widgets/Widget[sql:column("n.Num")]/Details/WidgetDetailItem') AS M(P)
) Details
JOIN @WidgetIds w on Details.PsuedoID = w.PseudoID
WHERE w.Action = 'INSERT' --We only want inserts by your spec
SELECT * FROM Widget;
SELECT * FROM WidgetDetailItem;