SQL Server 2016 并发限制?数据库并发调优

K_f*_*er9 6 performance sql-server functions sql-clr sql-server-2016 query-performance

我有一个包含约 10 亿条时间戳记录的表,每条记录都包含一个会话表的 FK(每天一个会话和每天 3-500,000 条记录),因此查找给定日期的记录只是一个整数连接。

我正在尝试分析此表中的数据(数据按会话分组),当使用客户端计算机上的 C# 控制台应用程序时,我可以在 70 分钟内运行完整的分析(每条记录)。当我尝试直接在 TSQL 中运行类似的分析时,需要 12 多个小时。我预计会有一些惩罚,因为 TSQL 查询使用标量函数和自定义聚合 (clr)。

我的问题:在 C# 中,我了解如何最大化和调整并发性,因此 70 分钟是一个调整过的数字。是否可以直接在 SQL 中调整最大并发的查询,还是最好留给 C# api?(我也可以在 R、数据库或外部完成这项工作,但 .Net 并发 API 给我留下了优越的印象。)

询问:

SELECT TypeNumber, SessionId, dbo.udf_SessionName([timestamp]) SessionName, 
CAST(max(price)-min(price) AS REAL) as Variance, sum(EventNumber) as Volume, 
dbo.Direction(price,[timestamp]) as MoveDirection
INTO temp.AnalysisResults
FROM MyTable
WHERE ISNULL(price,0)<>0
GROUP BY TypeNumber, SessionId, dbo.udf_SessionName([timestamp])
Run Code Online (Sandbox Code Playgroud)

杂项

  • 由于插入,为此查询启用了批量记录
  • 此查询中未使用主键(它是跨三个字段的复合键,此处不需要。但是,查询计划显示正在扫描此索引,而不是我在下面提到的索引(该计划最初推荐) )。
  • 行级压缩已启用
  • 数据跨越五年,每个月有一个只读文件组(按月分区);所有文件组都驻留在同一个 SSD 上(不太好,我知道)
  • 索引:非聚集在 SessionId asc 上,包括 TypeNumber、Timestamp、Price
  • 4 个 CPU 内核可用
  • 标量函数获取每个时间戳,使用 AT TIME ZONE(两次调用)将其转换为本地时间,并在包含 5 条记录的表中查找。
  • 自定义聚合使用自定义序列化程序,该序列化程序接受小数点和 datetime2 并返回一个字符串。序列化器传递然后需要解析的字符串(这不是很好)
  • 查看查询计划(删除了插入),迄今为止最昂贵的操作是排序(成本的 98%;我明确启动的唯一排序是在 clr 聚合器函数中): 查询计划

警告:我知道使用 CLR 聚合会消耗我的查询时间以及压缩时间。如果我使用控制台应用程序,我可以将所有分析工作卸载到更强大的机器上,让数据库服务器只做 IO。这是“显而易见的答案”,还是我可以将大部分工作保留在数据库中(通常,我可以直接在数据库中做的越多越好)。


我意识到通过这个数据库的设置方式和压缩设置,它更多地针对 IO 而不是 CPU。我不希望能够实现与数据库仅执行 IO 的纯 C 解决方案相提并论的性能;但是通过最大化 db 可以做的 cpu 工作可以获得很多好处。

udf_SessionName:

create function dbo.[udf_SessionName](@timestamp datetime2)
returns nvarchar(100)
begin
declare @localTime time = CAST(@timestamp at time zone 'UTC' at time zone 'Pacific Standard Time' as time)
declare @result nvarchar(100) = (select top 1 sessionname from MarketSessions where @localTime>=StartTime and @localTime < EndTime)
if (@result is null) set @result = 'European'
return @result
end
Run Code Online (Sandbox Code Playgroud)

SQL Fiddle 中的表结构


行动后报告:我已经实施了@SolomonRutzky 的建议,现在查询在 3 小时内完成,而不是 12+。

变更概要

  1. 将时区操作从标量 udf 更改为 clr 函数(SAFE没有的实现TimeZoneInfo)。
  2. 将该 clr 函数滚动到非持久化计算列中。
  3. 添加了一个新索引:

    CREATE NONCLUSTERED INDEX [inx_MyIndex] ON [dbo].MyTable (TypeNumber ASC, SessionId ASC) INCLUDE (SessionName,Price,Timestamp,Volume])

SessionName 作为索引中的键确实会更好,但即使它既精确又确定,因为它是一个 CLR 函数,除非它被持久化,否则它不能成为键,并且该列虽​​然大部分是静态的,但还不够静态要持久化。

  1. 已移除 ISNULL

修改后的查询

INSERT INTO temp.AnalysisResults
SELECT TypeNumber, SessionId, SessionName, 
CAST(max(price)-min(price) AS REAL) as Variance, sum(EventNumber) as Volume, 
dbo.Direction(price,[timestamp]) as MoveDirection
FROM MyTable
WHERE price <> 0 AND price IS NOT NULL
GROUP BY TypeNumber, SessionId, SessionName
Run Code Online (Sandbox Code Playgroud)

新的执行计划 更新的执行计划

Sol*_*zky 9

首先,我认为您需要尝试一些方法来减少 12 小时以上的查询:

  1. 我要检查的第一件事是索引。你有一个 GROUP BYTypeNumber, SessionId, dbo.udf_SessionName([timestamp])但索引是 on SessionId asc, INCLUDE TypeNumber,Timestamp,Price。意思是,顺序不一样(因此可能是索引被忽略而表被扫描的原因)。您至少需要索引TypeNumber, SessionId来匹配GROUP BY这些列的顺序。然后INCLUDE ([price], [timestamp], [EventNumber])使其成为覆盖索引。关于索引还有更多要说的,但这会导致下一部分......

  2. 接下来是标量 UDF。众所周知,由于多种原因,这些都是不好的。并且使用AT TIME ZONE不会非常快。因此,请考虑:

    1. 将其转换为内联 TVF 通常通常会产生奇迹,但不确定这是否会与最适合索引的内容发生冲突

    2. 使标量 UDF 变慢的部分原因是它们阻止了并行计划。标记为DataAccessand SystemDataAccessboth = noneAND 的SQLCLR 标量 UDFIsDeterministic=true 不会阻止并行计划:-)。如果要从 UTC 转换为本地时间(反之亦然),则可以使用可以在程序集中调用的类SAFE。如果您需要从不同的时区进行转换,那么您需要使用TimezoneInfo该类(我认为)并且这要求将程序集标记为UNSAFE. 存在UNSAFE不会剥夺允许并行计划的好处,但如果可能的话SAFE,那就去做吧。

    3. SQLCLR UDF 方法的一个复杂问题是您正在查找表:MarketSessions. 你提到这只是 5 行。那 5 行是相当静态的吗?如果是这样,您可能仍然可以通过在程序集中创建一个静态集合,然后在静态类构造函数中从表中填充它,使SQLCLR UDF进行任何数据访问。静态类构造函数将在加载程序集的任何时候执行,并且可以使用在udf_SessionNameUDF 中检查所需的值预填充集合。唯一的问题是context connection静态类构造函数中没有可用的内部,因此需要将程序集标记为EXTERNAL_ACCESS. 但是UDF不会调用SqlConnection,它只会从静态集合中读取:-)。

      如果其中的值MarketSessions更易变,您始终可以创建一个 SQLCLR UDF 或存储过程,它们调用相同的方法来填充类构造函数调用的静态集合。然后您可以在运行此查询之前执行它,以便内部静态集合具有该表中的“当前”记录。但是,在这种情况下,您可能无法执行以下两个步骤,因为索引值可能过时/不正确。但是您仍然会受益于能够制定并行计划。

    4. 无论您是添加(如果尚未这样做)WITH SCHEMABINDING到 T-SQL UDF还是将其转换为具有上述设置的属性的 SQLCLR UDF,您都应该向表中添加一列作为非持久计算列对 UDF 的调用。

    5. 一旦非持久化计算列存在,您就可以在以下位置创建实际索引: TypeNumber, SessionId, computedColumn INCLUDE ([price], [timestamp], [EventNumber])。您可能还需要设置SQLCLR UDF的SqlFunction属性IsPrecise=true以使其可索引。

  3. 可能需要重新考虑ISNULL. price可能只是一INCLUDE列,因此 ISNULL 函数可能不会在此处使用索引带来问题,但您可能需要将其分解为price <> 0 AND price IS NOT NULL.

  4. 我不确定对整体性能的影响,但我从来都不是这种SELECT...INTO结构的粉丝。最好先创建表然后再做INSERT INTO...SELECT

  5. 正如@LowlyDBA 在对该问题的评论中指出的那样:谨慎使用REAL(或FLOAT) 来获取财务价值。是的,它们压缩得更好,传输速度更快,但它们有时也可以在非常低的端获得额外的价值。如果您进行计算,我当然不会使用该数据类型。但仅仅传递回应用程序可能没问题。但是如果你在做计算,你真的应该使用DECIMAL()甚至MONEY.