SQL Server: joining NULL values
From Stack Overflow:
UPDATE n SET AddressID = a.AddressID FROM #NewAddress n JOIN dbo.Address a ON (a.[LineA] = n.[LineA] OR (a.[LineA] is null AND n.[LineA] is null)) AND (a.[LineB] = n.[LineB] OR (a.[LineB] is null AND n.[LineB] is null)) AND (a.[LineC] = n.[LineC] OR (a.[LineC] is null AND n.[LineC] is null)) WHERE n.Processed = 0Any ideas on how to UNION or EXISTS this query? This seems to be a pretty common join condition I'm encountering.
Here's a JOIN
condition: we should join two tables on a common key, also treating NULL values as equal. Normal equality conditions don't work here: in SQL, NULL
values are not considered equal (strictly speaking, their equality is a boolean NULL
rather than true or false).
ANSI standard defines a special predicate for that: IS NOT DISTINCT FROM
, which is supported by PostgreSQL. This treats values the same value as DISTINCT
does: two equal values, as well as two NULL
values, are considered not distinct. MySQL also supports similar operator, <=>
. Unfortunately, SQL Server supports neither.
In SQL Server, we can emulate this by using set operators which distinctify the queries. Set operators work on sets, so we would need some kind of a SELECT
query for them. This would also help us to overcome SQL Server's inability to compare tuples directly. Basically, we need to tell if two tuples: (a.lineA, a.lineB, a.lineC)
and (n.lineA, n.lineB, n.lineC)
are distinct or not.
We can do that by selecting those tuples in dummy queries (those without a FROM
clause), intersecting those queries (which would give us a one-record or zero-record resultset, depending on whether the two tuples are distinct or not) and applying EXISTS
to the resultset. EXISTS
is a boolean predicate and as such can serve as a join condition.
Let's check it. We'll create two tables, fill them with random values (including occasional NULLs here and there):
CREATE TABLE a ( id INT NOT NULL PRIMARY KEY IDENTITY, lineA VARCHAR(MAX), lineB VARCHAR(MAX), lineC VARCHAR(MAX) ) CREATE TABLE n ( id INT NOT NULL PRIMARY KEY IDENTITY, lineA VARCHAR(MAX), lineB VARCHAR(MAX), lineC VARCHAR(MAX) ) DECLARE @count INT = 0 SELECT RAND(0.20130508) WHILE @count <= 10000 BEGIN INSERT INTO a (lineA, lineB, lineC) VALUES ( 'string' + CASE WHEN RAND() > 0.03 THEN CAST(FLOOR(RAND() * 200) AS VARCHAR) END, 'string' + CASE WHEN RAND() > 0.03 THEN CAST(FLOOR(RAND() * 200) AS VARCHAR) END, 'string' + CASE WHEN RAND() > 0.03 THEN CAST(FLOOR(RAND() * 200) AS VARCHAR) END ) INSERT INTO n (lineA, lineB, lineC) VALUES ( 'string' + CASE WHEN RAND() > 0.03 THEN CAST(FLOOR(RAND() * 200) AS VARCHAR) END, 'string' + CASE WHEN RAND() > 0.03 THEN CAST(FLOOR(RAND() * 200) AS VARCHAR) END, 'string' + CASE WHEN RAND() > 0.03 THEN CAST(FLOOR(RAND() * 200) AS VARCHAR) END ) SET @count = @count + 1 END
and join them:
SELECT * FROM a JOIN n ON EXISTS ( SELECT a.lineA, a.lineB, a.lineC INTERSECT SELECT n.lineA, n.lineB, n.lineC )
id | lineA | lineB | lineC | id | lineA | lineB | lineC | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
8038 | string84 | 4494 | string84 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2726 | string49 | string51 | 1262 | string49 | string51 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
14 | string73 | string97 | 6239 | string73 | string97 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7086 | string101 | string110 | string99 | 7616 | string101 | string110 | string99 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
976 | string103 | string129 | string14 | 7176 | string103 | string129 | string14 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1030 | string111 | string145 | string105 | 4897 | string111 | string145 | string105 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1010 | string112 | string142 | string195 | 1670 | string112 | string142 | string195 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3525 | string130 | string117 | string7 | 5747 | string130 | string117 | string7 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8515 | string134 | string2 | string182 | 1250 | string134 | string2 | string182 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
293 | string17 | string118 | string16 | 9588 | string17 | string118 | string16 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3339 | string18 | string76 | string83 | 6817 | string18 | string76 | string83 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
239 | string181 | string18 | string88 | 497 | string181 | string18 | string88 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4343 | string196 | string72 | 5396 | string196 | string72 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4947 | string23 | string155 | 7858 | string23 | string155 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6833 | string23 | string182 | 225 | string23 | string182 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6453 | string54 | string191 | string166 | 1199 | string54 | string191 | string166 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8539 | string57 | string184 | string134 | 6607 | string57 | string184 | string134 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6452 | string6 | string67 | 9161 | string6 | string67 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1068 | string68 | string146 | string109 | 5068 | string68 | string146 | string109 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
19 rows fetched in 0.0008s (0.5156s) |
[Microsoft][SQL Server Native Client 10.0][SQL Server]Table 'Worktable'. Scan count 19, logical reads 115, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. [Microsoft][SQL Server Native Client 10.0][SQL Server]Table 'n'. Scan count 1, logical reads 59, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. [Microsoft][SQL Server Native Client 10.0][SQL Server]Table 'a'. Scan count 1, logical reads 59, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. [Microsoft][SQL Server Native Client 10.0][SQL Server] SQL Server Execution Times: CPU time = 516 ms, elapsed time = 520 ms.
|--Merge Join(Inner Join, MANY-TO-MANY MERGE:([ee].[dbo].[a].[lineA], [ee].[dbo].[a].[lineB], [ee].[dbo].[a].[lineC])=([ee].[dbo].[n].[lineA], [ee].[dbo].[n].[lineB], [ee].[dbo].[n].[lineC]), RESIDUAL:([ee].[dbo].[a].[lineA] = [ee].[dbo].[n].[lineA] AND [ee].[dbo].[a].[lineB] = [ee].[dbo].[n].[lineB] AND [ee].[dbo].[a].[lineC] = [ee].[dbo].[n].[lineC])) |--Sort(ORDER BY:([ee].[dbo].[a].[lineA] ASC, [ee].[dbo].[a].[lineB] ASC, [ee].[dbo].[a].[lineC] ASC)) | |--Clustered Index Scan(OBJECT:([ee].[dbo].[a].[PK__a__3213E83F76CD3E06])) |--Sort(ORDER BY:([ee].[dbo].[n].[lineA] ASC, [ee].[dbo].[n].[lineB] ASC, [ee].[dbo].[n].[lineC] ASC)) |--Clustered Index Scan(OBJECT:([ee].[dbo].[n].[PK__n__3213E83F7A9DCEEA]))
Note that the query uses Merge Join
which is only possible on equality conditions.
Indeed, we see this in the plan:
([ee].[dbo].[a].[lineA], [ee].[dbo].[a].[lineB], [ee].[dbo].[a].[lineC])=([ee].[dbo].[n].[lineA], [ee].[dbo].[n].[lineB], [ee].[dbo].[n].[lineC])
which is good old tuple comparison. As it turns out, SQL Server is capable of doing this behind the scenes, it's just something it does not implement in the front end.
Note that if we put original join clause here, it will yield exactly same plan:
SELECT * FROM a JOIN n ON (a.lineA = n.lineA OR (a.lineA IS NULL AND n.lineA IS NULL)) AND (a.lineB = n.lineB OR (a.lineB IS NULL AND n.lineB IS NULL)) AND (a.lineC = n.lineC OR (a.lineC IS NULL AND n.lineC IS NULL))
id | lineA | lineB | lineC | id | lineA | lineB | lineC | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
8038 | string84 | 4494 | string84 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2726 | string49 | string51 | 1262 | string49 | string51 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
14 | string73 | string97 | 6239 | string73 | string97 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7086 | string101 | string110 | string99 | 7616 | string101 | string110 | string99 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
976 | string103 | string129 | string14 | 7176 | string103 | string129 | string14 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1030 | string111 | string145 | string105 | 4897 | string111 | string145 | string105 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1010 | string112 | string142 | string195 | 1670 | string112 | string142 | string195 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3525 | string130 | string117 | string7 | 5747 | string130 | string117 | string7 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8515 | string134 | string2 | string182 | 1250 | string134 | string2 | string182 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
293 | string17 | string118 | string16 | 9588 | string17 | string118 | string16 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3339 | string18 | string76 | string83 | 6817 | string18 | string76 | string83 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
239 | string181 | string18 | string88 | 497 | string181 | string18 | string88 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4343 | string196 | string72 | 5396 | string196 | string72 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4947 | string23 | string155 | 7858 | string23 | string155 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6833 | string23 | string182 | 225 | string23 | string182 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6453 | string54 | string191 | string166 | 1199 | string54 | string191 | string166 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8539 | string57 | string184 | string134 | 6607 | string57 | string184 | string134 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6452 | string6 | string67 | 9161 | string6 | string67 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1068 | string68 | string146 | string109 | 5068 | string68 | string146 | string109 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
19 rows fetched in 0.0009s (0.5156s) |
[Microsoft][SQL Server Native Client 10.0][SQL Server]Table 'Worktable'. Scan count 19, logical reads 115, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. [Microsoft][SQL Server Native Client 10.0][SQL Server]Table 'n'. Scan count 1, logical reads 59, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. [Microsoft][SQL Server Native Client 10.0][SQL Server]Table 'a'. Scan count 1, logical reads 59, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. [Microsoft][SQL Server Native Client 10.0][SQL Server] SQL Server Execution Times: CPU time = 500 ms, elapsed time = 523 ms.
|--Merge Join(Inner Join, MANY-TO-MANY MERGE:([ee].[dbo].[a].[lineA], [ee].[dbo].[a].[lineB], [ee].[dbo].[a].[lineC])=([ee].[dbo].[n].[lineA], [ee].[dbo].[n].[lineB], [ee].[dbo].[n].[lineC]), RESIDUAL:([ee].[dbo].[a].[lineA] = [ee].[dbo].[n].[lineA] AND [ee].[dbo].[a].[lineB] = [ee].[dbo].[n].[lineB] AND [ee].[dbo].[a].[lineC] = [ee].[dbo].[n].[lineC])) |--Sort(ORDER BY:([ee].[dbo].[a].[lineA] ASC, [ee].[dbo].[a].[lineB] ASC, [ee].[dbo].[a].[lineC] ASC)) | |--Clustered Index Scan(OBJECT:([ee].[dbo].[a].[PK__a__3213E83F76CD3E06])) |--Sort(ORDER BY:([ee].[dbo].[n].[lineA] ASC, [ee].[dbo].[n].[lineB] ASC, [ee].[dbo].[n].[lineC] ASC)) |--Clustered Index Scan(OBJECT:([ee].[dbo].[n].[PK__n__3213E83F7A9DCEEA]))
, so this trick is a pure syntactic sugar. However, it's a good replacement for IS DISTINCT FROM
/ IS NOT DISTINCT FROM
and will make your queries look prettier.
Nice trick, excellent article. Thank you very much.
Minh
16 May 13 at 04:03