簡體   English   中英

從c#到SQL Server的批量插入策略

[英]Bulk insert strategy from c# to SQL Server

在我們當前的項目中,客戶將向我們的系統發送復雜/嵌套消息的集合。 這些消息的頻率約為。 1000-2000 msg /每秒。

這些復雜對象包含事務數據(要添加)以及主數據(如果未找到則將添加)。 但客戶不是傳遞主數據的ID,而是傳遞“名稱”列。

系統檢查這些名稱是否存在主數據。 如果找到,它將使用數據庫中的ID,否則首先創建此主數據,然后使用這些ID。

解析主數據ID后,系統會將事務數據插入SQL Server數據庫(使用主數據ID)。 每條消息的主實體數量約為15-20。

以下是我們可以采取的一些策略。

  1. 我們可以首先從C#代碼中解析master ID(如果沒有找到則插入主數據)並將這些id存儲在C#cache中。 解決所有ID后,我們可以使用SqlBulkCopy類批量插入事務數據。 我們可以訪問數據庫15次以獲取不同實體的ID,然后再次命中數據庫以插入最終數據。 我們可以使用相同的連接在完成所有這些處理后關閉它。

  2. 我們可以將包含主數據和事務數據的所有這些消息一次性發送到數據庫(以多個TVP的形式),然后在內部存儲過程中,首先為缺失的數據創建主數據,然后插入事務數據。

有人可以建議這個用例的最佳方法嗎?

由於一些隱私問題,我無法分享實際的對象結構。 但這是假設的對象結構,它非常接近我們的業務對象

其中一條消息將包含有關一個產品(其主數據)的信息以及來自不同供應商的價格詳細信息(交易數據):

主數據(如果未找到則需要添加)

產品名稱:ABC,ProductCateory:XYZ,制造商:XXX和其他一些細節(屬性數量在15-20范圍內)。

交易數據(將始終添加)

供應商名稱:A,ListPrice:XXX,折扣:XXX

供應商名稱:B,ListPrice:XXX,折扣:XXX

供應商名稱:C,ListPrice:XXX,折扣:XXX

供應商名稱:D,ListPrice:XXX,折扣:XXX

對於屬於一個產品的消息,大多數有關主數據的信息將保持不變(並且將更改頻率更低),但事務數據將始終波動。 因此,系統將檢查系統中是否存在產品“XXX”。 如果沒有,請檢查本產品中提到的“類別”是否存在。 如果沒有,它將為類別插入新記錄,然后為產品插入。 這將針對制造商和其他主數據進行。

多個供應商將同時發送有關多個產品(2000-5000)的數據。

因此,假設我們有1000個供應商,每個供應商都在發送大約10-15種不同產品的數據。 每2-3秒后,每個供應商都會向我們發送這10個產品的價格更新。 他可能會開始發送有關新產品的數據,但這種情況並不常見。

你可能最好用你的#2想法(即使用多個TVP一次性將所有15-20個實體發送到數據庫,並處理整組最多2000條消息)。

在應用層緩存主數據查找並在發送到數據庫之前進行翻譯聽起來很棒,但是錯過了一些東西:

  1. 無論如何,您將不得不點擊數據庫以獲取初始列表
  2. 無論如何,您將不得不點擊數據庫以插入新條目
  3. 查找字典中的值以替換為ID 正是數據庫的作用(假設每個這些名稱到ID查找都使用非聚集索引)
  4. 經常查詢的值會將其數據頁緩存在緩沖池(這一個內存緩存)中

為什么在應用層重復現在在DB層提供和發生的內容 ,特別是給出:

  • 15-20個實體可以有多達20k的記錄(這是一個相對較小的數字,特別是考慮到非聚集索引只需要兩個字段: NameID ,可以在使用時將許多行打包到單個數據頁中100%填充因子)。
  • 並非所有20k條目都是“活動”或“當前”,因此您無需擔心緩存所有條目。 因此,無論當前值是什么值都將被輕易識別為被查詢的值,並且那些數據頁(可能包括一些非活動條目,但在那里沒有大問題)將被緩存在緩沖池中。

因此,您無需擔心老條目的老化或由於可能更改的值(即特定ID更新Name )而導致任何密鑰到期或重新加載,因為這是自然處理的。

是的,內存中緩存是一種很棒的技術,可以大大加快網站的速度,但是這些場景/用例是指非數據庫進程在純粹的只讀目的中反復請求相同的數據。 但是這種特殊情況是合並數據並且查找值列表可能頻繁更改(更多因為新條目而不是更新條目)。


總而言之,選項#2是要走的路。 雖然沒有15個TVP,但我已經多次成功完成了這項技術。 可能需要對方法進行一些優化/調整以調整這種特定情況,但我發現效果很好的是:

  • 通過TVP接受數據。 我比SqlBulkCopy更喜歡這個,因為:
    • 它使得一個易於自包含的存儲過程成為可能
    • 它非常適合應用程序代碼,可以將集合完全流式傳輸到數據庫,而無需首先將集合復制到DataTable ,這會復制集合,這會浪費CPU和內存。 這要求您為每個返回IEnumerable<SqlDataRecord>的集合創建一個方法,接受集合作為輸入,並使用yield return; 發送forforeach循環中的每條記錄。
  • TVP不適合統計,因此不適合JOINing(盡管可以通過在查詢中使用TOP (@RecordCount)來減輕這種情況),但無論如何你都不需要擔心,因為它們只用於填充具有任何缺失值的真實表
  • 第1步:為每個實體插入缺少的名稱。 請記住,每個實體的[Name]字段都應該有一個NonClustered Index,並且假設該ID是Clustered Index,該值自然會成為索引的一部分,因此[Name]僅提供覆蓋索引除了幫助以下操作。 並且還要記住,此客戶端的任何先前執行(即大致相同的實體值)將導致這些索引的數據頁保持緩存在緩沖池(即內存)中。

     ;WITH cte AS ( SELECT DISTINCT tmp.[Name] FROM @EntityNumeroUno tmp ) INSERT INTO EntityNumeroUno ([Name]) SELECT cte.[Name] FROM cte WHERE NOT EXISTS( SELECT * FROM EntityNumeroUno tab WHERE tab.[Name] = cte.[Name] ) 
  • 步驟2:在簡單的INSERT...SELECT中插入所有“消息”,其中由於步驟1,查找表的數據頁(即“實體”)已經緩存在緩沖池中


最后,請記住,猜測/假設/有根據的猜測不能替代測試。 您需要嘗試一些方法來查看哪種方法最適合您的特定情況,因為可能還有其他未共享的詳細信息可能會影響此處的“理想”。

我會說,如果消息只是插入,那么弗拉德的想法可能會更快。 我在這里描述的方法我已經在更復雜的情況下使用,需要完全同步(更新和刪除),並進行了額外的驗證和相關操作數據的創建(而不是查找值)。 在直接插入時使用SqlBulkCopy 可能會更快(盡管只有2000條記錄,我懷疑是否存在很大差異),但這假設您直接加載到目標表(消息和查找)而不是中間/臨時表(我相信Vlad的想法是將SqlBulkCopy直接發送到目標表)。 然而,如上所述,由於更新查找值的問題,使用外部高速緩存(即不是緩沖池)也更容易出錯。 它可能需要更多的代碼來考慮使外部緩存無效,特別是如果使用外部緩存只是稍微快一些。 需要考慮額外的風險/維護,哪種方法總體上更好地滿足您的需求。


UPDATE

根據評論中提供的信息,我們現在知道:

  • 有多個供應商
  • 每個供應商提供多種產品
  • 產品並非供應商所獨有; 產品由1個或更多供應商銷售
  • 產品屬性是單一的
  • 定價信息具有可以包含多個記錄的屬性
  • 定價信息僅限INSERT(即時間點歷史記錄)
  • 唯一產品由SKU(或類似領域)確定
  • 一旦創建,使用現有SKU但不同屬性(例如類別,制造商等)的產品將被視為同一產品 ; 差異將被忽略

