USE master;
GO

IF OBJECT_ID('dbo.sp_BenchmarkTSQL', 'P') IS NULL EXEC('CREATE PROC dbo.sp_BenchmarkTSQL AS SELECT 1;');
GO


ALTER PROCEDURE dbo.sp_BenchmarkTSQL(
      @tsqlStatementBefore nvarchar(max) = NULL
    , @tsqlStatement       nvarchar(max)
    , @tsqlStatementAfter  nvarchar(max) = NULL
    , @numberOfExecution   int           = 10
    , @saveResults         bit           = 0
    , @skipTSQLCheck       bit           = 1
    , @clearCache          bit           = 0
    , @calcMedian          bit           = 0
    , @printStepInfo       bit           = 0
    , @durationAccuracy    varchar(5)    = 'ss'
    , @dateTimeFunction    varchar(16)   = 'SYSDATETIME'
    , @additionalInfo      bit           = 0
)
/*
.SYNOPSIS
    Run TSQL statement @numberOfExecution times and calculate each execution time, save results if needed or print it.

.DESCRIPTION
    Run TSQL statement specified times with sp_executesql, print step results, insert execution details into log table master.dbo.BenchmarkTSQL.

.PARAMETER @tsqlStatementBefore
    TSQL statement that executed before tested main TSQL statement - not taken into account when measuring @tsqlStatement.
    Possible values: any TSQL statements.
    Default value: NULL.

.PARAMETER @tsqlStatement
    TSQL statement for benchmarking.
    Mandatory parameter.
    Possible values: any TSQL statements.
    No defaults.

.PARAMETER @tsqlStatementAfter
    TSQL statement that executed after tested TSQL statement - not taken into account when measuring @tsqlStatement.
    Possible values: any TSQL statements.
    Default value: NULL.

.PARAMETER @numberOfExecution
    Number of execution TSQL statement.
    Possible values: 1 or 32000.
    Default value: 10.

.PARAMETER @saveResults
    Save benchmark details to master.dbo.BenchmarkTSQL table if @saveResults = 1.
    Create table if not exists (see 335 line: CREATE TABLE master.dbo.BenchmarkTSQL …).
    Possible values: 0 or 1.
    Default value: 0 - not saved.

.PARAMETER @skipTSQLCheck
    Checking for valid TSQL statement using sys.dm_exec_describe_first_result_set.
    Possible values: 0 or 1.
    Default value: 1 - skip checking.
    See https://docs.microsoft.com/sql/relational-databases/system-dynamic-management-views/sys-dm-exec-describe-first-result-set-transact-sql

.PARAMETER @clearCache
    Clear cached plan for TSQL statement before each run using DBCC FREEPROCCACHE(@planHandle).
    Possible values: 0 or 1.
    Default value: 0 - not clear.

.PARAMETER @calcMedian
    Calculate pseudo median of all execution times.
    Possible values: 0 or 1.
    Default value: 0 - not calculated.

.PARAMETER @printStepInfo
    PRINT detailed step information: step count, start step time, end step time, duration.
    Default value: 0 - not printed.

.PARAMETER @durationAccuracy
    Duration accuracy calculation.
    Possible values: ns, mcs, ms, ss, mi, hh, dd, wk.
    Default value: ss - seconds.
    See DATEDIFF https://docs.microsoft.com/sql/t-sql/functions/datediff-transact-sql

.PARAMETER @dateTimeFunction
    Define using datetime function.
    Possible values: SYSDATETIME, SYSUTCDATETIME.
    Default value: SYSDATETIME.
    See https://docs.microsoft.com/sql/t-sql/functions/date-and-time-data-types-and-functions-transact-sql

.PARAMETER @additionalInfo
    Save additional session parameteres (ANSI_WARNINGS, XACT_ABORT and etc) to XML column AdditionalInfo in log table master.dbo.BenchmarkTSQL.
    Possible values: 0 or 1.
    Default value: 0 - not saved.

.EXAMPLE
    EXEC sp_BenchmarkTSQL
         @tsqlStatement = 'SELECT * FROM , sys.databases;'
       , @skipTSQLCheck = 0;
    /* RETURN: Incorrect syntax near ','. */

