如何实现基于集合的算法/UDF

med*_*r19 14 sql-server ssis functions sql-server-2014

我有一个算法,我需要针对具有 800K 行和 38 列的表中的每一行运行该算法。该算法在 VBA 中实现,并使用来自某些列的值来处理其他列进行大量数学运算。

我目前正在使用 Excel (ADO) 来查询 SQL 并使用带有客户端游标的 VBA 通过循环遍历每一行来应用算法。它可以工作,但需要 7 个小时才能运行。

VBA 代码非常复杂,将其重新编码为 T-SQL 需要做很多工作。

我已经阅读了 CLR 集成和 UDF 作为可能的路由。我还考虑将 VBA 代码放在 SSIS 脚本任务中以更接近数据库,但我确信存在解决此类性能问题的专家方法。

理想情况下,我能够以基于并行集的方式对尽可能多的行(全部?)运行算法。

任何帮助都很大程度上取决于如何在此类问题上获得最佳性能。

- 编辑

感谢您的评论,我使用的是 MS SQL 2014 Enterprise,这里有更多详细信息:

该算法在时间序列数据中找到特征模式。算法中的函数执行多项式平滑、窗口化,并根据输入标准找到感兴趣的区域,返回十几个值和一些布尔结果。

我的问题更多是关于方法论而不是实际算法:如果我想一次在多行上实现并行计算,我的选择是什么。

我看到建议重新编码到 T-SQL 中,这是很多工作但可能的,但是算法开发人员在 VBA 中工作并且经常更改,因此我需要与 T-SQL 版本保持同步并重新验证每个改变。

T-SQL 是实现基于集合的函数的唯一方法吗?

Sol*_*zky 8

关于方法论,我相信你在咆哮错误的 b 树 ;-)。

我们所知道的:

首先,让我们巩固和回顾一下我们对这种情况的了解:

  • 需要进行一些复杂的计算:
    • 这需要在该表的每一行上发生。
    • 算法经常变化。
    • 该算法... [使用] 某些列中的值来操作其他列
    • 当前处理时间为:7小时
  • 桌子:
    • 包含 800,000 行。
    • 有 38 列。
  • 应用后端:
  • 数据库为 SQL Server 2014 企业版。
  • 有一个为每一行调用的存储过程:

    • 这需要 50 毫秒(我假设是 avg)来运行。
    • 它返回大约 4000 行。
    • 定义(至少部分)是:

      SELECT AVG([AD_Sensor_Data])
                 OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING)
                 as 'AD_Sensor_Data'
      FROM   [AD_Points]
      WHERE  [FileID] = @FileID
      ORDER BY [RowID] ASC
      
      Run Code Online (Sandbox Code Playgroud)

我们可以推测:

接下来,我们可以一起查看所有这些数据点,看看我们是否可以综合其他细节来帮助我们找到一个或多个瓶颈,并指出解决方案,或者至少排除一些可能的解决方案。

评论中当前的想法是主要问题是 SQL Server 和 Excel 之间的数据传输。真的是这样吗?如果为 800,000 行中的每一行调用存储过程,并且每次调用(即每行)花费 50 毫秒,则加起来为 40,000 秒(不是毫秒)。这相当于 666 分钟 (hhmm ;-),或刚好超过 11 小时。然而,据说整个过程只需要 7 个小时就可以运行。我们已经超过了总时间 4 小时,我们甚至增加了时间来进行计算或将结果保存回 SQL Server。所以这里有些不对劲。

查看存储过程的定义,只有一个输入参数@FileID;上没有任何过滤器@RowID。所以我怀疑正在发生以下两种情况之一:

  • 这个存储过程实际上并不是每行被调用,而是每行被调用@FileID,这似乎跨越大约 4000 行。如果声明的 4000 行返回的数量相当一致,那么在 800,000 行中只有 200 行。200 次执行每次需要 50 毫秒,在这 7 个小时中只需要 10 秒。
  • 如果此存储过程确实为每一行调用,那么第一次@FileID传入新行不会花费稍长的时间将新行拉入缓冲池,但接下来的 3999 次执行通常会返回更快,因为已经被缓存,对吗?

