Someone posted a question, they wanted to return the quarters and years within a range that were passed in. The problem they had is that they stored this data in a year and a quarter column. The table looked like this

CREATE TABLE Periods(PeriodQuarter INT,PeriodYear INT)
INSERT Periods VALUES (1,2009)
INSERT Periods VALUES (2,2009)
INSERT Periods VALUES (3,2009)
INSERT Periods VALUES (4,2009)
INSERT Periods VALUES (1,2010)
INSERT Periods VALUES (2,2010)
INSERT Periods VALUES (3,2010)
GO
CREATE CLUSTERED INDEX ix_Periods on Periods(PeriodYear,PeriodQuarter)
GO

When we do this simple select query

SELECT *
FROM Periods

We get the following table.

PeriodQuarter PeriodYear
1 2009
2 2009
3 2009
4 2009
1 2010
2 2010
3 2010

If we pass in a range from 2009-01-01 until 2009-09-28, then the following data should be returned:

PeriodQuarter PeriodYear
1 2009
2 2009
3 2009

Running the queries

There are a couple of ways to return that data – below are 3 queries and their execution plans

The first query creates a date from the 2 columns and then checks if that date falls between the end and start date passed in.

Run the statement below, it returns ‘2009-01-01 00:00:00.000’. Mess around with the numbers to to see how it works.

SELECT DATEADD(qq,1-1,DATEADD(yy,2009 -1900,0))

Query 1

Here is the first query:

declare @startDate datetime
declare @endDate datetime

select @startDate = '20090101',@endDate = '20090928'

SELECT *
FROM Periods
WHERE  DATEADD(qq,PeriodQuarter-1,DATEADD(yy,PeriodYear -1900,0)) 
BETWEEN @startDate AND @endDate
GO

This is the execution plan, as you can see it uses a Clustered Index Scan

|–Clustered Index Scan(OBJECT:([msdb].[dbo].[Periods].[ix_Periods]),

WHERE:(dateadd(quarter,[msdb].[dbo].[Periods].[PeriodQuarter]-(1),

dateadd(year,[msdb].[dbo].[Periods].[PeriodYear]-(1900),‘1900-01-01 00:00:00.000’))>=[@startDate]

AND dateadd(quarter,[msdb].[dbo].[Periods].[PeriodQuarter]-(1),

dateadd(year,[msdb].[dbo].[Periods].[PeriodYear]-(1900),‘1900-01-01 00:00:00.000’))<=[@endDate]))

Query 2

Query 2 is a little smarter, it checks for the year which is the first key in the composite clustered index and thus avoids a Clustered Index Scan like the query above.

declare @startDate datetime
declare @endDate datetime

select @startDate = '20090101',@endDate = '20090928'

SELECT *
FROM Periods
WHERE  PeriodYear between YEAR(@startDate) and YEAR(@endDate)
AND DATEADD(qq,PeriodQuarter-1,DATEADD(yy,PeriodYear -1900,0)) 
BETWEEN @startDate AND @endDate
GO

Here is the plan, as you can see it results in a Clustered Index Seek.

|–Clustered Index Seek(OBJECT:([msdb].[dbo].[Periods].[ix_Periods]),

SEEK:([msdb].[dbo].[Periods].[PeriodYear] >= datepart(year,[@startDate])

AND [msdb].[dbo].[Periods].[PeriodYear] <= datepart(year,[@endDate])), WHERE:(dateadd(quarter,[msdb].[dbo].[Periods].[PeriodQuarter]-(1), dateadd(year,[msdb].[dbo].[Periods].[PeriodYear]-(1900),‘1900-01-01 00:00:00.000’))>=[@startDate]

AND dateadd(quarter,[msdb].[dbo].[Periods].[PeriodQuarter]-(1),

dateadd(year,[msdb].[dbo].[Periods].[PeriodYear]-(1900),‘1900-01-01 00:00:00.000’))<=[@endDate]) ORDERED FORWARD)

Query 3

Query 3 is very similar to query 2 but instead of dateadd it uses arithmetic to grab the correct rows

SELECT *
FROM Periods
WHERE
       PeriodYear BETWEEN YEAR(@startdate) AND YEAR(@enddate)
       AND PeriodYear * 4 + PeriodQuarter
               BETWEEN Year(@startdate) * 4 + DATEPART(Quarter, @startdate)
               AND Year(@enddate) * 4 + DATEPART(Quarter, @enddate)

Here is the plan, as you can see it results in a Clustered Index Seek also.

|–Clustered Index Seek(OBJECT:([msdb].[dbo].[Periods].[ix_Periods]),

SEEK:([msdb].[dbo].[Periods].[PeriodYear] >= datepart(year,[@startDate])

AND [msdb].[dbo].[Periods].[PeriodYear] <= datepart(year,[@endDate])), WHERE:(([msdb].[dbo].[Periods].[PeriodYear]*(4)+[msdb].[dbo].[Periods].[PeriodQuarter]) >=datepart(year,[@startDate])*(4)+datepart(quarter,[@startDate])

AND ([msdb].[dbo].[Periods].[PeriodYear]*(4)+[msdb].[dbo].[Periods].[PeriodQuarter])

<=datepart(year,[@enddate])*(4)+datepart(quarter,[@endDate])) ORDERED FORWARD)

Another approach

Instead of converting and doing arithmetic, you could add a computed column to the table

ALTER TABLE Periods ADD PeriodDate AS DATEADD(qq,PeriodQuarter-1,DATEADD(yy,PeriodYear -1900,0))  
GO

Now, when we query the table

SELECT *
FROM Periods

… the data looks like this

PeriodQuarter PeriodYear PeriodDate
1 2009 2009-01-01 00:00:00.000
2 2009 2009-01-01 00:00:00.000
3 2009 2009-01-01 00:00:00.000
4 2009 2009-01-01 00:00:00.000
1 2010 2010-01-01 00:00:00.000
2 2010 2010-01-01 00:00:00.000
3 2010 2010-01-01 00:00:00.000

Now the query is much simpler

DECLARE @startDate DATETIME
DECLARE @endDate DATETIME

SELECT @startDate = '20090101',@endDate = '20090928'
SELECT *
FROM Periods
WHERE PeriodDate BETWEEN @startDate AND @endDate

|–Clustered Index Scan(OBJECT:([msdb].[dbo].[Periods].[ix_Periods]), WHERE:(dateadd(quarter,[msdb].[dbo].[Periods].[PeriodQuarter]-(1),dateadd(year,[msdb].[dbo].[Periods].[PeriodYear]-(1900),‘1900-01-01 00:00:00.000’))>=[@startDate] AND dateadd(quarter,[msdb].[dbo].[Periods].[PeriodQuarter]-(1),dateadd(year,[msdb].[dbo].[Periods].[PeriodYear]-(1900),‘1900-01-01 00:00:00.000’))<=[@endDate]))

So we still have an index scan, but if we create an index on the computed column now, we can find out if that helps.

CREATE INDEX ix_PeriodDate ON Periods(PeriodDate)
GO

Now, if we run the same query again.

DECLARE @startDate DATETIME
DECLARE @endDate DATETIME

SELECT @startDate = '20090101',@endDate = '20090928'

SELECT *
FROM Periods
WHERE PeriodDate BETWEEN @startDate AND @endDate

|–Index Seek(OBJECT:([msdb].[dbo].[Periods].[ix_PeriodDate]), SEEK:([msdb].[dbo].[Periods].[PeriodDate] >= [@startDate] AND [msdb].[dbo].[Periods].[PeriodDate] <= [@endDate]) ORDERED FORWARD)

And there is your index seek.

Just as an FYI, don’t just start creating computed columns and adding indexes left and right. If you can, try to modify the table so that you store the dates only; after that it is easy with the datepart function to figure out what year, quarter or month is stored in that column.

To keep this post to a reasonable length, I decided to create another post that will show you how to use a calendar table to the same kind of query. That post will be up in a day or two.

*** Remember, if you have a SQL related question, try our Microsoft SQL Server Programming forum or our Microsoft SQL Server Admin forum