Question Details

No question body available.

Tags

sql sql-server sql-server-2019

Answers (2)

Accepted Answer Available
Accepted Answer
October 29, 2025 Score: 1 Rep: 7,653 Quality: High Completeness: 100%

Testing for NULL in the WHERE of an anti-JOIN

You have put your condition in the JOIN … ON instead of in the WHERE.
The rule for an anti-JOIN is that you test your possibly NULL column (from the LEFT JOIN) in the WHERE.

In a way, instead of asking "join with employee, then return rows only if there is an employee match, or there is no emp match but a ZIP one",
you're asking "join if there is an employee match, or there is no emp match but a ZIP one, then return those rows without any other condition".

Thus simply changing your:

on e.mapid=m.mapid
and r.employeeid=isnull(e.employeeid,'') OR (r.employeeidisnull(e.employeeid,'') and r.zip5=m.zip5)
where m.mapid='7890'

to:

on e.mapid=m.mapid
where (r.employeeid=isnull(e.employeeid,'') OR (r.employeeidisnull(e.employeeid,'') and r.zip5=m.zip5))
and m.mapid='7890'

returns the expected result:

zip5 mapid Prodcode proddesc amount region
45678 7890 23458 POT 1234789 West S San Diago

(second query in this dbfiddle)

In case of need

In SQL, (condition1 AND condition2) OR ((NOT condition1) AND condition3)
can be written CASE WHEN condition1 THEN condition2 ELSE condition3 END
(avoiding the repetition of, and possibly typo on, condition1, or miss of a case in more complex conditions).

There's just a small problem: SQL Server doesn't make comparisons return a boolean type, so we cannot write:
where case when e.employeeid is not null then r.employeeid=e.employeeid else r.zip5=m.zip5 end

Of course we could use a where 1 = case when / something returning 1 in case of a match / end,
but why not being creative by returning something that matches the first condition?

where r.employeeid = coalesce(e.employeeid, case when r.zip5=m.zip5 then r.employeeid end)

This will read (thanks to the coalesce too):

  • if e.employeeid is not null, then the results of r.employeeid = e.employeeid
  • else if e.employeeid is null, the results of r.employeeid = case when r.zip5=m.zip5 then r.employeeid end
    which will match if r.zip5=m.zip5 (/!\ given that r.employeeid is never null)

(third query of the dbfiddle)

Apples and bananas

However, as both MatBailie and you point out, a problem remains. It all comes down to one thing: do not try to mix different realities in the same table alias, that is, do not try to have r represent both the region of the employee, and the region for the ZIP of the map.

Although you have an intermediate employee, this is essentially the same as a self-join, for example when picking an employee and its employer in a persons table, or apples and bananas from fruits if you want to compare apples to bananas (their number for example). In those cases, once you have "specialized" a row to a given subrole that you want to compare to other subroles of the same set, it should not try to play both roles in the same columns. Better have one row with columns coming from bananas and columns coming from apples, than one row for apples, one row for bananas: it is easier to compare columns than rows.

(well, it's not the only way: in fact you could succeed in having both regions in the same row, by left join #Region r on r.mapid=m.mapid or r.employeeid=e.employeeid (do not choose, keep all possibilities) then with window functions telling you if there's another region linked to the employee in the same set (= linked to the same map); but would it be worth the effort?)

So let's add an re alias for the employee's associated region.

As your requirement is "an employee necessarily with a region", I would avoid chaining left join #Employee e left join #Region re, which could return employees without a region, while the more formal left join (#employee e join #Region re) effectively discards employees without a matching region.
In your case as you are only interested in columns from re, not from e, the end result will be the same (the columns of re will be null if there's no employee, or if there's an employee without a region), but to take good habits I'll use the second form that ensures an all-or-nothing for columns of e too.

Finally, as we now have two different Regions, our switching logic is not in the join anymore, but in the select of the final columns, where we can use a simple coalesce.

select distinct
  m.*,
  coalesce(re.region, r.region) as region
from #map m
left join #Region r
on r.mapid=m.mapid and r.zip5=m.zip5
left join (#employee e join #Region re on re.employeeid=e.employeeid)
on e.mapid=m.mapid
zip5 mapid Prodcode proddesc amount region
00899 3457 24567 ISC 17645 null
00899 4567 24567 PIT 1234 south
04987 9879 24567 ISC 17645 South
45678 7890 23458 POT 1234789 West S San Diago
56333 5678 24567 MHT 23400 EastCentral

(note the null for region 3457: it has neither an employee, nor a matching ZIP in its mapid: I think this is what you expect)

(very last query of this reaugmented fiddle)

October 29, 2025 Score: 0 Rep: 5,531 Quality: Low Completeness: 40%

And what if you just do:

select m.mapid, m.prodcode, m.proddesc, m.amount, m.zip5, coalesce(re.region, r.region) as region from #map m left join #employee e join #region re on re.EmployeeID = e.EmployeeID on e.mapid = m.mapid left join #region r on r.zip5 = m.zip5 and e.EmployeeID is null