7

If I have a query that returns a query_plan, for instance like this:

    SELECT TOP 1000 st.TEXT
    ,cp.size_in_bytes
    ,cp.plan_handle
    ,QP.query_plan
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
WHERE cp.objtype = N'Adhoc'
    AND cp.usecounts = 1

Then I can click on a query_plan and hover over the left most icon, where tip-text will list the Estimated Subtree Cost.

Is there a way of getting that Estimated Subtree Cost out as a separate column for my query?

I understand that the number is unit-less, and refers to a particular developers PC some 20 years ago. Even so, I think it might tell me how long the query should take if statistics are not too far out.

I've tried really hard to Google for this info, but even dba.stackexchange.com comes up empty.

7

I believe you will have to do some XML query work to get that estimated cost.

See if this is what you are looking for:

   ;WITH XMLNAMESPACES  
    (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
    SELECT TOP 1000 st.text
        ,cp.size_in_bytes
        ,cp.plan_handle
        ,QP.query_plan
        ,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)
    WHERE cp.objtype = N'Adhoc'
        AND cp.usecounts = 1
    OPTION(RECOMPILE);

This will return the cost for individual statements inside of a batch. You might need to work in some grouping if you need the total estimated subtree cost for the entire batch.

1

Here is how I used the answer to my question.

I'm a great fan of sp_whoisactive. If you don't have that, stop reading; download here.

So I've set it up to collect a snap-shot every 10 minutes, like this:

DROP TABLE dbo.HESPOmonitoring_output
DECLARE @s VARCHAR(MAX)
EXEC sp_WhoIsActive 
    @output_column_list = '[login_name][dd%][session_id][program%][sql_com%][sql_text][block%][reads][writes][physical_reads][query_plan][used_memory][tempdb%][wait%][start_time][collection_time][host%][additional%]', @get_outer_command=1, @get_additional_info=1,
    @return_schema = 1, @get_plans=1, 
    @schema = @s OUTPUT
SET @s = REPLACE(@s, '<table_name>', 'dbo.HESPOmonitoring_output')
EXEC(@s)
ALTER TABLE dbo.HESPOmonitoring_output ADD HESPOmonitoring_outputID BIGINT IDENTITY(1,1) NOT NULL
go
SET NOCOUNT ON 
DECLARE @Started DATETIME=DATEADD(DAY, 3, GETDATE())
WHILE 1 > 0 BEGIN 
    EXEC sp_WhoIsActive 
        @output_column_list = '[login_name][dd%][session_id][program%][sql_com%][sql_text][block%][reads][writes][physical_reads][query_plan][used_memory][tempdb%][wait%][start_time][collection_time][host%][additional%]', @get_outer_command=1, @get_additional_info=1,
         @get_plans=1, 
        @destination_table = 'dbo.HESPOmonitoring_output'
    WAITFOR DELAY '00:10:00'
    IF GETDATE() > @Started BREAK  
END 

I've let it run for a while (up to 3 days). Then I convert the collected data like this:

/* this query turns HESPOmonitoring_output in a table with one row per SQL statement */
Begin TRY
    DROP TABLE #hespo
END TRY
BEGIN CATCH
END CATCH
;WITH XMLNAMESPACES  
    (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
     SELECT top 10000 H.Start_Time, H.session_id, MAX(H.program_name) AS program_name, MAX(CAST(H.sql_command AS VARCHAR(max))) AS sql_command
    , MAX(CAST(H.sql_text AS VARCHAR(max))) AS sql_text
    , MAX(H.reads) AS reads
    , MAX(H.physical_reads) AS physical_reads
    , MAX(H.writes) AS writes
    , MAX(H.collection_time) AS collection_time
    , MAX(DATEDIFF(second, start_time, collection_time)) AS RunTime
    , MAX(HESPOmonitoring_outputID) AS MaxHESPOmonitoring_outputID
    , MAX(H.blocking_session_id) AS MaxBlocking_session_id
    , Min(H.blocking_session_id) AS MinBlocking_session_id
    , count_big(*) as RowCnt 
    , MAX(TRY_CAST(n.value('(@StatementSubTreeCost)[1]', 'VARCHAR(128)') AS DECIMAL(18,3))) AS StatementSubTreeCost
    INTO #hespo
    FROM dbo.HESPOmonitoring_output H
    CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS qn(n)
    GROUP BY H.Start_Time, H.session_id

And finally I get a list that shows me where Estimated Cost is small compared to runtime, without the job being blocked at all

SELECT top 10000 
H.StatementSubTreeCost/NULLIF(H.RunTime, 0) AS Ratio, H.StatementSubTreeCost, H.RunTime, *
FROM #hespo H
WHERE H.MinBlocking_session_id IS NULL
AND H.RunTime>0
AND H.StatementSubTreeCost IS NOT NULL
ORDER BY 1 

It is a very interesting list, but it comes with a bit of noise. For a start, I've chosen to ignore quick small jobs that run less than 10 minutes, but that limit depends on your situation.

Now it is a lot easier to find jobs that run with a bad plan.
Thank you very much for your help.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.