'Best practice for changing SQL Server compatibility level from 120 to 140

We have SQL Server 2014 and we moved to 2017, we didn't update compatibility level yet to 140, so we keep it 120, but before doing it, I want to make sure it will not affect anything.

Is there any best practice to follow before changing compatibility level?, should be rebuild indexes, update statistics, change default setting or something like that?

Thank you.



Solution 1:[1]

I wrote this script to do some benchmarking between compatibility levels 100 and 150. You can change that easily. Follow the instructions in the comments. I save results to a table in dbo, but you could change that easily as well.

/*
This script will benchmark the differences between an older version of SQL and SQL 2019.  

THIS SCRIPT SHOULD NEVER BE RUN ON A PRODUCTION SERVER AS IT CLEARS THE QUERY CACHE!!!!

To avoid going crazy seeing a million results in the results pane, enable "discard results after query execution" 
and then after the script is run, turn it back off and view the temp table.  (SELECT * FROM Benchmark_performance_stats)
(Query -> Query Options -> Results -> Discard results after query execution)

You can set the schema to check in the next declare statement
*/
DECLARE @schema sysname = 'dbo'

IF (SELECT @@VERSION) NOT LIKE '%2019%'
BEGIN
    PRINT 'This server is not runnning SQL 2019. This script requires SQL 2019'
    PRINT 'Script aborting'
    SET NOEXEC ON
END

SET NOCOUNT ON
IF OBJECT_ID('Benchmark_performance_stats','U') IS NULL
CREATE TABLE Benchmark_performance_stats (
    ID INT IDENTITY(1,1),
    view_name NVARCHAR(255) NULL,
    old_sql_text NVARCHAR(1000),
    old_cpu_time BIGINT,
    old_query_cost FLOAT,
    new_sql_text NVARCHAR(1000),
    new_cpu_time BIGINT,
    new_query_cost float,
    cpu_time_ratio float,
    query_cost_ratio float
    )

DECLARE @counter INT = 1  --this keeps track of the temp table row so that we add both test results to the same row
DECLARE @view_name sysname
DECLARE @sql_command NVARCHAR(1000)

/*
Start process of cycling through all these views
First, close any open cursor
*/

IF CURSOR_STATUS('global', 'views_to_check') >= -1
BEGIN
    CLOSE views_to_check
    DEALLOCATE views_to_check
END

DECLARE views_to_check CURSOR FOR
    SELECT TOP 10 v.name        /* chanbge 10 to the number of views you want to check per each run */
    FROM sys.views v
    INNER JOIN sys.schemas s
    ON s.schema_id = v.schema_id
    WHERE s.name = @schema
        AND v.name NOT IN ( SELECT view_name FROM dbo.Benchmark_performance_stats)  /* do not test views already completed*/
    ORDER BY v.name

OPEN views_to_check
FETCH NEXT FROM views_to_check
INTO @view_name

WHILE @@FETCH_STATUS = 0
BEGIN
    /* Free memory and query cache*/
    DBCC FREEPROCCACHE WITH NO_INFOMSGS
    DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS

    SET @sql_command = 'SELECT * FROM [' + @schema + '].[' + @view_name + '] OPTION(USE HINT(''QUERY_OPTIMIZER_COMPATIBILITY_LEVEL_100''))'
    RAISERROR (@sql_command,0,1) WITH NOWAIT  --force output pane to flush

    BEGIN TRY
        EXEC sp_executesql @sql_command

        /* grab the worker time and estimate query cost from the DMVs */
        ;WITH XMLNAMESPACES  
        (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
        INSERT INTO Benchmark_performance_stats (view_name, old_sql_text,old_cpu_time,old_query_cost)
        SELECT 
            @view_name,
            st.text
            ,qs.last_worker_time
            ,n.value('(@StatementSubTreeCost)[1]', 'VARCHAR(128)') AS StatementSubTreeCost
        FROM sys.dm_exec_cached_plans AS cp
        CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
        CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) AS QP
        CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS qn(n)
        INNER JOIN sys.dm_exec_query_stats qs
        ON qs.plan_handle = cp.plan_handle
        WHERE text = @sql_command
        OPTION(RECOMPILE);
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE()
        INSERT INTO Benchmark_performance_stats (view_name, old_sql_text, old_cpu_time, old_query_cost)
        VALUES (@view_name, @sql_command, NULL, NULL)
    END CATCH 

    SET @counter = (SELECT MAX(ID) FROM Benchmark_performance_stats)  -- we do it this way becuase if there is an error, the counter would not match any longer

    /* now do the same trick for the new compatibility level */
    DBCC FREEPROCCACHE WITH NO_INFOMSGS
    DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS

    SET @sql_command = 'SELECT * FROM [' + @schema + '].[' + @view_name + '] OPTION (USE HINT(''QUERY_OPTIMIZER_COMPATIBILITY_LEVEL_150''))'
    RAISERROR (@sql_command,0,1) WITH NOWAIT  --force output pane to flush
    BEGIN TRY
        EXEC sp_executesql @sql_command

        ;WITH XMLNAMESPACES  
        (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
        UPDATE Benchmark_performance_stats
        SET new_sql_text = x.text,
            new_cpu_time = x.last_worker_time,
            new_query_cost = x.StatementSubTreeCost
        FROM (
            SELECT st.text
                ,qs.last_worker_time
                ,n.value('(@StatementSubTreeCost)[1]', 'VARCHAR(128)') AS StatementSubTreeCost
            FROM sys.dm_exec_cached_plans AS cp
            CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
            CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) AS QP
            CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS qn(n)
            INNER JOIN sys.dm_exec_query_stats qs
            ON qs.plan_handle = cp.plan_handle
            WHERE text = @sql_command
        ) X
        WHERE ID = @counter
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE()
    END CATCH 

    FETCH NEXT FROM views_to_check
    INTO @view_name
END

CLOSE views_to_check
DEALLOCATE views_to_check

UPDATE Benchmark_performance_stats
SET cpu_time_ratio = new_cpu_time / CAST (old_cpu_time AS FLOAT),
    query_cost_ratio = new_query_cost / CAST(old_query_cost AS FLOAT)

SET NOCOUNT OFF
SET NOEXEC OFF /* if we bailed earlier, turn execution back on now that we have reached end of script */

/*
When finished, turn discard query results after execution back off and run the follwing command to see results.
SELECT *
FROM dbo.Benchmark_performance_stats
*/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Robert Sievers