.EXAMPLE
    EXEC sp_BenchmarkTSQL
         @tsqlStatement = 'SELECT * FROM sys.databases;'
       , @skipTSQLCheck = 0;

.EXAMPLE
    EXEC sp_BenchmarkTSQL
         @tsqlStatement = 'SELECT TOP(100000) * FROM sys.objects AS o1 CROSS JOIN sys.objects AS o2 CROSS JOIN sys.objects AS o3;'
       , @numberOfExecution = 10
       , @saveResults       = 1
       , @calcMedian        = 1
       , @clearCache        = 1
       , @printStepInfo     = 1
       , @durationAccuracy  = 'ms';

.EXAMPLE
    EXEC sp_BenchmarkTSQL
         @tsqlStatementBefore = 'WAITFOR DELAY ''00:00:01'';'
       , @tsqlStatement       = 'BACKUP DATABASE [master] TO DISK = N''C:\master.bak'' WITH NOFORMAT, NOINIT;'
       , @tsqlStatementAfter  = 'WAITFOR DELAY ''00:00:02'';'
       , @numberOfExecution   = 5
       , @saveResults         = 1
       , @calcMedian          = 1
       , @clearCache          = 1
       , @printStepInfo       = 1
       , @durationAccuracy    = 'ss'
       , @dateTimeFunction    = 'SYSUTCDATETIME';

.EXAMPLE
    EXEC sp_BenchmarkTSQL
         @tsqlStatement       = 'SET NOCOUNT OFF; SELECT TOP(100000) * FROM sys.objects AS o1 CROSS JOIN sys.objects AS o2 CROSS JOIN sys.objects AS o3;'
       , @numberOfExecution   = 5
       , @saveResults         = 1
       , @calcMedian          = 1
       , @clearCache          = 1
       , @printStepInfo       = 1
       , @durationAccuracy    = 'ss'
       , @additionalInfo      = 1;

.EXAMPLE
    DECLARE @tsql nvarchar(max) = N'SET NOCOUNT OFF; DECLARE @tsql nvarchar(max) = N''BACKUP DATABASE [master] TO DISK = N''''C:\master'' +
                                   REPLACE(CAST(CAST(GETDATE() AS datetime2(7)) AS nvarchar(max)), '':'', '' '') +
                                   ''.bak'''' WITH NOFORMAT, NOINIT;''
                                   EXEC sp_executesql @tsql;';
    EXEC sp_BenchmarkTSQL
         @tsqlStatement     = @tsql
       , @numberOfExecution = 3
       , @saveResults       = 1
       , @calcMedian        = 1
       , @clearCache        = 1
       , @printStepInfo     = 1
       , @durationAccuracy  = 'ms'
       , @dateTimeFunction  = 'SYSUTCDATETIME'
       , @additionalInfo    = 1;

