在我们当前的项目中,客户将向我们的系统发送复杂/嵌套消息的集合。这些消息的频率约为。 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个产品的价格更新。他可能会开始发送有关新产品的数据,但这种数据并不常见。
答案 0 :(得分:2)
你可能最好用你的#2想法(即使用多个TVP一次性将所有15-20个实体发送到数据库,并处理整套最多2000条消息)。
在应用层缓存主数据查找并在发送到数据库之前进行翻译听起来很棒,但却遗漏了一些内容:
为什么要在应用层复制已在数据库层提供并立即发生的内容,尤其是:
Name
和ID
当使用100%填充因子时,可以将许多行打包到单个数据页中。因此,您不必担心老条目老化或因为可能更改的值(即特定Name
的更新ID
)而导致任何密钥过期或重新加载是自然处理的。
是的,内存缓存是一种很棒的技术,可以大大加快网站的速度,但这些场景/用例是指非数据库进程在纯粹的只读目的中反复请求相同的数据。但是这种特殊情况是合并数据并且查找值列表可能频繁更改(更多因为新条目而不是更新条目)。
所有人都说,选项#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步:插入所有&#34;消息&#34;在简单的INSERT...SELECT
中,由于步骤1,查找表的数据页(即&#34;实体&#34;)已经缓存在缓冲池中
最后,请记住,猜测/假设/有根据的猜测不能替代测试。您需要尝试一些方法来查看哪种方法最适合您的特定情况,因为可能还有其他未共享的细节可能影响所考虑的内容#34;理想&#34;这里。
我会说,如果消息只是插入,那么弗拉德的想法可能会更快。我在这里描述的方法我已经在更复杂的情况下使用,需要完全同步(更新和删除),并进行了额外的验证和相关操作数据的创建(而不是查找值)。使用SqlBulkCopy
可能在直接插入时更快(尽管只有2000条记录,我怀疑它有什么不同,如果有的话),但这假设你直接加载到目标表(消息)并且没有进入中间/临时表(我相信弗拉德的想法是SqlBulkCopy
直接到目的地表)。然而,如上所述,由于更新查找值的问题,使用外部高速缓存(即不是缓冲池)也更容易出错。它可能需要更多的代码来考虑使外部缓存无效,特别是如果使用外部缓存只是稍微快一些。需要将额外的风险/维护考虑在哪种方法总体上更好地满足您的需求。
<强>更新强>
根据评论中提供的信息,我们现在知道:
考虑到所有这些,我仍然会推荐TVP,但要重新思考这种方法并使其以供应商为中心,而不是以产品为中心。这里的假设是供应商随时发送文件。所以当你得到一个文件时,导入它。您提前做的唯一查询是供应商。这是基本布局:
SendRows
方法:
int BatchSize
IEnumerable<SqlDataRecord>
SqlDataRecord
以匹配TVP结构SqlDataRecord
yield return;
SendRows(FileStream, BatchSize)
获取TVP 使用此类结构,您将发送未使用的产品属性(即仅使用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
答案 1 :(得分:0)
从数据库的角度来看,没有比BULK INSERT快的东西(例如来自csv文件)。最好是尽快批量处理所有数据,然后使用存储过程对其进行处理。
C#层只会减慢进程,因为C#和SQL之间的所有查询都比Sql-Server可以直接处理的慢几千倍。