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; GO
We 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()); GO
SECOND:
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()); GO
Let 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.