【问题标题】:Bulk insert nested xml with foreign key as identity column of first table使用外键批量插入嵌套 xml 作为第一个表的标识列
【发布时间】:2014-09-30 15:36:31
【问题描述】:

我有一个xml如下:

<Records>
  <Record>
    <Name>Best of Pop</Name>
    <Studio>ABC studio</Studio>
    <Artists>
      <Artist>
        <ArtistName>John</ArtistName>
        <Age>36</Age>            
      </Artist> 
      <Artist>
        <ArtistName>Jessica</ArtistName>
        <Age>20</Age>            
      </Artist>
    </Artists>
  </Record>
  <Record>
    <Name>Nursery rhymes</Name>
    <Studio>XYZ studio</Studio>
    <Artists>
      <Artist>
        <ArtistName>Judy</ArtistName>
        <Age>10</Age>            
      </Artist> 
      <Artist>
        <ArtistName>Rachel</ArtistName>
        <Age>15</Age>            
      </Artist>
    </Artists>
  </Record>
</Records>

此文件可能包含数百万条记录。我的 MS SQL 数据库在 Azure SQL Database 上运行,有以下 2 个表来存储这些记录:

  1. Record(RecordId [PK、身份、自动增量]、名称、工作室)

  2. Artist(RecordId [外键指 Record.RecordId]、ArtistName、Age)

是否可以使用 xml 节点方法在一次遍历 xml 中将记录批量插入到 Record 表中,获取 RecordIds,然后将艺术家信息批量插入到 Artist 表中?

我一直在寻找一种有效的方法来做到这一点,但徒劳无功。

我尝试了类似于herehere 描述的方法,但我无法找到解决方案。

任何指向解决方案的指针都会有很大帮助。

更新: @srutzky:感谢您的解决方案。这完全符合我的要求。但是有一个问题。我必须使用节点方法来解决问题。我已经更改了查询的第一部分。但是我被困在了下半场。这就是我要做的。

DECLARE @Record TABLE (RecordId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                       Name NVARCHAR(400) UNIQUE,
                       Studio NVARCHAR(400));
DECLARE @Artist TABLE (ArtistId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                       RecordId INT NOT NULL,
                       ArtistName NVARCHAR(400), Age INT);

INSERT INTO @Record (Name, Studio)
   SELECT  T.c.value(N'(Name/text())[1]', 'NVARCHAR(400)'),
           T.c.value(N'(Studio/text())[1]', 'NVARCHAR(400)')
 FROM @ImportData.nodes('/Records/Record') T(c);

SELECT * FROM @Record

你能帮我完成第二部分吗?我是这种 xml 处理方法的新手。

UPDATE2:我明白了……我绞尽脑汁想了几个小时,尝试了几件事,终于找到了解决方案。

DECLARE @Record TABLE (RecordId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                       Name NVARCHAR(400) UNIQUE,
                       Studio NVARCHAR(400));
DECLARE @Artist TABLE (ArtistId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                       RecordId INT NOT NULL,
                       ArtistName NVARCHAR(400), 
                       Age INT);

INSERT INTO @Record (Name, Studio)
   SELECT  T.c.value(N'(Name/text())[1]', 'NVARCHAR(400)'),
           T.c.value(N'(Studio/text())[1]', 'NVARCHAR(400)')
 FROM @ImportData.nodes('/Records/Record') T(c);

INSERT INTO @Artist (RecordId, ArtistName, Age)
    SELECT  (SELECT RecordId FROM @Record WHERE Name=T.c.value(N'(../../Name/text())[1]', 'NVARCHAR(400)')),
            T.c.value(N'(ArtistName/text())[1]', 'NVARCHAR(400)'),
           T.c.value(N'(Age/text())[1]', 'INT')
 FROM @ImportData.nodes('/Records/Record/Artists/Artist') T(c);

 SELECT * FROM @Record
 SELECT * FROM @Artist

@srutzky:非常感谢您为我指明了正确的方向。欢迎提出任何改进此解决方案的建议。

【问题讨论】:

  • 为什么单次遍历需要它?另外,请向我们展示您已经拥有的东西。
  • 中的 元素是否唯一?
  • @srutzky:是的,名字是独一无二的。
  • @RBarryYoung:我只需要以一种有效的方式来做,而不一定是一次遍历。我尝试了我之前共享的 2 个链接中描述的内容,但只能获得解决方案的一半。结果与预期的结果相去甚远,因此没有分享任何其他内容。
  • @nikhil :抱歉,我没有看到您的 cmets 和更新。使用带有名称的“@”在问题或答案的正文中不起作用。你需要评论我的回答。为什么需要使用节点?这很好,但效率可能会降低。但我会考虑到这一点来更新我的答案。请使用那里的 cmets 进行交流:)。

标签: sql-server xml tsql azure-sql-database sqlxml


【解决方案1】:

无论如何,这不能一次性完成,因为您不能在同一个 DML 语句中插入两个表(好吧,在触发器和 OUTPUT 子句之外,这两者都无济于事)。但是它可以通过两次有效地完成。 &lt;Record&gt; 中的 &lt;Name&gt; 元素的事实是唯一的,因为这允许我们使用 Record 表作为第二遍的查找表(即当我们获得 Artist 行时)。

首先,您需要(嗯,应该)在Record (Name ASC) 上创建一个UNIQUE INDEX。在下面的示例中,我使用了UNIQUE CONSTRAINT,但这只是因为我使用表变量而不是临时表来使示例代码更容易重新运行(不需要在顶部显式地使用 IF EXISTS DROP)。该索引将有助于第二遍的性能。

该示例使用 OPENXML,因为这很可能比使用 .nodes() 函数更有效,因为同一个文档需要遍历两次。 OPENXML 函数的最后一个参数 2 指定文档是“基于元素”的,因为默认解析是寻找“基于属性”。

DECLARE @DocumentID INT, @ImportData XML;

SET @ImportData = N'
<Records>
  <Record>
    <Name>Best of Pop</Name>
    <Studio>ABC studio</Studio>
    <Artists>
      <Artist>
        <ArtistName>John</ArtistName>
        <Age>36</Age>            
      </Artist> 
      <Artist>
        <ArtistName>Jessica</ArtistName>
        <Age>20</Age>            
      </Artist>
    </Artists>
  </Record>
  <Record>
    <Name>Nursery rhymes</Name>
    <Studio>XYZ studio</Studio>
    <Artists>
      <Artist>
        <ArtistName>Judy</ArtistName>
        <Age>10</Age>            
      </Artist> 
      <Artist>
        <ArtistName>Rachel</ArtistName>
        <Age>15</Age>            
      </Artist>
    </Artists>
  </Record>
</Records>';


DECLARE @Record TABLE (RecordId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                       Name NVARCHAR(400) UNIQUE,
                       Studio NVARCHAR(400));
DECLARE @Artist TABLE (ArtistId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                       RecordId INT NOT NULL,
                       ArtistName NVARCHAR(400), Age INT);

EXEC sp_xml_preparedocument @DocumentID OUTPUT, @ImportData;

-- First pass: extract "Record" rows
INSERT INTO @Record (Name, Studio)
   SELECT Name, Studio
   FROM   OPENXML (@DocumentID, N'/Records/Record', 2) 
             WITH (Name    NVARCHAR(400)  './Name/text()', 
                   Studio  NVARCHAR(400)  './Studio/text()');


-- Second pass: extract "Artist" rows
INSERT INTO @Artist (RecordId, ArtistName, Age)
   SELECT rec.RecordId, art.ArtistName, art.Age
   FROM   OPENXML (@DocumentID, N'/Records/Record/Artists/Artist', 2) 
             WITH (Name        NVARCHAR(400)  '../../Name/text()',
                   ArtistName  NVARCHAR(400)  './ArtistName/text()', 
                   Age         INT  './Age/text()') art
   INNER JOIN @Record rec
           ON rec.[Name] = art.[Name];


EXEC sp_xml_removedocument @DocumentID;
-------------------

SELECT * FROM @Record ORDER BY [RecordID];
SELECT * FROM @Artist ORDER BY [RecordID];

参考资料:

编辑:
随着使用.nodes() 函数而不是OPENXML 的新要求,以下将起作用:

DECLARE @ImportData XML;

