【问题标题】:Split one large, denormalized table into a normalized database将一个大型非规范化表拆分为一个规范化数据库
【发布时间】:2016-11-07 05:49:25
【问题描述】:

我有一个大的(500 万行,300+ 列)csv 文件,我需要导入到 SQL Server 中的临时表中,然后运行脚本来拆分每一行并将数据插入到规范化数据库中的相关表中.源表的格式如下所示:

(fName, lName, licenseNumber1, licenseIssuer1, licenseNumber2, licenseIssuer2..., specialtyName1, specialtyState1, specialtyName2, specialtyState2..., identifier1, identifier2...)

有 50 个 licenseNumber/licenseIssuer 列、15 个 specialtyName/specialtyState 列和 15 个 identifier 列。其中至少有一个,但剩余的 49 或 14 个可能是 null。第一个标识符是唯一的,但不用作我们架构中 Person 的主键。

我的数据库架构如下所示

People(ID int Identity(1,1))
Names(ID int, personID int, lName varchar, fName varchar)
Licenses(ID int, personID int, number varchar, issuer varchar)
Specialties(ID int, personID int, name varchar, state varchar)
Identifiers(ID int, personID int, value)

在从 csv 添加新的之前,数据库中已经填充了一些 People

解决这个问题的最佳方法是什么?

我曾尝试使用select top 1 一次一行地遍历暂存表:

WHILE EXISTS (Select top 1 * from staging)
BEGIN
    INSERT INTO People Default Values
    SET @LastInsertedID = SCOPE_IDENTITY() -- might use the output clause to get this instead

    INSERT INTO Names (personID, lName, fName) 
    SELECT top 1 @LastInsertedID, lName, fName from staging

    INSERT INTO Licenses(personID, number, issuer)
    SELECT top 1 @LastInsertedID, licenseNumber1, licenseIssuer1 from staging

    IF (select top 1 licenseNumber2 from staging) is not null
    BEGIN
        INSERT INTO Licenses(personID, number, issuer)
        SELECT top 1 @LastInsertedID, licenseNumber2, licenseIssuer2 from staging
    END

    -- Repeat the above 49 times, etc...

    DELETE top 1 from staging
END

这种方法的一个问题是速度太慢了,所以我将它重构为使用游标。这很有效,而且速度明显更快,但让我为Fetch INTO 声明了 300 多个变量。

是否有一种基于集合的方法可以在这里工作?这将是可取的,因为我知道游标不受欢迎,但我不确定如何将身份从INSERT 获取到People 表中,以便在其他表中用作外键而不逐行- 暂存表中的行。

另外,我怎样才能避免将插入内容复制并粘贴到许可证表中?使用光标方法我可以尝试:

FETCH INTO ...@LicenseNumber1, @LicenseIssuer1, @LicenseNumber2, @LicenseIssuer2...
INSERT INTO #LicenseTemp (number, issuer) Values
(@LicenseNumber1, @LicenseIssuer1),
(@LicenseNumber2, @LicenseIssuer2),
... Repeat 48 more times...
.
.
.
INSERT INTO Licenses(personID, number, issuer)
SELECT @LastInsertedID, number, issuer
FROM #LicenseTEMP
WHERE number is not null

不过,那里似乎仍有一些多余的复制和粘贴。

为了总结问题,我正在寻找惯用的方法来:

  1. 将一个大型临时表分解为一组规范化表,从一个表中检索主键/标识并将其用作其他表中的外键
  2. 将多行插入到来自暂存表中许多重复列的规范化表中,使用较少的样板/复制和粘贴(上面的许可证和专业)

没有谨慎的答案,我也很乐意提供可以帮助我弄清楚这一点的资源和参考资料。

【问题讨论】:

  • 可以修改csv文件吗?
  • 修改 csv 文件可能不切实际(在 excel 中打开它是不可能的,并且在文本编辑器中检查和编辑很难看),但是可以修改暂存表。
  • 有什么可以提供fname/lname的唯一性吗?
  • @MikeNitchie,不,您只需要读取每条记录一次,然后调用构建许可证、专业和标识符的过程。但是 500 万条记录已经很多了。我认为标记每条记录是个好主意,以避免重复所有过程。
  • 我同意@mcNets:在游标和逐行逻辑上进行集合操作。您应该努力为每个目标访问一次临时表(即 5 次)。

标签: sql sql-server database tsql


【解决方案1】:

好的,我不是 SQL Server 专家,但这是我建议的“策略”。

