Technical Article

Calendar Tables III - Changing Business Rules

,

This article continues building on the calendar series. You can read Part I and Part II for background.

I recently had a client that wanted to change a date based business rule. They run the business based on weekly statistics. When I first worked for this client there was an Access database that one of the owners had created to keep track of various types of activities - calls by customers and prospects, appointments made, estimates and sales.

They use the concept of a week ending date (not uncommon in the business world) as the cutoff for certain statistics.

I converted the Access tables to SQL Server2005 and set up the front end to use a week based calendar table to determine in which week ending date the activity fell. The front end has now grown to a web based interface, but all tables and logic remain the same.

In this article I will be using a stripped down version of the actual data tables to show how easy it is to change date based business rules using a calendar table.

The client wanted to change their business week from Monday through Sunday to Sunday through Saturday. All reports and history had to remain the same, but going forward they wanted the week ending date to be Saturday rather than Sunday.

Here is a simplified version of their calendar table. I'm only using a 2 year table for this:

-- Create and populate the CalWeek table.
IF OBJECT_ID('CalWeek') IS NOT NULL
 DROP TABLE CalWeek
GO
CREATE TABLE CalWeek
( WeekID INT IDENTITY(1, 1)
, WkStart SMALLDATETIME
, WkEnd SMALLDATETIME
)
GO
ALTER TABLE CalWeek ADD CONSTRAINT PK_CalWeek
PRIMARY KEY CLUSTERED (WeekID) WITH (FILLFACTOR=100)
GO
-- Add records from the 1st week ending of 2010
-- to the last week of 2011
SET NOCOUNT ON;
DECLARE
 @WkStart SMALLDATETIME
, @WkEnd SMALLDATETIME
SET @WkStart = '2009-12-28' -- Last Monday of Dec 2009
SET @WkEnd = '2010-01-03'   -- First Sunday of Jan 2010
WHILE @WkStart < '2011-12-31'
 BEGIN
 INSERT INTO CalWeek
 ( WkStart, WkEnd )
 SELECT @WkStart, @WkEnd
 SET @WkStart = DATEADD(day, 7, @WkStart)
 SET @WkEnd = DATEADD(day, 7, @WkEnd)
 END
GO
CREATE UNIQUE INDEX CalWeek_StartEnd ON CalWeek (WkStart, WkEnd)
CREATE UNIQUE INDEX CalWeek_WeekEnd ON CalWeek (WkEnd)
GO

This is a stripped down version of the Activity table:

-- Create the Activity table.
IF OBJECT_ID('Activity') IS NOT NULL
 DROP TABLE Activity
GO
CREATE TABLE dbo.Activity
( ActivityID INT IDENTITY(1,1) NOT NULL
, ActivityDate DATETIME NULL
, WeekEndingDate DATETIME NULL
, ActivitySource INT NULL
, ActivityType INT NULL
, SalesmanID INT NULL
 ,SaleAmount MONEY NULL
)
GO
ALTER TABLE dbo.Activity ADD CONSTRAINT PK_Activity
PRIMARY KEY CLUSTERED (ActivityID)
GO
CREATE NONCLUSTERED INDEX Activity_ActivityDate ON Activity (ActivityDate)
CREATE NONCLUSTERED INDEX Activity_WeekEnding ON Activity (WeekEndingDate)
GO

Here are the related tables:

-- Create the related ActivitySource table.
IF OBJECT_ID('ActivitySource') IS NOT NULL
 DROP TABLE ActivitySource
GO
-- These are promo type sources that create calls from customers
-- and prospects.
CREATE TABLE ActivitySource
( SourceID INT PRIMARY KEY CLUSTERED
, SourceName VARCHAR(50)
)
GO
INSERT INTO ActivitySource
( SourceID, SourceName )
SELECT 0, 'Radio Add 1' UNION ALL
SELECT 1, 'Mailer 1' UNION ALL
SELECT 2, 'Web Promo 1' UNION ALL
SELECT 3, 'Billboard 1'
GO
-- Create the related ActivityTypes table.
IF OBJECT_ID('ActivityTypes') IS NOT NULL
 DROP TABLE ActivityTypes
GO
CREATE TABLE ActivityTypes
( AcivityTypeID INT PRIMARY KEY CLUSTERED
, ActivityType VARCHAR(20)
)
GO
INSERT INTO ActivityTypes
( AcivityTypeID, ActivityType )
SELECT 0, 'Call Received' UNION ALL
SELECT 1, 'Appt Made' UNION ALL
SELECT 2, 'Estimate' UNION ALL
SELECT 3, '2nd Estimate'
GO
-- Create the related SalesPersons table.
IF OBJECT_ID('SalesPersons') IS NOT NULL
  DROP TABLE SalesPersons
GO
CREATE TABLE SalesPersons
( SalesmanID    INT PRIMARY KEY CLUSTERED
, SalesPerson   VARCHAR(50)
)
GO
INSERT INTO SalesPersons
(SalesmanID, SalesPerson )
SELECT 0, 'Joe Blow' UNION ALL
SELECT 1, 'Jane Doe' UNION ALL
SELECT 2, 'John Smith' UNION ALL
SELECT 3, 'Mary Jones'
-- Initially populate the Activity table.
-- The dates are pure dates (no time) between 1 Jan 2010 and 15 Jan 2011.
-- The ActivitySource is a random number between 0 and 3
-- Same for ActivityType and SalesmanID
DECLARE
 @Lower INT
