Synopsis: Inner joins that can be logically eliminated are instead retained if there is an non-eliminated outer join earlier in the logical tree. Why?
Examples run in AdventureWorks2008R2 and later. I have added traceflags to give the overall context of successive trees and rules.
First example, for context:
- The left join to
Product
is eliminated during simplification (no data is required from the joined table and the referenced values are unique). - The inner join to
SalesOrderDetail
is then eliminated during join collapse aka Heuristic Join Reorder (no data is required from the joined table, the referrer is non nullable, and has an FK enforced)
SELECT sod.SalesOrderDetailID
FROM Sales.SalesOrderDetail AS sod
LEFT JOIN Production.Product AS p -- Eliminated during simplification (Rule: RedundantLOJN)
ON p.ProductID = sod.ProductID
JOIN Sales.SalesOrderHeader AS soh -- Eliminated during join collapse. (Annotated by TF 8619)
ON soh.SalesOrderID = sod.SalesOrderID
OPTION (RECOMPILE, QUERYTRACEON 8619, QUERYTRACEON 8621, QUERYTRACEON 8606, QUERYTRACEON 3604);
In this second example however, the join to SalesOrderHeader could logically be eliminated, but isn't.
- The left join is retained because data is required from
Product
. In the logical trees this join is defined as being prior to the join that does not eliminate. - The subsequent join to
SalesOrderHeader
could logically be eliminated, because the prior join can not invalidate the elimination requirement: not null referrer + FK integrity.
SELECT p.Name
FROM Sales.SalesOrderDetail AS sod
LEFT JOIN Production.Product AS p
ON p.ProductID = sod.ProductID
JOIN Sales.SalesOrderHeader AS soh -- Logically eligible for elimination.
ON soh.SalesOrderID = sod.SalesOrderID
OPTION (RECOMPILE, QUERYTRACEON 8619, QUERYTRACEON 8621, QUERYTRACEON 8606, QUERYTRACEON 3604);
Finally, three variants where the join is successfully eliminated.
In the query text, placing the outer join after the problem join changes the logical tree. The logical meaning is unchanged, but the inner join no longer has the outer join as a descendent in the logical tree.
NOTE! A rare example of where, in SQL Server, the order of the join statements in the query affects the query plan
SELECT p.Name
FROM Sales.SalesOrderDetail AS sod
JOIN Sales.SalesOrderHeader AS soh -- Eliminated during join collapse. (Annotated by TF 8619)
ON soh.SalesOrderID = sod.SalesOrderID
LEFT JOIN Production.Product AS p
ON p.ProductID = sod.ProductID
OPTION (RECOMPILE, QUERYTRACEON 8619, QUERYTRACEON 8621, QUERYTRACEON 8606, QUERYTRACEON 3604);
If the first join is changed to inner, the second join is successfully eliminated.
SELECT p.Name
FROM Sales.SalesOrderDetail AS sod
JOIN Production.Product AS p
ON p.ProductID = sod.ProductID
JOIN Sales.SalesOrderHeader AS soh -- Eliminated during join collapse. (Annotated by TF 8619)
ON soh.SalesOrderID = sod.SalesOrderID
OPTION (RECOMPILE, QUERYTRACEON 8619, QUERYTRACEON 8621, QUERYTRACEON 8606, QUERYTRACEON 3604);
Also, as a solution, we can instead change the second join to outer:
SELECT p.Name
FROM Sales.SalesOrderDetail AS sod
LEFT JOIN Production.Product AS p
ON p.ProductID = sod.ProductID
LEFT JOIN Sales.SalesOrderHeader AS soh -- Eliminated during simplification (Rule: RedundantLOJN)
ON soh.SalesOrderID = sod.SalesOrderID
OPTION (RECOMPILE, QUERYTRACEON 8621, QUERYTRACEON 8606, QUERYTRACEON 3604);
Conclusion
The above examples appear to demonstrate that an outer join may prevent a subsequent inner join elimination, despite it being logically possible.
My speculation is that properties that facilitate the inner join elimination (non null referrer, FK integrity) are not propagated up to the properties of the output of the outer join operator.
Can anyone confirm what the actual cause is?
The take away here is that if you create multi-purpose views that leverage join elimination for optimal plans, you need to be aware of this interaction, and potentially amend joins to avoid unnecessary work during execution.