This is a product defect as Martin Smith has already answered.
Under certain conditions, the optimizer becomes stuck in a loop adding the same implied predicates to the outer join as it has derived from the earlier join. Eventually, so many duplicate predicates are added that the process runs out of memory.
To encounter this issue you need:
- Multiple joins of different types that do not qualify for treatment as a single n-ary join
- A join predicate specified in the
ON
clause referencing a constant literal projected column
- A separate simple equijoin of column references without expressions
- The same column reference in both joins allowing predicate inference
- Different base types requiring an explicit conversion before comparison
- Two inequalities in the first join (
BETWEEN
expands to two inequalities)
- Explicit conversion required at both ends of the implied range
- The conversion must be constant foldable
A simplified example that meets all these conditions:
DECLARE @T1 AS table (i integer NULL);
SELECT NULL
FROM @T1 AS T1
JOIN (VALUES ('1', '9')) AS V (f, t)
ON T1.i > V.f
AND T1.i < V.t
LEFT JOIN @T1 AS T2
ON T2.i = T1.i;
The first join acquires a tree representation with explicit conversions (ScaOp_Convert
):
LogOp_Select
LogOp_Join
LogOp_Get TBL: @T1(alias TBL: T1) @T1
LogOp_ConstTableGet (1) [empty]
ScaOp_Const TI(bit,ML=1) XVAR(bit,Not Owned,Value=1)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T1].i
ScaOp_Convert int,Null,ML=4
ScaOp_Const TI(varchar collate 53256,Var,Trim,ML=1) XVAR(varchar,Not Owned,Value=Len,Data = (1,1))
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T1].i
ScaOp_Convert int,Null,ML=4
ScaOp_Const TI(varchar collate 53256,Var,Trim,ML=1) XVAR(varchar,Not Owned,Value=Len,Data = (1,9))
The second join acquires implied predicates:
LogOp_Select
LogOp_Get TBL: @T1(alias TBL: T2)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [T2].i
ScaOp_Identifier QCOL: [T1].i
The inferred predicates have been constant folded to T2.i > 1 AND T2.i < 9
.
This is great, but the optimizer now cannot tell that these predicates exactly match what can be inferred from the other join, which involve an explicit conversion from a string type to integer. There's a little more to it, concerning the way the optimizer reasons about implied constraints and domain ranges, but that's the gist of it.
The result is that the implied predicate rule matches again and produces a duplicate set of inferred predicates:
LogOp_Select
LogOp_Select
LogOp_Get TBL: @T1(alias TBL: T2) @T1
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [T2].i
ScaOp_Identifier QCOL: [T1].i
After the next iteration:
LogOp_Select
LogOp_Select
LogOp_Select
LogOp_Get TBL: @T1(alias TBL: T2) @T1
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Logical x_lopAnd
ScaOp_Comp x_cmpGt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=1)
ScaOp_Comp x_cmpLt
ScaOp_Identifier QCOL: [T2].i
ScaOp_Const TI(int,Null,ML=4) XVAR(int,Not Owned,Value=9)
ScaOp_Comp x_cmpEq
ScaOp_Identifier QCOL: [T2].i
ScaOp_Identifier QCOL: [T1].i
...and so on until we run out of memory.
The problem requires a very particular set of unusual circumstances to manifest. It is therefore easy to work around with query rewrites, as noted in the question body. It is also possible to disable the application of implied predicates in this particular case using undocumented trace flag 2324, which can be specified at the query level using QUERYTRACEON
.