您可能最好采用您的 #2 想法(即使用多个 TVP 一次将所有 15 - 20 个实体发送到数据库,并作为一组最多 2000 条消息进行处理)。
在应用程序层缓存主数据查找并在发送到数据库之前进行转换听起来不错,但遗漏了一些东西:
- 无论如何,你都必须访问数据库才能获取初始列表
- 无论如何,你都必须访问数据库来插入新条目
- 在字典中查找值以替换 ID 的方法是exactly数据库的作用(假设每个名称到 ID 查找都有一个非聚集索引)
- 经常查询的值会将其数据页缓存在缓冲池中(is内存缓存)
为什么要在应用层重复已经提供的内容正在发生在数据库层,特别是考虑到:
- 15 - 20 个实体最多可以有 20k 条记录(这是一个相对较小的数字,特别是考虑到非聚集索引只需要两个字段时:
Name
and ID
当使用 100% 填充因子时,可以将许多行打包到单个数据页中)。
- 并非所有 20k 条目都是“活动的”或“当前的”,因此您无需担心缓存所有条目。因此,无论当前值是什么,都可以轻松识别为正在查询的值,并且those数据页(可能包括一些不活动的条目,但没什么大不了的)将被缓存在缓冲池中。
因此,您无需担心旧条目老化或由于可能更改值(即更新)而强制任何密钥过期或重新加载Name
对于一个特定的ID
)因为这是自然处理的。
是的,内存缓存是一项出色的技术,可以极大地加快网站速度,但这些场景/用例适用于非数据库进程出于纯只读目的一遍又一遍地请求相同数据的情况。但在这种特殊场景中,数据正在被合并,并且查找值列表可能会频繁更改(更多是由于新条目而不是更新条目所致)。
综上所述,选项#2 是最佳选择。我已经多次使用此技术并取得了很大成功,尽管没有使用 15 个 TVP。可能需要对方法进行一些优化/调整来调整这种特定情况,但我发现效果很好的是:
- Accept the data via TVP. I prefer this over
SqlBulkCopy
because:
- 它使得一个简单的独立存储过程
- 它非常适合应用程序代码,可以将集合完全流式传输到数据库,而无需将集合复制到
DataTable
首先,这是重复集合,这会浪费 CPU 和内存。这要求您为每个返回的集合创建一个方法IEnumerable<SqlDataRecord>
,接受集合作为输入,并使用yield return;
发送每条记录for
or foreach
loop.
- TVP 不太适合统计,因此不太适合 JOINing(尽管这可以通过使用
TOP (@RecordCount)
在查询中),但无论如何您都不需要担心这一点,因为它们仅用于使用任何缺失值填充真实表
-
步骤 1:为每个实体插入缺失的名称。请记住,应该有一个非聚集索引[Name]
每个实体的字段,并假设 ID 是聚集索引,该值自然会成为索引的一部分,因此[Name]
only除了帮助后面的操作之外,还会提供一个覆盖索引。还要记住,该客户端的任何先前执行(即大致相同的实体值)将导致这些索引的数据页保留在缓冲池(即内存)中缓存。
;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
might直接插入速度更快(尽管对于只有 2000 条记录,我怀疑是否有很大差异),但这假设您直接加载到目标表(消息和查找)而不是加载到中间/临时表(我相信弗拉德的想法是SqlBulkCopy
直接到目标表)。然而,如上所述,由于更新查找值的问题,使用外部缓存(即不是缓冲池)也更容易出错。使外部缓存无效可能需要更多的代码,特别是如果使用外部缓存只是稍微快一点的话。需要考虑额外的风险/维护,以考虑哪种方法总体上更适合您的需求。
UPDATE
根据评论中提供的信息,我们现在知道:
- 有多个供应商
- 每个供应商提供多种产品
- 产品并非供应商独有;产品由 1 个或多个供应商销售
- 产品属性单一
- 定价信息的属性可以有多个记录
- 定价信息仅限插入(即时间点历史记录)
- 独特产品由 SKU(或类似字段)确定
- 创建后,将考虑具有现有 SKU 但具有不同属性(例如类别、制造商等)的产品相同的产品;差异将被忽略
考虑到所有这些,我仍然会推荐 TVP,但要重新思考该方法并使其以供应商为中心,而不是以产品为中心。这里的假设是供应商随时发送文件。因此,当您获得文件时,将其导入。您需要提前进行的唯一查找是供应商。这是基本布局:
- 假设此时您已经拥有 VendorID 似乎是合理的,因为为什么系统会从未知来源导入文件?
- 可以批量导入
- Create a
SendRows
method that:
- 接受 FileStream 或允许在文件中前进的东西
- 接受类似的东西
int BatchSize
- returns
IEnumerable<SqlDataRecord>
- 创建一个
SqlDataRecord
匹配TVP结构
- for 循环遍历 FileStream,直到满足 BatchSize 或文件中不再有记录
- 对数据执行任何必要的验证
- 将数据映射到
SqlDataRecord
- call
yield return;
- 打开文件
- While there is data in the file
- 调用存储过程
- 传入VendorID
- pass in
SendRows(FileStream, BatchSize)
对于TVP
- 关闭文件
- Experiment with:
- 在 FileStream 循环之前打开 SqlConnection 并在循环完成后关闭它
- 打开 SqlConnection,执行存储过程,然后关闭 FileStream 循环内的 SqlConnection
- 尝试不同的 BatchSize 值。从 100 开始,然后是 200、500,等等。
- 存储过程将处理插入新产品
使用这种类型的结构,您将发送未使用的产品属性(即仅使用 SKU 来查找现有产品)。但是,它的扩展性非常好,因为文件大小没有上限。如果供应商发送 50 个产品,没问题。如果他们发送 50k 产品,那很好。如果他们发送了 400 万个产品(这是我工作的系统,它确实处理了与其任何属性不同的更新产品信息!),那么很好。即使处理 1000 万个产品,也无需增加应用程序层或数据库层的内存。进口所需的时间应随着发送的产品数量而增加。
UPDATE 2
与源数据相关的新详细信息:
- 来自 Azure EventHub
- 以 C# 对象的形式出现(无文件)
- 产品详细信息通过 O.P. 系统的 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