将一个大的非规范化表拆分为规范化数据库

时间:2016-11-06 20:10:45

标签: sql sql-server database tsql

我有一个大的(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多个变量。

是否有基于集合的方法可以在这里工作?这是更好的,因为我理解游标是不受欢迎的,但我不知道如何从INSERTPeople表获取身份用作其他人的外键从临时表中逐行进行。

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

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. 将多行插入到来自登台表中许多重复列的规范化表中,并使用较少的样板/复制和粘贴(上面的许可证和特殊产品)
  3. 如果没有谨慎的答案,我也会非常满意对资源和参考资料的指示,这可以帮助我解决这个问题。

2 个答案:

答案 0 :(得分:3)

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

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

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

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

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

你可以这样继续:

人:

 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;

希望它有所帮助。

答案 1 :(得分:1)

你说:但是可以修改临时表

我会

  • 添加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

更新

您说:可能会导致与人员表中已存在的ID发生冲突

您没有说明现有的People ...

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

UPDATE StagingTable SET PersonID=xyz WHERE ...

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

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

为PersonID设置新ID仍为NULL。