Summary ==== After implementing some of the suggestions in answers provided, the whole SP appears to execute in 0-1 second, regardless of parameter values used. Much thanks to all who helped. If this seems to take performance hit in the future (or in binding the results of this to another table), I'll look into Rajesh's suggestion of storing a "conditional" value in a temp table. Unresolved issues ===== I'm not sure why it's using a clustered index scan rather than a seek for the following: WHERE CASE WHEN D.Stop IS NULL THEN NULL WHEN @Locations IS NULL THEN 1 -- full list, so binding to another list doesn't do us any good. WHEN Location IN (SELECT Location from #loclist) THEN 1 --does clustered index scan --alternate: EXISTS (SELECT 1 from (SELECT Location from #loclist as l where l.Location=D.Location) as ll) THEN 1 --does clustered index scan END is not null whereas, this does a seek WHERE D.Stop is not NULL AND EXISTS (SELECT 1 from (SELECT Location from #loclist as l where l.Location=D.Location) as ll). Additionally over the weekend, I was looking into whether fulltext indexing would be beneficial for my `LIKE x+'[_]%'` join, but I was unable to figure out what the default word splitters are (does `'_'` with language 1033 split apart word tokens? Or just true whitespace characters?) And I don't appear to have fulltext indexing installed (`SELECT * from sys.fulltext_languages` returns an empty result set, as does `EXEC sp_help_fulltext_system_components`). As I don't have install media I'd need to wait around for IT to reinstall SQL Server 2008 R2 to add fulltext capability, which may not even benefit me. **But, as I said, the whole mess takes 0-1 s to execute, so I'm satisfied for now.** Whole Stored Procedure ===== ALTER PROCEDURE [dbo].[dev_Tech@Locs2b] ( --CREATE or ALTER @Locations as nvarchar(MAX) = NULL -- = 'GG1,BenBr14,BenBr00,YB_ToeDrain_Base,SR_AbvTisdale_E1,GG5,Elephant' ,@rangeStart as DateTime = '1970-01-01' ,@rangeEnd as DateTime = '2099-12-31' ) AS BEGIN SET NOCOUNT ON; --otherwise concrete5 chokes for multi-table returns. CREATE TABLE #loclist ( Location nvarchar(50) PRIMARY KEY CLUSTERED ) IF @Locations is NULL INSERT INTO #loclist(Location) SELECT Location from Monitor_Locations-- order by Location ELSE INSERT INTO #loclist(Location) SELECT ML.Location FROM Monitor_Locations as ML join clr_splitString_delim(@Locations,',') as ss ON ML.Location=ss.substr OR ML.Location like ss.substr+'[_]%' -- ORDER BY ML.Location ; With subsitelist as ( SELECT ML.Location as intxt, CASE WHEN UPPER(t.substr) NOT IN ('RT','180RT','RT180','180','J','JST','JSAT','JSATS') THEN LEFT(ML.Location,MAX(t.sEnd)) -- or MAX(t.leng+t.pos?) --look at 1167 ELSE LEFT(ML.Location,COALESCE(MIN(t.sEnd-1),1)) END as baseTxt, case WHEN UPPER(t.substr) IN ('RT','180RT','RT180') THEN '_'+t.substr END as sRT, --ELSE NULL case WHEN UPPER(t.substr) IN ('180','180RT','RT180') THEN '_'+t.substr END as s180, case WHEN UPPER(t.substr) IN ('J','JST','JSAT','JSATS') THEN '_'+t.substr END as sJ from #loclist /*Monitor_Locations*/ as ML CROSS APPLY clr_splitString_delim(ML.Location, '_') as t group by ML.Location, t.substr ), deploys as ( SELECT D.Location, MIN(D.Start) as Start, MAX(D.[Stop]) as Stop FROM Deployments as D WHERE -- tSQL does not use traditional short-circiting in a WHERE clause with ANDs or ORs, so no guarantee that the join to the larger list won't happen when Stop is set. -- CASE is a way of getting around this. Unfortunately the execution plan is showing clustered index scans, rather than the optimal seeks for the CASEd version CASE WHEN D.Stop IS NULL THEN NULL WHEN @Locations IS NULL THEN 1 -- full list, so binding to another list doesn't do us any good. WHEN Location in (SELECT Location from #loclist) THEN 1 -- does clustered index SCAN --Alternate: EXISTS (SELECT 1 from (SELECT Location from #loclist as l where l.Location=D.Location) as ll) THEN 1 -- does clustered index SCAN END is NOT NULL -- D.Stop is NOT NULL AND EXISTS (SELECT 1 from (SELECT Location from #loclist as l where l.Location=D.Location) as ll) -- does clustered index SEEK GROUP BY D.Location -- CASE WHEN D.Stop IS NULL THEN 1 END --groups all terminating deployments together and seperates out the non-terminating deployment of that series. ), shortestBaseSiteName as ( SELECT sl.intxt, CASE WHEN MAX(COALESCE(sl.sRT,sl.s180,sl.sJ)) IS NOT NULL THEN MIN(sl.basetxt) ELSE sl.intxt END as baseName, MAX(sl.sRT) as sRT, MAX(sl.s180) as s180, MAX(sl.sJ) as sJ FROM subsitelist as sl GROUP BY sl.intxt ), longestSubSiteName as ( SELECT sbs.baseName, MAX(sbs.intxt) as longestSS, MAX(sbs.sRT) as sRT, MAX(sbs.s180) as s180, MAX(sbs.sJ) as sJ FROM shortestBaseSiteName as sbs GROUP by sbs.baseName ), baseNames as ( SELECT intxt, MAX(baseTxt) as baseName FROM subsitelist GROUP BY intxt, sRT, s180, sJ HAVING MIN(COALESCE(sRT,s180,sJ)) is NULL ), subSiteTally as ( SELECT intxt, MAX(sRT) as sRT, MAX(s180) as s180, MAX(sJ) as sJ FROM subsitelist GROUP BY intxt ), bigList as ( SELECT bn.baseName, MAX(sst.sRT) as sRT, MAX(sst.s180) as s180, MAX(sst.sJ) as sJ FROM subSiteTally as sst INNER JOIN baseNames as bn on bn.intxt=sst.intxt GROUP BY bn.baseName ), smat as ( SELECT baseName as Location, CASE WHEN baseName in (ML.Location) THEN baseName END as l69, baseName+s180 as l180, baseName+sJ as lJ, baseName+sRT as lRT69, baseName+sRT+s180 as lRT180, baseName+sRT+sJ as lRTJ FROM bigList as bl inner join --LEFT OUTER gives all site names, regardless if in the short list or not. RIGHT will return an all-null entry for "donkey" (which is not a site) #loclist as ML on bl.baseName=ML.Location ), depWithDets as ( SELECT smat.Location, CASE Dep.Location WHEN l69 THEN 1 END as d69, CASE Dep.Location WHEN l180 THEN 1 END as d180, CASE Dep.Location WHEN lJ then 1 END as dJ, CASE Dep.Location WHEN lRT69 THEN 1 END as dRT69, CASE Dep.Location WHEN lRT180 THEN 1 END as dRT180, CASE Dep.Location WHEN lRTJ THEN 1 END as dRTJ ,smat.l69 ,smat.l180 ,smat.lJ ,smat.lRT69 ,smat.lRT180 ,smat.lRTJ FROM smat INNER JOIN deploys as Dep ON (Dep.Location in (l69,l180,lJ,lRT69,lRT180,lRTJ)) WHERE (Dep.Start > @rangeStart AND Dep.Start < @rangeEnd) OR (Dep.Stop > @rangeStart AND Dep.Stop < @rangeEnd) OR (Dep.Start < @rangeStart AND Dep.Stop > @rangeEnd) ) SELECT Location, count(d69) as bool_auton_69, count(d180) as bool_auton_180, count(dJ) as bool_auton_JSATS, count(dRT69) as bool_rtime_69, count(dRT180) as bool_rtime_180, count(dRTJ) as bool_rtime_JSATS ,min(l69) as name_auton_69 ,min(l180) as name_auton_180 ,min(lJ) as name_auton_JSATS ,min(lRT69) as name_rtime_69 ,min(lRT180) as name_rtime_180 ,min(lRTJ) as name_rtime_JSATS from depWithDets group by Location Drop table #loclist END Execution plan gif ==== Final plan [!["Final" execution plan][1]][1] C# CLR function ==== As I've never before programmed in C#, I figured it would be a good idea to document my code modifications to a CLR that was linked in the comments here. clr_splitString_delim (for .NET Framework 3.5) based on Adam Machanic's [SQLCLR String Splitter][2]: using System; using System.Collections; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; /* Code adapted from http://sqlblog.com/blogs/adam_machanic/archive/2009/04/28/sqlclr-string-splitting-part-2-even-faster-even-more-scalable.aspx (author: Adam Machanic) alterations to the I/O of Adam's function: 1) change returned field "Item" to "substr" 2) add returned field "sIndex" * which position in the list is this substring. 0 indexed 3) add returned field "sStart" * At which character position the substring starts with respect to the input string 4) add returned field "sEnd" * At which character position the substring ends */ public partial class UserDefinedFunctions { public class SplitStringTable : object { public String item; public int index; public int start; // public int length; public int end; public SplitStringTable() { item = ""; index = 0; start = 0; end = 0; } public SplitStringTable(String i, int idx, int stp, int nd) { item = i; index = idx; start = stp; end = nd; } public override string ToString() { return item.ToString(); } } private static SplitStringTable fill_result(String obj, int sp, int l) { return (new SplitStringTable { item = obj.ToString(), start = sp, end = l }); } [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName = "FillRow_Multi", TableDefinition = "substr nvarchar(4000),sIndex int,sStart int,sEnd int", IsDeterministic =true ) ] public static SplitStringMulti SplitString_Multi( [SqlFacet(MaxSize = -1)] SqlChars Input, [SqlFacet(MaxSize = 255)] SqlChars Delimiter ) { SplitStringMulti ssm = (Input.IsNull || Delimiter.IsNull) ? new SplitStringMulti(new char[0], new char[0]) : new SplitStringMulti(Input.Value, Delimiter.Value); return (ssm); } public static void FillRow_Multi(object obj, out SqlString substr, out SqlInt32 sIndex, out SqlInt32 sStart, out SqlInt32 sEnd) { SplitStringTable res = (SplitStringTable)obj; substr = new SqlString(obj.ToString()); sIndex = res.index; sStart = res.start; sEnd = res.end; } public class SplitStringMulti : IEnumerator { private static SplitStringTable fill_result(object obj, int idx, int sp, int l) { return (new SplitStringTable { item = obj.ToString(), index = idx, start = sp, end = l }); } public SplitStringMulti(char[] TheString, char[] Delimiter) { theString = TheString; stringLen = TheString.Length; delimiter = Delimiter; delimiterLen = (byte)(Delimiter.Length); isSingleCharDelim = (delimiterLen == 1); lastPos = 0; nextPos = delimiterLen * -1; delimOccur = 0; //leng = nextPos - lastPos; } #region IEnumerator Members public object Current { get { var item = new string(theString, lastPos, nextPos - lastPos); var res = fill_result(item, delimOccur-1, lastPos, nextPos); return res; // return new string(theString, lastPos, nextPos - lastPos); } } public override String ToString() { return new string(theString, lastPos, nextPos - lastPos); } public bool MoveNext() { if (nextPos >= stringLen) return false; else { lastPos = nextPos + delimiterLen; for (int i = lastPos; i < stringLen; i++) { bool matches = true; //Optimize for single-character delimiters if (isSingleCharDelim) { if (theString[i] != delimiter[0]) matches = false; } else { for (byte j = 0; j < delimiterLen; j++) { if (((i + j) >= stringLen) || (theString[i + j] != delimiter[j])) { matches = false; break; } } } if (matches) { delimOccur++; nextPos = i; //Deal with consecutive delimiters if ((nextPos - lastPos) > 0) return true; else { i += (delimiterLen - 1); lastPos += delimiterLen; } } } delimOccur++; lastPos = nextPos + delimiterLen; nextPos = stringLen; if ((nextPos - lastPos) > 0) return true; else return false; } } public void Reset() { lastPos = 0; delimOccur = 0; nextPos = delimiterLen * -1; } #endregion public int lastPos; public int nextPos; public int delimOccur; private readonly char[] theString; private readonly char[] delimiter; private readonly int stringLen; private readonly byte delimiterLen; private readonly bool isSingleCharDelim; } }; Assembly of CLR into database, once the DLL has already been created in VisStudio: DROP FUNCTION dbo.clr_splitString_delim go DROP ASSEMBLY CLRUtilities GO CREATE ASSEMBLY CLRUtilities FROM 'c:\DLLs\CLRUtilities.dll' WITH PERMISSION_SET = SAFE; GO CREATE FUNCTION dbo.clr_splitString_delim ( @List NVARCHAR(MAX), @Delimiter NVARCHAR(255) ) RETURNS TABLE ( substr NVARCHAR(4000), sIndex int, sStart int, sEnd int ) EXTERNAL NAME CLRUtilities.UserDefinedFunctions.SplitString_Multi; GO [1]: https://i.sstatic.net/KNvWa.gif [2]: http://sqlblog.com/blogs/adam_machanic/archive/2009/04/28/sqlclr-string-splitting-part-2-even-faster-even-more-scalable.aspx