你最好采用方案二 (即使用多个TVP一次性将15-20个实体发送到数据库,并作为2000个消息的整个集合进行处理)。
在应用层缓存主数据查找并在发送到数据库之前进行转换听起来不错,但是却忽略了以下几点:
1. 无论如何,您都需要访问数据库来获取初始列表。
2. 无论如何,您都需要访问数据库来插入新条目。
3. 查找字典中的值以替换ID恰好就是数据库所做的事情(假设每个这些名称到ID查找都有一个非聚集索引)。
4. 频繁查询的值将在缓冲池中缓存它们的数据页(这是一个内存缓存)。
为什么要在应用程序层面上重复现在已经由数据库层面提供和发生的内容,尤其在考虑到以下事实时:
1. 15-20个实体可能有多达20k条记录(这是相对较少的数字,特别是考虑到非聚集索引只需要两个字段:
Name
和
ID
,当使用100%填充因子时,可以将许多行打包到单个数据页中)。
2. 并不是所有的20k条目都是“活动”或“当前”的,因此您无需担心缓存所有条目。因此,任何当前值都将很容易地被确定为正在查询的值,并且那些数据页(可能包括一些非活动条目,但在那里没有大问题)将被缓存在缓冲池中。
因此,您不需要担心老旧条目的老化或由于可能更改的值(即特定
ID
的更新
Name
)而强制执行任何关键过期或重新加载,因为这是自然处理的。
是的,在内存中缓存是美妙的技术,大大加快了网站的速度,但这些情景/用例是为仅具有纯只读目的的非数据库进程请求相同的数据而设计的。但是,这种情况是合并数据且查找值列表可能经常发生变化(更多是由于新条目而不是由于更新条目)的情况之一。
综上所述,选项2是最佳选择。我已经成功地使用了这种技术几次,虽然没有使用15个TVP。也许需要对该方法进行一些优化/调整以调整此特定情况,但我发现以下方法效果很好:
- Accept the data via TVP. I prefer this over
SqlBulkCopy
because:
- it makes for an easily self-contained Stored Procedure
- it fits very nicely into the app code to fully stream the collection(s) to the DB without needing to copy the collection(s) to a
DataTable
first, which is duplicating the collection, which is wasting CPU and memory. This requires that you create a method per each collection that returns IEnumerable<SqlDataRecord>
, accepts the collection as input, and uses yield return;
to send each record in the for
or foreach
loop.
- TVPs are not great for statistics and hence not great for JOINing to (though this can be mitigated by using a
TOP (@RecordCount)
in the queries), but you don't need to worry about that anyway since they are only used to populate the real tables with any missing values
Step 1: Insert missing Names for each entity. Remember that there should be a NonClustered Index on the [Name]
field for each entity, and assuming that the ID is the Clustered Index, that value will naturally be a part of the index, hence [Name]
only will provide a covering index in addition to helping the following operation. And also remember that any prior executions for this client (i.e. roughly the same entity values) will cause the data pages for these indexes to remain cached in the Buffer Pool (i.e. memory).
;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]
)
Step 2: INSERT all of the "messages" in simple INSERT...SELECT
where the data pages for the lookup tables (i.e. the "entities") are already cached in the Buffer Pool due to Step 1
最后,记住,假设/推测/有根据的猜测无法替代测试。您需要尝试几种方法,看看哪种方法最适合您的特定情况,因为可能会有未分享的其他细节会影响到这里被认为是“理想”的内容。
如果消息只是插入操作,则 Vlad 的想法可能更快。我在这里描述的方法是在更复杂且需要完全同步(更新和删除)的情况下使用的,并进行了额外的验证和相关操作数据的创建(不是查找值)。在仅进行插入(仅 2000 条记录的情况下,我怀疑是否有很大的区别),使用 SqlBulkCopy 可能更快,但这假设您直接将记录加载到目标表(消息和查找表)中,而不是中间表/暂存表(我认为 Vlad 的想法是直接将 SqlBulkCopy 复制到目标表中)。但是,如上所述,使用外部缓存(即不是缓存池)也更容易出现错误,因为存在更新查找值的问题。如果使用外部缓存仅略微更快,则需要编写更多代码来解决外部缓存失效的问题,这可能不值得。必须考虑这种额外风险/维护工作,以确定哪种方法对您的需求更为全面。
更新:
根据评论中提供的信息,我们现在知道:
- 有多个供应商
- 每个供应商提供多种产品
- 产品不是唯一属于供应商的;1个或多个供应商销售产品
- 产品属性是单数形式的
- 定价信息具有可以具有多条记录的属性
- 定价信息仅限于“插入”操作(即时点历史)
- 根据 SKU(或类似字段)确定唯一产品
- 一旦创建,对于通过具有不同属性的现有 SKU 提交的产品(例如类别、制造商等),将被视为相同的产品;差异将被忽略
考虑到所有这些,我仍然建议使用 TVP,但重新考虑方法,并使其以供应商为中心,而不是以产品为中心。这里的假设是供应商随时发送文件。因此,在获取文件时导入它。您事先进行的唯一查找是供应商,以下是基本布局:
- 可以合理地假设在此时您已经拥有了VendorID,因为系统为什么会从一个未知的来源导入文件呢?
- 您可以分批导入
- 创建一个
SendRows
方法:
- 接受一个FileStream或允许浏览文件的东西
- 接受类似于
int BatchSize
的东西
- 返回
IEnumerable<SqlDataRecord>
- 创建一个
SqlDataRecord
以匹配TVP结构
- 循环FileStream,直到BatchSize满足或文件中没有更多记录
- 对数据执行任何必要的验证
- 将数据映射到
SqlDataRecord
- 调用
yield return;
- 打开文件
- 当文件中有数据时
- 调用存储过程
- 传递VendorID
- 传递
SendRows(FileStream, BatchSize)
作为TVP
- 关闭文件
- 尝试以下操作:
- 在围绕FileStream的循环之前打开SqlConnection,在循环结束后关闭它
- 在FileStream循环内部打开SqlConnection,执行存储过程,并在其中关闭SqlConnection
- 尝试不同的BatchSize值。从100开始,然后是200、500等。
- 存储过程将处理插入新产品的工作
使用这种结构,您将发送未使用的产品属性(即仅使用SKU用于查找现有产品)。但是,它可以很好地扩展,因为没有关于文件大小的上限。如果供应商发送50个产品,那就好。如果他们发送了50k个产品,也可以。如果他们发送了400万个产品(这是我所使用的系统,并且它确实处理了与其任何属性不同的产品信息的更新!),那也可以。在应用程序层或DB层中没有增加内存来处理甚至1000万个产品。导入所需的时间应该随着发送的产品数量而增加。
更新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;
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