我认为专注于这个“过滤器”存储过程,或者任何从 SQL Server 到 Excel 的数据传输,都是一种红鲱鱼

目前,我认为表现不佳的最相关指标是:

  • 有 800,000 行
  • 该操作一次只对一行进行
  • 该数据被保存回SQL服务器,因此“[用途]从一些列的值可以操纵其他栏目” [我的EM PHAS是;-)]

我怀疑:

  • 虽然在数据检索和计算方面有一些改进空间,但改进这些并不会显着减少处理时间。
  • 主要瓶颈是发出 800,000 个单独的UPDATE语句,也就是 800,000 个单独的事务。

我的建议(基于目前可用的信息):

  1. 您最大的改进领域是一次更新多行(即在一个事务中)。您应该更新您的流程以根据 eachFileID而不是 each来工作RowID。所以:

    1. 将特定的所有 4000 行读FileID入数组
    2. 该数组应包含表示正在操作的字段的元素
    3. 循环遍历数组,像当前一样处理每一行
    4. 一旦FileID计算了数组中的所有行(即对于这个特定的):
      1. 开始交易
      2. 调用每个更新 RowID
      3. 如果没有错误,提交事务
      4. 如果发生错误,回滚并适当处理
  2. 如果您的聚集索引尚未定义为,(FileID, RowID)那么您应该考虑这一点(正如@MikaelEriksson 在对问题的评论中所建议的那样)。它不会帮助这些单例更新,但它至少会稍微改进聚合操作,例如您在“过滤器”存储过程中所做的事情,因为它们都基于FileID.

  3. 您应该考虑将逻辑移至编译语言。我建议创建一个 .NET WinForms 应用程序甚至控制台应用程序。我更喜欢控制台应用程序,因为它很容易通过 SQL 代理或 Windows 计划任务进行计划。无论是在 VB.NET 还是 C# 中完成都无关紧要。VB.NET 可能更适合您的开发人员,但仍然会有一些学习曲线。

    在这一点上,我看不出有任何理由转向 SQLCLR。如果算法频繁更改,则必须一直重新部署程序集会很烦人。重建控制台应用程序并将 .exe 放入网络上正确的共享文件夹中,这样您只需运行相同的程序并且它恰好始终是最新的,应该很容易做到。

    如果问题是我怀疑的并且您一次只执行一个更新,我认为将处理完全转移到 T-SQL 中不会有帮助。

  4. 如果处理转移到 .NET 中,那么您可以使用表值参数 ( UPDATETVP ),这样您就可以将数组传递到存储过程中,该存储过程将调用该连接到 TVP 表变量,因此是单个事务. TVP 应该比将 4000INSERT秒分组到单个事务中要快。但是,INSERT在 1 个事务中使用超过 4000秒的TVP 所带来的收益可能不会像从 800,000 个单独的事务移动到每个 4000 行的仅 200 个事务时所看到的改进那么显着。

    TVP 选项本身不适用于 VBA 端,但有人提出了一个可能值得测试的解决方法:

    从 VBA 到 SQL Server 2008 R2 时如何提高数据库性能?

  5. 如果 filter proc 仅FileIDWHERE子句中使用,并且如果该 proc 确实每行都被调用,那么您可以通过缓存第一次运行的结果并将它们用于其余的行来节省一些处理时间FileID,对?

  6. 一旦你完成了每个 FileID的处理,那么我们就可以开始讨论并行处理了。但这在那时可能没有必要:)。鉴于您正在处理 3 个相当重要的非理想部分:Excel、VBA 和 800k 事务,任何关于 SSIS 或平行四边形或不知道是什么的讨论都是过早的优化/马前车类型的东西. 如果我们可以将这个 7 小时的流程缩短到 10 分钟或更短,您还会考虑其他方法来加快流程吗?您是否有一个目标完成时间?请记住,一旦对每个 FileID 进行处理 基础上,如果您有一个 VB.NET 控制台应用程序(即命令行 .EXE),那么无论是通过 SQL Agent CmdExec 步骤还是 Windows 计划任务,都不会阻止您一次运行几个 FileID :),等等。

