Skip to content

Instantly share code, notes, and snippets.

@tcartwright
Created April 17, 2026 15:03
Show Gist options
  • Select an option

  • Save tcartwright/16070985fa6c091f75372238e4849506 to your computer and use it in GitHub Desktop.

Select an option

Save tcartwright/16070985fa6c091f75372238e4849506 to your computer and use it in GitHub Desktop.
SQL SERVER: Find plan cache queries using user defined functions
-- Finds cached plans whose showplan XML contains a UDF reference,
-- and tags them by DML statement type (SELECT/INSERT/UPDATE/DELETE/MERGE).
WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p)
SELECT
udf_ref.value('@Database', 'sysname') AS udf_database,
udf_ref.value('@Schema', 'sysname') AS udf_schema,
udf_ref.value('@Table', 'sysname') AS udf_name, -- showplan (oddly) labels UDF names with @Table
CASE
WHEN UPPER(LTRIM(stmt_text)) LIKE 'INSERT%' THEN 'INSERT'
WHEN UPPER(LTRIM(stmt_text)) LIKE 'UPDATE%' THEN 'UPDATE'
WHEN UPPER(LTRIM(stmt_text)) LIKE 'DELETE%' THEN 'DELETE'
WHEN UPPER(LTRIM(stmt_text)) LIKE 'MERGE%' THEN 'MERGE'
WHEN UPPER(LTRIM(stmt_text)) LIKE 'SELECT%' THEN 'SELECT'
WHEN UPPER(LTRIM(stmt_text)) LIKE 'WITH%' THEN 'SELECT (CTE)'
ELSE 'OTHER'
END AS statement_type,
cp.objtype,
DB_NAME(st.dbid) AS outer_database,
OBJECT_SCHEMA_NAME(st.objectid, st.dbid) AS outer_object_schema,
OBJECT_NAME(st.objectid, st.dbid) AS outer_object_name,
cp.usecounts,
qs.execution_count,
qs.total_worker_time,
qs.total_elapsed_time,
qs.total_logical_reads,
stmt_text AS statement_text,
st.text AS full_batch_text,
qp.query_plan
FROM sys.dm_exec_cached_plans cp
CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
CROSS APPLY qp.query_plan.nodes('//p:UDF') AS u(udf_ref)
OUTER APPLY (
SELECT TOP 1 qs.*
FROM sys.dm_exec_query_stats qs
WHERE qs.plan_handle = cp.plan_handle
ORDER BY qs.execution_count DESC
) qs
OUTER APPLY (
SELECT SUBSTRING(
st.text,
(ISNULL(qs.statement_start_offset, 0) / 2) + 1,
(( CASE ISNULL(qs.statement_end_offset, -1)
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - ISNULL(qs.statement_start_offset, 0)) / 2) + 1
) AS stmt_text
) s
WHERE qp.query_plan IS NOT NULL
AND cp.objtype IN ('Adhoc', 'Prepared', 'Proc', 'Trigger') -- tighten as needed
ORDER BY qs.execution_count DESC, cp.usecounts DESC;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment