1.0 Structure of the Plan Cache and Types of Cached Objects

The plan cache is comprised of 4 separate cache stores: Object Plans (CACHESTORE_OBJCP), SQL Plans (CACHESTORE_SQLCP), Bound Trees (CACHESTORE_PHDR), and Extended Stored Procedures (CACHESTORE_XPROC). Each of these four cache stores conform to a uniform caching policy with respect to costing and removal of entries. Within each cache store is a hash table that is divided into hash buckets. Each hash bucket may contain one or more cached entries. The hash value of all cache entries is computed as (object_id * database_id) mod (hash table size), and this specifies the hash bucket. A cache key is used to find an exact match for a plan in the cache store hash table bucket.

 

In the plan cache we cache the following types of objects: compiled plans (CP), execution plans (MXC), algebrizer trees (ProcHdr), Extended Procs (XProcs) and inactive cursors. Among these, compiled plans, algebrizer trees and extended procs are top level objects where as the execution plans and inactive cursors are dependant objects (dependant on the compiled plans). The section below describes each of these objects in greater detail including which of the four cache stores they reside in.

 

1.1 Types of Cached Objects

 

1.1.1 Compiled Plans (CP)

 

When a query is compiled, a compiled plan is generated for the query. The cost of compiling a query each time is large; therefore we cache the compiled plans. There are two cache stores in which compiled plans are stored depending on the type of the compiled plan. If the query is dynamic sql or prepared, the compiled plan is stored in the SQL Plans (CACHESTORE_SQLCP) cache store. For modules like stored procedures, functions and triggers, the compiled plan is stored in Object Plans (CACHESTORE_OBJCP) cache store. Since the compiled plans are valuable and should to be kept in cache, when the cache stores are under memory pressure, cache removal policies ensure that these entries are not amongst the first to be removed. Compiled plans are also shared across multiple users.

 

Compiled plans are generated for the entire batch, and not on a per statement (query) level. Therefore for a multi-statement batch, the compiled plan can be thought of as an array of plans containing the query plan for statements in that batch. It is important to understand that for a batch with multiple queries in it, the compiled plan will have the compiled query plans for all the queries in the batch.

 

Internally each individual statement in a batch is represented by a CStmt (short for Compiled Statement). Each CStmt has the query plan for that particular statement. A compiled plan therefore has an array of CStmts that are in turn stored in a plan skeleton. In addition to the plan skeleton, the compiled plan also contains the parameter collection, symbol table, the top level memory object and the execution plans.

 

Compiled Plans are non re-generatable entries since we can potentially get different compiled plans for queries compiled under different conditions or at different times. For consistency however, we want to keep the compiled plans in cache and re-execute the same plan to get the same behavior.

 

1.1.2 Execution Plans (MXC)

 

Execution plans are run time objects and are dependant objects of a compiled plan. They cannot exist independent of a compiled plan. Just like the compiled plans there are two types of MXCs: SQL MXC and OBJ MXC. Being dependant objects (of compiled plans) they don’t live a separate cachestore. The compiled plan has 2 linked lists for MXCs: a lookup (or free) list and an enum list. The lookup list stores the free (or currently not in use) MXCs. The lookup list is used to get the MXC memory needed to execute a batch. The enum list is used to enumerate all the MXCs associated with the compiled plan, and is used by some dynamic management views (DMVs) and to generate statistics like total memory used by a batch. MXCs themselves contain the runtime parameters, local variable information; object ids for objects created at run time, run time state like currently executing statement amongst other things.

 

During query execution, we generate an execution plan (MXC) from the compiled plan of the batch. Individual statement compiled plans get converted into to runtime query plans (XStmts). The XStmts are stored as a linked list inside the CStmts.

 

Unlike compiled plans, execution plans are single user. For example, if there are N users executing the same batch simultaneously, there will be N MXCs associated with the same compiled plan. There is therefore a 1:N mapping between compiled plans and execution plans.

 

MXCs are re-generatable given a compiled plan. Relative to compiled plans they are also relatively inexpensive to re-generate.

 

1.1.3 Algebrizer Tree (ProcHdr)

 

The algerbizer tree (ProcHdr) for views, constraints and defaults are cached in the Bound Trees (CACHESTORE_PHDR) cache store. The size of the bound trees cache store hash table is about 1/10th the size of the compiled plan cache store hash tables. The memory object from which each entry is allocated is 8K memory object.

 

1.1.4 Extended Procs (XProc)

 