而且,您始终可以采用“分阶段”方法并一次进行一些改进。例如从每个更新开始,FileID因此为该组使用一个事务。然后,看看是否可以让 TVP 工作。然后看看如何获​​取该代码并将其移动到 VB.NET(并且 TVP 在 .NET 中工作,因此它可以很好地移植)。


我们不知道的仍然可以提供帮助:

  • “过滤器”存储过程是按 RowID还是按 FileID运行?我们甚至有那个存储过程的完整定义吗?
  • 表的完整架构。这张桌子有多宽?有多少个可变长度字段?有多少字段可以为 NULL?如果有任何是 NULLable,有多少包含 NULL?
  • 此表的索引。是分区的吗?是否使用了 ROW 或 PAGE 压缩?
  • 以 MB/GB 为单位,此表有多大?
  • 这个表的索引维护是如何处理的?索引的碎片化程度如何?统计数据更新到什么程度?
  • 在这个 7 小时的进程发生时,是否有其他进程写入此表?可能的争用来源。
  • 在这个 7 小时的过程发生时,是否有任何其他过程从此表中读取?可能的争用来源。

更新1:

**关于 VBA (Visual Basic for Applications) 以及可以用它做什么似乎有些混乱,所以这只是为了确保我们都在同一个网页上:


更新 2:

还有一点需要考虑:如何处理连接?VBA 代码是按每个操作打开和关闭连接,还是在进程开始时打开连接并在进程结束时(即 7 小时后)关闭它?即使使用连接池(默认情况下,应该为 ADO 启用),打开和关闭一次与打开和关闭 800,200 或 1,600,000 次相比,仍然应该有相当大的影响。这些值基于至少 800,000 次 UPDATE 加上 200 或 800k 次 EXEC(取决于过滤器存储过程实际执行的频率)。

我上面概述的建议可以自动缓解连接过多的问题。通过创建一个事务并在该事务中执行所有更新,您将保持该连接打开并为每个UPDATE. 连接是否从初始调用开始保持打开状态以获取每个指定的 4000 行FileID,或者在“获取”操作之后关闭并再次打开以进行更新,影响要小得多,因为我们现在谈论的是两者之间的差异整个过程中总共有 200 或 400 个连接。

更新 3:

我做了一些快速测试。请记住,这是一个相当小规模的测试,而不是完全相同的操作(纯 INSERT 与 EXEC + UPDATE)。但是,与如何处理连接和事务相关的时间差异仍然相关,因此可以推断这些信息在此处具有相对相似的影响。

测试参数:

  • SQL Server 2012 开发人员版(64 位),SP2
  • 桌子:

     CREATE TABLE dbo.ManyInserts
     (
        RowID INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
        InsertTime DATETIME NOT NULL DEFAULT (GETDATE()),
        SomeValue BIGINT NULL
     );
    
    Run Code Online (Sandbox Code Playgroud)
  • 手术:

    INSERT INTO dbo.ManyInserts (SomeValue) VALUES ({LoopIndex * 12});
    
    Run Code Online (Sandbox Code Playgroud)
  • 每次测试的总插入次数:10,000
  • 每次测试重置:(TRUNCATE TABLE dbo.ManyInserts;鉴于此测试的性质,执行 FREEPROCCACHE、FREESYSTEMCACHE 和 DROPCLEANBUFFERS 似乎没有增加太多价值。)
  • 恢复模式:简单(日志文件中可能有 1 GB 可用空间)
  • 使用事务的测试只使用一个连接,而不管有多少事务。

结果:

Test                                   Milliseconds
-------                                ------------
10k INSERTs across 10k Connections     3968 - 4163
10k INSERTs across 1 Connection        3466 - 3654
10k INSERTs across 1 Transaction       1074 - 1086
10k INSERTs across 10 Transactions     1095 - 1169
Run Code Online (Sandbox Code Playgroud)

如您所见,即使到数据库的 ADO 连接已经在所有操作之间共享,使用显式事务(ADO 对象应该能够处理这个)将它们分组到批处理中也能保证显着(即超过 2 倍的改进)减少整体处理时间。