C# 如何在10秒内将200万行插入sqlserver中的两个表

C# 如何在10秒内将200万行插入sqlserver中的两个表,c#,sql-server,stored-procedures,optimization,C#,Sql Server,Stored Procedures,Optimization,我目前正在使用一个存储过程,在90到100秒内将100万条记录同时插入到两个表中。这在我的场景中是不可接受的。我想找到一种方法把时间缩短到10秒以内 我试着在订单之后插入一条记录,这非常慢,大约花了一个小时。然后我尝试使用表值参数一次性插入所有记录。这使得时间下降到90-100秒 这是c#调用代码: public Task<int> CreateGiftVoucher(IEnumerable<Gift> vouchersList) { GiftStreaming

我目前正在使用一个存储过程,在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();
    }
}
所有这些让我在90到100秒内插入了100万。
我想在不到10秒内完成这项工作。

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

您所做的TVP流(
SqlMetaData
)是正确的。在我的经验中,这是传输TVP数据最快的方式

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

如果在插入时必须执行复杂的逻辑,那么仍然可以插入到新表中,然后在查询时执行逻辑。这是更容易工作和出错的,但它可能允许您满足延迟要求


我希望这些想法能帮助你走上正确的道路。请随意评论。

为服务器购买更快的存储驱动器?仅仅说“我想要这个”并不能实现这一点。从大规模的重构到新的硬件,有很多事情可能行得通,也可能行不通。如果你想要闪电般的插入,有两种方法。有人能帮助这个人吗?或者给他一些有用的东西?不仅仅是嘲笑他,让他看起来很糟糕。太糟糕了,StackOverflow变成了这样一个地方!我见过TVPs流数据实现类似于SqlBulkCopy的性能。性能问题可能是“合并”的性能低于标准。@DanGuzman MERGE没有任何固有的性能劣势(尽管其声誉不佳)。性能由具体的查询和执行计划决定。在这里,他将有效地得到一个正常的插入计划,该计划由源和目标的左连接提供。这基本上与他在不存在的地方使用INSERT时得到的计划形状相同。一般来说,不说
MERGE
,这是一个问题,只是在这种情况下,可能需要对查询和索引进行调整,以将性能提高到可接受的水平。@DanGuzman我同意。特别是锁定可能会杀死他的平行插入。
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;
    }
}
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