.EXAMPLE
    WITH CTE AS (
        SELECT  TSQLStatementGUID
              , StartBenchmark
              , EndBenchmark
              , ClearCache
              , PrintStepInfo
              , DurationAccuracy
              , CASE WHEN DurationAccuracy = 'ns'  THEN DATEDIFF(ns,  StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'mcs' THEN DATEDIFF(mcs, StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'ms'  THEN DATEDIFF(ms,  StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'ss'  THEN DATEDIFF(ss,  StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'mi'  THEN DATEDIFF(mi,  StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'hh'  THEN DATEDIFF(hh,  StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'dd'  THEN DATEDIFF(dd,  StartBenchmark, EndBenchmark)
                     WHEN DurationAccuracy = 'wk'  THEN DATEDIFF(wk,  StartBenchmark, EndBenchmark)
                     ELSE 0
                 END AS BenchmarkDuration
              , CASE WHEN DurationAccuracy = 'ns'  THEN DATEDIFF(ns,  StartStep, EndStep)
                     WHEN DurationAccuracy = 'mcs' THEN DATEDIFF(mcs, StartStep, EndStep)
                     WHEN DurationAccuracy = 'ms'  THEN DATEDIFF(ms,  StartStep, EndStep)
                     WHEN DurationAccuracy = 'ss'  THEN DATEDIFF(ss,  StartStep, EndStep)
                     WHEN DurationAccuracy = 'mi'  THEN DATEDIFF(mi,  StartStep, EndStep)
                     WHEN DurationAccuracy = 'hh'  THEN DATEDIFF(hh,  StartStep, EndStep)
                     WHEN DurationAccuracy = 'dd'  THEN DATEDIFF(dd,  StartStep, EndStep)
                     WHEN DurationAccuracy = 'wk'  THEN DATEDIFF(wk,  StartStep, EndStep)
                     ELSE 0
                 END AS StepDuration
              , StepRowNumber
              , StartStep
              , EndStep
              , TsqlStatementBefore
              , TsqlStatement
              , TsqlStatementAfter
              , OriginalLogin
              , AdditionalInfo
        FROM    master.dbo.BenchmarkTSQL
        WHERE   TSQLStatementGUID IN ('D34C4BD6-01B8-4C9C-B627-E3C26F84D9FA', '02DF082C-7D3D-450E-B125-C738F015B35A')
    )
    SELECT   TSQLStatementGUID
           , StartBenchmark
           , EndBenchmark
           , ClearCache
           , PrintStepInfo
           , MAX(StepRowNumber) AS StepCount
           , DurationAccuracy
           , BenchmarkDuration
           , MIN(StepDuration) AS StepDurationMIN
           , MAX(StepDuration) AS StepDurationMAX
           , AVG(StepDuration) AS StepDurationAVG
           , TsqlStatement
    FROM     CTE
    GROUP BY TSQLStatementGUID
           , StartBenchmark
           , EndBenchmark
           , ClearCache
           , PrintStepInfo
           , DurationAccuracy
           , BenchmarkDuration
           , TsqlStatement;

.LICENSE MIT
Permission is here by granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

.NOTE
    Version: 5.9
    Created: 2017-12-14 by Konstantin Taranov
    Modified: 2019-08-26 by Konstantin Taranov
    Main contributors: Konstantin Taranov, Aleksei Nagorskii
    Source: https://rebrand.ly/sp_BenchmarkTSQL
*/
AS
BEGIN TRY

    SET NOCOUNT ON;

    DECLARE @startBenchmark datetime2(7) = CASE WHEN @dateTimeFunction = 'SYSDATETIME'    THEN SYSDATETIME()
                                                WHEN @dateTimeFunction = 'SYSUTCDATETIME' THEN SYSUTCDATETIME()
                                           END;

    DECLARE @originalLogin sysname = QUOTENAME(ORIGINAL_LOGIN()); /* https://sqlstudies.com/2015/06/24/which-user-function-do-i-use/ */
    DECLARE @err_msg       nvarchar(max);
    DECLARE @raiseError    nvarchar(2000);

    /* Using RAISEEROR for interactive step printing http://sqlity.net/en/984/print-vs-raiserror/ */
    SET @raiseError = 'Benchmark started at ' + CONVERT(varchar(27), @startBenchmark, 121) + ' by ' + @originalLogin;
    RAISERROR(@raiseError, 0, 1) WITH NOWAIT;

    DECLARE @productMajorVersion sql_variant = SERVERPROPERTY('ProductMajorVersion');
    IF CAST(@productMajorVersion AS int) < 10
    BEGIN
        DECLARE @MsgError varchar(2000) = 'Stored procedure sp_BenchmarkTSQL works only for SQL Server 2008 and higher. Yor ProductMajorVersion is ' +
                                           CAST(@productMajorVersion AS varchar(30)) +
                                           '. You can try to replace datetime2 data type on datetime, perhaps it will be enough.';
        THROW 55001, @MsgError, 1;
    END;

    IF @tsqlStatement IS NULL
        THROW 55002, '@tsqlStatement is NULL, please specify TSQL statement.', 1;
    IF @tsqlStatement = N''
        THROW 55003, '@tsqlStatement is empty, please specify TSQL statement.', 1;

    IF @durationAccuracy NOT IN (
          'ns'  /* nanosecond  */
        , 'mcs' /* microsecond */
        , 'ms'  /* millisecond */
        , 'ss'  /* second      */
        , 'mi'  /* minute      */
        , 'hh'  /* hour        */
        , 'dd'  /* day         */
        , 'wk'  /* week        */
    )
    THROW 55004, '@durationAccuracy accept only this values: ns, mcs, ms, ss, mi, hh, wk, dd. Default value is ss. See DATEDIFF https://docs.microsoft.com/sql/t-sql/functions/datediff-transact-sql' , 1;

    IF @dateTimeFunction NOT IN ('SYSDATETIME', 'SYSUTCDATETIME')
    THROW 55005, '@dateTimeFunction accept only SYSDATETIME and SYSUTCDATETIME. Default value is SYSDATETIME. See https://docs.microsoft.com/sql/t-sql/functions/date-and-time-data-types-and-functions-transact-sql', 1;

    IF @numberOfExecution <= 0 OR @numberOfExecution > 32000
        THROW 55006, '@numberOfExecution must be > 0 and <= 32000. If you want more execution then comment 273 and 274 lines.', 1;

    IF @skipTSQLCheck = 0
    BEGIN
        IF @tsqlStatementBefore IS NOT NULL AND @tsqlStatementBefore <> '' AND EXISTS (
            SELECT 1
            FROM sys.dm_exec_describe_first_result_set(@tsqlStatementBefore, NULL, 0)
            WHERE error_message   IS NOT NULL
              AND error_number    IS NOT NULL
              AND error_severity  IS NOT NULL
              AND error_state     IS NOT NULL
              AND error_type      IS NOT NULL
              AND error_type_desc IS NOT NULL
              )
        BEGIN
            SELECT @err_msg = [error_message]
            FROM sys.dm_exec_describe_first_result_set(@tsqlStatementBefore, NULL, 0)
            WHERE column_ordinal = 0;

            THROW 55007, @err_msg, 1;
        END;

        IF @tsqlStatement IS NOT NULL AND @tsqlStatement <> N'' AND EXISTS (
            SELECT 1
            FROM sys.dm_exec_describe_first_result_set(@tsqlStatement, NULL, 0)
            WHERE error_message IS NOT NULL
              AND error_number IS NOT NULL
              AND error_severity IS NOT NULL
              AND error_state IS NOT NULL
              AND error_type IS NOT NULL
              AND error_type_desc IS NOT NULL
              )
        BEGIN
            SELECT @err_msg = [error_message]
            FROM sys.dm_exec_describe_first_result_set(@tsqlStatement, NULL, 0)
            WHERE column_ordinal = 0;

            THROW 55008, @err_msg, 1;
        END;

        IF @tsqlStatementAfter IS NOT NULL AND @tsqlStatementAfter <> N'' AND EXISTS (
            SELECT 1
            FROM sys.dm_exec_describe_first_result_set(@tsqlStatementAfter, NULL, 0)
            WHERE error_message IS NOT NULL
              AND error_number IS NOT NULL
              AND error_severity IS NOT NULL
              AND error_state IS NOT NULL
              AND error_type IS NOT NULL
              AND error_type_desc IS NOT NULL
              )
        BEGIN
            SELECT @err_msg = [error_message]
            FROM sys.dm_exec_describe_first_result_set(@tsqlStatementAfter, NULL, 0)
            WHERE column_ordinal = 0;
    
            THROW 55009, @err_msg, 1;
        END;
    END;

    IF @saveResults = 1 AND OBJECT_ID(N'master.dbo.BenchmarkTSQL', 'U') IS NULL
    THROW 55010, 'Please create master.dbo.BenchmarkTSQL log table before run procedure with @saveResults = 1.
    CREATE TABLE master.dbo.BenchmarkTSQL(
          BenchmarkTSQLID     int IDENTITY  NOT NULL
        , TSQLStatementGUID   varchar(36)   NOT NULL
        , StepRowNumber       int           NOT NULL
        , StartBenchmark      datetime2(7)  NOT NULL
        , EndBenchmark        datetime2(7)  NOT NULL
        , StartStep           datetime2(7)  NOT NULL
        , EndStep             datetime2(7)  NOT NULL
        , StepDuration        bigint        NOT NULL
        , DurationAccuracy    varchar(10)   NOT NULL
        , TsqlStatementBefore nvarchar(max) NULL
        , TsqlStatement       nvarchar(max) NOT NULL
        , TsqlStatementAfter  nvarchar(max) NULL
        , ClearCache          bit           NOT NULL
        , PrintStepInfo       bit           NOT NULL
        , OriginalLogin       sysname       NOT NULL
        , AdditionalInfo      xml           NULL
    );', 1;

    DECLARE @crlf          nvarchar(10) = CHAR(13) + CHAR(10);
    DECLARE @stepNumber    int          = 0;
    DECLARE @min           bigint;
    DECLARE @avg           bigint;
    DECLARE @max           bigint;
    DECLARE @median        real;
    DECLARE @planHandle    varbinary(64);
    DECLARE @startStep     datetime2(7);
    DECLARE @endStep       datetime2(7);
    DECLARE @stepDuration  int;
    DECLARE @additionalXML xml;

    DECLARE @BenchmarkTSQL TABLE(
        StepNumber          int          NOT NULL
      , StartBenchmark      datetime2(7) NOT NULL
      , EndBenchmark        datetime2(7) NOT NULL
      , StartStep           datetime2(7) NOT NULL
      , EndStep             datetime2(7) NOT NULL
      , StepDuration        bigint       NOT NULL
      , ClearCache          bit          NOT NULL
      , PrintStepInfo       bit          NOT NULL
      , DurationAccuracy    varchar(10)  NOT NULL
      , AdditionalInfo      xml          NULL
      );

    IF @additionalInfo = 1
    SET @tsqlStatement = @tsqlStatement + @crlf + N'
        SET @additionalXMLOUT = (
        SELECT [Option], [Enabled]
        FROM (
               SELECT ''DISABLE_DEF_CNST_CHK'' AS "Option", CASE @@options & 1     WHEN 0 THEN 0 ELSE 1 END AS "Enabled" UNION ALL
               SELECT ''IMPLICIT_TRANSACTIONS''           , CASE @@options & 2     WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''CURSOR_CLOSE_ON_COMMIT''          , CASE @@options & 4     WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ANSI_WARNINGS''                   , CASE @@options & 8     WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ANSI_PADDING''                    , CASE @@options & 16    WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ANSI_NULLS''                      , CASE @@options & 32    WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ARITHABORT''                      , CASE @@options & 64    WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ARITHIGNORE''                     , CASE @@options & 128   WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''QUOTED_IDENTIFIER''               , CASE @@options & 256   WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''NOCOUNT''                         , CASE @@options & 512   WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ANSI_NULL_DFLT_ON''               , CASE @@options & 1024  WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''ANSI_NULL_DFLT_OFF''              , CASE @@options & 2048  WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''CONCAT_NULL_YIELDS_NULL''         , CASE @@options & 4096  WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''NUMERIC_ROUNDABORT''              , CASE @@options & 8192  WHEN 0 THEN 0 ELSE 1 END UNION ALL
               SELECT ''XACT_ABORT''                      , CASE @@options & 16384 WHEN 0 THEN 0 ELSE 1 END
        ) AS s
        FOR XML RAW
        );';

    IF @saveResults = 1
    BEGIN
        DECLARE @TSQLStatementGUID varchar(36) = NEWID();
        PRINT(N'TSQLStatementGUID in log table is: ' + @TSQLStatementGUID + @crlf + @crlf);
    END;

    WHILE @stepNumber < @numberOfExecution
    BEGIN
        SET @stepNumber = @stepNumber + 1;

        IF @clearCache = 1
        BEGIN
            SELECT @planHandle = plan_handle
            FROM sys.dm_exec_cached_plans
            CROSS APPLY sys.dm_exec_sql_text(plan_handle)
            WHERE [text] LIKE @tsqlStatement;  /* LIKE instead = (equal) because = (equal) ignore trailing spaces */

            IF @planHandle IS NOT NULL DBCC FREEPROCCACHE(@planHandle);
        END;

        IF @tsqlStatementBefore IS NOT NULL AND @tsqlStatementBefore <> ''
            EXEC sp_executesql @tsqlStatementBefore;

        BEGIN /* Run bencmark step and calculate it duration */
            SET @startStep = CASE WHEN @dateTimeFunction = 'SYSDATETIME'    THEN SYSDATETIME()
                                  WHEN @dateTimeFunction = 'SYSUTCDATETIME' THEN SYSUTCDATETIME()
                             END;

           IF @dateTimeFunction = 'SYSDATETIME'
           BEGIN
               IF @additionalInfo = 0
               BEGIN
                   EXEC sp_executesql @tsqlStatement;
                   SET @endStep = SYSDATETIME();
               END;

               IF @additionalInfo = 1
               BEGIN
                   EXEC sp_executesql @tsqlStatement, N'@additionalXMLOUT XML OUTPUT', @additionalXMLOUT = @additionalXML OUTPUT SELECT @additionalXML;
                   SET @endStep = SYSDATETIME();
               END;
           END;

           IF @dateTimeFunction = 'SYSUTCDATETIME'
           BEGIN
               IF @additionalInfo = 0
               BEGIN
                   EXEC sp_executesql @tsqlStatement;
                   SET @endStep = SYSUTCDATETIME();
               END;

               IF @additionalInfo = 1
               BEGIN
                   EXEC sp_executesql @tsqlStatement, N'@additionalXMLOUT XML OUTPUT', @additionalXMLOUT = @additionalXML OUTPUT SELECT @additionalXML;
                   SET @endStep = SYSUTCDATETIME();
               END;
           END;

        END;

        SET @stepDuration = CASE WHEN @durationAccuracy = 'ns'  THEN DATEDIFF(ns,  @startStep, @endStep)
                                 WHEN @durationAccuracy = 'mcs' THEN DATEDIFF(mcs, @startStep, @endStep)
                                 WHEN @durationAccuracy = 'ms'  THEN DATEDIFF(ms,  @startStep, @endStep)
                                 WHEN @durationAccuracy = 'ss'  THEN DATEDIFF(ss,  @startStep, @endStep)
                                 WHEN @durationAccuracy = 'mi'  THEN DATEDIFF(mi,  @startStep, @endStep)
                                 WHEN @durationAccuracy = 'hh'  THEN DATEDIFF(hh,  @startStep, @endStep)
                                 WHEN @durationAccuracy = 'dd'  THEN DATEDIFF(dd,  @startStep, @endStep)
                                 WHEN @durationAccuracy = 'wk'  THEN DATEDIFF(wk,  @startStep, @endStep)
                                 ELSE 0
                            END;

        INSERT @BenchmarkTSQL (
              StepNumber
            , StartBenchmark
            , EndBenchmark
            , StartStep
            , EndStep
            , StepDuration
            , DurationAccuracy
            , ClearCache
            , PrintStepInfo
            , AdditionalInfo
            )
        VALUES (
              @stepNumber
            , @startBenchmark
            , /* it does not matter which function use (this is NOT NULL column)
                  becasue we update this column later with correct values */
              SYSDATETIME()
            , @startStep
            , @endStep
            , @stepDuration
            , @durationAccuracy
            , @clearCache
            , @printStepInfo
            , @additionalXML
            );

       IF @saveResults = 1
       BEGIN
          INSERT INTO master.dbo.BenchmarkTSQL(
            TSQLStatementGUID
          , StepRowNumber
          , StartBenchmark
          , EndBenchmark
          , StartStep
          , EndStep
          , StepDuration
          , DurationAccuracy
          , TsqlStatementBefore
          , TsqlStatement
          , TsqlStatementAfter
          , ClearCache
          , PrintStepInfo
          , OriginalLogin
          , AdditionalInfo
          )
          SELECT @TSQLStatementGUID AS TSQLStatementGUID
               , @stepNumber AS StepRowNumber
               , StartBenchmark
               , EndBenchmark
               , StartStep
               , EndStep
               , StepDuration
               , DurationAccuracy
               , @tsqlStatementBefore
               , @tsqlStatement
               , @tsqlStatementAfter
               , ClearCache
               , PrintStepInfo
               , @originalLogin AS OriginalLogin
               , @additionalXML AS AdditionalInfo
           FROM @BenchmarkTSQL
           WHERE StepNumber = @stepNumber;
       END;

       IF @printStepInfo = 1
       BEGIN
       /* Using RAISEEROR for interactive step printing http://sqlity.net/en/984/print-vs-raiserror/ */
           SET @raiseError = 'Run ' + CASE WHEN @stepNumber < 10    THEN '    ' + CAST(@stepNumber AS varchar(30))
                                           WHEN @stepNumber < 100   THEN '   '  + CAST(@stepNumber AS varchar(30))
                                           WHEN @stepNumber < 1000  THEN '  '   + CAST(@stepNumber AS varchar(30))
                                           WHEN @stepNumber < 10000 THEN ' '    + CAST(@stepNumber AS varchar(30))
                                           ELSE CAST(@stepNumber AS varchar(30))
                                      END +
                              ', start: '    + CONVERT(varchar(27), @startStep, 121) +
                              ', finish: '   + CONVERT(varchar(27), CASE WHEN @dateTimeFunction = 'SYSDATETIME'    THEN SYSDATETIME()
                                                                         WHEN @dateTimeFunction = 'SYSUTCDATETIME' THEN SYSUTCDATETIME()
                                                                    END, 121) +
                              ', step duration: ' + REPLACE(CONVERT(varchar(30), CONVERT(money, @stepDuration), 1), '.00', '') + ' ' + @durationAccuracy + @crlf;
           RAISERROR(@raiseError, 0, 1) WITH NOWAIT;
        END;

        IF @tsqlStatementAfter IS NOT NULL AND @tsqlStatementAfter <> ''
            EXEC sp_executesql @tsqlStatementAfter;

    END;

    SELECT @min = MIN(StepDuration), @avg = AVG(StepDuration), @max = MAX(StepDuration)
      FROM @BenchmarkTSQL;

    DECLARE @endBenchmark datetime2(7) = CASE WHEN @dateTimeFunction = 'SYSDATETIME'    THEN SYSDATETIME()
                                              WHEN @dateTimeFunction = 'SYSUTCDATETIME' THEN SYSUTCDATETIME()
                                         END;

    IF @saveResults = 1
    BEGIN
        UPDATE master.dbo.BenchmarkTSQL
           SET EndBenchmark = @endBenchmark
         WHERE TSQLStatementGUID = @TSQLStatementGUID;
    END;

    IF @calcMedian = 1
    BEGIN
        SELECT @median =
        (
             (SELECT MAX(TMIN) FROM
                  (SELECT TOP(50) PERCENT
                          CASE WHEN @durationAccuracy = 'ns'  THEN DATEDIFF(ns,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'mcs' THEN DATEDIFF(mcs, StartStep, EndStep)
                               WHEN @durationAccuracy = 'ms'  THEN DATEDIFF(ms,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'ss'  THEN DATEDIFF(ss,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'mi'  THEN DATEDIFF(mi,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'hh'  THEN DATEDIFF(hh,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'dd'  THEN DATEDIFF(dd,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'wk'  THEN DATEDIFF(wk,  StartStep, EndStep)
                               ELSE 0
                          END AS TMIN
                   FROM @BenchmarkTSQL
                   ORDER BY TMIN
                  ) AS BottomHalf
             )
             +
             (SELECT MIN(TMAX) FROM
                  (SELECT TOP 50 PERCENT
                          CASE WHEN @durationAccuracy = 'ns'  THEN DATEDIFF(ns,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'mcs' THEN DATEDIFF(mcs, StartStep, EndStep)
                               WHEN @durationAccuracy = 'ms'  THEN DATEDIFF(ms,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'ss'  THEN DATEDIFF(ss,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'mi'  THEN DATEDIFF(mi,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'hh'  THEN DATEDIFF(hh,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'dd'  THEN DATEDIFF(dd,  StartStep, EndStep)
                               WHEN @durationAccuracy = 'wk'  THEN DATEDIFF(wk,  StartStep, EndStep)
                               ELSE 0
                          END AS TMAX
                   FROM @BenchmarkTSQL
                   ORDER BY TMAX DESC
                  ) AS TopHalf
             )
         ) / 2.0;
    END;

    DECLARE @endTime datetime2(7) = CASE WHEN @dateTimeFunction = 'SYSDATETIME'    THEN SYSDATETIME()
                                         WHEN @dateTimeFunction = 'SYSUTCDATETIME' THEN SYSUTCDATETIME()
                                    END;

    /* Using RAISEEROR for interactive step printing http://sqlity.net/en/984/print-vs-raiserror/ */
    SET @raiseError = @crlf +
         'Min:     ' + REPLACE(CONVERT(varchar(30), CONVERT(money, @min),    1), '.00', '') + ' ' + @durationAccuracy + @crlf +
         'Average: ' + REPLACE(CONVERT(varchar(30), CONVERT(money, @avg),    1), '.00', '') + ' ' + @durationAccuracy + @crlf +
         CASE WHEN @calcMedian = 1 THEN
         'Median:  ' + REPLACE(CONVERT(varchar(30), CONVERT(money, @median) ,1), '.00', '') + ' ' + @durationAccuracy
              ELSE ''
         END + @crlf +
         'Max:     ' + REPLACE(CONVERT(varchar(30), CONVERT(money, @max),    1), '.00', '') + ' ' + @durationAccuracy + @crlf + @crlf +
         'Benchmark ended at ' + CONVERT(varchar(23), @endTime, 121) + ' by ' + @originalLogin;
    RAISERROR(@raiseError, 0, 1) WITH NOWAIT;

    DECLARE @benchmarkDuration bigint = CASE WHEN @durationAccuracy = 'ns'  THEN DATEDIFF(ns,  @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'mcs' THEN DATEDIFF(mcs, @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'ms'  THEN DATEDIFF(ms,  @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'ss'  THEN DATEDIFF(ss,  @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'mi'  THEN DATEDIFF(mi,  @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'hh'  THEN DATEDIFF(hh,  @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'dd'  THEN DATEDIFF(dd,  @startBenchmark, @endBenchmark)
                                             WHEN @durationAccuracy = 'wk'  THEN DATEDIFF(wk,  @startBenchmark, @endBenchmark)
                                             ELSE 0
                                        END;

    /* Using RAISEEROR for interactive step printing http://sqlity.net/en/984/print-vs-raiserror/ */
    SET @raiseError = @crlf + 'Duration of benchmark: ' + REPLACE(CONVERT(varchar(30), CONVERT(money, @benchmarkDuration), 1), '.00', '') + ' ' + @durationAccuracy + @crlf;
    RAISERROR(@raiseError, 0, 1) WITH NOWAIT;

END TRY

BEGIN CATCH
    PRINT(@crlf +
            'Error: '       + CONVERT(varchar(50), ERROR_NUMBER())  +
          ', Severity: '  + CONVERT(varchar(5), ERROR_SEVERITY()) +
          ', State: '     + CONVERT(varchar(5), ERROR_STATE())    +
          ', Procedure: ' + ISNULL(ERROR_PROCEDURE(), '-')        +
          ', Line: '      + CONVERT(varchar(5), ERROR_LINE())     +
          ', User name: ' + CONVERT(sysname, ORIGINAL_LOGIN())
          );
    PRINT(ERROR_MESSAGE() + @crlf + @crlf);

    IF ERROR_NUMBER() = 535
    PRINT('Your @durationAccuracy = ' + @durationAccuracy +
    '. Try to use @durationAccuracy with a less precise datepart - seconds (ss) or minutes (mm) or hours (hh).' + @crlf +
    'But in log table master.dbo.BenchmarkTSQL (if you run stored procedure with @saveResult = 1) all times saving with datetime2(7) precise!' + @crlf +
    'You can manualy calculate difference after decreasing precise datepart.' + @crlf +
    'For analyze log table see latest example in document section.') + @crlf + @crlf;
END CATCH;
GO

EXEC master.sys.sp_MS_marksystemobject @objname = N'dbo.sp_BenchmarkTSQL';
GO