Extended Procs (Xprocs) are pre-defined system stored procedures like sp_ExecuteSql, sp_TraceCreate etc. They contain the function name and the DLL name of the implementation. They are stored in the Extended Stored Procedures (CACHESTORE_XPROC). The size of this cache store hash table is 127 entries. The memory object from which each entry is allocated is 256 bytes memory object.

 

1.1.5 Inactive Cursors

 

Inactive cursors are cached in the compiled plan. Just like MXCs there are two lists maintained here: the lookup list and the enum list. The lookup list stores the inactive cursors. When a cursor is de-allocated, the memory used to store the cursor is cached. On subsequent re-execution of the batch with this cursor, the cached cursor memory is reused and re-initialized as an active cursor. The enum list has the list of active and inactive cursors and is used to enumerate all cursors for a compiled plan.

 

Cursors are re-generatable given a compiled plan. Relative to compiled plans they are also relatively inexpensive to re-generate.

 

1.2 Understanding Memory Layout of Compiled Plan using DMVs

 

For a detailed summary of the memory layout refer to this posting. In this section we will take a look at the DMVs and learn more about the memory layout of compiled plans through examples.

 

Sys.dm_exec_cached_plans has the plan_handle and the memory_object_address associated with every compiled plan. The plan_handle is a hash value derived from the compiled plan of the entire batch, and is guaranteed to be unique for every compiled plan. The plan_handle therefore serves as an identifier for a given compiled plan and is used by sys.dm_exec_cached_plans to retrieve the compiled plan for a batch.

 

The DMV sys.dm_os_memory_objects can be used to get information on all the top-level and sub-level memory objects associated with a compiled plan. This DMV has the memory_object_address as well as the parent_address of all the memory objects. It has also has the pages used by the memory object. The total of the pages_allocated_count of all the top level objects represents the total memory used by the compiled plan. This DMV also tells us the type of the memory object. The following memory object types are associated with a compiled plan:

 

MEMOBJ_COMPILE_ADHOC: Top level 8K compiled plan PMO.

 

MEMOBJ_QUERYEXECCNTXTFORSE: Top level 8K PMO Query execution context for SE, one for every XStmt that is a query.

 

MEMOBJ_EXECUTE: Top level 8K MXC PMO, contains the non recompilable XStmts in the batch).

 

MEMOBJ_PLANSKELETON: Sub PMO 512 bytes, allocated from the top level CP PMO. Maintains an array of CStmts.

 

MEMOBJ_STATEMENT: Sub PMO 512 bytes per non-recompilable CStmt created from top level CP PMO

 

MEMOBJ_XSTMT: Sub PMO 512 bytes for every recompilable XSTMT created from top level CP PMO).

 

MEMOBJ_CURSOREXEC: Sub PMO 512 bytes, one for every cursor.

 

Therefore given a plan_handle or the memory_object_address of a compiled plan, the memory layout of a compiled plan can pretty much be re-constructed from the DMV sys.dm_os_memory_objects. The function below does precisely that:

 

create function CompPlanDetails(@current_plan_address int, @plan_handle varbinary(64))

returns @details table

(

      plan_handle varbinary(64) null,

      memory_object_address varbinary(8) not null,

      parent_address varbinary(8) null,

      type nvarchar(60) null,

      name nvarchar(256) null,

      pages_allocated_count int not null,

      page_size_in_bytes int not null,

      page_allocator_address varbinary(8) not null

)

as

begin

      -- Get the plan handle

      if (@plan_handle is null)    

select @plan_handle = plan_handle from sys.dm_exec_cached_plans where memory_object_address = @current_plan_address;

 

      -- Get all top level pmos into a temp table.

      with TopLevelPMOs as

      (

select @plan_handle as plan_handle, mo2.memory_object_address, mo2.parent_address, mo2.type, mo2.name, mo2.pages_allocated_count, mo2.page_size_in_bytes, mo2.page_allocator_address

      from sys.dm_os_memory_objects mo join sys.dm_os_memory_objects

mo2

      on mo2.page_allocator_address = mo.page_allocator_address

      where mo.memory_object_address = @current_plan_address

      )

 

-- find sub-pmos from all top level pmos, and add them plus the top level pmos into the result table.

      insert @details

select @plan_handle, mo3.memory_object_address, mo3.parent_address, mo3.type, mo3.name, mo3.pages_allocated_count, mo3.page_size_in_bytes, mo3.page_allocator_address

      from sys.dm_os_memory_objects mo3 join TopLevelPMOs

      on mo3.parent_address = TopLevelPMOs.memory_object_address

      union all

      select * from TopLevelPMOs

      return

end

go

 

Now let us illustrate how to use this function with an example. Consider the following single statement batch executed by exactly one user at a given time:

 

select t1.col2, t2.col2 from t1 join t2 on t1.col1 = t2.col1 where t1.col1 = 50000

go

 

Execute the query and look up the plan_handle and memory_object_address from the DMVs using the query below:

 

select text, plan_handle, memory_object_address

from sys.dm_exec_cached_plans cp

cross apply sys.dm_exec_sql_Text(cp.plan_handle)

go

 

Text

Plan_handle

Memory_object_address

  select t1.col2,

t2.col2 from t1

join t2 on t1.col1

= t2.col1

where t1.col1 = 50000 

0x0600010029A7DD06B

8012B04000000000000

000000000000

0x042B00C0

 

Now pass these plan_handle and memory_object_address to the function CompPlanDetails.

 

select * from dbo.CompPlanDetails(0x042B00C0, 0x0600010029A7DD06B8012B04000000000000000000000000)

go

 

The function returns data detailing the memory layout as below:

 

Plan_Handle

Memory_

object_

address

Parent_

address

Type

Name

Pages_

alloca

ted_

count

Page_

size

_in

_bytes

Page_

Alloc

ator_

address

0x060001002

9A7DD06B801

2B040000000

00000000000

000000

0x042

B0938

0x042

B00C0

MEMOBJ_

XSTMT

NULL

17

512

0x042

B0398

0x060001002

9A7DD06B801

2B040000000

00000000000

000000

0x042

B0528

0x042

B00C0

MEMOBJ_

COMPILE_

ADHOC

NULL

18

512

0x042

B0398

0x060001002

9A7DD06B801

2B040000000

00000000000

000000

0x042

B00C0

NULL

MEMOBJ_

COMPILE_

ADHOC

NULL

3

8192

0x036

141D0

0x060001002

9A7DD06B801

2B040000000

00000000000

000000

0x03F

98028

NULL

MEMOBJ_

EXECUTE

NULL

1

8192

0x036

141D0

0x060001002

9A7DD06B801

2B040000000

00000000000

000000

0x042

88040

NULL

MEMOBJ_

QUERYEX

ECCNTXT

FORSE

NULL

1

8192

0x036

141D0

 

Notice that the MEMOBJ_XSTMT and MEMOBJ_COMPILE_ADHOC in the first two rows are have parent address of the top level CP PMO in row three. There is only one top level MEMOBJ_EXECUTE since the query was executed by exactly one user. If multiple users had executed the query simultaneously, then there would be more than one top level MXC.

 

Now consider the stored procedure below:

 

create procedure p1

as

begin

      select col1 from t1

      select col2 from t1 where col1 = 50000

end

go

 

exec p1

go

 

Executing the function CompPlanDetails with the appropriate plan handle and memory_object_address returns the following data:

 

Plan_

 

Handle

Memory_

object_

address

Parent_

address

Type

Name

Pages_

alloca

ted_

count

Page_

size_

in_

bytes

Page_

Allocator_

address

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B3168

0x03E

B20C0

MEMOBJ_

XSTMT

NULL

17

512

0x03E

B23A8

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B2F60

0x03E

B20C0

MEMOBJ_

XSTMT

NULL

17

512

0x03E

B23A8

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B2538

0x03E

B20C0

MEMOBJ_

COMPILE_

ADHOC

NULL

2

512

0x03E

B23A8

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B2948

0x03E

B20C0

MEMOBJ_

PLAN

SKELETON

NULL

1

512

0x03E

B23A8

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B2B50

0x03E

B20C0

MEMOBJ_

STATEMENT

NULL

17

512

0x03E

B23A8

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B2D58

0x03E

B20C0

MEMOBJ_

STATEMENT

NULL

17

512

0x03E

B23A8

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x042

00028

NULL

MEMOBJ_

EXECUTE

NULL

1

8192

0x036

12380

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

32040

NULL

MEMOBJ_

QUERY

EXECCNTXT

FORSE

NULL

1

8192

0x036

12380

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x041

74040

NULL

MEMOBJ_

QUERY

EXECCNTXT

FORSE

NULL

1

8192

0x036

12380

0x05000100

95555D02B8

21EB030000

0000000000

0000000000

0x03E

B20C0

NULL

MEMOBJ_

COMPILE_

ADHOC

NULL

5

8192

0x036

12380

Notice that since the procedure has multiple statements we now have a memory object of type MEMOBJ_PLANSKELETON, which is an array of CStmts. CStmts have type MEMOBJ_STATEMENT and in the above example there are 2 CStmts. The memory object MEMOBJ_COMPILE_ADHOC with page size 512 bytes has the top level CP PMO as parent. This memory object is used to allocate structures for execution. The memory layout is represented pictorially in the attached file.

 

