Friday, 24 July 2015

MSSQL - Kill all my processes (except current)

Template to kill all my processes (connections) except current

  1. USE master
  2. GO
  3.  
  4. DECLARE @sql VARCHAR(MAX) = '-- Kill all my processes (except current: '
  5. + CAST(@@SPID AS VARCHAR(20)) + ')';
  6. SELECT @sql = @sql + CHAR(13) + CHAR(10) + 'kill ' + CONVERT(varchar(5), spid) + ';'
  7. FROM master..sysprocesses
  8. WHERE hostname = HOST_NAME() AND spid != @@SPID;
  9.  
  10. PRINT @sql;
  11. EXEC (@sql);

After execution we have something like this:

-- Kill all my processes (except current: 80)
kill 52;
kill 53;
kill 73;

Wednesday, 8 July 2015

MSSQL - Stored procedure to replace placeholders in texts (dynamic SQL)

It is often necessary to make the query text from constant pieces and variables.

For example we have a many tables with such structure:
  1. (
  2. ID INT IDENTITY(1,1) PRIMARY KEY,
  3. Number INT,
  4. ChangeDate DATETIME
  5. )
and we need dynamically, depending on variables with table name, [Number] min value and [ChangeDate] min value send queries to one or other table.
We can do it using Dynamic SQL method, for example so:

  1. CREATE PROCEDURE MySP
  2. @ObjectName SYSNAME,
  3. @MinNumber INT,
  4. @ValidFrom DATETIME
  5. AS BEGIN
  6. DECLARE @SQL NVarChar(MAX);
  7.  
  8. SET @SQL = '
  9. SELECT
  10. *
  11. FROM
  12. ' + @ObjectName + '
  13. WHERE
  14. Number >= ' + CAST(@MinNumber AS VARCHAR(20)) + '
  15. AND
  16. ChangeDate >= CONVERT(DATETIME, '''
  17. + CONVERT(VARCHAR(30), @ValidFrom, 121) + ''', 121);';
  18.  
  19. PRINT @SQL;
  20.  
  21. EXEC sp_executesql @SQL;
  22. END;
  23. GO
  24.  
  25. EXEC MySP 'MyTableNr1', 123, '2015-01-01 12:00';

It works, but SQL-Text format is not readable.
Much more readable looks the text converted to a template with placeholders:

  1. '
  2. SELECT
  3. *
  4. FROM
  5. {ObjectName}
  6. WHERE
  7. Number >= {MinNumber}
  8. AND
  9. ChangeDate >= CONVERT(DATETIME, ''{ValidFrom}'';
  10. '

In this case our example stored procedure will look like this:

  1. CREATE PROCEDURE MySP
  2. @ObjectName SYSNAME,
  3. @MinNumber INT,
  4. @ValidFrom DATETIME
  5. AS BEGIN
  6. DECLARE @SQL NVarChar(MAX);
  7.  
  8. SET @SQL = '
  9. SELECT
  10. *
  11. FROM
  12. {ObjectName}
  13. WHERE
  14. Number >= {MinNumber}
  15. AND
  16. ChangeDate >= CONVERT(DATETIME, ''{ValidFrom}'';'
  17.  
  18. SET @SQL = REPLACE(@SQL, '{ObjectName}', @ObjectName);
  19. SET @SQL = REPLACE(@SQL, '{MinNumber}', CAST(@MinNumber AS VARCHAR(20)));
  20. SET @SQL = REPLACE(@SQL, '{ValidFrom}', CONVERT(VARCHAR(30), @ValidFrom, 121));
  21.  
  22. PRINT @SQL;
  23.  
  24. EXEC sp_executesql @SQL;
  25. END;

It is good for so simple example, but if we will have more dynamical parameters and inside one stored procedure (or plain query) more then one template, then this query will have too many redundant call like this:
  1. SET @SQL = REPLACE(@SQL, ..., ...);

The solution is to create some help procedure which will take text template and dictionary with placeholder name:value pairs as parameters.
Stored procedure can not take table-value parameters directly.
Table-valued parameters have to be declared by using user-defined table types.
In our case:
  1. CREATE TYPE PlaceholderMappingType AS TABLE (
  2. Placeholder NVARCHAR(100),
  3. Value NVARCHAR(MAX)
  4. );

The complete script with usage example:
  1. USE tools;
  2. GO
  3. IF OBJECT_ID('tools.dbo.sp_ReplacePlaceholders') IS NOT NULL BEGIN
  4. PRINT '-- DROP PROCEDURE dbo.sp_ReplacePlaceholders';
  5. DROP PROCEDURE dbo.sp_ReplacePlaceholders;
  6. END;
  7. GO
  8. IF EXISTS (
  9. SELECT *
  10. FROM tools.INFORMATION_SCHEMA.DOMAINS
  11. WHERE DOMAIN_NAME = 'PlaceholderMappingType'
  12. ) BEGIN
  13. PRINT '-- DROP TYPE dbo.PlaceholderMappingType';
  14. DROP TYPE dbo.PlaceholderMappingType;
  15. END;
  16. GO
  17. PRINT '-- CREATE TYPE dbo.PlaceholderMappingType';
  18. -- TABLE-Domain (User Defined Type) for placeholders dictionary
  19. CREATE TYPE PlaceholderMappingType AS TABLE (
  20. Placeholder NVARCHAR(100),
  21. Value NVARCHAR(MAX)
  22. );
  23. GO
  24. CREATE PROCEDURE dbo.sp_ReplacePlaceholders (
  25. @Text NVARCHAR(MAX) OUTPUT,
  26. -- TABLE-Domain (User Defined Type) for placeholders dictionary
  27. @PlaceholderMappings PlaceholderMappingType READONLY,
  28. -- Flag to replace in placeholder values each single apostrof with double apostrof
  29. @EscapeApostrofs BIT = 1,
  30. -- Text to TrimLeft in each text line
  31. @IndentString VARCHAR(MAX) = '',
  32. -- Flag to PRINT additional diagnostic information
  33. @Debug BIT = 0
  34. )
  35. AS BEGIN
  36. SET NOCOUNT ON;
  37.  
  38. IF (@Debug = 1)
  39. PRINT '-- EXEC dbo.sp_ReplacePlaceholders'
  40.  
  41. DECLARE
  42. @CRLF CHAR(2) = CHAR(13)+CHAR(10),
  43. @Indent_CharIndex INT,
  44. @I INT = 0,
  45. @Char CHAR(1),
  46. @Indent VARCHAR(MAX) = '';
  47.  
  48. -- Trim rows left
  49. IF (CHARINDEX(@IndentString, @Text, 0) = 1)
  50. SET @Text = STUFF(@Text, 1, LEN(@IndentString), '');
  51. SET @Text = REPLACE(@Text, @CRLF + @IndentString, @CRLF);
  52.  
  53. -- Declare FOREACH(placeholderRow IN @PlaceholderMappings) cursor
  54. DECLARE
  55. @Placeholder NVARCHAR(102),
  56. @Value NVARCHAR(MAX);
  57. DECLARE Placeholders_Cursor CURSOR
  58. LOCAL STATIC READ_ONLY FORWARD_ONLY
  59. FOR
  60. SELECT
  61. Placeholder = '{' + REPLACE(REPLACE(Placeholder, '{', ''), '}', '') + '}',
  62. Value = CASE @EscapeApostrofs
  63. WHEN 1
  64. THEN REPLACE(Value, '''', '''''')
  65. ELSE Value
  66. END
  67. FROM @PlaceholderMappings;
  68.  
  69. OPEN Placeholders_Cursor;
  70.  
  71. FETCH NEXT FROM Placeholders_Cursor INTO @Placeholder, @Value;
  72. WHILE @@FETCH_STATUS = 0 BEGIN
  73. IF (@Debug = 1) AND (CHARINDEX(@Placeholder, @Text) = 0)
  74. PRINT '-- ==> Could not find Placeholder ' + @Placeholder;
  75. SET @Text = REPLACE(@Text, @Placeholder, @Value);
  76.  
  77. FETCH NEXT FROM Placeholders_Cursor INTO @Placeholder, @Value;
  78. END;
  79.  
  80. CLOSE Placeholders_Cursor
  81. DEALLOCATE Placeholders_Cursor
  82.  
  83. RETURN;
  84. END;
  85. GO
  86. DECLARE
  87. @SQL NVARCHAR(MAX),
  88. @PlaceholderMappings PlaceholderMappingType;
  89.  
  90. SET @SQL = '
  91. SELECT
  92. F1=''{aaa}'',
  93. F2=''{bbb}'',
  94. F3=''{ddd}'';';
  95. INSERT INTO @PlaceholderMappings
  96. VALUES
  97. ('aaa', 'A''A''A'),
  98. ('{bbb}', 'BBB'),
  99. ('ccc', 'CCC');
  100.  
  101. DECLARE @IndentString VARCHAR(MAX) = SPACE(8);
  102. EXEC dbo.sp_ReplacePlaceholders
  103. @SQL OUTPUT,
  104. @PlaceholderMappings,
  105. @IndentString=@IndentString,
  106. @Debug=1;
  107. PRINT '>>>' + @SQL + '<<<';

Tuesday, 7 July 2015

To get all list of all script objects (VIEWs, SPs, UDFs) which are depending on some other script object we can use help of sys.sql_expression_dependencies system view:

  1. SELECT
  2. object_name = OBJECT_NAME(D.referencing_id),
  3. O.type,
  4. O.type_desc
  5. FROM
  6. sys.sql_expression_dependencies D
  7. INNER JOIN sys.objects O ON D.referencing_id = O.object_id
  8. WHERE
  9. referenced_entity_name LIKE 'MyStoredProcedure';

Query result looks like this:



Now we can extend this query with navigation through all databases and save it as T-SQL Query Template:

  1. DECLARE
  2. @referenced_entity SYSNAME = '<referenced_entity, sysname, ???>',
  3. @referencing_entity SYSNAME = '<referencing_entity, sysname, %>',
  4. @sql NVARCHAR(MAX) = '
  5. SELECT
  6. server_name=NULL,
  7. database_name=NULL,
  8. object_name=NULL,
  9. object_type=NULL,
  10. object_type_desc=NULL
  11. WHERE 1=0';
  12.  
  13. SELECT @sql = @sql + REPLACE(REPLACE(REPLACE(REPLACE('
  14. UNION ALL
  15. SELECT
  16. server_name = @@SERVERNAME,
  17. database_name = ''{database_name}'',
  18. object_name = OBJECT_NAME(D.referencing_id, {database_id}),
  19. object_type = O.type,
  20. object_type_desc = O.type_desc
  21. FROM
  22. {database_name}.sys.sql_expression_dependencies D
  23. INNER JOIN {database_name}.sys.objects O ON D.referencing_id = O.object_id
  24. WHERE
  25. D.referenced_entity_name LIKE ''{referenced_entity}''
  26. AND
  27. OBJECT_NAME(D.referencing_id, {database_id}) LIKE ''{referencing_entity}''',
  28. '{database_id}', database_id),
  29. '{database_name}', name),
  30. '{referenced_entity}', @referenced_entity),
  31. '{referencing_entity}', @referencing_entity)
  32. FROM sys.databases
  33. WHERE state=0 /* ONLINE */
  34. ORDER BY name;
  35.  
  36. SET @sql = @sql + '
  37. ';
  38.  
  39. EXEC(@sql);



See also MSSQL - Query Templates

MSSQL - Very fast method to get statistic (rows and bytes) for all database tables

Typical way to count all rows in the table ist to send such query:

  1. SELECT COUNT(*) FROM MyTable;

This method has some disadvantages:
  1. We can only get a rows count, but not a physical size of table
  2. This query works on one table, name of which we have to define explicitly
  3. If table has no PRIMARY KEY, then this query can be very slow

There is some other method - to use a statistic from system tables.
This method uses for example this predefined SSMS report:


We can start Profiler (Menu =&rt; Tools =&rt; SQL Server Profiler) and extract for own usage a background query of this report.
I have already extracted this query and reformated as standalone VIEW:

  1. CREATE VIEW dbo.disk_usage_by_table
  2. AS
  3. SELECT TOP(100) PERCENT
  4. Database_Name = DB_NAME(),
  5. Schema_Name = CAST(a3.name AS SYSNAME),
  6. Table_Name = CAST(a2.name AS SYSNAME),
  7. Records = a1.records,
  8. Reserved_KB = (a1.reserved + ISNULL(a4.reserved,0))* 8,
  9. Data_KB = a1.data * 8,
  10. Indexes_KB = CASE
  11. WHEN (a1.used + ISNULL(a4.used,0)) > a1.data
  12. THEN (a1.used + ISNULL(a4.used,0)) - a1.data
  13. ELSE 0
  14. END * 8,
  15. Unused_KB = CASE
  16. WHEN (a1.reserved + ISNULL(a4.reserved,0)) > a1.used
  17. THEN (a1.reserved + ISNULL(a4.reserved,0)) - a1.used
  18. ELSE 0
  19. END * 8
  20. FROM
  21. (
  22. SELECT
  23. ps.object_id,
  24. records = SUM(
  25. CASE
  26. WHEN (ps.index_id < 2)
  27. THEN row_count
  28. ELSE 0
  29. END
  30. ),
  31. reserved = SUM(ps.reserved_page_count),
  32. data = SUM(
  33. CASE
  34. WHEN ps.index_id < 2
  35. THEN ps.in_row_data_page_count
  36. + ps.lob_used_page_count
  37. + ps.row_overflow_used_page_count
  38. ELSE ps.lob_used_page_count
  39. + ps.row_overflow_used_page_count
  40. END
  41. ),
  42. used = SUM(ps.used_page_count)
  43. FROM sys.dm_db_partition_stats ps
  44. GROUP BY ps.object_id
  45. ) AS a1
  46. INNER JOIN sys.all_objects a2 ON ( a1.object_id = a2.object_id )
  47. INNER JOIN sys.schemas a3 ON (a2.schema_id = a3.schema_id)
  48. LEFT OUTER JOIN (
  49. SELECT
  50. it.parent_id,
  51. SUM(ps.reserved_page_count) AS reserved,
  52. SUM(ps.used_page_count) AS used
  53. FROM sys.dm_db_partition_stats ps
  54. INNER JOIN sys.internal_tables it ON (it.object_id = ps.object_id)
  55. WHERE it.internal_type IN (202,204)
  56. GROUP BY it.parent_id
  57. ) AS a4 ON (a4.parent_id = a1.object_id)
  58. WHERE a2.type <> N'S' and a2.type <> N'IT'
  59. ORDER BY Table_Name;

