Pரத*_*ீப் 4 performance sql-server update query-performance
正在尝试优化程序。过程中有 3 个不同的更新查询。
update #ResultSet
set MajorSector = case
when charindex(' ', Sector) > 2 then rtrim(ltrim(substring(Sector, 0, charindex(' ', Sector))))
else ltrim(rtrim(sector))
end
update #ResultSet
set MajorSector = substring(MajorSector, 5, len(MajorSector)-4)
where left(MajorSector,4) in ('(00)','(01)','(02)','(03)','(04)','(05)','(06)','(07)','(08)','(09)')
update #ResultSet
set MajorSector = substring(MajorSector, 4, len(MajorSector)-3)
where left(MajorSector,3) in ('(A)','(B)','(C)','(D)','(E)','(F)','(G)','(H)','(I)','(J)','(K)','(L)','(M)','(N)','(O)','(P)','(Q)','(R)','(S)','(T)','(U)','(V)','(W)','(X)','(Y)','(Z)')
Run Code Online (Sandbox Code Playgroud)
要完成所有三个更新查询,只需不到10 秒。
所有三个更新查询的执行计划。
https://www.brentozar.com/pastetheplan/?id=r11BLfq7b
我的计划是将三个不同的更新查询变成一个更新查询,这样可以减少I/O。
;WITH ResultSet
AS (SELECT CASE
WHEN LEFT(temp_MajorSector, 4) IN ( '(00)', '(01)', '(02)', '(03)', '(04)', '(05)', '(06)', '(07)', '(08)', '(09)' )
THEN Substring(temp_MajorSector, 5, Len(temp_MajorSector) - 4)
WHEN LEFT(temp_MajorSector, 3) IN ( '(A)', '(B)', '(C)', '(D)','(E)', '(F)', '(G)', '(H)','(I)', '(J)', '(K)', '(L)','(M)', '(N)', '(O)', '(P)','(Q)', '(R)', '(S)', '(T)','(U)', '(V)', '(W)', '(X)','(Y)', '(Z)' )
THEN Substring(temp_MajorSector, 4, Len(temp_MajorSector) - 3)
ELSE temp_MajorSector
END AS temp_MajorSector,
MajorSector
FROM (SELECT temp_MajorSector = CASE
WHEN Charindex(' ', Sector) > 2 THEN Rtrim(Ltrim(Substring(Sector, 0, Charindex(' ', Sector))))
ELSE Ltrim(Rtrim(sector))
END,
MajorSector
FROM #ResultSet)a)
UPDATE ResultSet
SET MajorSector = temp_MajorSector
Run Code Online (Sandbox Code Playgroud)
但这需要大约1 分钟才能完成。我检查了执行计划,它与 first update query 相同。
上述查询的执行计划:
https://www.brentozar.com/pastetheplan/?id=SJvttz9QW
有人可以解释为什么它很慢吗?
用于测试的虚拟数据:
If object_id('tempdb..#ResultSet') is not null
drop table #ResultSet
;WITH lv0 AS (SELECT 0 g UNION ALL SELECT 0)
,lv1 AS (SELECT 0 g FROM lv0 a CROSS JOIN lv0 b) -- 4
,lv2 AS (SELECT 0 g FROM lv1 a CROSS JOIN lv1 b) -- 16
,lv3 AS (SELECT 0 g FROM lv2 a CROSS JOIN lv2 b) -- 256
,lv4 AS (SELECT 0 g FROM lv3 a CROSS JOIN lv3 b) -- 65,536
,lv5 AS (SELECT 0 g FROM lv4 a CROSS JOIN lv4 b) -- 4,294,967,296
,Tally (n) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM lv5)
SELECT CONVERT(varchar(255), NEWID()) as Sector,cast('' as varchar(1000)) as MajorSector
into #ResultSet
FROM Tally
where n <= 242906 -- my original table record count
ORDER BY n;
Run Code Online (Sandbox Code Playgroud)
注意:由于这不是我的原始数据,因此我上面提到的时间可能略有不同。单个更新查询仍然比前三个查询慢得多。
我尝试执行查询超过 10 次,以确保外部因素不会影响性能。前三个更新的所有 10 次都比上一次更新运行得快得多。
第一次更新读取和写入表中的每一行。第二个和第三个然后重新读取和重新写入这些行的子集。看看Actual Number of Rows
. 当这三个语句组合成一个时,优化器认为如果它必须读取所有内容以满足第一个更改,那么它可以为第二个和第三个更改捎带。
查看查询计划的 XML 版本,特别是<ComputeScalar>
运算符和<ScalarOperator ScalarString="">
部分。在最初的计划中,您会看到每个计划都相对简单,并且非常接近于 SQL。对于多合一计划,它是一个怪物。这是优化器将 SQL 重写为逻辑上等效的形式。计划通过将每一行通过运算符一次来工作1。当该行经过一次时,优化器正在做它必须做的所有工作来满足所有三个更改。
我希望第二个查询更快,因为数据只被读取和写入一次,而在第一个查询中它被触摸了 3 次。
由于第二个查询没有谓词(没有 WHERE 子句),优化器别无选择,只能读取每一行并对其进行处理。我很惊讶第二种形式比第一种形式花费的时间更长。两者都是从干净的缓冲区开始吗?系统上是否还有其他工作正在发生?由于它正在读取和写入临时表,因此 IO 发生在 tempdb 中。是否有文件增长或类似的事情发生?
通过一种措施,您已经实现了您想要的结果。您说您想进行更改“以便可以减少 IO”。多合一的 IO 比三个单独的语句的总和要少。然而,我怀疑您真正想要的是减少经过的时间,而这显然不会发生。
1或多或少,省略了很多细节。
我运行您的例程来生成测试数据,然后运行三个单更新语句和多合一语句。尽管存在一些差异(没有聚集索引,没有并行性),但我或多或少地得到了相同的结果。具体来说,计划大致相同,三个单独的查询在大约两秒内完成,一个大查询在大约三十到三十五秒内完成。
我设置
set nocount off;
set statistics io on;
set statistics time on;
Run Code Online (Sandbox Code Playgroud)
通过缓存中的计划和内存中的数据,我得到:
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.
Table '#ResultSet...'. Scan count 1, logical reads 125223, physical reads 0
SQL Server Execution Times:
CPU time = 1422 ms, elapsed time = 1417 ms.
(242906 row(s) affected)
Table '#ResultSet...'. Scan count 1, logical reads 125223, physical reads 0
SQL Server Execution Times:
CPU time = 344 ms, elapsed time = 337 ms.
(0 row(s) affected)
Table '#ResultSet...'. Scan count 1, logical reads 125223, physical reads 0
SQL Server Execution Times:
CPU time = 734 ms, elapsed time = 747 ms.
(0 row(s) affected)
Run Code Online (Sandbox Code Playgroud)
我删除了一些不相关的部分。由于physical reads
所有三个表都为零,因此该表适合内存。logical reads
所有三个都是一样的,这是有道理的。由于没有索引,唯一的方法是扫描表的每一行。第二个和第三个查询影响零行,因为我已经运行了几次。CPU 时间和已用时间为 2500 毫秒。
对于更大的查询,它是
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.
Table '#ResultSet...'. Scan count 1, logical reads 125223
SQL Server Execution Times:
CPU time = 33093 ms, elapsed time = 33137 ms.
(242906 row(s) affected)
Run Code Online (Sandbox Code Playgroud)
读取相同数量的页面,更新相同数量的行。巨大的差异是 CPU 时间。这反映在任务管理器的偶然观察中,它显示查询执行期间的利用率为 30%。问题是,为什么需要这么多?
单个查询分别具有简单的计算,其中两个语句具有谓词,可大大减少接触的行数。优化器有很好的启发式方法来处理这些并找到一个快速的计划。多合一查询应用怪物Compute Scalar
针对每一行。我的建议是,无论出于何种原因,优化器都无法将逻辑分解为一个快速运行并最终使用大量 CPU 的计划。优化器必须使用它给定的内容,在第二种情况下是复杂的嵌套 SQL。也许通过重构 SQL 优化器将遵循不同的启发式并获得更好的结果?也许一些(过滤的)索引或(过滤的)统计数据会说服它写一个不同的计划。也许持久化的计算列会帮助它。也许您只需要为优化器提供所需的东西,而您的第一次尝试确实是可以实现的最佳尝试,您需要找到一种方法来并行运行这三个。对不起,我不能更科学。
以后请注意您的测试数据。查询计划表明您的表上有聚集索引,但临时表没有聚集索引。在某些情况下,这可能会产生很大的不同。在我的机器上,三种UPDATE
方法在 3 秒内UPDATE
运行,单一方法在 5 秒内运行。与您看到的差异并不接近,但它似乎仍然有些违反直觉。单曲不是UPDATE
应该更快吗?
正如迈克尔格林在他的回答中指出的那样,这里的问题在于计算标量运算符。查询优化器不太擅长估算计算标量的成本。三组的第一次更新和第二次单独更新的查询计划可能看起来相同,但计算标量所做的工作量有很大差异。我们实际上可以获取代码并进行一些更改以将其转换为有效的SELECT
查询。查询是巨大的,完整的代码在这里。下面是一个大大简化的版本:
SELECT
(CONVERT(varchar(1000), CASE
WHEN SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (1), (4)) = '(09)' OR
...
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (1), (4)) = '(00)' THEN SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (5), LEN(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END) - (4))
ELSE CASE
WHEN SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (1), (3)) = '(Z)' OR
...
END, (1), (3)) = '(B)' OR
SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (1), (3)) = '(A)' THEN SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (4), LEN(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END) - (3))
ELSE CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END
END
END, 0))
FROM [#ResultSet];
Run Code Online (Sandbox Code Playgroud)
CASE
报表中的所有这些重复计算都不好。作为计算标量的一部分,SQL Server 可能会一遍又一遍地运行相同的计算。如果我只运行SELECT
它大约需要 3 秒,这是第一组UPDATE
查询的时间。
将重复的标量计算放在APPLY
派生表中通常可以提高查询的可读性。在某些情况下,它还可以显着提高性能。我接受了那个大查询,并通过将重复的表达式移动到APPLY
派生表来简化它。进一步的简化是可能的,但这应该给你基本的想法:
SELECT
(CONVERT(varchar(1000), CASE
WHEN a.sub4 IN ('(09)','(08)','(07)','(06)','(05)','(04)','(03)','(02)','(01)','(00)')
THEN SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN a.r_trim
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (5), LEN(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN a.r_trim
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END) - (4))
ELSE CASE
WHEN a.sub3 IN ('(Z)','(Y)','(X)','(W)','(V)','(U)','(T)','(S)','(R)','(Q)','(P)','(O)','(N)','(M)','(L)','(K)','(J)','(I)','(H)','(G)','(F)','(E)','(D)','(C)','(B)','(A)')
THEN SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN a.r_trim
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (4), LEN(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN a.r_trim
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END) - (3))
ELSE CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN a.r_trim
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END
END
END, 0))
FROM [#ResultSet]
OUTER APPLY
(
SELECT SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (1), (4))
, SUBSTRING(CASE
WHEN CHARINDEX(' ', [#ResultSet].[Sector]) > (2) THEN RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
ELSE LTRIM(RTRIM([#ResultSet].[Sector]))
END, (1), (3))
, RTRIM(LTRIM(SUBSTRING([#ResultSet].[Sector], (0), CHARINDEX(' ', [#ResultSet].[Sector]))))
) a (sub4, sub3, r_trim);
Run Code Online (Sandbox Code Playgroud)
现在SELECT
查询运行时间不到 1 秒。我使用OUTER APPLY
这样 SQL Server 会APPLY
为每一行计算一次派生表中的所有内容,而不是将其折叠到计算标量中。计算标量仍在查询计划中,但它的工作量比以前少得多:
如果我将该代码插入 CTE 以进行UPDATE
查询,则会得到以下性能数据:
CPU 时间 = 2125 毫秒,经过时间 = 2134 毫秒。
这比原始的三个查询集快一点:
CPU 时间 = 1734 毫秒,经过时间 = 1735 毫秒
CPU 时间 = 187 毫秒,经过时间 = 197 毫秒。
CPU 时间 = 343 毫秒,已用时间 = 368 毫秒。
可能会进一步优化单独查询,但我会将其留给您。
归档时间: |
|
查看次数: |
558 次 |
最近记录: |