Another useful stored procedure to analyze the memory layout of compiled plans is PlanBreakdown. The procedure PlanBreakdown gives a detailed summary of the memory object including how many CStmts, XStmts, Mxcs, Cursors, Plan Skeletons, and Query PMOs it contains and the size of the plan. This procedure generates two tables: PlanSummary and PlanDetails. The PlanSummary table has a row per plan with total size of the plan and number of CStmts, MXCs etc. The PlanDetails table has a row for every memory object. Note that while the total size of the plan as reported in PlanSummary table is accurate, it may not be the sum of the size of each of sub objects as reported in the PlanDetails table because they do not account for fragmentation.

 

create proc PlanBreakdown(@plan_addr varbinary(8) = null) as

begin

      set nocount on

 

      -- results tables

      if object_id('PlanDetails', 'U') is not null

            drop table PlanDetails

 

      if object_id('PlanSummary', 'U') is not null

            drop table PlanSummary

 

      create table PlanDetails

      (

            plan_handle varbinary(64) null,

            memory_object_address varbinary(8) not null,

            parent_address varbinary(8) null,

            type nvarchar(60) null,

            name nvarchar(256) null,

            pages_allocated_count int not null,

            page_size_in_bytes int not null,

            page_allocator_address varbinary(8) not null

      )

 

      create table PlanSummary

      (

            plan_handle varbinary(64) null,

            memory_object_address varbinary(8) not null,

            total_size int null,

            cstmt_count int not null,

            cstmt_size int not null,

            xstmt_count int not null,

            xstmt_size int not null,

            cursor_count int not null,

            cursor_size int not null,

            mxc_count int not null,

            mxc_size int not null,

            query_count int not null,

            query_size int not null,

            skeleton_count int not null

      )

 

-- temp table to hold the address of every plan that we're going to dump info for.

      create table #plans(plan_address varbinary(8))

 

      if (@plan_addr is null)

insert into #plans select memory_object_address from sys.dm_os_memory_objects

where type = 'MEMOBJ_COMPILE_ADHOC' and parent_address is NULL

      else

            insert into #plans values (@plan_addr)

     

declare plan_cursor cursor local for select plan_address from #plans

      open plan_cursor

 

      declare @current_plan_address varbinary(8)

      fetch next from plan_cursor into @current_plan_address

 

      while (@@fetch_status = 0)

      begin

            declare @plan_handle varbinary(64)

            declare @total_size int

            declare @cstmt_count int

            declare @cstmt_size int

            declare @xstmt_count int

            declare @xstmt_size int

            declare @cursor_count int

            declare @cursor_size int

            declare @mxc_count int

            declare @mxc_size int

            declare @query_count int

            declare @query_size int

            declare @skeleton_count int

            set @plan_handle = null;

            set @total_size = null;

            set @cstmt_count = 0;

            set @cstmt_size = 0;

            set @xstmt_count = 0;

            set @xstmt_size = 0;

            set @cursor_count = 0;

            set @cursor_size = 0;

            set @mxc_count = 0;

            set @mxc_size = 0;

            set @query_count = 0;

            set @query_size = 0;

            set @skeleton_count = 0;

           

-- grab the plan handle and total size for the plan we're working on, if it exists

            -- in dm_exec_cached_plans.

select @plan_handle = plan_handle, @total_size = size_in_bytes from

sys.dm_exec_cached_plans where memory_object_address = @current_plan_address

           

select * into #plan_details from CompPlanDetails(@current_plan_address, @plan_handle)

     

            -- add the plan details to the results

            insert into PlanDetails select * from #plan_details

 

-- If we didn't find this plan in sys.dm_exec_cached_plans, then go ahead and compute

-- the total_size by aggregating the pages used of all the top-level pmo's in the plan.

-- This really should be an accurate accounting of the plan memory usage too.

            if (@plan_handle is null)

select @total_size = sum(pages_allocated_count * page_size_in_bytes) from #plan_details

                  where parent_address is null

 

            -- form the summary information for the given plan.

            declare detail_cursor cursor local for

select type, pages_allocated_count, page_size_in_bytes from #plan_details

            open detail_cursor

 

            declare @type varchar(256)

            declare @pages int     

            declare @page_size int

           

fetch next from detail_cursor into @type, @pages, @page_size

 

            while (@@fetch_status = 0)

            begin

                  if (@type = 'MEMOBJ_XSTMT')

                  begin

                        set @xstmt_count = @xstmt_count + 1

set @xstmt_size = @xstmt_size + (@pages * @page_size)

                  end

                  else if (@type = 'MEMOBJ_EXECUTE')

                  begin

                        set @mxc_count = @mxc_count + 1

set @mxc_size = @mxc_size + (@pages * @page_size)

                  end

                  else if (@type = 'MEMOBJ_STATEMENT')

                  begin

                        set @cstmt_count = @cstmt_count + 1

set @cstmt_size = @cstmt_size + (@pages * @page_size)

                  end

                  else if (@type = 'MEMOBJ_CURSOREXEC')

                  begin

                        set @cursor_count = @cursor_count + 1

set @cursor_size = @cursor_size + (@pages * @page_size)

                  end

                  else if (@type = 'MEMOBJ_QUERYEXECCNTXTFORSE')

                  begin

                        set @query_count = @query_count + 1

set @query_size = @query_size + (@pages * @page_size)

                  end

                  else if (@type = 'MEMOBJ_PLANSKELETON')

                  begin

                        set @skeleton_count = @skeleton_count + 1

                  end

                 

fetch next from detail_cursor into @type, @pages, @page_size

            end

 

            close detail_cursor

            deallocate detail_cursor

           

insert into PlanSummary values(@plan_handle, @current_plan_address, @total_size, @cstmt_count, @cstmt_size, @xstmt_count, @xstmt_size, @cursor_count, @cursor_size, @mxc_count, @mxc_size, @query_count, @query_size, @skeleton_count)

                 

            drop table #plan_details

            fetch next from plan_cursor into @current_plan_address

      end

 

      close plan_cursor

      deallocate plan_cursor

end

go

 

To fetch the PlanSummary and PlanDetails for the stored procedure p1 in the above example use the queries below:

 

exec PlanBreakdown 0x040D40C0

go

 

-- See the summaries for each plan.

select * from PlanSummary

 

-- See the details for each plan.

select * from PlanDetails

go

 

Select * from PlanSummary returns one row as below: (note though that the data has been pivoted here for easy readability). The sizes reported here are in bytes.

 

plan_handle

0x05000100AB200

D4BB84137040000

000000000000000

00000

memory_object_address

0x043740C0           

total_size 

65536

cstmt_count

2

cstmt_size 

17408   

xstmt_count

2

xstmt_size 

17408

cursor_count

0

cursor_size

0

mxc_count  

1

mxc_size

8192

query_count

2

query_size 

16384

skeleton_count

1

 

Now lets what happens to the memory objects when a query re-compiles:

 

Connection 1:

 

alter table t1 add col3 int

go

 

Connection 2:

 

create procedure p1

as

begin

      ---long running queries

      select * from t1 where col1 = 5;

      select * from t2 where col1 = 10;

      ---long running queries

end

go

 

exec p1

go

 

Execute the queries in connection 1 and 2 simultaneously. The first select query inside the stored procedure will be recompiled. Use the function CompPlanDetails to get the memory layout before executing the query in connection 1 and after executing the alter table query in connection 1. A subset of the rows returned before the query recompilation is as below:

 

Plan_handle

Memory_object_address

Parent_address

Type

0x05000100EAE880

7FB8415A04000000

000000000000000000

0x045A4948

0x045A40C0

MEMOBJ_

PLANSKELETON

0x05000100EAE880

7FB8415A04000000

000000000000000000

0x045A4B50

0x045A40C0

MEMOBJ_

STATEMENT

0x05000100EAE880

7FB8415A04000000

000000000000000000

0x045A4D58

0x045A40C0

MEMOBJ_

STATEMENT

0x05000100EAE880

7FB8415A04000000

000000000000000000

0x04362028

NULL

MEMOBJ_

EXECUTE

 

A subset of the result set returned by function CompPlanDetails after recompilation is below. After query recompilation, notice that the plan skeleton and the CStmt memory object address has been updated, while the parent address and the plan handle remain unchanged.

 

Plan_handle

Memory_object_address

Parent_address

Type

0x05000100EAE8807

FB8415A0400000000

0000000000000000

0x045A4F60

0x045A40C0

MEMOBJ_

PLANSKELETON

0x05000100EAE8807

FB8415A0400000000

0000000000000000

0x04362028

NULL

MEMOBJ_

EXECUTE

0x05000100EAE8807

FB8415A0400000000

0000000000000000

0x045A5168

0x045A40C0

MEMOBJ_

STATEMENT

0x05000100EAE8807

FB8415A0400000000

0000000000000000

0x045A4D58

0x045A40C0

MEMOBJ_

STATEMENT

 

memory_layout_example.vsd