If we query this VIEW:

  1. SELECT * FROM dbo.disk_usage_by_table;

then we can see that [count rows], [reserved space], [space used for data], [space used for indexes] and [free space] for 106 tables takes less then 1 second:



We can also filter or join this view with other tables/views:

  1. SELECT Records FROM dbo.disk_usage_by_table WHERE Table_Name = 'dim_date';



This method has folowing advantages:
  1. Is very very fast
  2. Is independent of table names
  3. Shows
    • Count of records
    • Reserved space (in KBytes)
    • Space used by data (in KBytes)
    • Space used by indexes (in KBytes)
    • Unused space (in KBytes)

You can use this metod for example in ETL processes to count (outside of transformation) rows and space before and after load.

Monday, 6 July 2015

MSSQL - CHANGE TRACKING - How can I get DateTime of changes?

Change tracking is a lightweight solution that provides an efficient change tracking mechanism for applications.
You can get more information on the Microsoft Developer Network web site.

But on these pages you will not find an answer on this question:
How can I get DateTime of changes. You can find this information in the sys.dm_tran_commit_table DMV (Dinamic Management View).

There the complete T-SQL script:

  1. -- Recreate test table
  2. IF OBJECT_ID('dbo.TestCT') IS NOT NULL
  3. DROP TABLE dbo.TestCT;
  4. GO
  5. CREATE TABLE dbo.TestCT
  6. (
  7. ID INT IDENTITY(1,1) NOT NULL,
  8. CONSTRAINT [dbo.TestCT.PK] PRIMARY KEY CLUSTERED (ID),
  9. Code VARCHAR(10) NOT NULL,
  10. Description NVARCHAR(100) NULL
  11. );
  12.  
  13. GO
  14. -- Enable CHANGE TRACKING for test table
  15. ALTER TABLE dbo.TestCT ENABLE CHANGE_TRACKING
  16.  
  17. GO
  18. -- Fill test table with initial rows
  19. INSERT INTO dbo.TestCT(Code, Description)
  20. VALUES
  21. ('AAA', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'),
  22. ('BBB', 'Cras sed tincidunt dolor, in interdum lorem.'),
  23. ('CCC', 'Vestibulum eget condimentum orci.');
  24.  
  25. GO
  26. -- Wait for 30 seconds
  27. WAITFOR DELAY '00:00:30'
  28.  
  29. GO
  30. -- Change second row in the test table
  31. UPDATE dbo.TestCT
  32. SET Description = '!!! This line was changed !!!'
  33. WHERE Code = 'BBB';
  34.  
  35. GO
  36. -- Show CHANGE TRACKING information
  37. -- directly after initial filling (VersionNumber=0)
  38. SELECT
  39. CT.ID,
  40. CT.SYS_CHANGE_VERSION,
  41. CT.SYS_CHANGE_CREATION_VERSION,
  42. CT.SYS_CHANGE_OPERATION,
  43. COMM.commit_time
  44. FROM
  45. CHANGETABLE(CHANGES dbo.TestCT, 0) CT
  46. INNER JOIN sys.dm_tran_commit_table COMM ON
  47. CT.sys_change_version = COMM.commit_ts
  48. ORDER BY
  49. CT.ID, COMM.commit_time DESC;
  50.  
  51. -- Get initialisation version number
  52. DECLARE @MaxCreationVersion INT = (
  53. SELECT TOP(1) SYS_CHANGE_CREATION_VERSION
  54. FROM CHANGETABLE(CHANGES dbo.TestCT, 0) CT
  55. );
  56.  
  57. -- Show changes directly after initialisation
  58. SELECT
  59. CT.ID, CT.SYS_CHANGE_VERSION, SYS_CHANGE_OPERATION,
  60. COMM.commit_time
  61. FROM
  62. CHANGETABLE(CHANGES dbo.TestCT, @MaxCreationVersion) CT
  63. INNER JOIN sys.dm_tran_commit_table COMM ON
  64. CT.sys_change_version = COMM.commit_ts
  65. ORDER BY
  66. CT.ID, COMM.commit_time DESC;

Execution result:

MSSQL - Performance tuning of Scalar-Value User Defined Functions (UDFs)

In most of cases the scalar-value User Defined Functions (Scalar UDF) are very slow.
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:
  1. -- Recreate test table
  2. IF OBJECT_ID('dbo.TestUDFs') IS NOT NULL
  3. DROP TABLE dbo.TestUDFs;
  4. GO
  5. CREATE TABLE dbo.TestUDFs(
  6. Datum Date
  7. );
  8. GO
  9. -- SET NOCOUNT ON to prevent extra log messages returned as part of the result set:
  10. -- (See: https://msdn.microsoft.com/en-us/library/ms189837%28v=sql.110%29.aspx)
  11. SET NOCOUNT ON;
  12. -- Fill test-table
  13. DECLARE
  14. @Date Date = GetDate(),
  15. @i INT = 0;
  16. WHILE @i < 1000 * 365 BEGIN
  17. SET @Date = DATEADD(day, 1, @Date);
  18. INSERT INTO dbo.TestUDFs VALUES (@Date);
  19. SET @i = @i + 1;
  20. END;
  21. -- restore NOCOUNT
  22. SET NOCOUNT OFF;
  23. GO
  24. SELECT CNT = COUNT(*) FROM dbo.TestUDFs;
  25. 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.
  1. -- Recreate scalar-value UDF with variables in body
  2. IF OBJECT_ID('dbo.fn_scalar_with_vars') IS NOT NULL
  3. DROP FUNCTION dbo.fn_scalar_with_vars;
  4. GO
  5. CREATE FUNCTION dbo.fn_scalar_with_vars (@date DATETIME)
  6. RETURNS INT
  7. AS BEGIN
  8. DECLARE @DateKey INT;
  9. SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @date, 112));
  10. RETURN @DateKey;
  11. END;
  12. GO
  13. SELECT dbo.fn_scalar_with_vars(GetDate());
  14. 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.
  1. -- Recreate scalar-value UDF with direct expression (without variables) in body
  2. IF OBJECT_ID('dbo.fn_scalar_direct_expression') IS NOT NULL
  3. DROP FUNCTION dbo.fn_scalar_direct_expression;
  4. GO
  5. CREATE FUNCTION dbo.fn_scalar_direct_expression (@date DATETIME)
  6. RETURNS INT
  7. AS BEGIN
  8. RETURN CONVERT(INT, CONVERT(VARCHAR(8), @date, 112));
  9. END;
  10. GO
  11. SELECT DateKey = dbo.fn_scalar_direct_expression(GetDate());
  12. GO
