Maz*_*har 8 performance sql-server optimization functions sql-server-2016 query-performance
我正在尝试调整在 20 列上调用相同表值函数 (TVF) 的查询。
我做的第一件事是将标量函数转换为内联表值函数。
是否使用性能CROSS APPLY
最佳的方式在查询中的多个列上执行相同的函数?
一个简单的例子:
SELECT Col1 = A.val
,Col2 = B.val
,Col3 = C.val
--do the same for other 17 columns
,Col21
,Col22
,Col23
FROM t
CROSS APPLY
dbo.function1(Col1) A
CROSS APPLY
dbo.function1(Col2) B
CROSS APPLY
dbo.function1(Col3) C
--do the same for other 17 columns
Run Code Online (Sandbox Code Playgroud)
有更好的选择吗?
可以在针对 X 个列的多个查询中调用相同的函数。
这是函数:
CREATE FUNCTION dbo.ConvertAmountVerified_TVF
(
@amt VARCHAR(60)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH cteLastChar
AS(
SELECT LastChar = RIGHT(RTRIM(@amt), 1)
)
SELECT
AmountVerified = CAST(RET.Y AS NUMERIC(18,2))
FROM (SELECT 1 t) t
OUTER APPLY (
SELECT N =
CAST(
CASE
WHEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
THEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0)-1
WHEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0) >0
THEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0)-1
WHEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0) >0
THEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0)-1
ELSE
NULL
END
AS VARCHAR(1))
FROM
cteLastChar L
) NUM
OUTER APPLY (
SELECT N =
CASE
WHEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
THEN 0
WHEN CHARINDEX(L.LastChar COLLATE Latin1_General_CS_AS, 'JKLMNOPQRpqrstuvwxy', 0) >0
THEN 1
ELSE 0
END
FROM cteLastChar L
) NEG
OUTER APPLY(
SELECT Amt= CASE
WHEN NUM.N IS NULL
THEN @amt
ELSE
SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + Num.N
END
) TP
OUTER APPLY(
SELECT Y = CASE
WHEN NEG.N = 0
THEN (CAST(TP.Amt AS NUMERIC) / 100)
WHEN NEG.N = 1
THEN (CAST (TP.Amt AS NUMERIC) /100) * -1
END
) RET
) ;
GO
Run Code Online (Sandbox Code Playgroud)
这是我继承的标量函数版本,如果有人感兴趣的话:
CREATE FUNCTION dbo.ConvertAmountVerified
(
@amt VARCHAR(50)
)
RETURNS NUMERIC (18,3)
AS
BEGIN
-- Declare the return variable here
DECLARE @Amount NUMERIC(18, 3);
DECLARE @TempAmount VARCHAR (50);
DECLARE @Num VARCHAR(1);
DECLARE @LastChar VARCHAR(1);
DECLARE @Negative BIT ;
-- Get Last Character
SELECT @LastChar = RIGHT(RTRIM(@amt), 1) ;
SELECT @Num = CASE @LastChar collate latin1_general_cs_as
WHEN '{' THEN '0'
WHEN 'A' THEN '1'
WHEN 'B' THEN '2'
WHEN 'C' THEN '3'
WHEN 'D' THEN '4'
WHEN 'E' THEN '5'
WHEN 'F' THEN '6'
WHEN 'G' THEN '7'
WHEN 'H' THEN '8'
WHEN 'I' THEN '9'
WHEN '}' THEN '0'
WHEN 'J' THEN '1'
WHEN 'K' THEN '2'
WHEN 'L' THEN '3'
WHEN 'M' THEN '4'
WHEN 'N' THEN '5'
WHEN 'O' THEN '6'
WHEN 'P' THEN '7'
WHEN 'Q' THEN '8'
WHEN 'R' THEN '9'
---ASCII
WHEN 'p' Then '0'
WHEN 'q' Then '1'
WHEN 'r' Then '2'
WHEN 's' Then '3'
WHEN 't' Then '4'
WHEN 'u' Then '5'
WHEN 'v' Then '6'
WHEN 'w' Then '7'
WHEN 'x' Then '8'
WHEN 'y' Then '9'
ELSE ''
END
SELECT @Negative = CASE @LastChar collate latin1_general_cs_as
WHEN '{' THEN 0
WHEN 'A' THEN 0
WHEN 'B' THEN 0
WHEN 'C' THEN 0
WHEN 'D' THEN 0
WHEN 'E' THEN 0
WHEN 'F' THEN 0
WHEN 'G' THEN 0
WHEN 'H' THEN 0
WHEN 'I' THEN 0
WHEN '}' THEN 1
WHEN 'J' THEN 1
WHEN 'K' THEN 1
WHEN 'L' THEN 1
WHEN 'M' THEN 1
WHEN 'N' THEN 1
WHEN 'O' THEN 1
WHEN 'P' THEN 1
WHEN 'Q' THEN 1
WHEN 'R' THEN 1
---ASCII
WHEN 'p' Then '1'
WHEN 'q' Then '1'
WHEN 'r' Then '1'
WHEN 's' Then '1'
WHEN 't' Then '1'
WHEN 'u' Then '1'
WHEN 'v' Then '1'
WHEN 'w' Then '1'
WHEN 'x' Then '1'
WHEN 'y' Then '1'
ELSE 0
END
-- Add the T-SQL statements to compute the return value here
if (@Num ='')
begin
SELECT @TempAmount=@amt;
end
else
begin
SELECT @TempAmount = SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + @Num;
end
SELECT @Amount = CASE @Negative
WHEN 0 THEN (CAST(@TempAmount AS NUMERIC) / 100)
WHEN 1 THEN (CAST (@TempAmount AS NUMERIC) /100) * -1
END ;
-- Return the result of the function
RETURN @Amount
END
Run Code Online (Sandbox Code Playgroud)
样本测试数据:
SELECT dbo.ConvertAmountVerified('00064170') -- 641.700
SELECT * FROM dbo.ConvertAmountVerified_TVF('00064170') -- 641.700
SELECT dbo.ConvertAmountVerified('00057600A') -- 5760.010
SELECT * FROM dbo.ConvertAmountVerified_TVF('00057600A') -- 5760.010
SELECT dbo.ConvertAmountVerified('00059224y') -- -5922.490
SELECT * FROM dbo.ConvertAmountVerified_TVF('00059224y') -- -5922.490
Run Code Online (Sandbox Code Playgroud)
首先:应该提到的是,获得所需结果的绝对最快的方法是执行以下操作:
{name}_new
向具有DECIMAL(18, 3)
数据类型的表添加新列VARCHAR
列一次性迁移到DECIMAL
列{name}_old
{name}
{table_name}_new
使用DECIMAL(18, 3)
数据类型创建新表DECIMAL
表。_old
_new
从新表中删除话虽如此:您可以删除很多代码,因为它在很大程度上是不必要的重复。此外,至少有两个错误会导致输出有时不正确,有时会引发错误。这些错误被复制到 Joe 的代码中,因为它产生与 OP 代码相同的结果(包括错误)。例如:
这些值产生正确的结果:
00062929x
00021577E
00000509H
Run Code Online (Sandbox Code Playgroud)这些值会产生不正确的结果:
00002020Q
00016723L
00009431O
00017221R
Run Code Online (Sandbox Code Playgroud)此值会产生错误:
00062145}
anything ending with "}"
Run Code Online (Sandbox Code Playgroud)使用 将所有 3 个版本与 448,740 行进行比较SET STATISTICS TIME ON;
,它们的运行时间都刚刚超过 5000 毫秒。但是对于 CPU 时间,结果是:
设置:数据
下面创建一个表并填充它。这应该在运行 SQL Server 2017 的所有系统中创建相同的数据集,因为它们在spt_values
. 这有助于为在其系统上进行测试的其他人之间进行比较提供基础,因为随机生成的数据会影响系统间的时间差异,如果重新生成样本数据,甚至同一系统上的测试之间的时间差异也会被考虑在内。我开始使用与 Joe 相同的 3 列表,但使用问题中的示例值作为模板来提出各种数值,并附加每个可能的尾随字符选项(包括无尾随字符)。这也是我在列上强制使用排序规则的原因:我不希望我使用二进制排序规则实例来不公平地否定使用COLLATE
关键字以在 TVF 中强制使用不同的排序规则)。
唯一的区别在于表中行的顺序。
USE [tempdb];
SET NOCOUNT ON;
CREATE TABLE dbo.TestVals
(
[TestValsID] INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
[Col1] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL,
[Col2] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL,
[Col3] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL
);
;WITH cte AS
(
SELECT (val.[number] + tmp.[blah]) AS [num]
FROM [master].[dbo].[spt_values] val
CROSS JOIN (VALUES (1), (7845), (0), (237), (61063), (999)) tmp(blah)
WHERE val.[number] BETWEEN 0 AND 1000000
)
INSERT INTO dbo.TestVals ([Col1], [Col2], [Col3])
SELECT FORMATMESSAGE('%08d%s', cte.[num], tab.[col]) AS [Col1],
FORMATMESSAGE('%08d%s', ((cte.[num] + 2) * 2), tab.[col]) AS [Col2],
FORMATMESSAGE('%08d%s', ((cte.[num] + 1) * 3), tab.[col]) AS [Col3]
FROM cte
CROSS JOIN (VALUES (''), ('{'), ('A'), ('B'), ('C'), ('D'), ('E'), ('F'),
('G'), ('H'), ('I'), ('}'), ('J'), ('K'), ('L'), ('M'), ('N'),
('O'), ('P'), ('Q'), ('R'), ('p'), ('q'), ('r'), ('s'), ('t'),
('u'), ('v'), ('w'), ('x'), ('y')) tab(col)
ORDER BY NEWID();
-- 463698 rows
Run Code Online (Sandbox Code Playgroud)
设置:TVF
GO
CREATE OR ALTER FUNCTION dbo.ConvertAmountVerified_Solomon
(
@amt VARCHAR(50)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
WITH ctePosition AS
(
SELECT CHARINDEX(RIGHT(RTRIM(@amt), 1) COLLATE Latin1_General_100_BIN2,
'{ABCDEFGHI}JKLMNOPQRpqrstuvwxy') AS [Value]
),
cteAppend AS
(
SELECT pos.[Value] AS [Position],
IIF(pos.[Value] > 0,
CHAR(48 + ((pos.[Value] - 1) % 10)),
'') AS [Value]
FROM ctePosition pos
)
SELECT (CONVERT(DECIMAL(18, 3),
IIF(app.[Position] > 0,
SUBSTRING(RTRIM(@amt), 1, LEN(@amt) - 1) + app.[Value],
@amt))
/ 100. )
* IIF(app.[Position] > 10, -1., 1.) AS [AmountVerified]
FROM cteAppend app;
GO
Run Code Online (Sandbox Code Playgroud)
请注意:
_BIN2
比区分大小写的排序规则更快的二进制(即)排序规则,因为它不需要考虑任何语言规则。VARCHAR(50)
到VARCHAR(60)
,从NUMERIC (18,3)
到NUMERIC (18,2)
(良好的理由是“他们错了”),那么我会坚持带有原始签名/类型。100.
,-1.
,和1.
。这不是我这个 TVF 的原始版本(在这个答案的历史中),但我注意到CONVERT_IMPLICIT
XML 执行计划中有一些调用(因为100
是 anINT
但操作需要是NUMERIC
/ DECIMAL
)所以我只是提前处理了.CHAR()
函数创建了一个字符串字符,而不是将一个数字(例如'2'
)的字符串版本传递给一个CONVERT
函数(这是我最初所做的,再次在历史记录中)。这似乎要稍微快一点。只有几毫秒,但仍然如此。测试
请注意,我必须过滤掉以}
as结尾的行,因为这会导致 OP 和 Joe 的 TVF 出错。虽然我的代码处理}
正确,但我希望与在 3 个版本中测试的行保持一致。这就是为什么设置查询生成的行数略高于我在上面提到的测试结果中所测试的行数的原因。
SET STATISTICS TIME ON;
DECLARE @Dummy DECIMAL(18, 3);
SELECT --@Dummy = -- commented out = results to client; uncomment to not return results
cnvrtS.[AmountVerified]
FROM dbo.TestVals vals
CROSS APPLY dbo.ConvertAmountVerified_Solomon(vals.[Col1]) cnvrtS
WHERE RIGHT(vals.[Col1], 1) <> '}'; -- filter out rows that cause error in O.P.'s code
SET STATISTICS TIME OFF;
GO
Run Code Online (Sandbox Code Playgroud)
取消注释时--@Dummy =
,CPU 时间仅略低,并且 3 个 TVF 之间的排名相同。但有趣的是,当取消对变量的注释时,排名会发生一些变化:
不知道为什么 OP 的代码在这种情况下会表现得更好(而我和 Joe 的代码仅略有改进),但在许多测试中似乎是一致的。不,我没有查看执行计划差异,因为我没有时间进行调查。
更快
我已经完成了对替代方法的测试,它确实对上面显示的内容提供了轻微但明确的改进。新方法使用 SQLCLR,它的伸缩性似乎更好。我发现在将第二列添加到查询中时,T-SQL 方法的时间加倍。但是,当使用 SQLCLR 标量 UDF 添加其他列时,时间会增加,但与单列计时的时间不同。也许在调用 SQLCLR 方法时有一些初始开销(与应用程序域和程序集初始加载到应用程序域的开销无关),因为时间是(经过的时间,而不是 CPU 时间):
因此,时间(转储到变量,而不是返回结果集)可能有 200 毫秒 - 250 毫秒的开销,然后每个实例时间有 750 毫秒 - 800 毫秒。CPU 计时分别为:对于 1、2 和 3 个 UDF 实例,分别为 950 毫秒、1750 毫秒和 2400 毫秒。
代码
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class Transformations
{
private const string _CHARLIST_ = "{ABCDEFGHI}JKLMNOPQRpqrstuvwxy";
[SqlFunction(IsDeterministic = true, IsPrecise = true,
DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None)]
public static SqlDouble ConvertAmountVerified_SQLCLR(
[SqlFacet(MaxSize = 50)] SqlString Amt)
{
string _Amount = Amt.Value.TrimEnd();
int _LastCharIndex = (_Amount.Length - 1);
int _Position = _CHARLIST_.IndexOf(_Amount[_LastCharIndex]);
if (_Position >= 0)
{
char[] _TempAmount = _Amount.ToCharArray();
_TempAmount[_LastCharIndex] = char.ConvertFromUtf32(48 + (_Position % 10))[0];
_Amount = new string(_TempAmount);
}
decimal _Return = decimal.Parse(_Amount) / 100M;
if (_Position > 9)
{
_Return *= -1M;
}
return new SqlDouble((double)_Return);
}
}
Run Code Online (Sandbox Code Playgroud)
我最初用作SqlDecimal
返回类型,但与SqlDouble
/相比,使用它会降低性能FLOAT
。有时 FLOAT 有问题(因为它是一个不精确的类型),但我通过以下查询对 T-SQL TVF 进行了验证,没有检测到差异:
00062929x
00021577E
00000509H
Run Code Online (Sandbox Code Playgroud)
测试
SET STATISTICS TIME ON;
DECLARE @Dummy DECIMAL(18, 3), @Dummy2 DECIMAL(18, 3), @Dummy3 DECIMAL(18, 3);
SELECT @Dummy =
dbo.ConvertAmountVerified_SQLCLR(vals.[Col1])
, @Dummy2 =
dbo.ConvertAmountVerified_SQLCLR(vals.[Col2])
, @Dummy3 =
dbo.ConvertAmountVerified_SQLCLR(vals.[Col3])
FROM dbo.TestVals vals
WHERE RIGHT(vals.[Col1], 1) <> '}';
SET STATISTICS TIME OFF;
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
6661 次 |
最近记录: |