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.
Scalar-value UDF which returns not scalar value, but a table-result.
- -- 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());
- GO
For data storage uses UDF a table variable.
The body of function is placed into BEGIN-END container.
Finally the UDF, which returns a non-scalar value, but a table.
- -- 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());
- GO
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.