Scalar-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.
  1. -- Recreate table-value UDF which returns table-variable
  2. IF OBJECT_ID('dbo.fn_non_inline_table') IS NOT NULL
  3. DROP FUNCTION dbo.fn_non_inline_table;
  4. GO
  5. CREATE FUNCTION dbo.fn_non_inline_table (@date DATETIME)
  6. RETURNS @DateKey TABLE(DateKey INT)
  7. AS BEGIN
  8. INSERT INTO @DateKey
  9. VALUES (CONVERT(INT, CONVERT(VARCHAR(8), @date, 112)));
  10. RETURN;
  11. END
  12. GO
  13. SELECT DateKey FROM dbo.fn_non_inline_table(GetDate());
  14. GO
Finally 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".
  1. -- Recreate inlide table-value UDF
  2. IF OBJECT_ID('dbo.fn_inline_table') IS NOT NULL
  3. DROP FUNCTION dbo.fn_inline_table;
  4. GO
  5. CREATE FUNCTION dbo.fn_inline_table (@date DATETIME)
  6. RETURNS TABLE
  7. AS
  8. RETURN SELECT DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @date, 112));
  9. GO
  10. SELECT DateKey FROM dbo.fn_inline_table(GetDate());
  11. 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.

  1. -- Script to test calculations
  2. DECLARE
  3. @DateKey INT,
  4. @StartedAt DATETIME2 = GETDATE();
  5.  
  6. /* Put calculation there */
  7.  
  8. PRINT 'Elapsed time (Direct calculation): '
  9. + CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';

Calculation directly in query:
300 ms.
  1. SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), Datum, 112))
  2. FROM dbo.TestUDFs;

Scalar-value UDF with variables and body:
1570 ms.
  1. SELECT @DateKey = dbo.fn_scalar_with_vars(Datum)
  2. FROM dbo.TestUDFs;

Scalar-value UDF with direct expression and body:
1274 ms.
  1. SELECT @DateKey = dbo.fn_scalar_direct_expression(Datum)
  2. FROM dbo.TestUDFs;

Non-inline table-value UDF with table-variable and body:
29623 ms.
  1. SELECT @DateKey = DateKey
  2. 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.
  1. SELECT @DateKey = DateKey
  2. FROM dbo.TestUDFs CROSS APPLY dbo.fn_inline_table(Datum);



All-in-one Script (an execution takes about one minute):
  1. -- SET NOCOUNT ON to prevent extra log messages:
  2. -- See: https://msdn.microsoft.com/en-us/library/ms189837%28v=sql.110%29.aspx
  3. SET NOCOUNT ON;
  4. GO
  5. IF OBJECT_ID('dbo.TestUDFs') IS NOT NULL
  6. DROP TABLE dbo.TestUDFs;
  7. CREATE TABLE dbo.TestUDFs(
  8. [Date] Date
  9. );
  10. -- Fill test-table
  11. DECLARE
  12. @Date Date = GetDate(),
  13. @i INT = 0;
  14. WHILE @i < 1000 * 365 BEGIN
  15. SET @Date = DATEADD(day, 1, @Date);
  16. INSERT INTO dbo.TestUDFs VALUES (@Date);
  17. SET @i = @i + 1;
  18. END;
  19. -- Check if table is filled
  20. DECLARE @CNT INT = (SELECT COUNT(*) FROM dbo.TestUDFs);
  21. PRINT 'Rows in TestUDFs table: ' + CAST(@CNT AS VARCHAR(20));
  22. GO
  23. IF OBJECT_ID('dbo.fn_scalar_with_vars') IS NOT NULL
  24. DROP FUNCTION dbo.fn_scalar_with_vars;
  25. GO
  26. CREATE FUNCTION dbo.fn_scalar_with_vars (@Date DATETIME)
  27. RETURNS INT
  28. AS BEGIN
  29. DECLARE @DateKey INT;
  30. SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
  31. RETURN @DateKey;
  32. END;
  33. GO
  34. IF OBJECT_ID('dbo.fn_scalar_direct_expression') IS NOT NULL
  35. DROP FUNCTION dbo.fn_scalar_direct_expression;
  36. GO
  37. CREATE FUNCTION dbo.fn_scalar_direct_expression (@Date DATETIME)
  38. RETURNS INT
  39. AS BEGIN
  40. RETURN CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
  41. END;
  42. GO
  43. IF OBJECT_ID('dbo.fn_non_inline_table') IS NOT NULL
  44. DROP FUNCTION dbo.fn_non_inline_table;
  45. GO
  46. CREATE FUNCTION dbo.fn_non_inline_table (@Date DATETIME)
  47. RETURNS @DateKey TABLE(DateKey INT)
  48. AS BEGIN
  49. INSERT INTO @DateKey
  50. VALUES (CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112)));
  51. RETURN;
  52. END
  53. GO
  54. IF OBJECT_ID('dbo.fn_inline_table') IS NOT NULL
  55. DROP FUNCTION dbo.fn_inline_table;
  56. GO
  57. -- Recreate scalar-value UDF with direct expression (without variables) in body
  58. IF OBJECT_ID('dbo.fn_scalar_direct_expression') IS NOT NULL
  59. DROP FUNCTION dbo.fn_scalar_direct_expression;
  60. GO
  61. CREATE FUNCTION dbo.fn_scalar_direct_expression (@Date DATETIME)
  62. RETURNS INT
  63. AS BEGIN
  64. RETURN CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
  65. END;
  66. GO
  67. -- Recreate table-value UDF which returns table-variable
  68. IF OBJECT_ID('dbo.fn_non_inline_table') IS NOT NULL
  69. DROP FUNCTION dbo.fn_non_inline_table;
  70. GO
  71. CREATE FUNCTION dbo.fn_non_inline_table (@Date DATETIME)
  72. RETURNS @DateKey TABLE(DateKey INT)
  73. AS BEGIN
  74. INSERT INTO @DateKey
  75. VALUES (CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112)));
  76. RETURN;
  77. END
  78. GO
  79. -- Recreate inlide table-value UDF
  80. IF OBJECT_ID('dbo.fn_inline_table') IS NOT NULL
  81. DROP FUNCTION dbo.fn_inline_table;
  82. GO
  83. CREATE FUNCTION dbo.fn_inline_table (@Date DATETIME)
  84. RETURNS TABLE
  85. AS
  86. RETURN SELECT DateKey = CONVERT(INT, CONVERT(VARCHAR(8), @Date, 112));
  87. GO
  88. /********** Test all methods **********/
  89. DECLARE
  90. @DateKey INT,
  91. @StartedAt DATETIME2 = GETDATE();
  92.  
  93. PRINT '
  94. Calculation directly in query:'
  95. SET @StartedAt = GETDATE();
  96. SELECT @DateKey = CONVERT(INT, CONVERT(VARCHAR(8), [Date], 112))
  97. FROM dbo.TestUDFs;
  98. PRINT ' Elapsed time: '
  99. + CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
  100.  
  101. PRINT '
  102. Scalar-value UDF with variables and body:'
  103. SET @StartedAt = GETDATE();
  104. SELECT @DateKey = dbo.fn_scalar_with_vars([Date])
  105. FROM dbo.TestUDFs;
  106. PRINT ' Elapsed time: '
  107. + CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
  108.  
  109. PRINT '
  110. Scalar-value UDF with direct expression and body:'
  111. SET @StartedAt = GETDATE();
  112. SELECT @DateKey = dbo.fn_scalar_direct_expression([Date])
  113. FROM dbo.TestUDFs;
  114. PRINT ' Elapsed time: '
  115. + CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
  116.  
  117. PRINT '
  118. Non-inline table-value UDF with table-variable and body:'
  119. SET @StartedAt = GETDATE();
  120. SELECT @DateKey = DateKey
  121. FROM dbo.TestUDFs CROSS APPLY dbo.fn_non_inline_table([Date]);
  122. PRINT ' Elapsed time: '
  123. + CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
  124.  
  125. PRINT '
  126. Inline table-value UDF:'
  127. SET @StartedAt = GETDATE();
  128. SELECT @DateKey = DateKey
  129. FROM dbo.TestUDFs CROSS APPLY dbo.fn_inline_table([Date]);
  130. PRINT ' Elapsed time: '
  131. + CAST(DATEDIFF(ms, @StartedAt, GETDATE()) AS VarChar(20)) + ' ms';
  132.  
  133. GO
  134. -- Restore SET NOCOUNT OFF
  135. SET NOCOUNT OFF;

An execution of this Script generates this result message:

  1. Rows in TestUDFs table: 365000
  2.  
  3. Calculation directly in query:
  4. Elapsed time: 300 ms
  5.  
  6. Scalar-value UDF with variables and body:
  7. Elapsed time: 1570 ms
  8.  
  9. Scalar-value UDF with direct expression and body:
  10. Elapsed time: 1274 ms
  11.  
  12. Non-inline table-value UDF with table-variable and body:
  13. Elapsed time: 29623 ms
  14.  
  15. Inline table-value UDF:
  16. Elapsed time: 210 ms