OR in a JOIN: Preserve Matching Pairs When Rewriting

OR in a JOIN can match either key without returning a matching row pair twice. A rewrite must preserve that rule. Test duplicate pairs and NULL inputs before investigating its execution plan.

Two gravel approaches converge around a planted garden bed and continue as one path toward a blue courtyard door

Build cases that expose the rule

Run these four blocks in one isolated query window, in order. They use session-local temporary tables. The inputs include both-key matches, one-key matches, missing keys and distinct rows with identical key values.

CREATE TABLE #JoinA(Id int PRIMARY KEY,X int NULL,Y int NULL);
CREATE TABLE #JoinB(Id int PRIMARY KEY,X int NULL,Y int NULL);
INSERT #JoinA VALUES (1,10,20),(2,30,40),(3,NULL,50),
                     (4,60,NULL),(5,NULL,NULL);
INSERT #JoinB VALUES (1,10,20),(2,10,99),(3,88,40),
                     (4,NULL,50),(5,60,NULL),(6,999,999),(7,10,20);
SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.X=b.X OR a.Y=b.Y
ORDER BY AId,BId;

The expected pairs are (1,1), (1,2), (1,7), (2,3), (3,4) and (4,5). Pair (1,1) matches both conditions but appears once. Row 7 has the same key values as row 1, but remains a different target row.

Keep pair identity when using UNION

SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.X=b.X
UNION
SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.Y=b.Y
ORDER BY AId,BId;

UNION removes duplicate projected rows. Including both primary-key identifiers makes the projection identify the original row pair. Projecting only equal-looking payload values can collapse legitimate pairs.

Plain UNION ALL returns a pair twice when both branches match it. In this input, pairs (1,1) and (1,7) expose that mistake. Faster execution would not make the extra rows correct.

Make the second branch disjoint, including NULL

SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.X=b.X
UNION ALL
SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.Y=b.Y
AND (a.X<>b.X OR a.X IS NULL OR b.X IS NULL)
ORDER BY AId,BId;

The first branch accepts X matches. The second accepts Y matches only when X did not match. A simple X inequality loses valid Y matches when either X value is NULL.

The added NULL tests retain those cases. Two NULL keys do not equal each other under normal SQL equality. The rewrite still emits each matching row pair once.

Compare multiplicity in both directions

This comparison groups each pair with its count before applying EXCEPT. Both differences should be empty. Plain set comparison without counts can conceal repeated copies of the same pair.

;WITH OriginalPairs AS (
SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.X=b.X OR a.Y=b.Y
), RewrittenPairs AS (
SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.X=b.X
UNION ALL
SELECT a.Id AS AId,b.Id AS BId FROM #JoinA AS a
JOIN #JoinB AS b ON a.Y=b.Y
AND (a.X<>b.X OR a.X IS NULL OR b.X IS NULL)
), OriginalCounts AS (
 SELECT AId,BId,COUNT_BIG(*) AS Copies
 FROM OriginalPairs GROUP BY AId,BId
), RewrittenCounts AS (
 SELECT AId,BId,COUNT_BIG(*) AS Copies
 FROM RewrittenPairs GROUP BY AId,BId
)
SELECT N'Original minus rewrite' AS Difference,AId,BId,Copies
FROM (SELECT * FROM OriginalCounts
      EXCEPT SELECT * FROM RewrittenCounts) AS Missing
UNION ALL
SELECT N'Rewrite minus original',AId,BId,Copies
FROM (SELECT * FROM RewrittenCounts
      EXCEPT SELECT * FROM OriginalCounts) AS Extra;
DROP TABLE #JoinA;
DROP TABLE #JoinB;

The final statements remove the two temporary tables. Preserve both-match, NULL and equal-payload cases when changing the projection or relationship rule. Unique pair identifiers are part of this example’s equivalence argument.

Actual SSMS six OR join row pairs and empty bidirectional pair-count comparison
Actual results from the bounded examples above. The first grid contains the six original row pairs; the second is empty after comparing each pair and its count in both directions. This demonstrates the tested input, without making a performance claim. Open the image for a larger view.
Safe OR join rewrite

Measure performance after proving the result

No speedup is claimed from this tiny input. On representative data, compare actual plans, logical reads and elapsed time under the same conditions. Include the cost of duplicate removal and any additional indexes.

The optimizer chooses physical operators from the schema, statistics and data distribution. Splitting the predicate does not guarantee seeks or a faster plan. Apply this rewrite to inner joins deliberately; outer joins need their own unmatched-row analysis.

Run the pairs on your own tables before you trust any speedup.

A faster rewrite is not a correct rewrite, it is a candidate until the pairs match.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Developer, SQL Scripts, SQL Server
Previous Post
STPointOnSurface: Choose a Point Inside a Polygon
Next Post
ReorientObject: Swap a Geographic Ring’s Interior

Related Posts

Leave a Reply

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

Fill out this field
Fill out this field
Please enter a valid email address.