We have an application generated query using a view that has two tables joined on a LEFT OUTER join. When filtering by fields from just one table (either table) an index seek happens and it's reasonably fast.
It is valid (= guaranteed to always produce correct results) to push a selection (aka filter, predicate) below an inner join when the selection involves only attributes from one table.
It is also valid to push such a selection down the preserved side of an outer join. A single-table selection that rejects nulls (as yours does) can also be pushed down the non-preserved (null-padded) side of an outer join. This works because rejecting nulls turns the outer join into an inner join, so the previous rule applies.
Once a predicate is below the join, it may be matched to one or more indexes.
Note: SQL Server cannot use a disjunctive ("or") selection over multiple attributes to seek using b-tree indexes directly. It achieves this result via index union transformation. This rewrite locates the set of rows matching any of the individual disjunctive tests, using a separate index per attribute, then removing duplicates. This transform only applies to a single table, and does not apply to attribute-to-attribute comparisons.
When the where clause includes conditions for fields from both tables using an OR the query plan switches to a table scan and doesn't utilize any of the indexes.
A disjunctive selection that references both joined tables cannot be pushed below a join directly — it must be evaluated at, or after the join. There are other valid transformations for conjunctive ("and") predicates, but you do not have those.
While it is not possible to push a disjunctive selection referencing both tables directly, it is possible to rewrite the query to achieve the same result. SQL Server does not contain this transformation, so you have to perform it manually. To be clear, it is not a question of cardinality estimation, or any other input to the optimizer's choices — SQL Server just can't do it.
One such manual rewrite is shown in Rob Farley's answer. As noted there, you do have to be careful to preserve all the semantics when using UNION
, and the cost of the de-duplicating aspect can be significant.
Another valid rewrite is included in Oracle's "or-expansion" feature. This uses UNION ALL
rather than UNION
, so the problem with ntext
not being comparable does not arise. The end result may also be cheaper to execute since duplicate removal is not necessary:
The essential idea is to guarantee the components of the UNION ALL
are disjoint by explicitly excluding rows that match any of the preceding conditions.
It can be tricky to get the required negation correct in three-valued logic. Oracle uses the LNNVL
built-in for this purpose. LNNVL(test)
returns true if the tested predicate is false or unknown, and false if the tested predicate is true. I have simulated LNNVL
in SQL Server using IIF
.
SELECT *
FROM T1617
WHERE C260100004 LIKE @P1
UNION ALL
SELECT *
FROM T1617
WHERE
--- Exclude rows found in the previous step
0 = IIF(C260100004 LIKE @P1, 1, 0)
-- New tests
AND
(
C1402001100 LIKE @P0
OR C200000001 LIKE @P2
OR C200000020 LIKE @P3
)
Now, the question shows you do not care about preserving the exact semantics of the outer join, so a small improvement is possible by replacing the (correct) IIF
test with a simple (but less correct) NOT
. This rejects nulls, so the outer join is simplified to an inner join, and the NOT
test can be pushed down to the seek on T1011 as a non-sargable predicate:
SELECT *
FROM T1617
WHERE C260100004 LIKE @P1
UNION ALL
SELECT *
FROM T1617
WHERE
(
NOT C260100004 LIKE @P1
)
AND
(
C1402001100 LIKE @P0
OR C200000001 LIKE @P2
OR C200000020 LIKE @P3
)
Online demo