, @Upper INT
SET @Lower = 40177 -- 1 Jan 2010
SET @Upper = 40571 -- 30 Jan 2011
INSERT INTO Activity
( ActivityDate, ActivitySource, ActivityType, SalesmanID )
SELECT TOP 5000
  CONVERT(DATETIME, ROUND(((@Upper - @Lower -1) 
  * RAND(CHECKSUM(NEWID())) + @Lower), 0))
, ABS(CHECKSUM(NEWID())) & 3
, ABS(CHECKSUM(NEWID())) & 3
, ABS(CHECKSUM(NEWID())) & 3
FROM sys.syscolumns S1
CROSS JOIN sys.syscolumns S2

The table now has 5000 random activities with random activity types, random activity sources and random sales persons. When the records were initially created in Access or the web front end, the WeekendingDate was already populated by using a query like:

SELECT WkEnd FROM CalWeek WHERE {Today's Date} BETWEEN WkStart And WkEnd

Since we only have a test set here we have to set the WeekendingDate in the Activity table. Normally this was done when the Activity record was originally inserted using a query like the one above:

-- Update the Activity table with a Week Ending Date
UPDATE A
SET
 A.WeekEndingDate = CW.WkEnd
FROM Activity A
INNER JOIN CalWeek CW ON
 A.ActivityDate BETWEEN CW.WkStart And CW.WkEnd

Some activities involve sales. We'll fill in the applicable activities with a sales amount:

-- Create some sales for activities 2 and 3 (estimate and 2nd estimate)
UPDATE Activity
SET
 SaleAmount = ABS(CHECKSUM(NEWID())) & 1000
WHERE ActivityType IN (2, 3)

Now we have some test data. A simplified, but typical query on this data would look like:

-- The most effective Source of promotion in the last 4 weeks for Calls.
SELECT
 S.SourceName, COUNT(*) AS Calls
FROM
 (SELECT TOP 4 WkEnd FROM CalWeek
 WHERE WkEnd <= DATEADD(DAY, DATEDIFF(day, 0, GETDATE()), 0)
 ORDER BY WkEnd DESC
 ) AS Wks
INNER JOIN Activity A ON
 A.WeekEndingDate = Wks.WkEnd
INNER JOIN CalWeek CW ON
 A.WeekEndingDate = CW.WkEnd
INNER JOIN ActivitySource S ON
 A.ActivitySource = S.SourceID
WHERE
 A.ActivityType = 0 -- Call In
GROUP BY S.SourceName

This gives a result set that looks like:

Looks like Billboards and Radio add 1 produced the most calls.

The sales amount in the Activities table is used for commissions and such. A typical query to give the last 4 weeks of sales by sales person might look like this:

-- Last 4 weeks of sales by salesperson
-- Use a variable for the date to allow for testing
-- for date ranges in the past.
DECLARE
 @Wk SMALLDATETIME
SET @Wk = DATEADD(DAY, DATEDIFF(day, 0, GETDATE()), 0)
; WITH TheWeeks AS  -- Get the last 4 complete weeks.
( SELECT TOP 4 WeekID, WkEnd
 FROM CalWeek
 WHERE WkEnd <= @Wk
 ORDER BY WkEnd DESC
), WksNum AS        -- Number them for readability
( SELECT WkEnd, ROW_NUMBER() OVER (ORDER BY WkEnd) AS WkNum
 FROM TheWeeks
)
SELECT
 SP.SalesPerson, A.WeekEndingDate, WN.WkNum
, COUNT(*) AS Appointments
, SUM(A.SaleAmount) AS Sales
FROM
Activity A
INNER JOIN WksNum WN ON
 A.WeekEndingDate = WN.WkEnd
INNER JOIN ActivityTypes AT ON
 A.ActivityType = AT.AcivityTypeID
INNER JOIN SalesPersons SP ON
 A.SalesmanID = SP.SalesmanID
WHERE
 A.ActivityType IN (2, 3)
GROUP BY SP.SalesPerson, A.WeekEndingDate, WN.WkNum
ORDER BY WeekEndingDate, SalesPerson

And the result set would look like:

Now comes the part where the client wants to change business rules. Instead of Monday through Sunday, they now want their business week to be Sunday through Saturday starting with week ending 16 Jan 2011. This means week ending date 16 Jan 2011 needs to change to 15 Jan 2011 (making it a short week) and all subsequent weeks need to be adjusted.

Using the calendar table technique I did this in 3 queries:

-- WkEnd 2011-01-16 (Sunday) becomes 2011-01-15 (Saturday)
UPDATE CalWeek
SET WkEnd = '2011-01-15'
WHERE WkEnd = '2011-01-16'
-- Everything else gets moved back 1 day.
UPDATE CalWeek
SET WkStart = DATEADD(day, -1, WkStart)
, WkEnd = DATEADD(day, -1, WkEnd)
WHERE WkStart >= '2011-01-17'
-- Change the Week Ending date in Activity
-- to match the calendar table.  This handles anything already entered
-- with the old Weekending Date for the current week.
UPDATE A
SET
 A.WeekEndingDate = CW.WkEnd
FROM Activity A
INNER JOIN CalWeek CW ON
 A.ActivityDate BETWEEN CW.WkStart And CW.WkEnd
WHERE A.WeekEndingDate <> CW.WkEnd

After running the above update we now run the sales query from above. The new result set looks like:

Notice the change in week ending date for week #2 and subsequent weeks. All numbers have been adjusted to the new week ending date and no history is changed since all report queries use the calendar table to determine the week to which the activity belongs.

Since I insisted that NO week ending calculations were to be done in any front end (Access or Web), all new activity records will have the correct week ending date.

I hope this can be us use to someone.

Todd Fifield

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating