Query Optimizer In SQL Server: Part 2 Physical Operators

in the previous blog, we talked about logical tree generation

today:

  • Trivial plan and full optimization
  • how does SQL Server handle joins
  • transformation rules
  • memo

and stuff related to it, most of the trace flags and DMVs are undocumented so we can't use them in a production environment.

Trivial Plan

since cost-based optimization is an expensive operation, SQL server tries to not use it if it can

when it does that? in trivial plans

what is a trivial plan? it is plan that has one or an obvious best plan from the get go

there is no other way of execution

or this is the best one without much research

so it passes that plan to the execution engine right away for example

USE AdventureWorks2022
BEGIN TRAN 
SELECT Firstname
FROM Person.Person
OPTION(RECOMPILE,QUERYTRACEON 8605,QUERYTRACEON 8606,QUERYTRACEON 8607,
       QUERYTRACEON 8608,QUERYTRACEON 8621,QUERYTRACEON 3604)
COMMIT TRAN 

In the output tree:

*** Output Tree: (trivial plan) ***

        PhyOp_NOP

            PhyOp_Range TBL: Person.Person(2) ASC  Bmk ( QCOL: [AdventureWorks2022].[Person].[Person].BusinessEntityID) IsRow: COL: IsBaseRow1000 

********************

or using sys.dm_exec_query_optimizer_info( for ease of use we are going to include the whole code from the previous blog, but you should follow the rules introduced there to get what we get)

use adventureworks2022
BEGIN TRAN
SELECT *
INTO AFTER_OPTIMIZER
FROM sys.dm_exec_query_optimizer_info;
COMMIT TRAN

BEGIN TRAN
SELECT *
INTO BEFORE_OPTIMIZER
FROM sys.dm_exec_query_optimizer_info;
COMMIT TRAN

BEGIN TRAN
DROP TABLE AFTER_OPTIMIZER;
DROP TABLE BEFORE_OPTIMIZER;
COMMIT TRAN

BEGIN TRAN
SELECT *
INTO BEFORE_OPTIMIZER
FROM sys.dm_exec_query_optimizer_info;
COMMIT TRAN

BEGIN TRAN
SELECT PhoneNumberTypeID
FROM Person.PersonPhone
OPTION (RECOMPILE);
COMMIT TRAN

BEGIN TRAN 
SELECT *
INTO AFTER_OPTIMIZER
FROM sys.dm_exec_query_optimizer_info;
COMMIT TRAN 

BEGIN TRAN 
SELECT 
    AFTER_OPTIMIZER.COUNTER,
    (AFTER_OPTIMIZER.OCCURRENCE - BEFORE_OPTIMIZER.OCCURRENCE) AS OCCURRENCE_DIFFERENCE,
    (AFTER_OPTIMIZER.OCCURRENCE * AFTER_OPTIMIZER.VALUE - BEFORE_OPTIMIZER.OCCURRENCE * BEFORE_OPTIMIZER.VALUE) AS VALUE
FROM 
    BEFORE_OPTIMIZER
JOIN 
    AFTER_OPTIMIZER ON BEFORE_OPTIMIZER.COUNTER = AFTER_OPTIMIZER.COUNTER
WHERE 
    BEFORE_OPTIMIZER.OCCURRENCE <> AFTER_OPTIMIZER.OCCURRENCE;
COMMIT TRAN

and this would pop up

as you can see it is a trivial plan

or we could see in the execution plan xml

SET STATISTICS XML ON;
GO
SELECT PhoneNumberTypeID
FROM Person.PersonPhone
OPTION(RECOMPILE);
GO
SET STATISTICS XML OFF;

click on the XML in the results

...  <BatchSequence>
    <Batch>
      <Statements>
        <...... StatementOptmLevel="TRIVIAL" ...>
          <StatementSetOptions ....>
          <....
</ShowPlanXML>

so it is trivial

or in the execution plan viewer in SSMS

click on show me execution plan the properties on the select operator

and you will see:

now if change the query just a little bit:

SELECT PhoneNumberTypeID
FROM Person.PersonPhone
WHERE PhoneNumberTypeID <> 3
OPTION(RECOMPILE);

and run the test we ran

we would get

why? inequality predicate

we can disable trivial plans by trace flag 8757

so if we:

SELECT PhoneNumberTypeID
FROM Person.PersonPhone
OPTION(RECOMPILE,QUERYTRACEON 8757);

We will get

it searched, no trivial plans

Transformation Rules In The Query Optimizer

as we saw in trace flag 8621, SQL server uses certain methods to rewrite a query or a physical operator in the most efficient way, some of them are used during the simplification process, so we call them simplification rules, others are used during the full optimization process. the first one converts the logical tree we had in mind to simpler one like predicate pushdown, so logical to logical, and it does not use cost-based optimization it is just a preparatory phase, transformation on the other hand uses the estimated cost we had through statistics and cardinality estimation to see if the rules could be applied and how useful are they and evaluate the alternatives.

one other type is: Exploration rules. these are cost-based conversion of the simplified logical tree to another one that costs less. so here we are still in logical to logical.

another type of these: Implementation rules. here it would convert the best logical trees it found in the trivial or the cost optimized plans and then convert them to the algorithms that SQL server uses to process the data. for example it might transfer the relational operator LogOp_Join to one of the physical ones like nested loop join.

so what makes a difference between FULL optimization and the trivial plan is the usage of the last two types of rules.

how could we see which rules were applied

sys.dm_exec_query_transformation_stats

first, let’s run select* and see the columns:

SELECT*
FROM  sys.dm_exec_query_transformation_stats

This query shows cumulative results since the last server restart:

here it shows the:

  • name: the name of the rule applied for example the first one JNtoNL means a join to a nested loop join, the second one means LOJNtoNL left outer join to a nested loop join, and the third one LSJNtoNL left semi join to a nested loop join
  • promise_total: how many time the optimizer has been asked to apply this rule
  • promised: how many times it did actually apply it
  • built_substitute: how many times it built a substitue physical or logical tree for the rule
  • succeeded: means how many times it succeeded doing it and added that viable alternative tree to the memo( more on it later)
  • promise_average: promise_total/promised

so here we can see each rule and how many times it got applied on the whole server,

if we want to see it on a specific query we have to use snapshots just like we did in the previous blog but different columns:

USE AdventureWorks2022;
BEGIN TRAN
SELECT *
INTO AFTER_TRANSFORMATION
FROM sys.dm_exec_query_transformation_stats;
COMMIT TRAN

BEGIN TRAN
SELECT *
INTO BEFORE_TRANSFORMATION
FROM sys.dm_exec_query_transformation_stats;
COMMIT TRAN

BEGIN TRAN
DROP TABLE AFTER_TRANSFORMATION;
DROP TABLE BEFORE_TRANSFORMATION;
COMMIT TRAN

BEGIN TRAN
SELECT *
INTO BEFORE_TRANSFORMATION
FROM sys.dm_exec_query_transformation_stats;
COMMIT TRAN

SELECT PhoneNumberTypeID
FROM Person.PersonPhone
OPTION(RECOMPILE);

BEGIN TRAN
SELECT *
INTO AFTER_TRANSFORMATION
FROM sys.dm_exec_query_transformation_stats;
COMMIT TRAN

BEGIN TRAN;
SELECT 
    AFTER_TRANSFORMATION.name,
    (AFTER_TRANSFORMATION.promised - BEFORE_TRANSFORMATION.promised) AS PROMISED
FROM 
    BEFORE_TRANSFORMATION
JOIN 
    AFTER_TRANSFORMATION ON BEFORE_TRANSFORMATION.name = AFTER_TRANSFORMATION.name
WHERE 
    BEFORE_TRANSFORMATION.succeeded <> AFTER_TRANSFORMATION.succeeded;
COMMIT TRAN;

and the output would be:

so here it basically did these two once each:

GetidxToRng: so here it converted the get index meaning the index seek to an index range scan, since it was more efficient to retrive all the phone ids instead of seeking each one separately

GetToTrivialScan: here it says it converted the get logical operator to the trivial scan, our trivial plan’s table scan since it needed all the rows from the table

now let’s disable the trivial and run it again:

SELECT PhoneNumberTypeID
FROM Person.PersonPhone
OPTION(RECOMPILE,querytraceon 8757,querytraceon 8619,querytraceon 8620,querytraceon 3604)

now here it optimized and:

  • GetIdxToRng: it converted the logical get index to a range but it never even tried to complete it
  • GetToIdxScan: it converted the get logical to an index scan
  • GetToScan: it converted the get logical to a table scan logical

now for the output of the trace flags:

---------------------------------------------------

Apply Rule: GetToScan - Get()->Scan()
    Args: Grp 0 0 LogOp_Get (Distance = 0)

---------------------------------------------------

Apply Rule: GetToIdxScan - Get -> IdxScan
    Args: Grp 0 0 LogOp_Get (Distance = 0)

it does not show the first one, since it never got executed

it shows the second and the third

and shows the memo stuff that we will talk about later

now if we use a where clause

SELECT PhoneNumberTypeID
FROM Person.PersonPhone
WHERE PhoneNumberTypeID<> 3
OPTION(RECOMPILE,querytraceon 8619,querytraceon 8620,querytraceon 3604)

And the traces:

---------------------------------------------------

Apply Rule: SelToIdxStrategy - Sel Table -> Index expression 
    Args: Grp 4 0 LogOp_Select 3 2 (Distance = 0)
---------------------------------------------------

Apply Rule: GetIdxToRng - GetIdx -> Rng
    Args: Grp 5 0 <SelToIdxStrategy>LogOp_GetIdx (Distance = 1)

---------------------------------------------------

Apply Rule: SelectToFilter - Select()->Filter()
    Args: Grp 4 0 LogOp_Select 3 2 (Distance = 0)

---------------------------------------------------

Apply Rule: GetToScan - Get()->Scan()
    Args: Grp 3 0 LogOp_Get (Distance = 0)

---------------------------------------------------

Apply Rule: GetToIdxScan - Get -> IdxScan
    Args: Grp 3 0 LogOp_Get (Distance = 0)

so the first says use an index

the second says instead of an index use a table or range scan

the third is don’t select immediately filter first

the fourth converts the get to a table scan

the 5th says convert the get to a scan

so it considered each of these and then, it used the idxscan at the end based on the cost

so these are some of the rules and how could we explore them

we could explore a more complicated query:

SELECT P.ProductID,COUNT(*)
FROM Production.Product P INNER JOIN Production.ProductInventory I 
                            ON P.ProductID = I.ProductID
GROUP BY P.ProductID
OPTION(RECOMPILE,QUERYTRACEON 8606,QUERYTRACEON 8607,
QUERYTRACEON 8608,QUERYTRACEON 3604,
QUERYTRACEON 8619,QUERYTRACEON 8620,
QUERYTRACEON 8621,QUERYTRACEON 8615,
QUERYTRACEON 8675)

The DMV:

A lot of rules.

most authors focus on GbAggBeforeJoin because it is in the execution plan and reduces resource consumption a lot, so we are going to explain it first:

so here in the group by we specified P.ProductID, and it already exists in the P table

so instead of joining and then later group by it, we can filter the results by it, then after that, we could produce the output thus reducing cost

inGbAggToStAgg: it converts the group by aggregate function to a stream aggregate

in the GetToIdxScan it converts the get logical operator to an index scan

in JoinCommute it uses the commutativity rule in which a join b = b join a

in JNtoSM it converts a join to a merge join

and in the execution plan, we can see each of these

So, it first converted the logical to physical, compared the results of each based on statistics, and then started converting to physical operators. After that, it compared the estimated results and chose the best one.

if we look up the total cost in the execution plan:

now if we disable some rules like GbAggBeforeJoin using the code provided below:

DBCC RULEOFF('GbAggBeforeJoin')

and run the query again:

the stream aggregate happened after the join and the cost would be 0.0314982 so there is a slight increase

now if we disable the

DBCC RULEOFF ('GbAggToStrm')

Then execute:

so it used GbAggToHS and the cost 0.0579313 almost doubled now if we disable that

DBCC RULEOFF ('GbAggToHs')

And execute we would get this error


Query processor could not produce a query plan because of the hints defined
 in this query. Resubmit the query without specifying any
  hints and without using SET FORCEPLAN.

so now since the the optimizer can not convert the group by to a physical operator

now we can turn them on by:

DBCC RULEON ('GbAggToHs') ;
DBCC RULEOn('GbAggToStrm') ;
DBCC RULEOn('GbAggBeforeJoin') ;

and our query will return to its original shape

Memo

when SQL Server tries to explore the alternative plans for a query, it stores them in the memo.

these could be logical or physical ones.

each optimization has its own memo.

if the query did not go through optimization, meaning it was trivial it would not be there

if it did go through the process and got some transformation rules applied on it, it would have a memo.

the memo does not store each and every step, it recognizes similarities groups them, and stores them in an efficient way.

the costs of each physical alternative are stored, then the lower one wins.

let’s explore it

we have two trace flags to show us the structure: 8608 for the initial and 8615 for the final one assuming trace flag 3604 is enabled.

we’ll look at a trivial plan, then disable it using 8757

then look at some fully optimized plan, and see how it is stored in the memo.

so first:

BEGIN TRAN 
SELECT ProductID
FROM Production.Product
OPTION(RECOMPILE,QUERYTRACEON 3604,QUERYTRACEON 8608, QUERYTRACEON 8615)
COMMIT TRAN 

As you can see no output

now let’s disable trivial plan using 8757:

BEGIN TRAN 
SELECT ProductID
FROM Production.Product
OPTION(RECOMPILE,QUERYTRACEON 8757,QUERYTRACEON 3604,QUERYTRACEON 8608, QUERYTRACEON 8615)
COMMIT TRAN 

And the output would be:

--- Initial Memo Structure ---

Root Group 0: Card=504 (Max=10000, Min=0)

   0 LogOp_Get (Distance = 0)

------------------------------

--- Final Memo Structure ---

Root Group 0: Card=504 (Max=10000, Min=0)

   2 PhyOp_Range 4 ASC   Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 0.00457714 (Distance = 1)

   0 LogOp_Get (Distance = 0)

----------------------------

so first it has its root group which is 0

it has a cardinality estimation of 504

it contains the Logical operator Get

this operator’s identifier is 0

so whenever SQL Server sees 0 in this plan it would be LogOp_Get

Distance: how many steps did the optimizer use to get to this specific operator

now in the final memo, we see an additional physical operator

it has one group

it is the initial root group

it contains two operators

PhyOp_Range which is identified as 2 and it has a distance of 1 meaning the optimizer needed one step to find it

the initial with no optimization operator 0

now if we run a non-trivial plan:

BEGIN TRAN 
SELECT ProductID
FROM Production.Product
where ProductID <> 3
OPTION(RECOMPILE,QUERYTRACEON 3604,QUERYTRACEON 8608,QUERYTRACEON 8609, QUERYTRACEON 8615)
COMMIT TRAN 

and the output

--- Initial Memo Structure ---

Root Group 4: Card=503 (Max=10000, Min=0)

   0 LogOp_Select 3 2 (Distance = 0)

Group 3: Card=504 (Max=10000, Min=0)

   0 LogOp_Get (Distance = 0)

Group 2: 
   0 ScaOp_Comp  0 1 (Distance = 0)

Group 1: 
   0 ScaOp_Const  (Distance = 0)

Group 0: 
   0 ScaOp_Identifier  (Distance = 0)

so we have:

  • the root group which was the first group to be explored
  • it has one logical operator identified as 0 and it references the other groups 3 and 2
  • now in 3 we see the cardinality estimation which is 504, it has one logical operator that does not reference anything which is the LogOp_Get which is basically a select and it did not explore much to get it
  • in 2 we see the ScaOp_Comp it is group by distance( as depicted in the output of 8609) and it has one operator that references 0 and meaning it checks if the identifier equals the constant
  • in group 1 we see the ScaOp_Const which corresponds to the 3 we queried
  • in 0 we see the ScaOp_Identifer which verifies that we have the correct row from the correct table
  • so the plan has the first logOp_select has a get that uses a comparison operator who looks if the row we want does not equal 3

then in the output of 8609:

Tasks executed: 34
Rel groups: 2
Log ops: 2
Phy ops: 0
Sca groups: 3
Sca ops: 3
Total log ops: 2
Count of log ops, grouped by exploration distance
 0: 2
Total phy ops: 0
Count of phy ops, grouped by exploration distance
Total sca ops: 3
Count of sca ops, grouped by exploration distance
 0: 3

it shows what it did to get the initial memo plus optimize it

so it has 2 relational groups = LogOp operators

0 physical still playing with logical

3 scalar groups

then it counts and groups them

then in the final memo:


--- Final Memo Structure ---

Group 8: 
   0 ScaOp_Logical  6.0 7.0  Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 7 (Distance = 1)
Group 7: 
   0 ScaOp_Comp  0.0 1.0  Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 3 (Distance = 1)
Group 6: 
   0 ScaOp_Comp  0.0 1.0  Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 3 (Distance = 1)
Group 5: Card=504 (Max=10000, Min=0)
   1 PhyOp_Range 2 ASC   Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 0.00457714 (Distance = 2)
   0 LogOp_GetIdx (Distance = 1)
Root Group 4: Card=503 (Max=10000, Min=0)
   1 PhyOp_Filter 5.1 8.0  Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 0.00502066 (Distance = 1)
   0 LogOp_Select 3 2 (Distance = 0)
Group 3: Card=504 (Max=10000, Min=0)
   0 LogOp_Get (Distance = 0)
Group 2: 
   0 ScaOp_Comp  0 1 (Distance = 0)
Group 1: 
   0 ScaOp_Const   Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 1 (Distance = 0)
Group 0: 
   0 ScaOp_Identifier   Cost(RowGoal 0,ReW 0,ReB 0,Dist 0,Total 0)= 1 (Distance = 0)

The ReW and Reb are rewinds and rebinds( more on them in another blog)

  • Groups 0, 1, 2, 3 are the same
  • root group 4 added a physical operator now, PhyOp_Filter that references 5 and 1 so it filters on 5 and 1 or it filters on 8 and 0
  • then it shows the cost
  • group 5: it has the scanning range operator in asc way so an index scan referencing group 2 which is the comparison operator, and one GetIdx operator
  • group 6: has a comparison operator that references 0 and 0 or 0 and 1 so it is like group 2 in terms of comparing the identifier to the constant
  • group 7: did the same
  • group 8: combines 8 and 7 in one scalar

so the root group is the chosen plan, which was the original one.

we did not see any other physical alternative in here

now for the output of 8609 for the final memo:

Tasks executed: 66
Rel groups: 3
Log ops: 3
Phy ops: 2
Sca groups: 6
Sca ops: 6
Total log ops: 3
Count of log ops, grouped by exploration distance
 0: 2
 1: 1
Total phy ops: 3
Count of phy ops, grouped by exploration distance
 0: 0
 1: 2
 2: 1
Total sca ops: 6
Count of sca ops, grouped by exploration distance
 0: 3
 1: 3

so it added the physicals and the search distance says which type of search it did is it search 0, 1, or 2

more on them later

Statistics

in order for SQL Server to optimize a query, it needs to compare costs

and in order to do that you need to know the data in the table

now since you can’t access all the data every time you tune a query you need a sample

based on this sample .SQL server tries to create an optimized plan if it is necessary ( not trivial)

it contains information on the distribution of values across columns, indexes, etc.(more on it in another blog)

we can see that in the properties of the execution plan in optimizer stats usage:

or we could use trace flags 9292 and 9204

both only work on the legacy cardinality estimator so we have to enable it first then show it

use adventureworks2022
ALTER DATABASE SCOPED CONFIGURATION
SET LEGACY_CARDINALITY_ESTIMATION = ON 

Now:

BEGIN TRAN 
SELECT ProductID
FROM Production.Product
where ProductID <> 3
OPTION(RECOMPILE,QUERYTRACEON 3604,QUERYTRACEON 9292,QUERYTRACEON 9204)
COMMIT TRAN

and the output would be:

Stats header loaded: DbName: AdventureWorks2022, 
ObjName: Production.Product, IndexId: 1, ColumnName: ProductID, EmptyTable: FALSE

Stats loaded: DbName: AdventureWorks2022, 
ObjName: Production.Product, IndexId: 1, ColumnName: ProductID, EmptyTable: FALSE

now let’s create some statistics objects:

CREATE STATISTICS ProductID_Stats ON Production.Product (ProductID);

now let’s run the query again:

Stats header loaded: DbName: AdventureWorks2022,
 ObjName: Production.Product, IndexId: 1, ColumnName: ProductID, EmptyTable: FALSE

Stats loaded: DbName: AdventureWorks2022, 
ObjName: Production.Product, IndexId: 1, ColumnName: ProductID, EmptyTable: FALSE

Stats header loaded: DbName: AdventureWorks2022, 
ObjName: Production.Product, IndexId: 9, ColumnName: ProductID, EmptyTable: FALSE

so now we can see how many stats we have, we can drop them:

DROP STATISTICS Production.Product.ProductID_Stats

now it will be cleared, let’s run the new cardinality estimator again

use adventureworks2022
ALTER DATABASE SCOPED CONFIGURATION
SET LEGACY_CARDINALITY_ESTIMATION = off 

FULL Optimization process:

now we know about the rules, memo, trivial plans, and stats.

how does SQL Server use the following to optimize a plan?

so if the plan is trivial = no optimization = none of this is necessary

but if it is:

there are a lot of execution plans that could be generated for each plan, sometimes it could be millions.

so does SQL Server search the millions and then take the best one? no, it does not

instead it costs the logical operators generated, and applies rules if the cost could be reduced by them.

how? Heuristic algorithms know if a specific rule could reduce the cost or not

if it does, then it gets applied and costs the logical alternative plans at each generation

if one of them costs a lot, the other alternatives that come through that plan will be discarded

and it will focus on the cheapest. then it re-iterates the process and generates alternatives for the subnodes of the cheapest and if one of them even remotely looks expensive it gets discarded and it only focuses on the cheapest one…….. until either a good enough plan gets generated.

if it finds one then it presents it.

if not then there are two options, it either stops the process since it has a limit on time that could be spent on each step, or it could move to a higher level of algorithms if the plan looks more complex than it seemed at first look, now if the plan goes the other search it could get generated in time so a good enough plan will be there, or it could time_out and the reason for termination of the optimization process would be not enough time.

there are 3 phases of optimization: search 0, 1, and 2 which are the distance in the previous trace flags.

we can see the phases using sys.dm_exec_query_optimizer_info.

Search 0 (transaction processing)

it is the first phase of optimization, used for simple OLTP queries

it would try to find a plan as quickly as possible

it needs at least 3 tables

before the optimization, it generates the joins required

looks which one would be the cheapest to join first using the commutativity and associativity rules

then it starts with the smallest or the most selective

then it starts looking for the cheapest physical operators

it keeps on going until it finds a good enough plan based on that statistics

or it times out

or it moves to another phase if the query looks more complex now

for example:

SELECT E.JobTitle, P.PayFrequency, COUNT(E.JobTitle)
FROM HumanResources.Employee E inner join HumanResources.EmployeePayHistory P
                               ON E.BusinessEntityID = P.BusinessEntityID
                               INNER JOIN HumanResources.EmployeeDepartmentHistory D
                               ON E.BusinessEntityID = D.BusinessEntityID
group by  E.JobTitle, P.PayFrequency
OPTION(RECOMPILE,querytraceon 3604)

We would get

so it searched in 0 1 time the value was 229

Search 1 (quick plan)

it uses additional transformation rules

some join reordering

In the end, it compares the cost to the internal cost threshold

if it matches it or below then a good enough plan is generated

if not , it looks if a parallel query is necessary

if it could be generated it would generate the parallel plans

then it compares their costs to the serial plan

and picks the cheapest one

then it uses it in the next phase which is Search 2

for example:

SELECT*
FROM HumanResources.Employee
WHERE NationalIDNumber <> 50 
OPTION (RECOMPILE)

the output of the DMV would be:

went to search 1 right away

Now it could search 0 first then move on to search 1 for example:

SELECT DISTINCT 
    p.ProductID,
    p.Name AS ProductName,
    wor.ScheduledStartDate,
    e.JobTitle
FROM 
    Production.WorkOrder wo
JOIN 
    Production.WorkOrderRouting wor ON wo.WorkOrderID = wor.WorkOrderID
JOIN 
    Production.Product p ON wor.ProductID = p.ProductID
JOIN 
    HumanResources.Employee e ON wo.StockedQty = e.BusinessEntityID  -- Example join condition
WHERE 
    p.ProductID = 800
OPTION 
    (RECOMPILE,QUERYTRACEON  3604, QUERYTRACEON 8675);

and the output would be:

now here i first went through 0

did not suffice

went through 1

but the query took to long to be processed

so it timed out

it could not show us any gain from going through 1

but it might and then it would show us how much it saves from it

but here it costs more to go through the search one since it costs 0.005 instead of 0.003

In the trace output:

End of simplification, time: 0.001 net: 0.001 total: 0.001 net: 0.001

end exploration, tasks: 227 no total cost time: 0.003 net: 0.003 total: 0.004 net: 0.004

end search(0),  cost: 1.299 tasks: 641 time: 0.005 net: 0.005 total: 0.01 net: 0.01

end exploration, tasks: 1234 Cost = 1.299 time: 0.002 net: 0.002 total: 0.012 net: 0.012

*** Optimizer time out abort at task 1595 ***

end search(1),  cost: 1.299 tasks: 1595 time: 0.001 net: 0.001 total: 0.014 net: 0.014

*** Optimizer time out abort at task 1595 ***

End of post optimization rewrite, time: 0 net: 0 total: 0.014 net: 0.014

End of query plan compilation, time: 0 net: 0 total: 0.014 net: 0.014

here it shows what we just explained

Search 2:( full optimization)

this is designed for complex to very complex queries.

more transformation rules

more parallel operators

advanced strategies

for example:

SELECT 
    soh.SalesPersonID,
    st.Name AS TerritoryName,
    p.Name AS ProductName,
    sod.LineTotal,
    SUM(sod.LineTotal) OVER (PARTITION BY soh.SalesPersonID ORDER BY soh.OrderDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotalSales,
    RANK() OVER (PARTITION BY soh.TerritoryID ORDER BY SUM(sod.LineTotal) DESC) AS SalesRank
FROM 
    Sales.SalesOrderHeader soh
JOIN 
    Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
JOIN 
    Production.Product p ON sod.ProductID = p.ProductID
JOIN 
    Sales.SalesTerritory st ON soh.TerritoryID = st.TerritoryID
JOIN 
    HumanResources.Employee e ON soh.SalesPersonID = e.BusinessEntityID
JOIN 
    Person.Person pe ON e.BusinessEntityID = pe.BusinessEntityID
GROUP BY 
    soh.SalesPersonID, 
    st.Name, 
    p.Name, 
    sod.LineTotal, 
    soh.OrderDate,
    soh.TerritoryID
ORDER BY 
    SalesRank option ( recompile,querytraceon 3604,querytraceon 8675, querytraceon 2372)

and the output:

so here it went through all the phases 0, 1, and 2

it shows us the gain stage 0 to stage 1 which means there 10 percent decrease in cost

if we look up the output of our trace flags first 8675:

End of simplification, time: 0.001 net: 0.001 total: 0.001 net: 0.001

end exploration, tasks: 238 no total cost time: 0.004 net: 0.004 total: 0.006 net: 0.006

end exploration, tasks: 243 no total cost time: 0 net: 0 total: 0.006 net: 0.006

end search(0),  cost: 5.87246 tasks: 981 time: 0.003 net: 0.003 total: 0.009 net: 0.009

end exploration, tasks: 1755 Cost = 5.87246 time: 0.003 net: 0.003 total: 0.012 net: 0.012

end exploration, tasks: 1761 Cost = 5.87246 time: 0 net: 0 total: 0.012 net: 0.012

end search(1),  cost: 5.27046 tasks: 3970 time: 0.011 net: 0.011 total: 0.026 net: 0.026

end exploration, tasks: 4546 Cost = 5.27046 time: 0.002 net: 0.002 total: 0.028 net: 0.028

end exploration, tasks: 4553 Cost = 5.27046 time: 0 net: 0 total: 0.029 net: 0.029

end search(2),  cost: 5.27046 tasks: 5570 time: 0.003 net: 0.003 total: 0.033 net: 0.033

End of post optimization rewrite, time: 0 net: 0 total: 0.033 net: 0.033

End of query plan compilation, time: 0.001 net: 0.001 total: 0.035 net: 0.035

as you can see at the end of stage 2 it did not decrease the cost so no gain there, it did not time out it just chose the same plan

but it decreased the cost from stage 0 to 1 which was the gain from 5.87246 to 5.27046 which is like 10 percent

we can see how much memory it used, how long did it take, in each step

for 2372:

Memory before NNFConvert: 28

Memory after NNFConvert: 28

Memory before project removal: 31

Memory after project removal: 34

Memory before simplification: 34

Memory after simplification: 81

Memory before heuristic join reordering: 81

Memory after heuristic join reordering: 91

Memory before project normalization: 91

Memory after project normalization: 96

Memory before stage TP: 116

Memory after stage TP: 157

Memory before stage QuickPlan: 157

Memory after stage QuickPlan: 210

Memory before stage Full: 210

Memory after stage Full: 241

Memory before copy out: 242

Memory after copy out: 246

so here we can see how much memory it used at each step, it did store in the memo the alternative search 2 plan, it just did not use it

here we can’t see the reason for early termination in the execution plan, XML, or anywhere

my guess it went through Phase 2 it did not find a parallel plan but it saved cost during phase 1, it looked in the time_out limit of Phase 2 so it did not exceed it, but the parallel plan was not better than Phase 1 plan so it did not include an output.

after that the estimated execution plan will be passed to the execution engine

and that would be the end of the query optimization part 2.

see you later.

One response to “Query Optimizer In SQL Server: Part 2 Physical Operators”

Leave a Reply

Your email address will not be published. Required fields are marked *