从C#到SQL Server的批量插入策略

2023-11-24

在我们当前的项目中,客户将向我们的系统发送复杂/嵌套消息的集合。这些消息的频率约为。 1000-2000 条消息/每秒。

这些复杂对象包含交易数据(待添加)以及主数据(如果找不到,将添加)。但客户不传递主数据的 ID,而是传递“名称”列。

系统检查这些名称的主数据是否存在。如果找到,它将使用数据库中的 ID,否则首先创建此主数据,然后使用这些 ID。

解析主数据 ID 后,系统会将事务数据插入 SQL Server 数据库(使用主数据 ID)。每条消息的主实体数量约为 15-20 个。

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

  1. 我们可以首先从 C# 代码中解析主 ID(如果未找到,则插入主数据)并将这些 ID 存储在 C# 缓存中。一旦解析了所有 ID,我们就可以使用以下命令批量插入事务数据SqlBulkCopy班级。我们可以访问数据库 15 次来获取不同实体的 id,然后再访问数据库一次以插入最终数据。我们可以使用相同的连接,在完成所有这些处理后将其关闭。

  2. 我们可以将所有这些包含主数据和事务数据的消息一次性发送到数据库(以多个TVP的形式),然后在存储过程中,首先为丢失的主数据创建主数据,然后插入事务数据。

有人可以建议这个用例中的最佳方法吗?

由于一些隐私问题,我无法分享实际的对象结构。但这是假设的对象结构,它与我们的业务对象非常接近.

一条这样的消息将包含有关一种产品的信息(其主数据)及其来自不同供应商的价格详细信息(交易数据):

主数据(如果没有找到需要添加)

产品名称:ABC,产品类别:XYZ,制造商:XXX 和其他一些详细信息(属性数量在 15-20 范围内)。

交易数据(将始终添加)

供应商名称:A,标价:XXX,折扣:XXX

供应商名称:B,标价:XXX,折扣:XXX

供应商名称:C,标价:XXX,折扣:XXX

供应商名称:D,标价:XXX,折扣:XXX

对于属于一种产品的消息,有关主数据的大部分信息将保持不变(并且更改频率较低),但交易数据始终会波动。因此,系统将检查系统中是否存在产品“XXX”。如果不存在,则检查该产品提到的“类别”是否存在。如果没有,它将为类别插入一条新记录,然后为产品插入一条新记录。这将为制造商和其他主数据完成。

多个供应商将同时发送有关多个产品 (2000-5000) 的数据。

因此,假设我们有 1000 个供应商,每个供应商发送有关 10-15 种不同产品的数据。每隔 2-3 秒,每个供应商都会向我们发送这 10 种产品的价格更新。他可能会开始发送有关新产品的数据,但不会很频繁。


您可能最好采用您的 #2 想法(即使用多个 TVP 一次将所有 15 - 20 个实体发送到数据库,并作为一组最多 2000 条消息进行处理)。

在应用程序层缓存主数据查找并在发送到数据库之前进行转换听起来不错,但遗漏了一些东西:

  1. 无论如何,你都必须访问数据库才能获取初始列表
  2. 无论如何,你都必须访问数据库来插入新条目
  3. 在字典中查找值以替换 ID 的方法是exactly数据库的作用(假设每个名称到 ID 查找都有一个非聚集索引)
  4. 经常查询的值会将其数据页缓存在缓冲池中(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,但要重新思考该方法并使其以供应商为中心,而不是以产品为中心。这里的假设是供应商随时发送文件。因此,当您获得文件时,将其导入。您需要提前进行的唯一查找是供应商。这是基本布局:

  1. 假设此时您已经拥有 VendorID 似乎是合理的,因为为什么系统会从未知来源导入文件?
  2. 可以批量导入
  3. Create a SendRows method that:
    • 接受 FileStream 或允许在文件中前进的东西
    • 接受类似的东西int BatchSize
    • returns IEnumerable<SqlDataRecord>
    • 创建一个SqlDataRecord匹配TVP结构
    • for 循环遍历 FileStream,直到满足 BatchSize 或文件中不再有记录
    • 对数据执行任何必要的验证
    • 将数据映射到SqlDataRecord
    • call yield return;
  4. 打开文件
  5. While there is data in the file
    • 调用存储过程
    • 传入VendorID
    • pass in SendRows(FileStream, BatchSize)对于TVP
  6. 关闭文件
  7. Experiment with:
    • 在 FileStream 循环之前打开 SqlConnection 并在循环完成后关闭它
    • 打开 SqlConnection,执行存储过程,然后关闭 FileStream 循环内的 SqlConnection
  8. 尝试不同的 BatchSize 值。从 100 开始,然后是 200、500,等等。
  9. 存储过程将处理插入新产品

使用这种类型的结构,您将发送未使用的产品属性(即仅使用 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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从C#到SQL Server的批量插入策略 的相关文章

  • 实体框架中的重复键异常?

    我试图捕获当我将具有给定用户名的现有用户插入数据库时 引发的异常 正如标题所说 我正在使用 EF 当我尝试将用户插入数据库时 引发的唯一异常是 UpdateException 如何提取此异常以识别其是否是重复异常或其他异常 catch Up
  • WPF - 按多列排序时使用自定义比较器

    我有一个 ListView GridView 我想按 2 列排序 因此如果第 1 列中有 2 个以上的项目具有相同的值 它将按第 2 列排序 非常简单 但是在对 A Z 进行排序时 空字符串会出现在顶部 我想把它们移到底部 我制作了一个比较
  • 使用 QSet 作为 Qt 地图容器中的键

    我需要一个映射 其中键是唯一的 并且每个键都是一组或自定义 POD 结构 其中包含 3 个数据项 这些值只是指向对象实例的指针 从阅读Qt 的 QMap 与 QHash 的文档 http qt project org doc qt 4 8
  • 当我尝试使用 AVX 功能时,Clang 生成错误

    我使用的是 Windows 10 使用 Clang 版本 5 最近安装 当我编译以下内容时 define AVX define AVX2 include
  • 如何在 C++ 的子目录中创建文件?

    这是我的代码 如何在子目录联系人中创建文件 每次创建该文件时 它都会出现在与我的程序相同的目录中 int main ofstream myfile contacts myfile open a myfile close 在构造函数中指定完整
  • CMake 警告:无法为目标生成安全的链接器搜索路径

    在为 pcl 项目运行 CMake 时 我收到一条警告消息 Configuring done CMake Warning at CMakeLists txt 12 add executable Cannot generate a safe
  • 未定义异常变量时通过引用捕获

    捕获异常时 标准指导是按值抛出 按引用捕获 据我了解 这有两个原因 如果由于内存不足异常而引发异常 我们将不会调用可能终止程序的复制构造函数 如果异常是继承层次结构的一部分 我们可能会对异常进行对象切片 如果我们有一个场景 我们没有在 ca
  • 首先EntityFramework数据库 - 类型映射 - 将binary(8)从SQL映射到C#中的int

    在 SQL 内部 我有一个主键为二进制 8 的表 当我使用该表添加到我的模型中时Update Model from Database我可以看到该列有 type Binary 在 C 中 我将该列设为byte 我可以将该列映射到 int 吗
  • 使用 Microsoft Graph 创建用户

    如何使用 Microsoft graph 创建用户 因为我在保存过程中遇到了权限失败的问题 我确实有几个问题 在图中调用创建用户 API 将在哪里创建用户 是在 Azure AD 还是其他地方 我尝试通过传递 json 和必需的标头来调用创
  • 在 C++ 中处理音频缓冲区时,如何执行从 float -> double -> float 的转换

    我目前正在开发一个应用程序 其中音频样本帧在以下回调中进行处理 void Eav07AudioProcessor processBlock AudioSampleBuffer buffer for int channel 0 channel
  • 从 ef core 的子集合中删除一些项目

    我有一个父表和子表 其中父表与子表具有一对多关系 我想删除一些子项 并且希望父项的子集合反映该更改 如果我使用删除选定的子项RemoveRange 那么子集合不会更新 如果我使用Remove从子集合中删除子集合然后 显然 它不如使用效率高R
  • 基于 C++ 范围的 for 循环

    尝试使用基于范围的 for 循环执行某些操作 可以使用常规的 for 循环来完成 如下所示 vector
  • 在 C# 中生成随机值

    如何使用以下命令生成随机 Int64 和 UInt64 值RandomC 中的类 这应该可以解决问题 这是一个扩展方法 因此您可以像调用普通方法一样调用它Next or NextDouble上的方法Random目的 public stati
  • 为什么我的 ITexthandler 不工作?我正在尝试将 XML 解析为 ITextSharp 文档

    我正在使用 Visual Developer 2010 MVC 3 c 我正在尝试将 XML 解析为 iTextSharp 文档 如下所示 ITextHandler textHandler new ITextHandler doc text
  • 语义问题 Qt Creator:命名空间“std”中没有名为“cout”的成员

    我开始使用 Qt Creator 编写代码 对于 C 文件 我遇到很多语义问题 99 是 命名空间 yyy 中没有名为 xxx 的成员cpp文件构建 编译和输出没有问题 如果我点击例如cout 我已链接到 iostream 我是否需要在 Q
  • 我可以采取哪些措施来提高 SQL Server 中纯用户定义函数的性能?

    我制作了一个简单但计算相对复杂的 UDF 用于查询很少更改的表 在典型用法中 该函数会在一个非常小的参数域上从 WHERE 子句中多次调用 如何才能更快地使用 UDF 我的想法是应该有某种方式告诉 SQL Server 我的函数使用相同的参
  • Intel 和 AMD 处理器有相同的汇编程序吗?

    C语言被用来编写Unix以实现可移植性 使用不同编译器编译的同一个C语言程序会产生不同的机器指令 为什么 Windows 操作系统能够在两者上运行Intel https en wikipedia org wiki Intel and AMD
  • 计算两个日期之间的工作日数?

    在C 中 如何计算business 或工作日 两个日期之间的天数 我以前曾经遇到过这样的任务 并且我已经找到了解决方案 当可以避免的时候 我会避免列举其间的所有日子 这里就是这种情况 正如我在上面的一个答案中看到的那样 我什至没有提到创建一
  • 如何设置 Swashbuckle 与 Microsoft.AspNetCore.Mvc.Versioning

    我们有asp net core webapi 我们添加了Microsoft AspNetCore Mvc Versioning and Swashbuckle拥有招摇的用户界面 我们将控制器指定为 ApiVersion 1 0 Route
  • C# 中成员访问中的问号是什么意思?

    有人可以向我解释一下以下代码中会员访问中的问号是什么意思吗 它是标准 C 的一部分吗 尝试在 Xamarin Studio 中编译此文件时出现解析错误 this AnalyzerLoadFailed Invoke this new Anal

随机推荐

  • Django 错误消息“在定义中添加 related_name 参数”

    D zjm code basic project gt python manage py syncdb Error One or more models did not validate topics topic Accessor for
  • 如何处理 MS Word 添加的“特殊”字符?

    我想知道你如何清理 MS Word 中的特殊字符 例如 m 和 n 破折号以及弯引号 我经常发现自己从 Word 中复制客户端内容并粘贴到静态 HTML 页面中 但内容最终会出现奇怪的字符 因为特殊字符未转换为正确的 ACSII 代码 因此
  • 将字符串转换为 &strs 时,切片和显式重新借用之间有区别吗?

    下面两个例子等价吗 示例1 let x String new let y x 示例2 let x String new let y x 一种比另一种更有效率还是它们基本相同 如果是String and Vec 他们做同样的事情 在gener
  • Node.js/Async - 如何避免异步回调地狱?

    我是后端 Node Js 和 JavaScript Web 开发的新手 我发现回调中的回调可能会很痛苦 并且有一些模块可以避免这种情况 这些模块之一是异步的 https github com caolan async 我已阅读文档 但很难开
  • 如何在解决方案资源管理器上保留源文件夹层次结构?

    我在Linux上做了一个C 项目 我将源文件分组在许多目录中以自己组织 我使用 CMake 进行编译 每个子目录上都有一个 CMakeFiles txt srcs folderA Toto cpp Tata cpp folderB Foo
  • htaccess重写库

    我的根目录中有两个目录 dev and live 这些目录中的所有内容都有相对路径 例如 css style css or js home js 我希望能够使用 htaccess 更改根目录 以便相对路径变为 live css style
  • 将字符串数组转换为打字稿中的对象键

    我有下一个数组 const arrayData a b as const 该数组有下一个类型 ArrayType a b 我想获得与下一个类型一致的对象 type Type a boolean b boolean 预期对象 const re
  • 在Python中异步播放声音

    我有一个while loop让我的相机 带有 opencv 在物体移动时拍照 我也想调用一个函数来播放声音 但是当我调用并播放它时 它将在该执行时间内停止循环 我试过ThreadPoolExecutor但不知道如何将它与我的代码混合 因为我
  • 使用 gettext() 将 settings.LANGUAGES 与正确翻译的名称一起使用

    来自 Django 文档 如果您定义了自定义LANGUAGES设置 标记语言就可以了 作为翻译字符串 如 上面显示的默认值 但是 使用 假人 gettext 函数 不 中的一个django utils translation 你永远不应该导
  • 结构体总是分配堆栈还是有时分配堆?

    我的印象是 在 C 中 结构元素是在堆栈上分配的 因此从创建它们的方法返回时会消失 但是如果我将结构值放入列表中并返回它会发生什么 元素得以幸存 结构体实例有时会分配在堆上吗 internal struct Stru public int
  • file_get_contents 是否使用缓存?

    我有一个函数可以生成一个包含数据库内容的表 有些单元格具有自定义 HTML 我通过模板系统使用 file get contents 读取这些 HTML 小内容是相同的 但此操作可能执行 15 次 我限制每页 15 个表行 那么file ge
  • Lollipop 设置默认本地不起作用

    我在大多数 Android API 版本上运行此方法来使用语言 字符串等 设置应用程序 protected void setDefaultLocale Context context Locale locale Locale setDefa
  • 仅锁定 ID

    我有一个方法需要专门运行一段代码 但我只想在确实需要时添加此限制 根据 Id 值 Int32 我将加载 修改不同的对象 因此锁定所有线程的访问没有意义 这是这样做的第一次尝试 private static readonly Concurre
  • 直接输入地址栏时,您必须使用 API 密钥来验证对 Google Maps Platform API 的每个请求

    我收到这样的错误 error message This API project is not authorized to use this API results status REQUEST DENIED 每当我运行这个 https ma
  • Web api 从处理程序内部获取路由模板

    在将问题放在这里之前 我进行了很多搜索 但搜索得越多 我就越感到困惑 所以我创建了一个处理程序 我试图获取这样的路线 public class ExecutionDelegatingHandler DelegatingHandler pro
  • 扩展模块中的类方法

    我正在研究 ruby 的元编程功能 我发现它有点棘手 我正在尝试使用模块包装方法调用 目前 我正在这样做 module Bar module ClassMethods def wrap method class eval do old me
  • 如何让 只接受图像文件?

    我只需要通过上传图像文件
  • Android:位图允许的最大宽度和高度

    我正在创建一个应用程序 需要将大图像解码为位图以显示在 ImageView 中 如果我只是尝试将它们直接解码为位图 我会收到以下错误 位图太大 无法上传到纹理中 1944x2592 最大 2048x2048 因此 为了能够显示太高分辨率的图
  • 将数组键从下划线大小写递归转换为驼峰大小写

    我必须想出一种方法将使用下划线 下划线大小写 的数组键转换为驼峰命名法 这必须递归完成 因为我不知道哪些数组将被输入到该方法中 我想出了这个 private function convertKeysToCamelCase apiRespon
  • 从C#到SQL Server的批量插入策略

    在我们当前的项目中 客户将向我们的系统发送复杂 嵌套消息的集合 这些消息的频率约为 1000 2000 条消息 每秒 这些复杂对象包含交易数据 待添加 以及主数据 如果找不到 将添加 但客户不传递主数据的 ID 而是传递 名称 列 系统检查