如何在不到10秒的时间内将200万行插入到sqlserver中的两个表中

时间:2019-02-01 10:19:47

标签: c# sql-server stored-procedures optimization

我目前正在使用存储过程在90到100秒内将100万条记录同时插入两个表中。在我的情况下,这是不可接受的。我想找到一种将时间减少到10秒以内的方法。

我尝试在订单很慢之后插入一个记录-大约花了一个小时。然后,我尝试使用表值参数插入所有记录一次。时间缩短到90-100秒。

这是c#调用代码:

public Task<int> CreateGiftVoucher(IEnumerable<Gift> vouchersList)
{

    GiftStreamingSqlRecord record = new GiftStreamingSqlRecord(vouchersList);

    foreach (var t in vouchersList)
    {
        Console.WriteLine($"<<<<<gfts>>> {t}");
    }

    try
    {
        var connection = Connection;

        if (connection.State == ConnectionState.Closed) connection.Open();

        string storedProcedure = "dbo.usp_CreateGiftVoucher";

        var command = new SqlCommand(storedProcedure, connection as SqlConnection);
        command.CommandType = CommandType.StoredProcedure;

        var param = new SqlParameter();
        param.ParameterName = "@tblGift";
        param.TypeName = "dbo.GiftVoucherType";   
        param.SqlDbType = SqlDbType.Structured;             
        param.Value = record;

        command.Parameters.Add(param);
        command.CommandTimeout = 60;
        return command.ExecuteNonQueryAsync();                 

    }
    catch (System.Exception)
    {
        throw;
    }
    finally
    {
        Connection.Close();
    }
}

这是GiftStreamingRecord类

public GiftStreamingSqlRecord(IEnumerable<Gift> gifts) =>  this._gifts = gifts;

public IEnumerator<SqlDataRecord> GetEnumerator()
{
    SqlMetaData[] columnStructure = new SqlMetaData[11];
    columnStructure[0] = new SqlMetaData("VoucherId",SqlDbType.BigInt, 
                useServerDefault: false,  
                isUniqueKey: true, 
                columnSortOrder:SortOrder.Ascending, sortOrdinal: 0);
    columnStructure[1] = new SqlMetaData("Code", SqlDbType.NVarChar, maxLength: 100);
    columnStructure[2] = new SqlMetaData("VoucherType", SqlDbType.NVarChar, maxLength: 50);
    columnStructure[3] = new SqlMetaData("CreationDate", SqlDbType.DateTime);
    columnStructure[4] = new SqlMetaData("ExpiryDate", SqlDbType.DateTime);
    columnStructure[5] = new SqlMetaData("VoucherStatus", SqlDbType.NVarChar, maxLength: 10);
    columnStructure[6] = new SqlMetaData("MerchantId", SqlDbType.NVarChar, maxLength: 100);
    columnStructure[7] = new SqlMetaData("Metadata", SqlDbType.NVarChar, maxLength: 100);
    columnStructure[8] = new SqlMetaData("Description", SqlDbType.NVarChar, maxLength: 100);
    columnStructure[9] = new SqlMetaData("GiftAmount", SqlDbType.BigInt);
    columnStructure[10] = new SqlMetaData("GiftBalance", SqlDbType.BigInt);

    var columnId = 1L;

    foreach (var gift in _gifts)
    {
        var record =  new SqlDataRecord(columnStructure);
        record.SetInt64(0, columnId++);
        record.SetString(1, gift.Code);
        record.SetString(2, gift.VoucherType);
        record.SetDateTime(3, gift.CreationDate);
        record.SetDateTime(4, gift.ExpiryDate);
        record.SetString(5, gift.VoucherStatus);
        record.SetString(6, gift.MerchantId);
        record.SetString(7, gift.Metadata);
        record.SetString(8, gift.Description);
        record.SetInt64(9, gift.GiftAmount);
        record.SetInt64(10, gift.GiftBalance);
        yield return record;
    }
}

这是存储过程和tvp:

CREATE TYPE [dbo].GiftVoucherType AS TABLE (
[VoucherId] [bigint] PRIMARY KEY,
[Code] [nvarchar](100) NOT NULL,
[VoucherType] [nvarchar](50) NOT NULL,
[CreationDate] [datetime] NOT NULL,
[ExpiryDate] [datetime] NOT NULL,
[VoucherStatus] [nvarchar](10) NOT NULL,
[MerchantId] [nvarchar](100) NOT NULL,
[Metadata] [nvarchar](100) NULL,
[Description] [nvarchar](100) NULL,
[GiftAmount] [bigint] NOT NULL,
[GiftBalance] [bigint] NOT NULL
)

GO

CREATE PROCEDURE [dbo].[usp_CreateGiftVoucher]

@tblGift [dbo].GiftVoucherType READONLY
AS

    DECLARE @idmap TABLE (TempId BIGINT NOT NULL PRIMARY KEY, 
                            VId BIGINT UNIQUE NOT NULL)

BEGIN TRY
    BEGIN TRANSACTION CreateGiftVoucher

        MERGE Voucher V 
        USING (SELECT [VoucherId], [Code], [VoucherType], [MerchantId], [ExpiryDate],
            [Metadata], [Description] FROM @tblGift) TB ON 1 = 0
        WHEN NOT MATCHED BY TARGET THEN
        INSERT ([Code], [VoucherType], [MerchantId], [ExpiryDate], [Metadata], [Description])
        VALUES(TB.Code, TB.VoucherType, TB.MerchantId, TB.ExpiryDate, TB.Metadata, TB.[Description])
        OUTPUT TB.VoucherId, inserted.VoucherId INTO @idmap(TempId, VId);

        -- Insert rows into table 'GiftVoucher'
        INSERT GiftVoucher
        (
        GiftAmount, GiftBalance, VoucherId
        )
        SELECT TB.GiftAmount, TB.GiftBalance, i.VId
        FROM @tblGift TB
        JOIN @idmap i ON i.TempId = TB.VoucherId

    COMMIT TRANSACTION CreateGiftVoucher
END TRY
BEGIN CATCH
    ROLLBACK
END CATCH
GO

所有这些使我能够在90到100秒内插入100万。 我想在10秒内完成。

1 个答案:

答案 0 :(得分:6)

插入大量行的最快方法是使用批量插入(SqlBulkCopy或其他API)。我可以看到您正在使用MERGE。批量复制无法使用该功能,因此该设计将强制使用表值参数,就像您现在正在使用它们一样。 TVP在使用更多CPU方面要慢一些。您也可以尝试批量插入临时表,然后使用MERGE。据我了解,TVP实际上是一个临时表。没有真正的流媒体正在进行。服务器将您在C#代码中流式传输到其中的所有数据简单地插入到自动管理的表中。

您执行的TVP流(SqlMetaData)是正确的。根据我的经验,这是传输TVP数据的最快方法。

您将需要并行化。根据经验,对于相当简单的行,在最佳条件下很难超过每秒10万行。此时,CPU在一个内核上已饱和。您可以在记录的某些条件下在多个内核上并行插入。索引结构有一些要求。您可能还会遇到锁定问题。解决这些问题的肯定方法是插入独立的表或分区中。但这当然会迫使您更改针对这些表运行的其他查询。

如果在插入时必须执行复杂的逻辑,则仍可以插入到新表中,然后在查询时执行逻辑。这比较麻烦且容易出错,但可以满足您的延迟要求。

我希望这些想法能帮助您走上正确的道路。随时发表评论。