patternsqlModerate
SARGable WHERE clause for two date columns
Viewed 0 times
columnswheresargabledatetwoforclause
Problem
I have what is, to me, an interesting question on SARGability. In this case, it's about using a predicate on the difference between two date columns. Here's the setup:
What I'll see pretty frequently, is something like this:
...which definitely isn't SARGable. It results in an index scan, reads all 1000 rows, no good. Estimated rows stink. You'd never put this in production.
It would be nice if we could materialize CTEs, because that would help us make this, well, more SARGable-er, technically speaking. But no, we get the same execution plan as up top.
And of course, since we are not using constants, this code changes nothing, and is not even half SARGable. No fun. Same execution plan.
If you're feeling lucky, and you're obeying all the ANSI SET options in your connection strings, you could add a computed co
USE [tempdb]
SET NOCOUNT ON
IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END
SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]
ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])What I'll see pretty frequently, is something like this:
/*definitely not sargable*/
SELECT
* ,
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
[#sargme] AS [s]
WHERE
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;...which definitely isn't SARGable. It results in an index scan, reads all 1000 rows, no good. Estimated rows stink. You'd never put this in production.
It would be nice if we could materialize CTEs, because that would help us make this, well, more SARGable-er, technically speaking. But no, we get the same execution plan as up top.
/*would be nice if it were sargable*/
WITH [x] AS ( SELECT
* ,
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
FROM
[#sargme] AS [s])
SELECT
*
FROM
[x]
WHERE
[x].[ddif] >= 48;And of course, since we are not using constants, this code changes nothing, and is not even half SARGable. No fun. Same execution plan.
/*not even half sargable*/
SELECT
* ,
DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
[#sargme] AS [s]
WHERE
[s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])If you're feeling lucky, and you're obeying all the ANSI SET options in your connection strings, you could add a computed co
Solution
Just adding this quickly so it exists as an answer (though I know it's not the answer you want).
An indexed computed column is usually the right solution for this type of problem.
It:
To be clear on that last point, the computed column is not required to be persisted in this case:
Now the query:
...gives the following trivial plan:
As Martin Smith said, if you have connections using the wrong set options, you could create a regular column and maintain the computed value using triggers.
All this only really matters (code challenge aside) if there's a real problem to solve, of course, as Aaron says in his answer.
This is fun to think about, but I don't know any way to achieve what you want reasonably given the constraints in the question. It seems like any optimal solution would require a new data structure of some type; the closest we have being the 'function index' approximation provided by an index on a non-persisted computed column as above.
An indexed computed column is usually the right solution for this type of problem.
It:
- makes the predicate an indexable expression
- allows automatic statistics to be created for better cardinality estimation
- does not need to take any space in the base table
To be clear on that last point, the computed column is not required to be persisted in this case:
-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);
-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);Now the query:
SELECT
S.ID,
S.DateCol1,
S.DateCol2,
DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
#sargme AS S
WHERE
DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;...gives the following trivial plan:
As Martin Smith said, if you have connections using the wrong set options, you could create a regular column and maintain the computed value using triggers.
All this only really matters (code challenge aside) if there's a real problem to solve, of course, as Aaron says in his answer.
This is fun to think about, but I don't know any way to achieve what you want reasonably given the constraints in the question. It seems like any optimal solution would require a new data structure of some type; the closest we have being the 'function index' approximation provided by an index on a non-persisted computed column as above.
Code Snippets
-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);
-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);SELECT
S.ID,
S.DateCol1,
S.DateCol2,
DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
#sargme AS S
WHERE
DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;Context
StackExchange Database Administrators Q#132437, answer score: 18
Revisions (0)
No revisions yet.