SQL – Procedure and Function Caller Hierarchy

The “who do I call?” hierarchy.

Issue

You are tasked with debugging the slow performance of a stored procedure. The issue could be in the stored procedure or one of the functions it calls or missing indexes on tables. To get a complete picture you need to see the call hierarchy starting with the stored procedure or function thru to the tables.

Solution

SQL Server provides sys.sql_expression_dependencies which lists the relationship between caller and callee. To get the complete hierarchical tree from the stored procedure to the tables, we will call @Depends within a recursive CTE.

Let’s begin by creating tables, functions and a stored procedure to demonstrate.


-- Create tables to be the leaf nodes of the hierarchical tree.
CREATE TABLE Table_A (
     ID INT IDENTITY NOT NULL PRIMARY KEY
    ,Name varchar(10) NOT NULL DEFAULT 'A'
)
GO

CREATE TABLE Table_B (
     ID INT IDENTITY NOT NULL PRIMARY KEY
    ,Name varchar(10) NOT NULL DEFAULT 'B'
)
GO

CREATE TABLE Table_C (
     ID INT IDENTITY NOT NULL PRIMARY KEY
    ,Name varchar(10) NOT NULL DEFAULT 'C'
)
GO

-- Create functions to be the intermediate levels in the hierarchical tree.
CREATE FUNCTION Table_A_Count()
RETURNS INT
AS
BEGIN 
    RETURN (
        SELECT COUNT(1)
        FROM Table_A
    )
END
GO

CREATE FUNCTION Table_B_Count()
RETURNS INT
AS
BEGIN 
    RETURN (
        SELECT COUNT(1)
        FROM Table_B
    )
END
GO

CREATE FUNCTION Table_C_Count()
RETURNS INT
AS
BEGIN 
    RETURN (
        SELECT COUNT(1)
        FROM Table_C
    )
END
GO

-- Create stored procedure to be the root of the hierarchical tree.
CREATE PROCEDURE dbo.Table_Count
AS
BEGIN
    SELECT [Table] = 'A', [Row Count] = dbo.Table_A_Count()
    UNION
    SELECT [Table] = 'B', [Row Count] = dbo.Table_B_Count()
    UNION
    SELECT [Table] = 'C', [Row Count] = dbo.Table_C_Count()
END
GO

Within the CTE, there are two selects, the first is the base case and populates the initial caller (stored procedure) and it’s callees (functions).

The second select is the recursive case which recursively calls HierarchyCTE to get the callees for the caller. This recursive call continues until @Depends has no callees for the caller or the recursion is 100 levels deep (a CTE limitation).


-- ----------------------------------------------------------------------------
-- Create a temp table of distinct caller/callee relationships.  DISTINCT is
-- used since sys.sql_expression_dependencies may have multiple rows for a 
-- relationship.
-- The columns are also renamed as nouns instead of verbs.
DECLARE @Dependencies TABLE (
     CallerID INT
    ,CallerName nvarchar(257)
    ,CallerType char(2)
    ,CallerTypeName nvarchar(128)
    ,CalleeID INT
    ,CalleeName nvarchar(257)
    ,CalleeType char(2)
    ,CalleeTypeName nvarchar(128)
)
INSERT INTO @Dependencies (
     CallerID
    ,CallerName
    ,CallerType
    ,CallerTypeName
    ,CalleeID
    ,CalleeName
    ,CalleeType
    ,CalleeTypeName
)
SELECT DISTINCT 
     [CallerID] = referencing_id
    ,[CallerName] = CAST(callerSchema.name + '.' + caller.name AS nvarchar(257))
    ,[CallerType] = caller.type
    ,[CallerTypeName] = caller.type_desc
    ,[CalleeID] = referenced_id
    ,[CalleeName] = CAST(calleeSchema.name + '.' + callee.name AS nvarchar(257))
    ,[CalleeType] = callee.type
    ,[CalleeTypeName] = callee.type_desc
FROM sys.sql_expression_dependencies depends
JOIN sys.objects caller ON caller.object_id = depends.referencing_id
JOIN sys.schemas callerSchema on callerSchema.schema_id = caller.schema_id
JOIN sys.objects callee ON callee.object_id = depends.referenced_id
JOIN sys.schemas calleeSchema on calleeSchema.schema_id = callee.schema_id


-- ----------------------------------------------------------------------------
-- Get a hierarchical list of stored procedures, functions and tables used by 
-- the specified stored procedure or function.
;WITH HierarchyCTE
AS (
    -- Base Case: Get the root level of caller and callee relationship.
    SELECT --
         [CallerID]
        ,[CallerName]
        ,[CallerType]
        ,[CallerTypeName]
        ,[CalleeID]
        ,[CalleeName]
        ,[CalleeType]
        ,[CalleeTypeName]
        ,[Path] = CAST(CallerName + ' » ' + CalleeName AS nvarchar(MAX))
        ,[Depth] = 0
        ,[Root] = CallerName
        ,[Leaf] = CalleeName
        ,[Count] = 1
    FROM @Dependencies
    WHERE CallerID = OBJECT_ID(N'dbo.Table_Count')			-- < Enter stored procedure or function here.

    UNION ALL
    
    -- Recursive Case: Recursively call HierarchyCTE to get the callees for the callee.
    SELECT --
         depends.[CallerID]
        ,depends.[CallerName]
        ,depends.[CallerType]
        ,depends.[CallerTypeName]
        ,depends.[CalleeID]
        ,depends.[CalleeName]
        ,depends.[CalleeType]
        ,depends.[CalleeTypeName]
        ,[Path] = CAST(cte.Path + ' » ' + depends.CalleeName AS nvarchar(MAX))
        ,[Depth] = cte.Depth + 1
        ,[Root] = cte.Root
        ,[Leaf] = depends.CalleeName
        ,[Count] = COUNT(depends.CallerID) OVER (PARTITION BY depends.CallerID)
    FROM HierarchyCTE cte
    JOIN @Dependencies depends ON depends.CallerID = cte.CalleeID
    WHERE depends.CallerID != depends.CalleeID -- Avoid recursive CTE's
         AND cte.Count = 1		-- Prevent adding a callee more than once.
    )
SELECT *
INTO #HierarchyCTE
FROM HierarchyCTE

-- ------------------------------------
SELECT --  Complete hierarchical list.
	 Depth, CallerName, CallerTypeName, CalleeName, CalleeTypeName, Path, Root, Leaf
FROM #HierarchyCTE
ORDER BY
	Depth, CallerName, CalleeName

-- ------------------------------------
SELECT -- Just the tables involved in the query.
    DISTINCT [Root], [Leaf]
FROM #HierarchyCTE
WHERE [CalleeType] = 'U'
ORDER BY 
	Root, Leaf

-- ------------------------------------
SELECT -- Just the tables involved in the query and each path to that table.
    [Root], [Leaf], [Path]
FROM #HierarchyCTE
WHERE [CalleeType] = 'U'
ORDER BY 
	Root, Leaf

IF OBJECT_ID('tempdb..#HierarchyCTE') IS NOT NULL DROP TABLE #HierarchyCTE

The first select lists all calls starting with the specified stored procedure, Table_Count, thru to the tables. Depth indicates the number of calls starting with Table_Count to the current count. Path shows the breadcrumbs along the call chain starting with Table_Count until the current call. Root is always Table_Count since that is where we started and Leaf shows the callee at this point in the call chain.

Output: List of caller and callee at each level
DepthCallerNameCallerTypeNameCalleeNameCalleeTypeNamePathRootLeaf
0dbo.Table_CountSQL_STORED_PROCEDUREdbo.Table_A_CountSQL_SCALAR_FUNCTIONdbo.Table_Count » dbo.Table_A_Countdbo.Table_Countdbo.Table_A_Count
0dbo.Table_CountSQL_STORED_PROCEDUREdbo.Table_B_CountSQL_SCALAR_FUNCTIONdbo.Table_Count » dbo.Table_B_Countdbo.Table_Countdbo.Table_B_Count
0dbo.Table_CountSQL_STORED_PROCEDUREdbo.Table_C_CountSQL_SCALAR_FUNCTIONdbo.Table_Count » dbo.Table_C_Countdbo.Table_Countdbo.Table_C_Count
1dbo.Table_C_CountSQL_SCALAR_FUNCTIONdbo.Table_CUSER_TABLEdbo.Table_Count » dbo.Table_C_Count » dbo.Table_Cdbo.Table_Countdbo.Table_C
1dbo.Table_B_CountSQL_SCALAR_FUNCTIONdbo.Table_BUSER_TABLEdbo.Table_Count » dbo.Table_B_Count » dbo.Table_Bdbo.Table_Countdbo.Table_B
1dbo.Table_A_CountSQL_SCALAR_FUNCTIONdbo.Table_AUSER_TABLEdbo.Table_Count » dbo.Table_A_Count » dbo.Table_Adbo.Table_Countdbo.Table_A

The second select returns a distinct list of just the root and leaf nodes. This is useful when you are not interested in the intermediate calls.

Output: A distinct list of just the root and leaf nodes.
RootLeaf
dbo.Table_Countdbo.Table_A
dbo.Table_Countdbo.Table_B
dbo.Table_Countdbo.Table_C

The third select returns just the root and leaf nodes but shows all paths that link them.

Output: List of all paths between the root and leaf nodes.
RootLeafPath
dbo.Table_Countdbo.Table_Cdbo.Table_Count » dbo.Table_C_Count » dbo.Table_C
dbo.Table_Countdbo.Table_Bdbo.Table_Count » dbo.Table_B_Count » dbo.Table_B
dbo.Table_Countdbo.Table_Adbo.Table_Count » dbo.Table_A_Count » dbo.Table_A

Conclusion

Knowing the hierarchy of calls between a stored procedure / function and the tables provides a better understanding on the scope of work and possible points within that hierarchy that impact the performance.