考慮到所有這些,我仍然會推薦TVP,但要重新思考這種方法並使其以供應商為中心,而不是以產品為中心。 這里的假設是供應商隨時發送文件。 所以當你得到一個文件時,導入它。 您提前做的唯一查詢是供應商。 這是基本布局:

  1. 似乎有理由假設您此時已經有VendorID,因為系統為什么要從未知來源導入文件?
  2. 您可以批量導入
  3. 創建一個SendRows方法:
    • 接受FileStream或允許通過文件前進的東西
    • 接受類似int BatchSize東西
    • 返回IEnumerable<SqlDataRecord>
    • 創建一個SqlDataRecord以匹配TVP結構
    • for循環通過FileStream直到滿足BatchSize或文件中沒有更多記錄
    • 對數據執行任何必要的驗證
    • 將數據映射到SqlDataRecord
    • 呼叫yield return;
  4. 打開文件
  5. 雖然文件中有數據
    • 調用存儲過程
    • 傳遞VendorID
    • 傳入TVP的SendRows(FileStream, BatchSize)
  6. 關閉文件
  7. 試驗:
    • 在圍繞FileStream循環之前打開SqlConnection,並在循環完成后關閉它
    • 打開SqlConnection,執行存儲過程,並關閉FileStream循環內部的SqlConnection
  8. 試驗各種BatchSize值。 從100開始,然后是200,500等。
  9. 存儲過程將處理插入新產品

使用這種類型的結構,您將發送未使用的產品屬性(即僅使用SKU查找現有產品)。 但是,它的擴展非常好,因為文件大小沒有上限。 如果賣方發送50個產品,那很好。 如果他們發送50k產品,罰款。 如果他們發送400萬個產品(這是我工作的系統,它確實處理了更新任何屬性的產品信息!),那么很好。 應用層或數據庫層的內存不會增加,甚至不能處理1000萬個產品。 導入所用的時間應隨着發送的產品數量的增加而增加。


更新2
與源數據相關的新詳細信息:

  • 來自Azure EventHub
  • 以C#對象的形式出現(沒有文件)
  • 產品詳細信息通過OP系統的API提供
  • 收集在單個隊列中(只需將數據拉出插入數據庫)

如果數據源是C#對象,那么我肯定會使用TVP,因為你可以通過我在第一次更新中描述的方法(即返回IEnumerable<SqlDataRecord> )發送它們。 針對每個供應商的價格/優惠詳細信息發送一個或多個TVP,但針對單個屬性屬性定期輸入參數。 例如:

CREATE PROCEDURE dbo.ImportProduct
(
  @SKU             VARCHAR(50),
  @ProductName     NVARCHAR(100),
  @Manufacturer    NVARCHAR(100),
  @Category        NVARCHAR(300),
  @VendorPrices    dbo.VendorPrices READONLY,
  @DiscountCoupons dbo.DiscountCoupons READONLY
)
SET NOCOUNT ON;

-- Insert Product if it doesn't already exist
IF (NOT EXISTS(
         SELECT  *
         FROM    dbo.Products pr
         WHERE   pr.SKU = @SKU
              )
   )
BEGIN
  INSERT INTO dbo.Products (SKU, ProductName, Manufacturer, Category, ...)
  VALUES (@SKU, @ProductName, @Manufacturer, @Category, ...);
END;

...INSERT data from TVPs
-- might need OPTION (RECOMPILE) per each TVP query to ensure proper estimated rows

從數據庫的角度來看,沒有比BULK INSERT快的東西(例如來自csv文件)。 最好是盡快批量處理所有數據,然后使用存儲過程對其進行處理。

AC#層只會減慢進程,因為C#和SQL之間的所有查詢都比Sql-Server可以直接處理的慢幾千倍。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM