繁体   English   中英

C#将数据插入SQL数据库的最快方法

[英]C# fastest way to insert data into SQL database

我正在从外部源(通过Lightstreamer)接收(流式处理)数据到我的C#应用​​程序中。 我的C#应用​​程序从侦听器接收数据。 来自侦听器的数据存储在队列(ConcurrentQueue)中。 每0.5秒使用TryDequeue将队列清除到DataTable中。 然后,将使用SqlBulkCopy将DataTable复制到SQL数据库中。 SQL数据库处理从登台表到最终表的新数据。 我目前每天大约收到30万行(在接下来的几周内可能会增加),我的目标是从收到数据之日起直到最终SQL表中可用这些数据时,将其保持在1秒以内。 目前,我必须处理的每秒最大行数约为50行。

不幸的是,由于接收到越来越多的数据,我的逻辑性能变得越来越慢(仍然远远低于1秒,但我想不断提高)。 到目前为止,主要瓶颈是将过渡数据(在SQL数据库上)处理到最终表中。 为了提高性能,我想将登台表切换为内存优化表。 最终表已经是内存优化表,因此可以肯定地一起使用。

我的问题:

  1. 有没有办法对内存优化的表使用SqlBulkCopy(C#之外)? (据我所知尚无办法)
  2. 有什么建议以最快的方式将我的C#应用​​程序中接收到的数据写入内存优化的暂存表中?

编辑(与解决方案):

经过评论/回答和性能评估后,我决定放弃大容量插入,并使用SQLCommand将带有数据作为表值参数的IEnumerable移交给本机编译存储过程,以将数据直接存储在经过内存优化的最终表中(以及复制到现在用作存档的“ staging”表中)。 性能显着提高(甚至我还没有考虑并行化插件(将在稍后阶段))。

这是代码的一部分:

内存优化的用户定义表类型(用于将数据从C#切换到SQL(存储过程):

CREATE TYPE [Staging].[CityIndexIntradayLivePrices] AS TABLE(
    [CityIndexInstrumentID] [int] NOT NULL,
    [CityIndexTimeStamp] [bigint] NOT NULL,
    [BidPrice] [numeric](18, 8) NOT NULL,
    [AskPrice] [numeric](18, 8) NOT NULL,
    INDEX [IndexCityIndexIntradayLivePrices] NONCLUSTERED 
(
    [CityIndexInstrumentID] ASC,
    [CityIndexTimeStamp] ASC,
    [BidPrice] ASC,
    [AskPrice] ASC
)
)
WITH ( MEMORY_OPTIMIZED = ON )

本机编译的存储过程可将数据插入最终表和登台(在本例中为存档):

create procedure [Staging].[spProcessCityIndexIntradayLivePricesStaging]
(
    @ProcessingID int,
    @CityIndexIntradayLivePrices Staging.CityIndexIntradayLivePrices readonly
)
with native_compilation, schemabinding, execute as owner
as 
begin atomic
with (transaction isolation level=snapshot, language=N'us_english')


    -- store prices

    insert into TimeSeries.CityIndexIntradayLivePrices
    (
        ObjectID, 
        PerDateTime, 
        BidPrice, 
        AskPrice, 
        ProcessingID
    )
    select Objects.ObjectID,
    CityIndexTimeStamp,
    CityIndexIntradayLivePricesStaging.BidPrice, 
    CityIndexIntradayLivePricesStaging.AskPrice,
    @ProcessingID
    from @CityIndexIntradayLivePrices CityIndexIntradayLivePricesStaging,
    Objects.Objects
    where Objects.CityIndexInstrumentID = CityIndexIntradayLivePricesStaging.CityIndexInstrumentID


    -- store data in staging table

    insert into Staging.CityIndexIntradayLivePricesStaging
    (
        ImportProcessingID,
        CityIndexInstrumentID,
        CityIndexTimeStamp,
        BidPrice,
        AskPrice
    )
    select @ProcessingID,
    CityIndexInstrumentID,
    CityIndexTimeStamp,
    BidPrice,
    AskPrice
    from @CityIndexIntradayLivePrices


end

IEnumerable充满了来自队列:

private static IEnumerable<SqlDataRecord> CreateSqlDataRecords()
{


    // set columns (the sequence is important as the sequence will be accordingly to the sequence of columns in the table-value parameter)

    SqlMetaData MetaDataCol1;
    SqlMetaData MetaDataCol2;
    SqlMetaData MetaDataCol3;
    SqlMetaData MetaDataCol4;

    MetaDataCol1 = new SqlMetaData("CityIndexInstrumentID", SqlDbType.Int);
    MetaDataCol2 = new SqlMetaData("CityIndexTimeStamp", SqlDbType.BigInt);
    MetaDataCol3 = new SqlMetaData("BidPrice", SqlDbType.Decimal, 18, 8); // precision 18, 8 scale
    MetaDataCol4 = new SqlMetaData("AskPrice", SqlDbType.Decimal, 18, 8); // precision 18, 8 scale


    // define sql data record with the columns

    SqlDataRecord DataRecord = new SqlDataRecord(new SqlMetaData[] { MetaDataCol1, MetaDataCol2, MetaDataCol3, MetaDataCol4 });


    // remove each price row from queue and add it to the sql data record

    LightstreamerAPI.PriceDTO PriceDTO = new LightstreamerAPI.PriceDTO();

    while (IntradayQuotesQueue.TryDequeue(out PriceDTO))
    {

        DataRecord.SetInt32(0, PriceDTO.MarketID); // city index market id
        DataRecord.SetInt64(1, Convert.ToInt64((PriceDTO.TickDate.Replace(@"\/Date(", "")).Replace(@")\/", ""))); // @ is used to avoid problem with / as escape sequence)
        DataRecord.SetDecimal(2, PriceDTO.Bid); // bid price
        DataRecord.SetDecimal(3, PriceDTO.Offer); // ask price

        yield return DataRecord;

    }


}

每0.5秒处理一次数据:

public static void ChildThreadIntradayQuotesHandler(Int32 CityIndexInterfaceProcessingID)
{


    try
    {

        // open new sql connection

        using (SqlConnection TimeSeriesDatabaseSQLConnection = new SqlConnection("Data Source=XXX;Initial Catalog=XXX;Integrated Security=SSPI;MultipleActiveResultSets=false"))
        {


            // open connection

            TimeSeriesDatabaseSQLConnection.Open();


            // endless loop to keep thread alive

            while(true)
            {


                // ensure queue has rows to process (otherwise no need to continue)

                if(IntradayQuotesQueue.Count > 0) 
                {


                    // define stored procedure for sql command

                    SqlCommand InsertCommand = new SqlCommand("Staging.spProcessCityIndexIntradayLivePricesStaging", TimeSeriesDatabaseSQLConnection);


                    // set command type to stored procedure

                    InsertCommand.CommandType = CommandType.StoredProcedure;


                    // define sql parameters (table-value parameter gets data from CreateSqlDataRecords())

                    SqlParameter ParameterCityIndexIntradayLivePrices = InsertCommand.Parameters.AddWithValue("@CityIndexIntradayLivePrices", CreateSqlDataRecords()); // table-valued parameter
                    SqlParameter ParameterProcessingID = InsertCommand.Parameters.AddWithValue("@ProcessingID", CityIndexInterfaceProcessingID); // processing id parameter


                    // set sql db type to structured for table-value paramter (structured = special data type for specifying structured data contained in table-valued parameters)

                    ParameterCityIndexIntradayLivePrices.SqlDbType = SqlDbType.Structured;


                    // execute stored procedure

                    InsertCommand.ExecuteNonQuery();


                }


                // wait 0.5 seconds

                Thread.Sleep(500);


            }

        }

    }
    catch (Exception e)
    {

        // handle error (standard error messages and update processing)

        ThreadErrorHandling(CityIndexInterfaceProcessingID, "ChildThreadIntradayQuotesHandler (handler stopped now)", e);

    };


}

使用SQL Server 2016(尚未使用RTM,但在内存优化表方面,它已经比2014更好)。 然后,使用内存优化的表变量,或仅在事务中处理大量本机存储过程调用,每个存储过程调用一次插入,具体取决于您的方案中运行速度更快(这有所不同)。 需要注意的几件事:

  • 在一个事务中进行多次插入对于节省网络往返至关重要。 尽管内存中的操作非常快,但是SQL Server仍然需要确认每个操作。
  • 根据生成数据的方式,您可能会发现并行化插入可以加快速度(不要过度处理;您会很快达到饱和点)。 不要在这里变得非常聪明。 利用async / await和/或Parallel.ForEach
  • 如果要传递表值参数,最简单的方法是将DataTable作为参数值传递,但这不是最有效的方法-传递IEnumerable<SqlDataRecord> 您可以使用迭代器方法生成值,因此仅分配恒定数量的内存。

您将不得不做一些实验以找到最佳的数据传输方式。 这在很大程度上取决于数据的大小以及如何获取。

将临时表中的数据批处理到最终表中的行数少于5k,我通常使用4k,并且不要将它们插入事务中。 相反,如果需要,请执行程序化事务。 插入的行数保持在5k以下,可以防止将行锁的数量升级为表锁,而表锁必须等待其他所有人退出表锁。

您确定是逻辑放慢了,而不是数据库的实际事务放慢了吗? 例如,在尝试插入大量行并且变得非常慢时,由于缺乏更好的术语,因此实体框架是“敏感的”。

我使用了Codeplex上的第三方库BulkInsert,可以很好地批量插入数据: https : //efbulkinsert.codeplex.com/

如果EF也这样做,也可以基于记录计数,则可以在DBContext上编写自己的扩展方法。 5000行以下的任何东西都使用Save(),超过此值的任何东西都可以调用自己的批量插入逻辑。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM