SQLServerCentral Article

Bones of SQL - Practical Calendar Queries

,

In a previous article, we created a calendar table for a large number of dates using a tally table. In addition to the dates, we added a number of fields that flagged specific attributes for each date. In this article, we will examine some practical queries that make use of the various flags in our calendar table.

You can read more about tally tables in The "Numbers" or "Tally" Table: What it is and how it replaces a loop

Choosing the Attributes

In our first article, we chose to include these columns as attributes of the dates in the Calendar table:

  • calendar day (DayNo)
  • month number (MonthNo)
  • year (YearNo)
  • day of the week (DayofWeek)
  • last day of the month (EndOfMonth)
  • whether the day is a weekend (Weekend)
  • whether the day is a holiday (Holiday)
  • week of the year (WeekNoAlt)
  • the ordinal position (1st,2nd,3rd,4th) of a given DayofWeek  (DoWAsc)
  • flag for last DayofWeek for the month (DoWInMonth)

Each of these columns has some value. For example, we may use the flag for weekends for quickly determining shipping or receiving times. Some delivery companies do not deliver on weekends, so we would want the next date after some date flagged as a weekend.

If you are unsure of what the calendar table looks like, be sure to review the previous article.

Why not just use date functions?

This is a fair question. After all we used those same date functions to define the columns of the Calendar table. But consider this example. Back in 2014 the following query would have produced that date for Thanksgiving Day.

SELECT DATEADD( DAY, 3, DATEADD( WEEK, -1, DATEADD( MONTH, 11, DATEADD(yy, datediff(yy, 0, GETDATE()), 0))));

If any newcomers would like to see an explanation of all those nested functions, please ask in the discussion section of this article. By comparison, the query from a calendar table would be

SELECT CalendarDate as Thanksgiving
FROM dbo.Calendar
WHERE YearNo = 2014
  and LastDoWInMonth = 1          -- Last
  and DayOfWeek = 5               -- Thursday
  and MonthNo = 11                -- In November

Which is easier to read and understand? Which would be easier to create? And one more thing: which one works for all for all years? Unfortunately, the SELECT DATEADD query doesn’t work every year. So, both simplicity and reliability favor Calendar table queries.

Practical Queries

Let's look at some queries to answer real life questions clients or bosses might have. For example, imagine that you are looking at an employee's work schedule. Suppose we want to verify that the dates the employee worked are correct. We might ask the question:  How many workdays fall between June 27 and July 19th, 2016?

SELECT SUM(1) as Workdays
FROM dbo.Calendar
WHERE CalendarDate BETWEEN '6/27/2016' and '7/19/2016'
  AND WorkDay = 1

When we run this, we get 16 days. However, suppose we want to compare this to the data in the EmployeeSchedule table to determine if all the days an individual worked were actually marked as workdays. We could do this:

SELECT es.EmployeeID
     , es.DateWorked
     , c.Workday
     , c.Weekend
     , c.Holiday
FROM dbo.Calendar c
  INNER JOIN EmployeeSchedule es
    ON c.CalendarDate = es.DateWorked
WHERE CalendarDate BETWEEN '6/27/2016' and '7/19/2016'

This might be of use in finding simple mistakes in recordkeeping, or perhaps even fraud. If a business isn't open on weekends and holidays, we would not normally expect hours to be posted for those days. It might be possible for inventory or maintenance work to be scheduled, but this is information that should be brought to management’s attention. If valid, this could still be useful in calculating the employees’ pay if they get overtime for weekdays and holidays.

Scheduling Work

Perhaps we need to estimate whether or not we can complete a job by a given date. We've estimated that 9 workdays are going to be needed and the client says we can’t start until June 28th. The following query would list the dates we could work, omitting weekdays and holidays, and tell us that work could not be completed before Monday July 11th.

DECLARE @JobCosting TABLE (JobNo int, EstWorkdays int, StartDate date)
INSERT INTO @JobCosting 
    VALUES (1,9,'6/28/2016')
SELECT CalendarDate as Workday, DATENAME(DW,CalendarDate) as WorkDayName
FROM @JobCosting j
CROSS APPLY (SELECT Top(EstWorkDays)  CalendarDate
             FROM dbo.Calendar c
             WHERE CalendarDate >= StartDate
               and WorkDay = 1
             ORDER BY CalendarDate) C1

Perhaps we use a six-day workweek (including Saturdays but excluding Sundays and holidays), how many workdays fall between June 27 and July 19. We can easily find out like this.

