Login or Sign Up to become a member!
LessThanDot Sit Logo

LessThanDot

Data Management

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Lessthandot twitter Lessthandot Linkedin Lessthandot friendfeed Lessthandot facebook Lessthandot rss

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Your profile

    Search

    XML Feeds

    Google Ads

    « The Lazy DBA Series: Wizards!The Lazy DBA Series: High Availability »
    comments

    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

    1. CREATE TABLE Periods(PeriodQuarter INT,PeriodYear INT)
    2. INSERT Periods VALUES (1,2009)
    3. INSERT Periods VALUES (2,2009)
    4. INSERT Periods VALUES (3,2009)
    5. INSERT Periods VALUES (4,2009)
    6. INSERT Periods VALUES (1,2010)
    7. INSERT Periods VALUES (2,2010)
    8. INSERT Periods VALUES (3,2010)
    9. GO
    10. CREATE CLUSTERED INDEX ix_Periods on Periods(PeriodYear,PeriodQuarter)
    11. GO



    When we do this simple select query

    1. SELECT *
    2. FROM Periods



    We get the following table.

    PeriodQuarterPeriodYear
    12009
    22009
    32009
    42009
    12010
    22010
    32010



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

    PeriodQuarterPeriodYear
    12009
    22009
    32009



    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.

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




    Query 1
    Here is the first query:

    1. declare @startDate datetime
    2. declare @endDate datetime
    3.  
    4. select @startDate = '20090101',@endDate = '20090928'
    5.  
    6. SELECT *
    7. FROM Periods
    8. WHERE  DATEADD(qq,PeriodQuarter-1,DATEADD(yy,PeriodYear -1900,0))
    9. BETWEEN @startDate AND @endDate
    10. 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.

    1. declare @startDate datetime
    2. declare @endDate datetime
    3.  
    4. select @startDate = '20090101',@endDate = '20090928'
    5.  
    6. SELECT *
    7. FROM Periods
    8. WHERE  PeriodYear between YEAR(@startDate) and YEAR(@endDate)
    9. AND DATEADD(qq,PeriodQuarter-1,DATEADD(yy,PeriodYear -1900,0))
    10. BETWEEN @startDate AND @endDate
    11. 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

    1. SELECT *
    2. FROM Periods
    3. WHERE
    4.        PeriodYear BETWEEN YEAR(@startdate) AND YEAR(@enddate)
    5.        AND PeriodYear * 4 + PeriodQuarter
    6.                BETWEEN Year(@startdate) * 4 + DATEPART(Quarter, @startdate)
    7.                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

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

    Now, when we query the table

    1. SELECT *
    2. FROM Periods

    ... the data looks like this



    PeriodQuarterPeriodYearPeriodDate
    120092009-01-01 00:00:00.000
    220092009-01-01 00:00:00.000
    320092009-01-01 00:00:00.000
    420092009-01-01 00:00:00.000
    120102010-01-01 00:00:00.000
    220102010-01-01 00:00:00.000
    320102010-01-01 00:00:00.000



    Now the query is much simpler

    1. DECLARE @startDate DATETIME
    2. DECLARE @endDate DATETIME
    3.  
    4. SELECT @startDate = '20090101',@endDate = '20090928'
    5. SELECT *
    6. FROM Periods
    7. 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.

    1. CREATE INDEX ix_PeriodDate ON Periods(PeriodDate)
    2. GO

    Now, if we run the same query again.

    1. DECLARE @startDate DATETIME
    2. DECLARE @endDate DATETIME
    3.  
    4. SELECT @startDate = '20090101',@endDate = '20090928'
    5.  
    6. SELECT *
    7. FROM Periods
    8. 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

    About the Author

    User bio imageDenis has been working with SQL Server since version 6.5. Although he worked as an ASP/JSP/ColdFusion developer before the dot com bust, he has been working exclusively as a database developer/architect since 2002. In addition to English, Denis is also fluent in Croatian and Dutch, but he can curse in many other languages and dialects (just ask the SQL optimizer) He lives in Princeton, NJ with his wife and three kids.
    Social SitingsTwitterFacebookLinkedInHomePageFlickrLTD RSS Feed
    1328 views
    Instapaper

    2 comments

    Comment from: David Forck (thirster42) [Member]
    very interesting. another blog to hold on to as a resource!
    08/25/10 @ 07:11
    Comment from: Naomi Nosonovsky [Member]
    Naomi Nosonovsky Nice - very popular question
    08/25/10 @ 08:20

    Leave a comment


    Your email address will not be revealed on this site.

    Your URL will be displayed.
    (Line breaks become <br />)
    (Name, email & website)
    (Allow users to contact you through a message form (your email will not be revealed.)