SET @ImportData = N'
<Records>
  <Record>
    <Name>Best of Pop</Name>
    <Studio>ABC studio</Studio>
    <Artists>
      <Artist>
        <ArtistName>John</ArtistName>
        <Age>36</Age>            
      </Artist> 
      <Artist>
        <ArtistName>Jessica</ArtistName>
        <Age>20</Age>            
      </Artist>
    </Artists>
  </Record>
  <Record>
    <Name>Nursery rhymes</Name>
    <Studio>XYZ studio</Studio>
    <Artists>
      <Artist>
        <ArtistName>Judy</ArtistName>
        <Age>10</Age>            
      </Artist> 
      <Artist>
        <ArtistName>Rachel</ArtistName>
        <Age>15</Age>            
      </Artist>
    </Artists>
  </Record>
</Records>';

IF (OBJECT_ID('tempdb..#Record') IS NOT NULL)
BEGIN
   DROP TABLE #Record;
END;
IF (OBJECT_ID('tempdb..#Artist') IS NOT NULL)
BEGIN
   DROP TABLE #Artist;
END;

CREATE TABLE #Record (RecordId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                      Name NVARCHAR(400) UNIQUE,
                      Studio NVARCHAR(400));
CREATE TABLE #Artist (ArtistId INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
                      RecordId INT NOT NULL,
                      ArtistName NVARCHAR(400),
                      Age INT);


-- First pass: extract "Record" rows
INSERT INTO #Record (Name, Studio)
   SELECT col.value(N'(./Name/text())[1]', N'NVARCHAR(400)') AS [Name],
          col.value(N'(./Studio/text())[1]', N'NVARCHAR(400)') AS [Studio]
   FROM   @ImportData.nodes(N'/Records/Record') tab(col);


-- Second pass: extract "Artist" rows
;WITH artists AS
(
   SELECT col.value(N'(../../Name/text())[1]', N'NVARCHAR(400)') AS [RecordName],
          col.value(N'(./ArtistName/text())[1]', N'NVARCHAR(400)') AS [ArtistName],
          col.value(N'(./Age/text())[1]', N'INT') AS [Age]
   FROM   @ImportData.nodes(N'/Records/Record/Artists/Artist') tab(col)
)
INSERT INTO #Artist (RecordId, ArtistName, Age)
   SELECT rec.RecordId, art.ArtistName, art.Age
   FROM artists art
   INNER JOIN #Record rec
           ON rec.[Name] = art.RecordName;

-- OR --
-- INSERT INTO #Artist (RecordId, ArtistName, Age)
   SELECT rec.RecordId,
          col.value(N'(./ArtistName/text())[1]', N'NVARCHAR(400)') AS [ArtistName],
          col.value(N'(./Age/text())[1]', N'INT') AS [Age]
   FROM   @ImportData.nodes(N'/Records/Record/Artists/Artist') tab(col)
   INNER JOIN #Record rec
           ON rec.Name = col.value(N'(../../Name/text())[1]', N'NVARCHAR(400)');

-------------------

SELECT * FROM #Record ORDER BY [RecordID];
SELECT * FROM #Artist ORDER BY [RecordID];

有两个选项可以插入上面显示的#Artist。第一个使用 CTE 将 XML 提取从 INSERT / SELECT 查询中抽象出来。另一种是简化版,类似于您在问题的UPDATE 2中的查询。

【讨论】:

  • 再次感谢您的优化。我必须使用节点,因为我们正在处理 SQL Azure 数据库。它不是 SQL 服务器。 OpenXML 在那里不起作用。
  • @nikhil:不客气。它在 Azure SQL DB 上运行的事实是一个非常重要的细节,因此我编辑了问题以添加该信息并为其添加标签。
  • 你真好。谢谢。我会牢记这些以备不时之需。
  • @SolomonRutzky 对于第一个表,如果它是实际表中的插入,那么我们如何查询并获取 pk 值并插入到另一个表中。请指教。
  • @Zaker 嗨。不确定我是否理解与我在此处发布的内容的区别。 “实际”表是什么意思?我的答案中显示的插入表格都是实际表格。在我的示例中,我将 XML 中的一个值加入到第一个表中,以获取要插入到第二个表中的 PK 值。
猜你喜欢
  • 1970-01-01
  • 2013-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-13
  • 2016-10-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多