SELECT CalendarDate as Workdays
FROM CALENDAR
WHERE CalendarDate BETWEEN '6/27/2016' and '7/19/2016'
  AND [DayofWeek] <> 1
  AND Holiday = 0

Suppose you work for Scrooge and Marley. Weekdays and holidays? Bah!  Humbug!  Count yourself fortunate, Bob Cratchit, that I don’t work you on the Sabbath. And no more coal for the stove!

SELECT SUM(1) as Workdays
FROM CALENDAR
WHERE CalendarDate BETWEEN '12/20/2016' and '1/5/2017'
  AND [DayofWeek] <> 1
  -- AND Holiday = 0

Or maybe we run a restaurant that closes on Sunday and Monday. How many days could we be open in February?

SELECT SUM(1) as Workdays
FROM CALENDAR
WHERE YearNo = 2016
  AND MonthNo = 2
  AND [DayofWeek] NOT IN (1,2)
  AND Holiday = 0

Finding All the Meetings

I'm sure most of you don't love meetings, but sometimes we need to schedule a few. Suppose we're scheduling meetings for the second half of 2016, and we want to print out a calendar. Which dates fall on every 2nd and 4th Wednesday?

  SELECT *
  FROM dbo.Calendar 
  WHERE YearNo = 2016
    and MonthNo > 6
    and [DayofWeek] = 4
    and DoWAsc in (2,4)
  ORDER BY CalendarDate

We changed our mind about when and how often to meet. Which dates fall on the last Friday of every month?

SELECT *
FROM dbo.Calendar 
WHERE YearNo = 2016
  and MonthNo > 6
  and [DayofWeek] = 6
  and LastDoWInMonth = 1 
ORDER BY CalendarDate

Total Hours

While we usually need some accurate record keeping system, we might want estimates for prediction or even some KPI dashboard. The #employeetime table has dates and hours worked. For each employee total up their hours per alternate week (7 day periods which always start on January 1st ).

IF OBJECT_ID(N'tempdb..#employeetime') is not null DROP TABLE #employeetime
CREATE TABLE  #employeetime (EmployeeID int, WorkDate date, Intime time, OutTime time) 
Insert into #employeetime
VALUES (1001, '1/3/2016', '8:00', '17:00'),
  (1001, '1/4/2016', '8:00', '17:00'),
  (1001, '1/5/2016', '8:00', '17:00'),
  (1001, '1/6/2016', '8:00', '17:00'),
  (1001, '1/7/2016', '8:00', '17:00'), -- end of 7 days starting January 1
  (1001, '1/8/2016', '8:00', '17:00'),
  (1001, '1/9/2016', '8:00', '17:00')
  
SELECT t.EmployeeID, c.WeekNoAlt
      ,SUM(DATEDIFF(hour,InTime,OutTime)-1) as TotalHours
FROM #employeetime t
JOIN dbo.Calendar c on t.WorkDate = c.CalendarDate
GROUP BY T.EmployeeID, WeekNoAlt
-- Or, even more detailed 
SELECT t.EmployeeID, c.WeekNoAlt
      ,SUM(DATEDIFF(hour,InTime,OutTime)-1) as TotalHours
      ,SUM(CASE WHEN Workday = 1 THEN DATEDIFF(hour,InTime,OutTime)-1
           ELSE 0 END
       ) as WorkdayHours
      ,SUM(CASE WHEN Weekend = 1 THEN DATEDIFF(hour,InTime,OutTime)-1
           ELSE 0 END
       ) as WeekendHours
     ,SUM(CASE WHEN Holiday = 1 THEN DATEDIFF(hour,InTime,OutTime)-1
           ELSE 0 END
       ) as HolidayHours
FROM #employeetime t
JOIN dbo.Calendar c on t.WorkDate = c.CalendarDate
GROUP BY T.EmployeeID, WeekNoAlt

Conclusion

While many of these queries can be duplicated with nested date functions, nesting functions may cause queries to be complex, hard to understand, and hard to maintain in T-SQL. As we have seen, queries against a calendar table are simpler to create, understand, and maintain than queries using nested date functions. They also make it much simpler to account for holidays, for which there is no date function. Joining to a calendar table can greatly simplify your coding. Finally, you or anyone else looking at the code in the future should easily be able to understand what dates the query is intended to produce.

Rate

4.59 (17)

You rated this post out of 5. Change rating

Share

Share

Rate

4.59 (17)

You rated this post out of 5. Change rating