3

We have recently made a change to the CTFP setting on our test server with a view to rolling this out to live.

Our current CTFP is 15 so we tried setting it to 50 which is often seen as a better value.

MAXDOP for the server is set at 8 and has been for some time, the server has 16 CPU cores.

To evaluate the effectiveness of the change, we decided to use query store. While CTFP is a server wide setting, our main workload is in one database, on which we have query store enabled.

To analyse the effectiveness of the change, we first dumped a list of all the queries that had a cost of 16 - 49 which would no longer be eligible for paralellism and then made the change

USE MyDB
GO

;WITH XMLNAMESPACES(DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT  q.query_id,
        qt.statement_sql_handle
INTO    SomeDb.dbo.AffectedCTFPQueries
FROM    sys.query_store_plan p
        JOIN sys.query_store_query q
            ON p.query_id = q.query_id
        JOIN sys.query_store_query_text qt
            ON qt.query_text_id = q.query_text_id
WHERE   TRY_CONVERT(XML,p.query_plan).value('(/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple/@StatementSubTreeCost)[1]','FLOAT') > 15 AND
        TRY_CONVERT(XML,p.query_plan).value('(/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple/@StatementSubTreeCost)[1]','FLOAT') < 50


/* change CTFP */
EXEC sys.sp_configure N'cost threshold for parallelism', N'50'
GO
RECONFIGURE WITH OVERRIDE
GO

/* free proc cache so new queries get a compilation */
DBCC FREEPROCCACHE

We then let our workload run for a few days and then analyzed the results:

First dump all the executions of the affected queries into a temp table

DECLARE @DBCCFREEPROCCACHETime DATETIME = '2022-06-30T15:54:54' 

/* get all of the executions since the change */
SELECT  a.query_id,
        p.plan_id AS PlanIDD,
        r.*
INTO    #Executions
FROM    (SELECT DISTINCT * FROM SomeDb.dbo.AffectedCTFPQueries) a
        JOIN sys.query_store_plan p
                ON a.query_id = p.query_id
        JOIN sys.query_store_runtime_stats r
                ON r.plan_id = p.plan_id
WHERE   r.first_execution_time >= @DBCCFREEPROCCACHETime AND
        count_executions = 1

Then we can see:

How many of the affected queries have run since the change

/* how many affected queries have run since the CTFP change time */
SELECT      'NumAffectedRunSinceChange' AS NumAffectedRunSinceChange,
            COUNT(DISTINCT query_id) AS NumAffectedRunSinceChange
FROM        #Executions

Which of the affected queries that have run have gone parallel despite being eligible for serial execution - the explanation seems to be this

SELECT      *
FROM        #Executions
WHERE       avg_dop > 1

Which of the affected queries that have run are now running in serial - add these to a temp table so we can analyze how running in serial has affected their performance

SELECT      DISTINCT query_id
INTO        #Dop1Queries
FROM        #Executions
WHERE       avg_dop = 1  AND query_id NOT IN(
                                                /* ignore queries that have run in serial AND paralell */
                                                SELECT      COUNT(DISTINCT query_id) AS [NumAffectedRunInDop>1]
                                                FROM        #Executions
                                                WHERE       avg_dop > 1
                                                )

Queries that have improved / regressed in terms of duration. I also did some CPU time analysis but have excluded those queries for brevity

/* workload period before change */
SELECT  a.query_id,
        p.plan_id AS PlanIDD,
        r.*
INTO    #BeforeChange
FROM    #Dop1Queries a
        JOIN sys.query_store_plan p
            ON a.query_id = p.query_id
        JOIN sys.query_store_runtime_stats r
            ON r.plan_id = p.plan_id
WHERE   r.first_execution_time >= '2022-06-28T17:00:00' AND
        r.first_execution_time <= '2022-06-29T08:00:00'

/* workload period after change */
SELECT  a.query_id,
        p.plan_id AS PlanIDD,
        r.*
INTO    #AfterChange
FROM    #Dop1Queries a
        JOIN sys.query_store_plan p
            ON a.query_id = p.query_id
        JOIN sys.query_store_runtime_stats r
            ON r.plan_id = p.plan_id
WHERE   r.first_execution_time >= '2022-06-30T17:00:00' AND
        r.first_execution_time <= '2022-07-01T08:00:00'

/* improved duration queries */
SELECT  'Improved Duration Queries',
        t.query_sql_text,
        a.query_id,
        a.avg_duration / 1000000 AS BeforeDurationSec,
        b.avg_duration / 1000000 AS AfterDurationSec,
        (b.avg_duration - a.avg_duration) / 1000000 AS DurationDifferenceSec,
        SUM(((b.avg_duration - a.avg_duration) /1000000)) OVER () AS TotalDurationDifferenceSec
FROM    #BeforeChange a
        JOIN #AfterChange b
            ON a.query_id = b.query_id
        JOIN sys.query_store_query q
            ON b.query_id = q.query_id
        JOIN sys.query_store_query_text t
            ON t.query_text_id = q.query_text_id
WHERE   a.avg_duration > b.avg_duration
ORDER BY b.avg_duration - a.avg_duration DESC

/* regressed duration queries */
SELECT  'Regressed Duration Queries',
        t.query_sql_text,
        a.query_id,
        a.avg_duration / 1000000 AS BeforeDurationSec,
        b.avg_duration /1000000 AS AfterDurationSec,
        (b.avg_duration / 1000000) -( a.avg_duration / 1000000) AS DurationDifferenceSec,
        SUM(((b.avg_duration - a.avg_duration) / 1000000)) OVER () AS TotalDurationDifferenceSec
FROM    #BeforeChange a
        JOIN #AfterChange b
            ON a.query_id = b.query_id
        JOIN sys.query_store_query q
            ON b.query_id = q.query_id
        JOIN sys.query_store_query_text t
            ON t.query_text_id = q.query_text_id
WHERE   a.avg_duration < b.avg_duration
ORDER BY b.avg_duration - a.avg_duration DESC

The top 5 improved: enter image description here

The top 5 regressed: enter image description here

My conclusion from these findings is the effect of this change has been negligible, barring one query which appears to have significantly regressed and therefore there is no merit in changing CTFP to 50.

My questions

  • Have I gone the correct way about evaluating the impact of this change
  • Is there anything I have missed / overlooked?
  • Are my conclusions correct?
2
  • Remember cost threshold for parallelism is only a piece of the puzzle. max degree of parallelism is as, if not more, influential on your parallel plan performance. Sometimes SQL makes terrible decisions because it has too many cores available to throw at queries, so adjusting both settings is likely required. FWIW, you're doing a great job taking this approach and are ahead of the curve most DBAs take (e.g. slap a setting in there and forget about it until they see CXPACKET waits all over the place). Jul 13, 2022 at 20:56
  • @JohnEisbrener thanks, that's good to know :) I will update the question with detail of our MAXDOP settings
    – SE1986
    Jul 14, 2022 at 8:16

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.