计算临时表上的 personId 正如@Shnugo 在我之前建议的那样,计算临时表中的 personId 将简化后续步骤

对 personID 使用序列 在 SQL Server 2012 中,您可以定义序列。如果您将它用于每个人的插入,您将永远不会冒 ID 重叠的风险。如果您(看起来)在序列之前加载了 personId,您可以创建以第一个空闲 personID 作为起始值的序列

创建一个数字表 创建一个从 1 到 n 的实用程序表(您需要 n 至少为 50.. 对于某些实现,您可以 look at this question

使用集合逻辑进行插入 我会避免游标和逐行逻辑:你说得对,最好限制对表的访问次数,但我想说你应该努力将其限制为对目标表的一次访问。

你可以这样继续:

人:

 INSERT INTO People (personID) 
 SELECT personId from staging;

姓名:

 INSERT INTO Names (personID, lName, fName) 
 SELECT personId, lName, fName from staging;

许可证: 这里我们需要数字表

 INSERT INTO Licenses (personId, number, issuer)
 SELECT * FROM (
    SELECT personId, 
           case nbrs.n 
                when 1 then licenseNumber1 
                when 2 then licenseNumber2
                ...
                when 50 then licenseNumber50
            end as licenseNumber,    
           case nbrs.n 
                when 1 then licenseIssuer1 
                when 2 then licenseIssuer2
                ...
                when 50 then licenseIssuer50
            end as licenseIssuer
      from staging 
           cross join 
           (select n from numbers where n>=1 and n<=50) nbrs
  ) WHERE licenseNumber is not null;

专业:

 INSERT INTO Specialties(personId, name, state)
 SELECT * FROM (
    SELECT personId, 
           case nbrs.n 
                when 1 then specialtyName1
                when 2 then specialtyName2
                ...
                when 15 then specialtyName15
            end as specialtyName,    
           case nbrs.n 
                when 1 then specialtyState1
                when 2 then specialtyState2
                ...
                when 15 then specialtyState15
            end as specialtyState
      from staging 
           cross join 
           (select n from numbers where n>=1 and n<=15) nbrs
 ) WHERE specialtyName is not null;

标识符:

 INSERT INTO Identifiers(personId, value)
 SELECT * FROM (
    SELECT personId, 
           case nbrs.n 
                when 1 then identifier1
                when 2 then identifier2
                ...
                when 15 then identifier15
            end as value
      from staging 
           cross join 
           (select n from numbers where n>=1 and n<=15) nbrs
 ) WHERE value is not null;

希望对你有帮助。

【讨论】:

  • 谢谢你,它最终工作得很好。我所做的一项更改是我使用了 Shnugo 建议的 UNION 方法,而不是数字表
【解决方案2】:

你说:但是暂存表可以修改

我愿意

  • 添加一个PersonID INT NOT NULL 列并用DENSE_RANK() OVER(ORDER BY fname,lname) 填充它

  • 为这个 PersonID 添加一个索引

  • 将此 ID 与 GROUP BY 结合使用来填充您的人员表

  • 对你的名字表做同样的事情

  • 然后将此 ID 用于基于集合的插入到您的三个边表中

这样做

SELECT AllTogether.PersonID, AllTogether.TheValue
FROM
(
           SELECT PersonID,SomeValue1 AS TheValue FROM StagingTable
 UNION ALL SELECT PersonID,SomeValue2             FROM StagingTable
 UNION ALL ... 
) AS AllTogether
WHERE AllTogether.TheValue IS NOT NULL

更新

您说:可能会导致与 People 表中已经存在的 ID 冲突

你没有告诉任何关于现有People...

是否有任何确定和独特的标记来识别它们?使用简单的

UPDATE StagingTable SET PersonID=xyz WHERE ...

将现有的 PersonID 设置到临时表中,然后使用类似

UPDATE StagingTable 
SET PersonID=DENSE RANK() OVER(...) + MaxExistingID
WHERE PersonID IS NULL

为仍为 NULL 的 PersonID 设置新 ID。

【讨论】:

  • 好建议。但是,DENSE_RANK() 可能会导致与 People 表中已存在的 ID 发生冲突。有没有办法将 PersonID 添加到我们知道 People 表中不存在的临时表中?
猜你喜欢
  • 2013-01-23
  • 1970-01-01
  • 1970-01-01
  • 2013-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-23
  • 1970-01-01
相关资源
最近更新 更多