在 SQL 服务器中批量插入忽略和 return id 的最佳方法?

Best way to do batch insert ignore and return id in SQL Server?

当我从不同来源(Json 文件、其他数据库和 REST API)导入各种数据时,我需要对它们进行重复数据删除,首先我将它们加载到一个 table 中,它定义了为它们键入并将数据存储为 Json,因此稍后当我 运行 批处理时,我可以查找类型并将数据插入到 suitable table秒。导入的行数不同(每种类型去不同 table/tables),但总是超过 100 万(如果我将它们以 Json 格式放在单个 [=64 中,总共约 10G 数据) =] 使用 VARCHAR(MAX)).

正如我提到的,我需要处理重复项,因此我尝试为目标 table 定义唯一索引并启用 Ignore Duplicate Keys,当我 'only' 时会发出警告插入现有数据。问题是,这只在少数情况下有效。大多数时候,我需要使用 5+ varchar(255) 个字段,并且由于限制(900 字节,src),我无法将它们添加到唯一索引。

我苦苦挣扎的另一件事是,在批量插入期间,我需要插入关系数据,这意味着一个 table 将具有另一个的外键。所以首先我需要处理依赖关系,在我得到它们插入的 ID 之后,使用它们我可以插入数据。就像一个产品有一个制造商,所以我首先插入当前批次中的所有制造商名称,然后使用这些 ID 插入产品。

需要 returning ID 并执行重复数据删除导致我想实现的查询:

首先,我尝试通过创建如下存储过程来处理此问题:

  1. 尝试select数据,如果找到,returnId
  2. 如果没有找到,开始交易
  3. 再次检查它是否已被另一个线程插入。
  4. 如果没有,请插入并 return 新 ID。

此代码示例。:

CREATE PROCEDURE [dbo].usp_insert_pdproductdetails
    @GDDataSourceVersionId INT,
    @ManufacturerNameId BIGINT,
    @ManufacturerReference NVARCHAR(255),
    @PropertiesJson NVARCHAR(MAX),
    @OriginalContentPage NVARCHAR(MAX),
    @NewId BIGINT OUT
AS
BEGIN
    SET NOCOUNT ON;

    SELECT @NewId = [Id] FROM PDProductDetails
    WHERE GDDataSourceVersionId = @GDDataSourceVersionId AND
          ManufacturerId = @ManufacturerNameId AND
          ManufacturerReference = @ManufacturerReference;

    IF @NewId IS NULL
    BEGIN
        SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
        BEGIN TRANSACTION
            SELECT @NewId = [Id] FROM PDProductDetails
            WHERE GDDataSourceVersionId = @GDDataSourceVersionId AND
                  ManufacturerId = @ManufacturerNameId AND
                  ManufacturerReference = @ManufacturerReference;
            IF @NewId IS NULL
            BEGIN 
                INSERT INTO PDProductDetails (GDDataSourceVersionId, ManufacturerId, ManufacturerReference, PropertiesJson, OriginalContentPage)
                VALUES(@GDDataSourceVersionId, @ManufacturerNameId, @ManufacturerReference, @PropertiesJson, @OriginalContentPage);
                SELECT @NewId = SCOPE_IDENTITY();
            END
        COMMIT TRANSACTION
    END

    SELECT @NewId;
END
GO

多个线程会调用它并插入产品详细信息。但是,使用它我很快陷入僵局。我改为使用 Merge:

的不同方法
CREATE PROCEDURE [dbo].usp_insert_pdproductdetails
    @GDDataSourceVersionId INT,
    @ManufacturerNameId BIGINT,
    @ManufacturerReference NVARCHAR(255),
    @PropertiesJson NVARCHAR(MAX),
    @OriginalContentPage NVARCHAR(MAX),
    @NewId BIGINT OUT
AS
BEGIN
    SET NOCOUNT ON;

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION;
        MERGE
        INTO [dbo].[PDProductDetails] T
        USING (SELECT @GDDataSourceVersionId, @ManufacturerNameId, @ManufacturerReference, @PropertiesJson, @OriginalContentPage)
            AS Source (GDDataSourceVersionId, ManufacturerNameId, ManufacturerReference, PropertiesJson, OriginalContentPage)
        ON T.GDDataSourceVersionId = Source.GDDataSourceVersionId AND
            T.ManufacturerId = Source.ManufacturerNameId AND
            T.ManufacturerReference = Source.ManufacturerReference
        WHEN NOT MATCHED THEN
            INSERT (GDDataSourceVersionId, ManufacturerId, ManufacturerReference, PropertiesJson, OriginalContentPage)
            VALUES(Source.GDDataSourceVersionId, Source.ManufacturerNameId, 
                Source.ManufacturerReference, Source.PropertiesJson, Source.OriginalContentPage);
    COMMIT TRANSACTION;

    SELECT @NewId = [Id] FROM PDProductDetails (NOLOCK)
    WHERE GDDataSourceVersionId = @GDDataSourceVersionId AND
          ManufacturerId = @ManufacturerNameId AND
          ManufacturerReference = @ManufacturerReference;

    SELECT @NewId;
END
GO

这总是合并行和 selects 之后。它仍然死锁困难,不像另一个那么快,但仍然如此。

如何实现 insert ignore 和 return inserted id 功能,在并发环境下不会死锁?

在 @ta.speot.is 提到您可以通过合并执行 OUTPUT 之后,我搜索了如何将其分配给变量和 answer mentioned it.

我使用了这个存储过程。:

CREATE PROCEDURE [dbo].usp_insert_pdproductdetails
    @GDDataSourceVersionId INT,
    @ManufacturerNameId BIGINT,
    @ManufacturerReference NVARCHAR(255),
    @PropertiesJson NVARCHAR(MAX),
    @OriginalContentPage NVARCHAR(MAX),
    @NewId BIGINT OUT
AS
BEGIN
    SET NOCOUNT ON;

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION;
        MERGE
        INTO [dbo].[PDProductDetails] T
        USING (SELECT @GDDataSourceVersionId, @ManufacturerNameId, @ManufacturerReference, @PropertiesJson, @OriginalContentPage)
            AS Source (GDDataSourceVersionId, ManufacturerNameId, ManufacturerReference, PropertiesJson, OriginalContentPage)
        ON T.GDDataSourceVersionId = Source.GDDataSourceVersionId AND
            T.ManufacturerId = Source.ManufacturerNameId AND
            T.ManufacturerReference = Source.ManufacturerReference
        WHEN MATCHED THEN
            UPDATE SET @NewId = T.Id
        WHEN NOT MATCHED THEN
            INSERT (GDDataSourceVersionId, ManufacturerId, ManufacturerReference, PropertiesJson, OriginalContentPage)
            VALUES(Source.GDDataSourceVersionId, Source.ManufacturerNameId, 
                Source.ManufacturerReference, Source.PropertiesJson, Source.OriginalContentPage);

    SET @NewId = ISNULL(@NewId, SCOPE_IDENTITY());

    COMMIT TRANSACTION;

    SELECT @NewId;
END
GO

编辑:如@ta.speot.is 所述,使用相同的方法使用 table 值参数进行批处理请求会更好(MERGE 将使用 table 作为来源输入)。