{"id":2077,"date":"2015-09-07T00:00:00","date_gmt":"2015-08-24T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/how-to-avoid-conditional-joins-in-t-sql\/"},"modified":"2026-03-18T14:16:24","modified_gmt":"2026-03-18T14:16:24","slug":"how-to-avoid-conditional-joins-in-t-sql","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/performance-sql-server\/how-to-avoid-conditional-joins-in-t-sql\/","title":{"rendered":"SQL Conditional JOIN: Why It&#8217;s Slow &#038; What to Use Instead"},"content":{"rendered":"<div id=\"pretty\">\n<p>A conditional JOIN &#8211; where you use CASE in the ON clause to choose which column to join on based on a condition &#8211; is a T-SQL anti-pattern that causes significant performance problems. The query optimizer cannot use index seeks efficiently when the join predicate is wrapped in a CASE expression, typically resulting in full table scans. Better alternatives include using LEFT OUTER JOIN to all possible tables with CASE in the SELECT (simplest), UNION ALL of separate targeted queries (best performance), or CROSS APPLY with a subquery (most flexible). This article benchmarks all three approaches against the conditional JOIN pattern using one million rows.<\/p>\n<h2>Introduction<\/h2>\n<p class=\"start\">Just because certain SQL JOINs are possible doesn&#8217;t mean that they are a good option.\u00a0 The &#8216;conditional JOIN&#8217; is one of these.<\/p>\n<p>So what is a conditional join? This is where one key in a table is used\u00a0 to join either with one table or others, depending on some criterion. It sounds extraordinary, but it can happen, particularly in highly complex, sometimes auto-generated queries. It usually represents an attempt to use a polymorphic association between tables, and is a <a href=\"https:\/\/www.simple-talk.com\/books\/sql-books\/119-sql-code-smells\/\">SQL Code Smell<\/a>.<\/p>\n<p>The term \u00a0&#8216;Conditional JOIN&#8217; is somewhat ambiguous, in that people refer to different problems when they use it.\u00a0 It seems as if programmers wish to either JOIN a table to more than one other table or to JOIN a column in a table to different columns in a second table, choosing the JOIN column based on a condition.<\/p>\n<p>Since T-SQL \u00a0has no syntax that would allow for putting a table name into a CASE statement, the first definition of the conditional JOIN really has no means to resolve other than to simply JOIN all the tables (likely as LEFT OUTER JOINs) and use CASE statements to pull the specific data item(s) required from the secondary tables as appropriate.<\/p>\n<p>Recently I was writing a query that could be resolved using the second case.\u00a0 While I thought about several other ways to do it, from a coding standpoint using a CASE statement in the ON clause of the JOIN resulted in the simplest syntax to make the query work.\u00a0 However, was syntax that looked simple the most efficient way of doing it?<\/p>\n<p>Let&#8217;s face it, sometimes in programming you don&#8217;t have the luxury of standing back and redesigning the system to avoid the requirement, but it is always best if we can optimize the performance of the SQL that meets the requirement.<\/p>\n<h2>Sample Data<\/h2>\n<p>We&#8217;ll start with a simple example to illustrate a conditional join.\u00a0 This code creates two tables, with one million rows of test data in the secondary table that we&#8217;ll be doing the conditional JOIN to.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">CREATE TABLE dbo.SampleLeftTable\n(\n\u00a0\u00a0\u00a0 ID\u00a0\u00a0\u00a0\u00a0\u00a0 INT IDENTITY PRIMARY KEY\n\u00a0\u00a0\u00a0 ,num\u00a0\u00a0\u00a0 INT\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 NOT NULL\n);\n\u00a0\nINSERT INTO dbo.SampleLeftTable (num)\nVALUES(21),(44),(53),(78);\n\u00a0\nCREATE TABLE dbo.ConditionalJoinExample\n(\n\u00a0\u00a0\u00a0 ID\u00a0\u00a0\u00a0\u00a0\u00a0 INT IDENTITY PRIMARY KEY\n\u00a0\u00a0\u00a0 ,N1\u00a0\u00a0\u00a0\u00a0 INT\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 NOT NULL\n\u00a0\u00a0\u00a0 ,N2\u00a0\u00a0\u00a0\u00a0 INT\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 NOT NULL\n\u00a0\u00a0\u00a0 ,N3\u00a0\u00a0\u00a0\u00a0 INT\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 NOT NULL\n\u00a0\u00a0\u00a0 ,N4\u00a0\u00a0\u00a0\u00a0 INT\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 NOT NULL\n);\n\u00a0\nWITH Tally(n) AS \n(\n\u00a0\u00a0\u00a0 SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))\n\u00a0\u00a0\u00a0 FROM(VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a(n)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 -- 10 rows\n\u00a0\u00a0\u00a0 CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x10 rows\n\u00a0\u00a0\u00a0 CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x10 rows\n\u00a0\u00a0\u00a0 CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d(n) -- x10 rows\n\u00a0\u00a0\u00a0 CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) e(n) -- x10 rows\n\u00a0\u00a0\u00a0 CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) f(n) -- x10 rows\n)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 -- = 1M rows\nINSERT INTO dbo.ConditionalJoinExample\n(\n\u00a0\u00a0\u00a0 N1, N2, N3, N4\n)\n-- Populate each column with a random number in the range {1,100}\nSELECT N1\u00a0\u00a0 = 1+ABS(CHECKSUM(NEWID()))%100\n\u00a0\u00a0\u00a0 ,N2\u00a0\u00a0\u00a0\u00a0 = 1+ABS(CHECKSUM(NEWID()))%100\n\u00a0\u00a0\u00a0 ,N3\u00a0\u00a0\u00a0\u00a0 = 1+ABS(CHECKSUM(NEWID()))%100\n\u00a0\u00a0\u00a0 ,N4\u00a0\u00a0\u00a0\u00a0 = 1+ABS(CHECKSUM(NEWID()))%100\nFROM Tally;\n\u00a0\n\u00a0\nGO\n--DROP TABLE dbo.SampleLeftTable;\n--DROP TABLE dbo.ConditionalJoinExample;\nHere are the results.\nSELECT *\nFROM dbo.SampleLeftTable;\n\u00a0\nID\u00a0 num\n1\u00a0\u00a0 21\n2\u00a0\u00a0 44\n3\u00a0\u00a0 53\n4\u00a0\u00a0 78\n\u00a0\nSELECT TOP 10 *\nFROM dbo.ConditionalJoinExample;\n\u00a0\nID\u00a0 N1\u00a0\u00a0 N2\u00a0\u00a0 N3\u00a0\u00a0 N4\n1\u00a0\u00a0 19\u00a0\u00a0 30\u00a0\u00a0 1\u00a0\u00a0\u00a0 66\n2\u00a0\u00a0 61\u00a0\u00a0 92\u00a0\u00a0 69\u00a0\u00a0 51\n3\u00a0\u00a0 9\u00a0\u00a0\u00a0 1\u00a0\u00a0\u00a0 20\u00a0\u00a0 74\n4\u00a0\u00a0 81\u00a0\u00a0 78\u00a0\u00a0 76\u00a0\u00a0 79\n5\u00a0\u00a0 62\u00a0\u00a0 100\u00a0 37\u00a0\u00a0 75\n6\u00a0\u00a0 59\u00a0\u00a0 83\u00a0\u00a0 58\u00a0\u00a0 43\n7\u00a0\u00a0 89\u00a0\u00a0 70\u00a0\u00a0 76\u00a0\u00a0 30\n8\u00a0\u00a0 40\u00a0\u00a0 11\u00a0\u00a0 85\u00a0\u00a0 91\n9\u00a0\u00a0 97\u00a0\u00a0 16\u00a0\u00a0 67\u00a0\u00a0 84\n10\u00a0 22\u00a0\u00a0 50\u00a0\u00a0 74\u00a0\u00a0 30\n<\/pre>\n<p>You can see that the four data columns (N1, N2, N3 and N4) contain random numbers in the range 1 to 100.\u00a0 We&#8217;ve selected the TOP 10 rows just to illustrate, and if you&#8217;re following along you&#8217;ll get different but similar results in the second results set.\u00a0 The commented-out DROPs are provided to clean up your sandbox later if you want to run these examples on your server.\u00a0 Note that we&#8217;ll be running them using SQL 2012, but SQL 2008 or SQL 2005 can also be used.<\/p>\n<h2>Scenario 1: Conditional JOIN Based on Data in the Left Table<\/h2>\n<p>Like this article? Check out these below:<br \/><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/performance-sql-server\/identifying-and-solving-index-scan-problems\/\">Identifying and solving index scan problems<\/a><br \/><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/performance-sql-server\/sql-server-deadlocks-by-example\/\">SQL Server deadlocks by example<\/a><br \/><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/performance-sql-server\/computed-column-performance-in-sql-server\/\">Computed column performance in SQL Server<\/a><br \/><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/using-subqueries-in-a-t-sql-select-statement\/\">Using subqueries in a T-SQL SELECT statement<\/a><br \/><br \/>Now suppose we have a business requirement that states we want to perform a JOIN from the left table (4 rows) to the secondary table based on the range that the value in the left table falls into.\u00a0 Such a JOIN may look like this.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">SELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = CASE\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 WHEN num BETWEEN 1 AND 25 THEN b.N1\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 WHEN num BETWEEN 26 AND 50 THEN b.N2\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 WHEN num BETWEEN 51 AND 75 THEN b.N3\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ELSE b.N4\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 END;\n<\/pre>\n<p>And it displays results that look like this (only the first five rows are shown, out of about 40,000 returned).<\/p>\n<pre>ID\u00a0\u00a0 num\u00a0\u00a0 ID\u00a0\u00a0 N1\u00a0\u00a0 N2\u00a0\u00a0 N3\u00a0\u00a0 N4\n3\u00a0\u00a0\u00a0 53\u00a0\u00a0\u00a0 23\u00a0\u00a0 52\u00a0\u00a0 70\u00a0\u00a0 53\u00a0\u00a0 4\n3\u00a0\u00a0\u00a0 53\u00a0\u00a0\u00a0 72\u00a0\u00a0 48\u00a0\u00a0 49\u00a0\u00a0 53\u00a0\u00a0 3\n3\u00a0\u00a0\u00a0 53\u00a0\u00a0\u00a0 227\u00a0 89\u00a0\u00a0 81\u00a0\u00a0 53\u00a0\u00a0 1\n3\u00a0\u00a0\u00a0 53\u00a0\u00a0\u00a0 269\u00a0 19\u00a0\u00a0 28\u00a0\u00a0 53\u00a0\u00a0 77\n3\u00a0\u00a0\u00a0 53\u00a0\u00a0\u00a0 393\u00a0 83\u00a0\u00a0 72\u00a0\u00a0 53\u00a0\u00a0 78\n<\/pre>\n<p>You can see that because the value of Num of the row in the left table is 53, it is matching on the third N column (N3) in the table JOINed to.<\/p>\n<p>I was interested in TIME and IO STATISTICS so I ran that query with them on and we got these results.<\/p>\n<pre>(40104 row(s) affected)\n\u00a0\nTable 'SampleLeftTable'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'ConditionalJoinExample'. Scan count 4, logical reads 14400, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 11676892, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n\u00a0\n\u00a0SQL Server Execution Times:\n\u00a0\u00a0 CPU time = 20015 ms,\u00a0 elapsed time = 5617 ms.\n<\/pre>\n<p>While the query only took about five and a half seconds to run, it looked suspiciously expensive in terms of CPU.\u00a0 The query plan for it was this.<\/p>\n<p class=\"illustration\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2270-clip_image001.png\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2270-clip_image002.jpg\" alt=\"2270-clip_image002.jpg\" width=\"624\" height=\"299\" \/><\/a><\/p>\n<p>First off, we should not be surprised to see a Parallelism operator in the plans, because from our timing results we see that CPU time used is way in excess of the elapsed time for the query.<\/p>\n<p>We also see that both tables are using a Clustered Index Scan operator, but when we look a little deeper into what is going on with the right table (by expanding the details of the Table Spool and Clustered Index Scan operators), we see that they&#8217;re trying to operate on four million (actual) rows of data, when there are only one million rows of data in the table!<\/p>\n<p>Our first thought of course is that perhaps we can build an INDEX to speed this bad boy up a bit.\u00a0 So let&#8217;s try that.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">CREATEINDEX ix5ON dbo.ConditionalJoinExample(N1,N2,N3,N4);<\/pre>\n<p>When we run that same query again, we get these timing results.<\/p>\n<pre>SQL Server parse and compile time: \n\u00a0\u00a0 CPU time = 0 ms, elapsed time = 2 ms.\n\u00a0\n(40104 row(s) affected)\n\u00a0\nTable 'SampleLeftTable'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'ConditionalJoinExample'. Scan count 4, logical reads 12928, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 11676892, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n\u00a0\n\u00a0SQL Server Execution Times:\n\u00a0\u00a0 CPU time = 20279 ms,\u00a0 elapsed time = 5949 ms.\n<\/pre>\n<p>And they are not very impressive, being of approximately the same order of magnitude as the first.\u00a0 As to the query plan:<\/p>\n<p class=\"illustration\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2270-clip_image003.png\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2270-clip_image004.jpg\" alt=\"2270-clip_image004.jpg\" width=\"623\" height=\"121\" \/><\/a><\/p>\n<p>We see that while the INDEX we created is being used, it didn&#8217;t seem to help much.\u00a0 If you repeat this query and hover your cursor over the Index Scan (NonClustered) and Table Spool operators in the graphical query plan, you will find that the same four million actual rows were being used in the query.<\/p>\n<p>It is definitely time to rewrite this query and make it run faster by finding an alternative to the conditional join.<\/p>\n<p>Instead of our conditional JOIN, we can partition the big table into four parts, doing successive JOINs on each of the partitions, and then recombining the parts like this (using UNION ALL):<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Drop the annoyingly unhelpful INDEX\nDROP INDEX ix5 ON dbo.ConditionalJoinExample;\n\u00a0\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N1\nWHERE num BETWEEN 1 AND 25\nUNION ALL\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N2\nWHERE num BETWEEN 26 AND 50\nUNION ALL\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N3\nWHERE num BETWEEN 51 AND 75\nUNION ALL\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N4\nWHERE num BETWEEN 76 AND 100;\n<\/pre>\n<p>While that appears to be a much more complex query (it certainly takes a lot longer to write even using copy and paste), we are pretty impressed by the timing results:<\/p>\n<pre>(40104 row(s) affected)\n\u00a0\nTable 'SampleLeftTable'. Scan count 4, logical reads 8, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'ConditionalJoinExample'. Scan count 20, logical reads 14600, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n\u00a0\n\u00a0SQL Server Execution Times:\n\u00a0\u00a0 CPU time = 249 ms,\u00a0 elapsed time = 389 ms.\n<\/pre>\n<p>While this query is now running in less than half a second (with CPU time down by two orders of magnitude), it does produce a much more (seemingly) complex query plan.<\/p>\n<p class=\"illustration\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2270-clip_image005.png\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2270-clip_image006.jpg\" alt=\"2270-clip_image006.jpg\" width=\"624\" height=\"345\" \/><\/a><\/p>\n<p>Even though SQL is doing four Clustered Index Scans of the big table, the results are overwhelmingly in favor of this query over the conditional JOIN.<\/p>\n<p>SQL 2012 grumbled somewhat that an INDEX was missing that could improve the query a bit, so let&#8217;s go ahead and create the INDEX that it recommends and try again.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Add the recommended INDEX\nCREATE NONCLUSTERED INDEX ix1 ON dbo.ConditionalJoinExample (N4) INCLUDE (ID, N1, N2, N3);\n<\/pre>\n<p>Now we get these timing results:<\/p>\n<pre>(40104 row(s) affected)\n\u00a0\nTable 'SampleLeftTable'. Scan count 4, logical reads 8, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'ConditionalJoinExample'. Scan count 16, logical reads 9807, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n\u00a0\n\u00a0SQL Server Execution Times:\n\u00a0\u00a0 CPU time = 265 ms,\u00a0 elapsed time = 398 ms.\n<\/pre>\n<p>It looks to me like that recommended INDEX didn&#8217;t help one bit!\u00a0 And I did in fact check the query plan and found that the optimizer used the recommended INDEX.<\/p>\n<p>From my perspective I&#8217;d say that the rewrite worked sufficiently that I wouldn&#8217;t hesitate to use it in production without the recommended INDEX, thus saving the space that building that INDEX would entail, not to mention the overhead having that INDEX would impose on INSERT, UPDATE and DELETE statements.<\/p>\n<p>I should also point out that in our first (conditional JOIN) attempt, we were working with columns (N1, &#8230;, N4) that were all of the same data type.\u00a0 If in your case, you are not, there are likely additional performance detractors in getting them to the same data type, that you should watch for and consider.<\/p>\n<p>Let&#8217;s drop that INDEX before we proceed with our next scenario.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">DROP INDEXix1 ONdbo.ConditionalJoinExample;<\/pre>\n<h2>Scenario 2: Conditional JOIN Based on an External Parameter<\/h2>\n<p>Sometimes you may have a SQL problem that makes you believe that you need to drive the conditional JOIN key column by a switch of some sort, perhaps one that is passed in as a parameter in a stored procedure.\u00a0 Let&#8217;s look at a simplified example that does not employ a stored procedure:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">DECLARE @Switch INT = 1;\n\u00a0\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = CASE @Switch\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 WHEN 1 THEN b.N1\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 WHEN 2 THEN b.N2\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 WHEN 3 THEN b.N3\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ELSE b.N4\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 END;\n<\/pre>\n<p>Here the @Switch local variable controls the column we&#8217;ll JOIN on.\u00a0 Timing results look suspiciously similar to the first case we explored.<\/p>\n<pre>(40156 row(s) affected)\n\u00a0\nTable 'SampleLeftTable'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'ConditionalJoinExample'. Scan count 4, logical reads 14400, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 11676892, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n\u00a0\n\u00a0SQL Server Execution Times:\n\u00a0\u00a0 CPU time = 19297 ms,\u00a0 elapsed time = 5284 ms.\n<\/pre>\n<p>Indeed, the query plan, which we won&#8217;t show here, looks remarkably similar to the first query plan we showed, right down to the four million actual row counts in the Clustered Index Scan and Table Spool operators.<\/p>\n<p>Using the identical refactoring approach:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">DECLARE @Switch INT = 1;\n\u00a0\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N1\nWHERE @Switch = 1\nUNION ALL\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N2\nWHERE @Switch = 2\nUNION ALL\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N3\nWHERE @Switch = 3\nUNION ALL\nSELECT a.ID, num, b.ID, N1, N2, N3, N4\nFROM dbo.SampleLeftTable a\nJOIN dbo.ConditionalJoinExample b\nON num = b.N4\nWHERE @Switch NOT IN (1,2,3);\n<\/pre>\n<p>We end up with a much more complicated looking query (with a similar, much more complicated query plan), that runs lickety-split!<\/p>\n<pre>(40156 row(s) affected)\n\u00a0\nTable 'SampleLeftTable'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'ConditionalJoinExample'. Scan count 5, logical reads 3650, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\nTable 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.\n\u00a0\n\u00a0SQL Server Execution Times:\n\u00a0\u00a0 CPU time = 108 ms,\u00a0 elapsed time = 324 ms.\n<\/pre>\n<p>Now, if you were to be passing the @Switch variable into a stored procedure, it is quite possible that you could run into parameter sniffing issues in either or both of these cases.\u00a0 But we won&#8217;t explore the details here (nor how to resolve them), as they&#8217;ve been covered pretty thoroughly by <a href=\"https:\/\/mvp.microsoft.com\/en-us\/PublicProfile\/5440?fullName=erland%20sommarskog\"> SQL MVP Erland Sommarskog<\/a> in his excellent <a href=\"http:\/\/www.sommarskog.se\/\">blog<\/a> on this subject &#8220;<a href=\"http:\/\/www.sommarskog.se\/query-plan-mysteries.html\">Slow in the Application, Fast in SSMS?<\/a>&#8220;<\/p>\n<h2>Conclusions<\/h2>\n<p>You can never be certain that code that looks elegant on-screen will be executed quickly. The query that uses a conditional JOIN may seem to solve the problem, but it is worth \u00a0exploring alternatives to make sure you don&#8217;t run into performance issues when your data grows large.<\/p>\n<p>Some other lessons learned from the two scenarios we explored in this article are:<\/p>\n<ul>\n<li>\u00a0The simplest looking query plan may not always perform the most efficiently.<\/li>\n<li>\u00a0INDEXes recommended by SQL 2012 in the graphic query plan (and presumably by SQL Tuning Advisor) may not always help your query perform faster, even when the Optimizer chooses to use them.<\/li>\n<li>\u00a0If performance counts, and it usually does, check your queries for performance issues and consider alternative ways of achieving the same result.<\/li>\n<\/ul>\n<p>Finally, you may be wondering what happened in the case of my query that I mentioned early on.\u00a0 It wasn&#8217;t quite as simple of course as the two scenarios we explored here.\u00a0 It was using a big table generated by a schema-bound VIEW and there weren&#8217;t only four cases to consider (although there were four potential columns to JOIN upon).\u00a0 The use of a conditional JOIN got the business solution working quickly, however when I tested the performance I found it wanting.\u00a0 So I went that extra mile to improve upon it, and ultimately the extra effort yielded satisfactory results.<\/p>\n<p>Thanks for reading, \u00a0folks!\u00a0 I hope that one day you can use the lessons I learned here to improve the speed of at least one of your queries.<\/p>\n<\/div>\n\n\n<section id=\"my-first-block-block_ffcdb3ccfaf526072c0c16268f39fd7c\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Fast, reliable and consistent SQL Server development&#8230;<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8230;with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-toolbelt-essentials\/\" class=\"btn btn--secondary btn--lg\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: How to avoid conditional JOINs in T-SQL<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is a conditional JOIN in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>A conditional JOIN uses a CASE expression in the ON clause to dynamically choose which column to join on. For example, joining TableA to TableB ON CASE WHEN condition THEN TableA.Col1 = TableB.Col1 ELSE TableA.Col2 = TableB.Col2 END. While syntactically valid, this pattern prevents the optimizer from using index seeks and typically produces execution plans with full table scans, making it a significant performance anti-pattern in T-SQL.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Why are conditional JOINs slow in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>When the join predicate is inside a CASE expression, the SQL Server query optimizer cannot determine which index to use at compile time because the join column depends on a runtime condition. This forces a scan of the entire table or index instead of an efficient seek operation. With large data sets, this difference can be orders of magnitude &#8211; what should take milliseconds with a seek can take seconds or minutes with a scan.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. What should I use instead of a conditional JOIN in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Three proven alternatives, in order of typical performance: (1) UNION ALL &#8211; write separate queries for each join condition and combine results; this gives the optimizer clean predicates for each branch. (2) CROSS APPLY with a subquery that handles the conditional logic. (3) LEFT OUTER JOIN to all possible tables with CASE in the SELECT clause to pick the correct value. Each approach lets the optimizer use index seeks instead of scans.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Learn why conditional JOINs using CASE in the ON clause cause poor SQL Server performance. Compare alternatives: LEFT OUTER JOIN with CASE, UNION ALL, and CROSS APPLY approaches with benchmarks.&hellip;<\/p>\n","protected":false},"author":221942,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143529],"tags":[4168,4150,5842,4183],"coauthors":[6800],"class_list":["post-2077","post","type-post","status-publish","format-standard","hentry","category-performance-sql-server","tag-database","tag-sql","tag-sql-monitor","tag-t-sql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/2077","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221942"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=2077"}],"version-history":[{"count":10,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/2077\/revisions"}],"predecessor-version":[{"id":109368,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/2077\/revisions\/109368"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=2077"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=2077"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=2077"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=2077"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}