[英]Bulk insert strategy from c# to SQL Server
在我們當前的項目中,客戶將向我們的系統發送復雜/嵌套消息的集合。 這些消息的頻率約為。 1000-2000 msg /每秒。
這些復雜對象包含事務數據(要添加)以及主數據(如果未找到則將添加)。 但客戶不是傳遞主數據的ID,而是傳遞“名稱”列。
系統檢查這些名稱是否存在主數據。 如果找到,它將使用數據庫中的ID,否則首先創建此主數據,然后使用這些ID。
解析主數據ID后,系統會將事務數據插入SQL Server數據庫(使用主數據ID)。 每條消息的主實體數量約為15-20。
以下是我們可以采取的一些策略。
我們可以首先從C#代碼中解析master ID(如果沒有找到則插入主數據)並將這些id存儲在C#cache中。 解決所有ID后,我們可以使用SqlBulkCopy
類批量插入事務數據。 我們可以訪問數據庫15次以獲取不同實體的ID,然后再次命中數據庫以插入最終數據。 我們可以使用相同的連接在完成所有這些處理后關閉它。
我們可以將包含主數據和事務數據的所有這些消息一次性發送到數據庫(以多個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條消息)。
在應用層緩存主數據查找並在發送到數據庫之前進行翻譯聽起來很棒,但是錯過了一些東西:
為什么在應用層重復現在在DB層提供和發生的內容 ,特別是給出:
Name
和ID
,可以在使用時將許多行打包到單個數據頁中100%填充因子)。 因此,您無需擔心老條目的老化或由於可能更改的值(即特定ID
更新Name
)而導致任何密鑰到期或重新加載,因為這是自然處理的。
是的,內存中緩存是一種很棒的技術,可以大大加快網站的速度,但是這些場景/用例是指非數據庫進程在純粹的只讀目的中反復請求相同的數據。 但是這種特殊情況是合並數據並且查找值列表可能頻繁更改(更多因為新條目而不是更新條目)。
總而言之,選項#2是要走的路。 雖然沒有15個TVP,但我已經多次成功完成了這項技術。 可能需要對方法進行一些優化/調整以調整這種特定情況,但我發現效果很好的是:
SqlBulkCopy
更喜歡這個,因為:
DataTable
,這會復制集合,這會浪費CPU和內存。 這要求您為每個返回IEnumerable<SqlDataRecord>
的集合創建一個方法,接受集合作為輸入,並使用yield return;
發送for
或foreach
循環中的每條記錄。 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
根據評論中提供的信息,我們現在知道:
考慮到所有這些,我仍然會推薦TVP,但要重新思考這種方法並使其以供應商為中心,而不是以產品為中心。 這里的假設是供應商隨時發送文件。 所以當你得到一個文件時,導入它。 您提前做的唯一查詢是供應商。 這是基本布局:
SendRows
方法:
int BatchSize
東西 IEnumerable<SqlDataRecord>
SqlDataRecord
以匹配TVP結構 SqlDataRecord
yield return;
SendRows(FileStream, BatchSize)
使用這種類型的結構,您將發送未使用的產品屬性(即僅使用SKU查找現有產品)。 但是,它的擴展非常好,因為文件大小沒有上限。 如果賣方發送50個產品,那很好。 如果他們發送50k產品,罰款。 如果他們發送400萬個產品(這是我工作的系統,它確實處理了更新任何屬性的產品信息!),那么很好。 應用層或數據庫層的內存不會增加,甚至不能處理1000萬個產品。 導入所用的時間應隨着發送的產品數量的增加而增加。
更新2
與源數據相關的新詳細信息:
如果數據源是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.