It is enough to move the scalar expression in to UDF and query performance will be several times reduced.
Let us check ...
For testing, we need a test table with the dates from about a thousand years:
-- Recreate test table
IF OBJECT_ID('dbo.TestUDFs') IS NOT NULL
DROP TABLE dbo.TestUDFs;
GO
CREATE TABLE dbo.TestUDFs(
Datum Date
);
GO
-- SET NOCOUNT ON to prevent extra log messages returned as part of the result set:
-- (See: https://msdn.microsoft.com/en-us/library/ms189837%28v=sql.110%29.aspx)
SET NOCOUNT ON;
-- Fill test-table
DECLARE
@Date Date = GetDate(),
@i INT = 0;
WHILE @i < 1000 * 365 BEGIN
SET @Date = DATEADD(day, 1, @Date);
INSERT INTO dbo.TestUDFs VALUES (@Date);
SET @i = @i + 1;
END;
-- restore NOCOUNT
SET NOCOUNT OFF;
GO
SELECT CNT = COUNT(*) FROM dbo.TestUDFs;
GOWe will create four different UDFs:
FIRST:
Scalar-value UDF, which uses variables for intermediate calculations.
The body of function is placed into BEGIN-END container.
-- Recreate scalar-value UDF with variables in body
IF OBJECT_ID('dbo.fn_scalar_with_vars') IS NOT NULL
DROP FUNCTION dbo.fn_scalar_with_vars;
GO
CREATE FUNCTION dbo.fn_scalar_with_vars (@date DATETIME)
RETURNS INT
AS BEGIN
DECLARE @DateKey INT;
SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @date, 112));
RETURN @DateKey;
END;
GO
SELECT dbo.fn_scalar_with_vars(GetDate());
GOSECOND:
Scalar-value UDF, which does not use intermediate calculations, and almost immediately returns the result of the calculation.
The body of function is placed into BEGIN-END container.
-- Recreate scalar-value UDF with direct expression (without variables) in body
IF OBJECT_ID('dbo.fn_scalar_direct_expression') IS NOT NULL
DROP FUNCTION dbo.fn_scalar_direct_expression;
GO
CREATE FUNCTION dbo.fn_scalar_direct_expression (@date DATETIME)
RETURNS INT
AS BEGIN
RETURN CONVERT(INT, CONVERT(VARCHAR(8), @date, 112));
END;
GO
SELECT DateKey = dbo.fn_scalar_direct_expression(GetDate());
GOScalar-value UDF which returns not scalar value, but a table-result.For data storage uses UDF a table variable.
The body of function is placed into BEGIN-END container.
-- Recreate table-value UDF which returns table-variable
IF OBJECT_ID('dbo.fn_non_inline_table') IS NOT NULL
DROP FUNCTION dbo.fn_non_inline_table;
GO
CREATE FUNCTION dbo.fn_non_inline_table (@date DATETIME)
RETURNS @DateKey TABLE(DateKey INT)
AS BEGIN
INSERT INTO @DateKey
VALUES (CONVERT(INT, CONVERT(VARCHAR(8), @date, 112)));
RETURN;
END
GO
SELECT DateKey FROM dbo.fn_non_inline_table(GetDate());
GOFinally the UDF, which returns a non-scalar value, but a table.There is no any BEGIN-END container, and the expression is placed immediately after the AS expression.
Such UDFs are called "Inline Table-Value UDF".
-- Recreate inlide table-value UDF
IF OBJECT_ID('dbo.fn_inline_table') IS NOT NULL
DROP FUNCTION dbo.fn_inline_table;
GO
CREATE FUNCTION dbo.fn_inline_table (@date DATETIME)
RETURNS TABLE
AS
RETURN SELECT DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @date, 112));
GO
SELECT DateKey FROM dbo.fn_inline_table(GetDate());
GOLet us test the direct evaluation of the expression in the query, and calls of all four UDFs.
Tests were performed on a virtual machine running on my NoteBook-e.
On other computers the results may differ.
The test script is defined so:
- in the first step we will store the current date and time in the variable
- for each row of the test table we will performed a calculation
- the test request substitute this text: / * Put calculation there * /.
We will count the number of milliseconds between start and time.
-- Script to test calculations
DECLARE
@DateKey INT,
@StartedAt DATETIME2 = GETDATE();
/* Put calculation there */
PRINT 'Elapsed time (Direct calculation): '
+ CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';Calculation directly in query:
300 ms.
SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), Datum, 112)) FROM dbo.TestUDFs;
Scalar-value UDF with variables and body:
1570 ms.
SELECT @DateKey = dbo.fn_scalar_with_vars(Datum) FROM dbo.TestUDFs;
Scalar-value UDF with direct expression and body:
1274 ms.
SELECT @DateKey = dbo.fn_scalar_direct_expression(Datum) FROM dbo.TestUDFs;
Non-inline table-value UDF with table-variable and body:
29623 ms.
SELECT @DateKey = DateKey FROM dbo.TestUDFs CROSS APPLY dbo.fn_non_inline_table(Datum);
And now surprise - Inline table-value UDF:
210 ms!!! - at least the same result as in the first case, where a computation request occurs immediately.
SELECT @DateKey = DateKey FROM dbo.TestUDFs CROSS APPLY dbo.fn_inline_table(Datum);
All-in-one Script (an execution takes about one minute):
-- SET NOCOUNT ON to prevent extra log messages:
-- See: https://msdn.microsoft.com/en-us/library/ms189837%28v=sql.110%29.aspx
SET NOCOUNT ON;
GO
IF OBJECT_ID('dbo.TestUDFs') IS NOT NULL
DROP TABLE dbo.TestUDFs;
CREATE TABLE dbo.TestUDFs(
[Date] Date
);
-- Fill test-table
DECLARE
@Date Date = GetDate(),
@i INT = 0;
WHILE @i < 1000 * 365 BEGIN
SET @Date = DATEADD(day, 1, @Date);
INSERT INTO dbo.TestUDFs VALUES (@Date);
SET @i = @i + 1;
END;
-- Check if table is filled
DECLARE @CNT INT = (SELECT COUNT(*) FROM dbo.TestUDFs);
PRINT 'Rows in TestUDFs table: ' + CAST(@CNT AS VARCHAR(20));
GO
IF OBJECT_ID('dbo.fn_scalar_with_vars') IS NOT NULL
DROP FUNCTION dbo.fn_scalar_with_vars;
GO
CREATE FUNCTION dbo.fn_scalar_with_vars (@Date DATETIME)
RETURNS INT
AS BEGIN
DECLARE @DateKey INT;
SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
RETURN @DateKey;
END;
GO
IF OBJECT_ID('dbo.fn_scalar_direct_expression') IS NOT NULL
DROP FUNCTION dbo.fn_scalar_direct_expression;
GO
CREATE FUNCTION dbo.fn_scalar_direct_expression (@Date DATETIME)
RETURNS INT
AS BEGIN
RETURN CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
END;
GO
IF OBJECT_ID('dbo.fn_non_inline_table') IS NOT NULL
DROP FUNCTION dbo.fn_non_inline_table;
GO
CREATE FUNCTION dbo.fn_non_inline_table (@Date DATETIME)
RETURNS @DateKey TABLE(DateKey INT)
AS BEGIN
INSERT INTO @DateKey
VALUES (CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112)));
RETURN;
END
GO
IF OBJECT_ID('dbo.fn_inline_table') IS NOT NULL
DROP FUNCTION dbo.fn_inline_table;
GO
-- Recreate scalar-value UDF with direct expression (without variables) in body
IF OBJECT_ID('dbo.fn_scalar_direct_expression') IS NOT NULL
DROP FUNCTION dbo.fn_scalar_direct_expression;
GO
CREATE FUNCTION dbo.fn_scalar_direct_expression (@Date DATETIME)
RETURNS INT
AS BEGIN
RETURN CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
END;
GO
-- Recreate table-value UDF which returns table-variable
IF OBJECT_ID('dbo.fn_non_inline_table') IS NOT NULL
DROP FUNCTION dbo.fn_non_inline_table;
GO
CREATE FUNCTION dbo.fn_non_inline_table (@Date DATETIME)
RETURNS @DateKey TABLE(DateKey INT)
AS BEGIN
INSERT INTO @DateKey
VALUES (CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112)));
RETURN;
END
GO
-- Recreate inlide table-value UDF
IF OBJECT_ID('dbo.fn_inline_table') IS NOT NULL
DROP FUNCTION dbo.fn_inline_table;
GO
CREATE FUNCTION dbo.fn_inline_table (@Date DATETIME)
RETURNS TABLE
AS
RETURN SELECT DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
GO
/********** Test all methods **********/
DECLARE
@DateKey INT,
@StartedAt DATETIME2 = GETDATE();
PRINT '
Calculation directly in query:'
SET @StartedAt = GETDATE();
SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), [Date], 112))
FROM dbo.TestUDFs;
PRINT ' Elapsed time: '
+ CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
PRINT '
Scalar-value UDF with variables and body:'
SET @StartedAt = GETDATE();
SELECT @DateKey = dbo.fn_scalar_with_vars([Date])
FROM dbo.TestUDFs;
PRINT ' Elapsed time: '
+ CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
PRINT '
Scalar-value UDF with direct expression and body:'
SET @StartedAt = GETDATE();
SELECT @DateKey = dbo.fn_scalar_direct_expression([Date])
FROM dbo.TestUDFs;
PRINT ' Elapsed time: '
+ CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
PRINT '
Non-inline table-value UDF with table-variable and body:'
SET @StartedAt = GETDATE();
SELECT @DateKey = DateKey
FROM dbo.TestUDFs CROSS APPLY dbo.fn_non_inline_table([Date]);
PRINT ' Elapsed time: '
+ CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
PRINT '
Inline table-value UDF:'
SET @StartedAt = GETDATE();
SELECT @DateKey = DateKey
FROM dbo.TestUDFs CROSS APPLY dbo.fn_inline_table([Date]);
PRINT ' Elapsed time: '
+ CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
GO
-- Restore SET NOCOUNT OFF
SET NOCOUNT OFF;
An execution of this Script generates this result message:
Rows in TestUDFs table: 365000
Calculation directly in query:
Elapsed time: 300 ms
Scalar-value UDF with variables and body:
Elapsed time: 1570 ms
Scalar-value UDF with direct expression and body:
Elapsed time: 1274 ms
Non-inline table-value UDF with table-variable and body:
Elapsed time: 29623 ms
Inline table-value UDF:
Elapsed time: 210 ms
No comments:
Post a Comment
Note: only a member of this blog may post a comment.