Sunday, August 7, 2011

Setup Date Dimension

You cannot think of business intelligence without date dimension and it is so much so that all our measures mostly spins around date dimension for date/time slicing, YTD, MTD, Month to Month comparison and goes on. In my current assignment I had to build date dimension and that’s when I thought of checking with the BI core group in my company. I got plenty of replies from the group. However I was looking for something which is very easy to understand and implement and thinking so I built one date dimension. Note that in my post I have not kept attributes for fiscal year but if you need to, you would merely have to add bunch of attributes to store fiscal month, quarter etc.

I am straight away jumping into the script. We need 4 scripts as explained below -

  1. Date dimension table. Name of the table is [OS_Date_Dimension].
  2. Stored procedure to populate date dimension table.
  3. Script to populate your fact with UNKNOWN time member if lookup does not find one key in the date dimension.
  4. Call stored procedure to fill date table.

Date dimension table. Name of the table is [OS_Date_Dimension]

CREATE TABLE [dbo].[Os_Dim_Date](
--Keys
[DateKey] [int] PRIMARY KEY CLUSTERED NOT NULL, --20110101
[AlternateDateKey] [nvarchar](10) NOT NULL, --2001-07-01
[CalenderDate] [datetime] NOT NULL, --2000-01-01 00:00:00.000

--Day
[DayOfWeek] [nvarchar](9) NOT NULL, --Sunday
[ShortDayOfWeek] [nvarchar](3) NOT NULL, --Sun
[DayNumOfYear] [int] NOT NULL, --1, 2, 3..365
[DayNumOfQuarter] [int] NOT NULL, --1, 2, 3..90
[DayNumOfMonth] [int] NOT NULL, --1, 2, 3..31
[DayNumOfWeek] [int] NOT NULL, --1, 2, 3..7

--Week
[WeekName] [nvarchar](20) NOT NULL, --2000-Week-1
[WeekNumOfYear] [int] NOT NULL, --1, 2, 3..49
[WeekNumOfQuarter] [int] NOT NULL, --1, 2, 3..14
[WeekNumOfMonth] [int] NOT NULL, --1, 2, 3..6

--Month
[MonthName] [nvarchar](20) NOT NULL, --January 2001
[MonthShortName] [nvarchar](6) NOT NULL, --Jan
[MonthNumOfYear] [int] NOT NULL, --1, 2..12
[MonthNumOfQuarter] [int] NOT NULL, --1, 2 and 3

--Quarter
[QuarterName] [nvarchar](10) NOT NULL, --2000 Q1
[QuarterShortName] [nvarchar](5) NOT NULL, --Q1
[Quarter] [int] NOT NULL, --1,2,3 and 4

--Year
[YearName] [nvarchar](7) NOT NULL, --2011
[Year] [int] NOT NULL, --2011

--Additional
[SemiMonth] [nvarchar](50) NULL, --SemiMonthP1 January 2011
[Semester] [int] NOT NULL, --1 and 2
[SemesterName] [nvarchar](11) NOT NULL --H1 CY 2001
)
GO


Stored procedure to populate date dimension table


--Stored procedure to create date dimension table
CREATE PROCEDURE [dbo].[Os_Populate_Date_Dim]
(
@FIRST_DATE DATETIME,
@LAST_DATE DATETIME
)
AS
BEGIN
WITH TE_Os_Dim_Date as
(
SELECT Cast (@FIRST_DATE as DateTime) Date --Start Date
UNION ALL
SELECT Date + 1 FROM TE_Os_Dim_Date
WHERE Date + 1 < = @LAST_DATE --End date
)
INSERT INTO [dbo].[Os_Dim_Date] (
[DateKey]
,[AlternateDateKey]
,[CalenderDate]
,[DayOfWeek]
,[ShortDayOfWeek]
,[DayNumOfYear]
,[DayNumOfQuarter]
,[DayNumOfMonth]
,[DayNumOfWeek]
,[WeekName]
,[WeekNumOfYear]
,[WeekNumOfQuarter]
,[WeekNumOfMonth]
,[MonthName]
,[MonthShortName]
,[MonthNumOfYear]
,[MonthNumOfQuarter]
,[QuarterName]
,[QuarterShortName]
,[Quarter]
,[YearName]
,[Year]
,[SemiMonth]
,[Semester]
,[SemesterName]
)
SELECT
CAST(CONVERT(NVARCHAR(30),DATE, 112) AS INT) AS DateKey
,LEFT(CAST(CONVERT(NVARCHAR(30),DATE, 120) AS VARCHAR), 10) AS AlternateDateKey
,DATE AS CalenderDate

,DateName (dw, date) as DayOfWeek
,LEFT (DateName (dw, date), 3) as ShortDayOfWeek
,DATEPART (DY, DATE) AS DayNumOfYear
,DATEDIFF(DD,DATEADD(QQ,DATEDIFF(QQ,0,DATE),0),DATE)+1 AS DayNumOfQuarter
,DAY (DATE) AS DayNumOfMonth
,DATEPART (DW, DATE) AS DayNumOfWeek

,'Week ' + CAST(DatePart (wk, Date) AS VARCHAR) + ' CY ' + convert(varchar(4),YEAR(Date)) AS WeekName
,DatePart (wk, Date) as WeekNumOfYear
,datediff(wk,dateadd(qq,datediff(qq,0,date),0),date)+1 as WeekNumOfQuarter
,datediff(wk,dateadd(mm,datediff(mm,0,date),0),date)+1 as WeekNumOfMonth

,DateName (mm, date) + ' ' + datename(year,Date) as MonthName
,LEFT ( DateName (mm, date), 3) MonthShortName
,MONTH (date) as MonthNumOfYear
,DateDiff(mm,DateAdd(qq,DateDiff(qq,0,date),0),date)+1 as MonthNumOfQuarter

,'Q' + datename(quarter,Date) + ' CY ' + datename(year,Date) AS QuarterName
,'Q'+ datename(quarter,Date) AS QuarterShortName
,DatePart ( qq, date) as Quarter

,datename(year,Date) AS YearName
,YEAR (date) as Year

,'SemiMonthP' + CASE WHEN Day(Date) <= 15 then '1' ELSE '2' END + ' ' + DateName (mm, date) + ' ' + datename(year,Date) AS SemiMonth
,CASE WHEN MONTH(Date) <= 6 then 1 ELSE 2 END AS Semester
,'H' + CAST(CASE WHEN MONTH(Date) <= 6 then 1 ELSE 2 END AS VARCHAR) + ' CY ' + datename(year,Date) AS [SemesterName]
FROM TE_Os_Dim_Date

OPTION (MAXRECURSION 0)
END
GO


Script to populate your fact with UNKNOWN time member if lookup does not find one key in the date dimension


--Insert a record for unknown lookup
INSERT INTO [dbo].[Os_Dim_Date] (
[DateKey]
,[AlternateDateKey]
,[CalenderDate]
,[DayOfWeek]
,[ShortDayOfWeek]
,[DayNumOfYear]
,[DayNumOfQuarter]
,[DayNumOfMonth]
,[DayNumOfWeek]
,[WeekName]
,[WeekNumOfYear]
,[WeekNumOfQuarter]
,[WeekNumOfMonth]
,[MonthName]
,[MonthShortName]
,[MonthNumOfYear]
,[MonthNumOfQuarter]
,[QuarterName]
,[QuarterShortName]
,[Quarter]
,[YearName]
,[Year]
,[SemiMonth]
,[Semester]
,[SemesterName]
)
VALUES
(
19000101
,'1900-01-01'
,'1900-01-01 00:00:00'
,'Unknown' ,'Unk', 1, 1 ,1 ,1
,'Unknown', 1 ,1 ,1
,'Unknown', 'Unk' ,1 ,1
,'Unkown', 'Q1', 1
,'1900', 1900
,'Unknown', 1
,'Unknown'
)
GO


Call stored procedure to fill date table


--Populate Date from 2001 to 2020
EXEC [dbo].[Os_Populate_Date_Dim] '2001-01-01', '2020-12-31'
GO


Now let’s setup dimension attribute key, name and hierarchies in the cube. Your Dimension structure (Hierarchies) and attributes relationship should look like this at the end of the exercise.


image


image


Set attribute key and names as illustrated below -




























Attribute Name


KeyColumns Property


NameColumn Property

DateKeyOs_Dim_Date.DateKey (Integer)Os_Dim_Date.CalenderDate (WChar)
ValueColumn: Os_Dim_Date.CalenderDate (WChar)
Month NameOs_Dim_Date.Year (Integer)
Os_Dim_Date.MonthNumOfYear (Integer)
Os_Dim_Date.MonthName (WChar)
QuarterOs_Dim_Date.Year (Integer)
Os_Dim_Date.Quarter (Integer)
Os_Dim_Date.QuarterName (WChar)
YearOs_Dim_Date.Year (Integer)Os_Dim_Date.YearName (WChar)
Week NameOs_Dim_Date.MonthName (WChar)
Os_Dim_Date.WeekNumOfMonth (Integer)
Os_Dim_Date.WeekName (WChar)

Next process dimension and browse Date and Week Hier. Find below the illustrations.


image


image


You may need more attributes related to fiscal year, holidays etc. which you should be able to add easily by extending table and stored procedure. Hope this helps.

Thursday, June 16, 2011

Reports using Cube or Relational Database..some points to consider

We should try and use cube and not relational database for building SSRS reports. Here are the some of the advantages that cube has over relational database –

  1. Cube is much faster source for canned and ad-hoc reporting. Reason, aggregated and processed data is stored in cube database (of course if implemented using MOLAP).
  2. Information or the measures are organized in a way which is easy to build excel pivot table which business user can easily understand.
  3. It is very easy to implement hierarchy and drill down using cube and MDX compared with Relational database and SQL queries.
  4. With cube we can use power of MDX to build complex reports, like YTD, Running total, Semi additive measure, trend, parallel period etc.
  5. Calculated measures can be stored in the cube and can be used by developer to build report or business user browsing them directly using any client application or excel. Other hand we never store calculated column in the relational database table.
  6. Even ad-hoc reporting becomes handy and easy with cube as some of the client software like ProClarity only work with cube.
  7. Using cube we can easily get different view of a measure using linked dimensions. Say spend by vendor, location etc.
  8. However not all the report should be build using the cube. For example, detail reports where we want to view detail data as they were pulled from the source system, we should use SQL report. In this case we are not making use of aggregated data from the cube and SQL query can returns the detail rows much master than MDX.

Monday, June 6, 2011

SSAS NULL value default behavior

One of my direct reports asked me last week about how SSAS handles NULL measure if present in the fact table. I think he encountered this in one of his business intelligence job interview :).

I thought let’s write a small post about the topic and this way others will also be get benefited plus I don’t have to explain it to him separately.

I have a sales fact table which is linked to product, measure type, organization and time dimensions. Here is part of the fact table -
image 
As you see we have got plenty of NULL value under plan and forecast measures. We will stick to Actual and Plan measures for our discussion. Let’s browse the cube and put this in perspective for you to see how SSAS treat NULL measure value. Any guess at this point?
image 
I have filter products with PRODUCT_SK in (48, 55 and 56) where we have NULL under plan but has got some Actual sales value. This is bit strange with SSAS which replaces NULL with 0 by default, meaning does not preserve NULL. However, this may mislead the business and you may want to preserve NULL for all intense purposes. In this case it is just that business has not decided on the plan, forecast etc for certain products does not necessarily mean that plan amount is 0.

Lets get straight to the SSAS measure properties and there is a way to preserver NULL value. Go to the cube solution, select you measure and change NullProcessing property under Source from Automatic to Preserve. Next process the cube and reconnect to OLAP db to refresh.
image 
Here is what I mean. We don’t see 0 anymore. You should carefully decide whether or not you want to keep NULL and make use of this property.

image

Please leave any feedback or suggestions you have!

NON EMPTY vs NONEMPTY(<set>, <set>)

To remove NULL values from the query output we use NON EMPTY or NONEMPTY functions. There is small difference between NON EMPTY and NONEMPTY function, I am providing difference with examples through this post.

Let’s run following MDX query -

SELECT 
{
[Measures].[INV PROD SALES ACTUAL],
[Measures].[LEAD TO CALL PRODUCT ACTUAL]} ON COLUMNS,
{
[MEASURE TYPE].[MEASURE TYPE].
Children
}
ON ROWS
FROM
[YOUR CUBE]

Result of the above query. As you see there are plenty of NULL values we got for the MEASURE TYPE set.
image


Now let’s use NON EMPTY on the set, here is the changed query -





SELECT 
{
[Measures].[INV PROD SALES ACTUAL],
[Measures].[LEAD TO CALL PRODUCT ACTUAL]} ON COLUMNS,
NON EMPTY
{
[MEASURE TYPE].[MEASURE TYPE].
Children
}
ON ROWS
FROM
[YOUR CUBE]

Here is the output with NON EMPTY -
image



As you see now wherever we had NULL for both the measure those measure types have been filtered (meaning removed). So NON EMPTY works on the overall result, meaning after set get first evaluated, all the tuples with NULL values for all the measures, are removed.


On the other hand NONEMPTY() takes 2 sets as parameters. Its uses tuples of the first set and filtered out all empty tuples based on the cross product with second set. Meaning behind the scene what happens is, NONEMPTY is evaluated when the set defining the rows axis is evaluated. Let’s analyze the query -





SELECT 
NON EMPTY
{
[Measures].[INV PROD SALES ACTUAL],
[Measures].[LEAD TO CALL PRODUCT ACTUAL]} ON COLUMNS,
{
NONEMPTY([MEASURE TYPE].[MEASURE TYPE].Children,
[Measures].[LEAD TO CALL PRODUCT ACTUAL])
}
ON ROWS
FROM
[YOUR CUBE]

Before we get into the result, let’s analyze the above query. As we discussed above NONEMPTY will be evaluated with LEAD TO CALL PRODUCT ACTUAL measure in the ROW axis first and will bring all the MEASURE TYPE tuples wherever we have value for LEAD TO CALL PRODUCT ACTUAL measure. Also note that if we don’t pass any measure in the NONEMPTY it takes default measure and filter non empty value for the set. From the first query output I am graying out the cells that we don’t expect in the query result.
image


Here is the above query output.
image 
NONEMPTY should be used to get rid of empty tuples in the set for specified measure and it helps improve query performance. Specially when you cross join the dimension(s) members you should always consider NONEMPTY. Leave your comments..see you back soon!

Friday, June 3, 2011

SCD Type – 2 using MERGE and Output $action

In this article we will talk about achieving SCD behavior using Sql MERGE statement. MERGE join works well based on join condition with source table and through available condition you can have INSERT, UPDATE or DELETE actions. Primarily what it does is that it compare key value of target with source table and perform different operations specified like WHEN MATCHED, WHEN NOT MATCHED etc.

The main focus of this post is to make use of $action virtual output column when we execute MERGE sql and decide as in whether INSERT or UPDATE has been performed on the source.

Let get to the business area and see these in action.

Create a table first and populate some records, or you can use the same table from my previous post.
http://takeatourtobi.blogspot.com/2011/06/delete-and-truncate-behavior.html

--Create student table
CREATE TABLE [DBO].[TTB_STUDENT]
(
ID INT
,
NAME VARCHAR(100
) NULL,
AGE INT
NULL,
ACTIVE CHAR(1
)
)

--Insert some records
INSERT INTO [DBO].[TTB_STUDENT]
VALUES
(1, 'Mark', 32, 'Y'
),
(
2, 'Elaine', 31, 'Y'
),
(
3, 'Elton', 33, 'Y'
),
(
4, 'John', 40, 'Y'
),
(
5, 'Smith', 30, 'Y')

Select student table. This is our target table -


image


Create another table which will have the delta which we have to apply on student table. Note that we will be using ID column value as key to decide whether or not student already exists. Here is the delta table script and rows..





CREATE TABLE [DBO].[TTB_STG_STUDENT](
ID INT PRIMARY KEY
,
NAME VARCHAR(100
) NULL,
AGE INT
NULL
)

INSERT INTO [DBO].[TTB_STG_STUDENT]
VALUES
(1, 'Mark Luther', 42
),
(
2, 'Elaine Wang', 41
),
(
6, 'Elton B', 22)


Since we are doing SCD Type – 2, this is what will be the scenarios -



  • If get a new student (New ID value), we will insert into the target table and set Active = ‘Y’
  • If get an existing student, we will insert it to the target table with Active = ‘Y’ and set Active = ‘N’ for all other records of that student. Meaning, at a any given point in time, we should have only one active record for each student.

As said earlier idea here is to make use of OUTPUT $action to retrieve the records that got updated and once we have this list, we can insert it finally and set flag to ‘N’ for all other records for a student.


Here is the final SQL statement and target table output.





INSERT INTO [DBO].[TTB_STUDENT]    
SELECT ID, NAME, AGE, 'Y'
FROM
(
MERGE [DBO].[TTB_STUDENT] AS TRG
USING [DBO].[TTB_STG_STUDENT] AS SRC
ON (SRC.ID = TRG.ID AND TRG.ACTIVE = 'Y'
)
WHEN NOT MATCHED
THEN
INSERT VALUES
(SRC.ID, SRC.NAME, SRC.AGE, 'Y'
)
WHEN MATCHED
THEN
UPDATE SET
TRG.ACTIVE =
'N'
OUTPUT $action, SRC.ID, SRC.NAME, SRC.AGE

) AS DELTA (action, ID, Name, Age
)
WHERE action = 'Update'

image


As you see, we had 2 existing students in the delta table for which active is set to ‘N’ and 2 new entries have been created in the table with Active = ‘Y’. There were one new student (ID = 6), which got inserted into the table. This way we can achieve SCD Type – 2 using MERGE SQL. Please leave your comments/feedback.

DELETE and TRUNCATE behavior

We will create a student table then apply Delete and Truncate within Transaction and rollback. Leave your comment about the behavior and/or output you expect.
--Create student table
CREATE TABLE [DBO].[TTB_STUDENT]
(
ID INT IDENTITY(1, 1) PRIMARY KEY
,
NAME VARCHAR(100
) NULL,
AGE INT
NULL,
ACTIVE CHAR(1
))

--Insert some records
INSERT INTO [DBO].[TTB_STUDENT]
VALUES
('Mark', 32, 'Y'
),
(
'Elaine', 32, 'Y'
),
(
'Elton', 32, 'Y'
),
(
'John', 32, 'Y'
),
(
'Smith', 32, 'Y'
)

--Delete records using DELETE
--Rollback to bring back the records
--You can because it write to the log and
--can be rolled back
BEGIN TRANSACTION
DELETE
[DBO].[TTB_STUDENT]
ROLLBACK

--Select table
SELECT * FROM [DBO].[TTB_STUDENT]
Here is the output of the above select - 
image


--Do the same but use TRUNCATE TABLE
--Note that it does not write to log by definition
BEGIN TRANSACTION
TRUNCATE TABLE
[DBO].[TTB_STUDENT]
ROLLBACK

--What do you expect ??
SELECT * FROM [DBO].[TTB_STUDENT]

Thursday, June 2, 2011

Dynamic vs Static set in SSAS

We deal with named set a lot when it comes to access multi dimensional data from the cube. There are two types of named set – Static and Dynamic. Till SQL Server 2005 we had only Static named set but now in SQL Server 2008 both are supported (static and dynamic named set).

You create named set either using CREATE SET (when defined in the cube) or WITH SET MDX (when defined in the MDX/Report) syntax. In case of static member, it get evaluated when the CREATE SET statement is executed or MDX is executed in case when it is defined in MDX using WITH SET. In case of static named set once it is executed it persists static during whole session or query or cube. In effect what will happen since named set is static and cached it will give same set of members even when you query with different condition and expect different result.

Assume a case where you need to get top 10 product by sales for whole period and then for a month. So after you run the query for whole period, you will end up getting same set of products for a month also when you execute next. So in such glitches we now have option to create dynamic set which works well.

I will explain what exactly I mean by all this.

Create a static set in the cube as under. In my example I am retrieving top 5 products based on sale.

CREATE SET CURRENTCUBE.[TOP PRODUCT SALES]
AS TOPCOUNT(
[INVESTMENT PRODUCT].[PRODUCT NAME].[PRODUCT NAME].MEMBERS,
5,
[MEASURES].[INV PROD SALES ACTUAL]) ;
Next, run following MDX to get top 5 product sales. Note that I am not using any time slicer here so what I am getting in here is across the time.
SELECT 
{
[MEASURES].[INV PROD SALES ACTUAL]
} ON COLUMNS,
{
NONEMPTY([TOP PRODUCT SALES],
[MEASURES].[INV PROD SALES ACTUAL])
}
ON ROWS
FROM
[YOUR_CUBE];
Here is output of above MDX.
image
Next let’s apply February time filter in the MDX and I expect at least 1-2 different product than one we got above. 
SELECT 
{
[MEASURES].[INV PROD SALES ACTUAL]
} ON COLUMNS,
{
NONEMPTY([TOP PRODUCT SALES],
[MEASURES].[INV PROD SALES ACTUAL])
} ON ROWS
FROM
[YOUR CUBE]
WHERE [TIME].[YQMD].[Month].&[2011-02-01T00:00:00]
Here is the output of above MDX - 
image
Note that though we now got different sales actual, it is the same set of products we earlier got when we ran without using any time filter. So what is happening here is when we created the set using CREATE SET statement in the cube and processed it, it evaluated the top 5 products and same set is being returned.
Now let’s change the set type to dynamic and retry the MDX and this time we expect it to be dynamic. Meaning, we should get different set of products based on top 5 sales when passed different time filter.
CREATE DYNAMIC SET CURRENTCUBE.[TOP PRODUCT SALES]
AS TOPCOUNT(
[INVESTMENT PRODUCT].[PRODUCT NAME].[PRODUCT NAME].MEMBERS,
5,
[MEASURES].[INV PROD SALES ACTUAL]) ;
Now run above 2 MDX queries and analyze the result.They should be different now.
SELECT
{
[MEASURES].[INV PROD SALES ACTUAL]
} ON COLUMNS,
{
NONEMPTY([TOP PRODUCT SALES],
[MEASURES].[INV PROD SALES ACTUAL])
} ON ROWS
FROM
[YOUR CUBE]


image

SELECT
{
[MEASURES].[INV PROD SALES ACTUAL]
} ON COLUMNS,
{
NONEMPTY([TOP PRODUCT SALES],
[MEASURES].[INV PROD SALES ACTUAL])
} ON ROWS
FROM
[YOUR CUBE]
WHERE [TIME].[YQMD].[Month].&[2011-02-01T00:00:00]

image 
Now you would have noticed that we are getting different set of top 5 products and it is correct now and reflecting per sales. Please leave your feedback/comment.

Wednesday, June 1, 2011

Date dimension to support multiple fiscal year

If you have requirement to present your data based on the fiscal year user is using, you can provide user with option to either select fiscal year he wants or can make it dynamic in the report such that it dynamically select the current user location and render the report.

This is one way to model your time dimension to support multiple fiscal year. There are obviously other ways to support various fiscal year like by adding more columns to time dimensions, storing into separate fact for each single fiscal year you may need to support, building separate hierarchies for each fiscal year etc. I insist the approach because you don’t have to add columns in the database time dimension table to support and of course it is not considered good to add columns to support piece of business. We should always try and see if it can be done using same table columns.

We will create 3 tables. Find script below to create these tables. We will create a calendar dimension table with all the attributes, a Date dimension table with key and date and a bridge table to connect these two dimension tables (Calendar and Date).

Here is the script to create 3 tables -

//Calendar dimension table
CREATE TABLE [DBO].[TTB_DIM_CALENDAR](
[CALENDAR_KEY] [INT] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[CALENDAR_DATE_ID] [VARCHAR](100) NOT NULL,
[CALENDAR_NAME] [VARCHAR](50) NOT NULL,
[CALENDAR_ORDER] [TINYINT] NOT NULL,
[ACTUAL_DATE] [DATE] NOT NULL,
[DATE_NAME] [VARCHAR](50) NOT NULL,
[YEAR] [DATE] NOT NULL,
[YEAR_NAME] [CHAR](4) NOT NULL,
[QUARTER] [DATE] NOT NULL,
[QUARTER_NAME] [VARCHAR](9) NOT NULL,
[MONTH] [DATE] NOT NULL,
[MONTH_NAME] [VARCHAR](14) NOT NULL,
[WEEK] [DATE] NOT NULL,
[WEEK_NAME] [VARCHAR](12) NOT NULL,
[DAY_OF_YEAR] [SMALLINT] NOT NULL,
[DAY_OF_YEAR_NAME] [VARCHAR](7) NOT NULL,
[DAY_OF_QUARTER] [TINYINT] NOT NULL,
[DAY_OF_QUARTER_NAME] [VARCHAR](6) NOT NULL,
[DAY_OF_WEEK] [TINYINT] NOT NULL,
[DAY_OF_WEEK_NAME] [VARCHAR](5) NOT NULL,
[WEEK_OF_YEAR] [TINYINT] NOT NULL,
[WEEK_OF_YEAR_NAME] [VARCHAR](7) NOT NULL,
[MONTH_OF_YEAR] [TINYINT] NOT NULL,
[MONTH_OF_YEAR_NAME] [VARCHAR](36) NOT NULL,
[MONTH_OF_QUARTER] [TINYINT] NOT NULL,
[MONTH_OF_QUARTER_NAME] [VARCHAR](8) NOT NULL,
[QUARTER_OF_YEAR] [TINYINT] NOT NULL,
[QUARTER_OF_YEAR_NAME] [VARCHAR](9) NOT NULL,
)//
Date dimension table
CREATE TABLE
[DBO].[TTB_DIM_DATE](
[DATE_KEY] [INT] NOT NULL PRIMARY KEY,
[ACTUAL_DATE] [DATETIME] NOT NULL,
)//
Bridge fact table
CREATE TABLE
[DBO].[TTB_DIM_DATE_CALENDAR](
[DATE_KEY] [INT] NOT NULL REFERENCES
[DBO].[TTB_DIM_DATE]([DATE_KEY]),
[CALENDAR_KEY] [INT] NOT NULL REFERENCES
[DBO].[TTB_DIM_CALENDAR]([CALENDAR_KEY])
)
GO

Let’s setup data source view in the OLAP. Add above 3 tables in the DSV, you should get this -


image


Next populate above tables using SQL script. Make sure you have entries for each single day in this table. You need to have entries per Gregorian calendar and same number of entries for each month of a year starting February (and of course for each year) . Reason, we are not creating entries for January because if fiscal starts from January it will be same as calendar year.


Here is the sample script to populate above tables.





DECLARE @STARTDATE DATETIME = '2008-01-01'
DECLARE @ENDDATE DATETIME = '2010-12-31'

DECLARE @COUNTER INT
SET
@COUNTER = 1
;WITH DATES( D ) AS
(
SELECT @STARTDATE AS D
UNION ALL
SELECT DATEADD(DAY, 1, D)
FROM DATES
WHERE D < @ENDDATE
)
,
MONTHS(M) AS
(
SELECT @COUNTER AS M
UNION ALL
SELECT M + 1
FROM MONTHS
WHERE M <= 11
)

INSERT INTO TTB_DIM_CALENDAR (
CALENDAR_DATE_ID, CALENDAR_NAME, CALENDAR_ORDER, ACTUAL_DATE,
DATE_NAME, YEAR, YEAR_NAME, QUARTER, QUARTER_NAME, MONTH,
MONTH_NAME, WEEK, WEEK_NAME, DAY_OF_YEAR, DAY_OF_YEAR_NAME,
DAY_OF_QUARTER, DAY_OF_QUARTER_NAME, DAY_OF_WEEK,
DAY_OF_WEEK_NAME, WEEK_OF_YEAR, WEEK_OF_YEAR_NAME, MONTH_OF_YEAR,
MONTH_OF_YEAR_NAME,
MONTH_OF_QUARTER, MONTH_OF_QUARTER_NAME, QUARTER_OF_YEAR,
QUARTER_OF_YEAR_NAME)
SELECT
CASE WHEN
LEN(FISCALSTARTMONTH) = 1 THEN '0' ELSE '' END +
CONVERT
(VARCHAR(8), FISCALSTARTMONTH) + '_' + CALENDARKEY AS
CALENDARDATEID
,CALENDARNAME
,FISCALSTARTMONTH
,ACTUALDATE
,[DATENAME]
,[YEAR]
,CONVERT(CHAR(4), DATEPART(YYYY, [YEAR])) AS YEARNAME
,DATEADD(Q, CEILING((DATEDIFF(MONTH, [YEAR], ACTUALDATE) +
1)/3.0) - 1, [YEAR]) AS [QUARTER]
,CONVERT(CHAR(4), DATEPART(YYYY, [YEAR])) + ' Q' + CONVERT
(
VARCHAR,CEILING((DATEDIFF(MONTH, [YEAR], ACTUALDATE) + 1)/3.0))
AS
QUARTERNAME
,DATEADD(MM, DATEDIFF(MM, 0, ACTUALDATE), 0) AS [MONTH]
,CONVERT(VARCHAR, DATENAME(MM, ACTUALDATE)) + ' ' + CONVERT
(
CHAR(4), DATEPART(YYYY, ACTUALDATE)) AS [MONTHNAME]
--,(DATEDIFF(MM, [YEAR], ACTUALDATE) + 1) AS [MONTHNUMBER]
,DATEADD(WK, DATEDIFF(WK, 0, ACTUALDATE), 0) AS [WEEK]
,'WEEK ' + CONVERT(VARCHAR,(DATEDIFF(WK, [YEAR], ACTUALDATE) +
1)) + ' ' + CONVERT(CHAR(4), DATEPART(YYYY, [YEAR])) AS [WEEKNAME]
,DATEDIFF(DY, [YEAR], ACTUALDATE) + 1 AS [DAYOFYEAR]
,'DAY ' + CONVERT(VARCHAR, DATEDIFF(DY, [YEAR], ACTUALDATE) +
1) AS DAYOFYEARNAME
,DATEDIFF(DY, DATEADD(Q, CEILING((DATEDIFF(MONTH, [YEAR],
ACTUALDATE) + 1)/3.0) - 1, [YEAR]), ACTUALDATE) + 1 AS DAYOFQUARTER
,'DAY ' + CONVERT(VARCHAR, DATEDIFF(DY, DATEADD(Q, CEILING
((
DATEDIFF(MONTH, [YEAR], ACTUALDATE) + 1)/3.0) - 1, [YEAR]),
ACTUALDATE) + 1) AS DAYOFQUARTERNAME

,DATEPART(DW, ACTUALDATE) AS [DAYOFWEEK]
,'DAY ' + CONVERT(VARCHAR, DATEPART(DW, ACTUALDATE)) AS
DAYOFWEEKNAME
,DATEDIFF(WK, [YEAR], ACTUALDATE) + 1 AS WEEKOFYEAR
,'WEEK ' + CONVERT(VARCHAR, DATEDIFF(WK, [YEAR], ACTUALDATE) +
1) AS WEEKOFYEARNAME
,DATEDIFF(MM, [YEAR], ACTUALDATE) + 1 AS MONTHOFYEAR
,'MONTH ' + CONVERT(VARCHAR, DATEDIFF(MM, [YEAR], ACTUALDATE)
+
1) AS MONTHOFYEARNAME
,DATEDIFF(MM, DATEADD(Q, CEILING((DATEDIFF(MONTH, [YEAR],
ACTUALDATE) + 1)/3.0) - 1, [YEAR]), DATEADD(MM, DATEDIFF(MM, 0,

ACTUALDATE), 0)) + 1 AS MONTHOFQUARTER
,'MONTH ' + CONVERT(VARCHAR, DATEDIFF(MM, DATEADD(Q, CEILING
((DATEDIFF(MONTH, [YEAR], ACTUALDATE) + 1)/3.0) - 1, [YEAR]),
DATEADD
(MM, DATEDIFF(MM, 0, ACTUALDATE), 0)) + 1) AS
MONTHOFQUARTERNAME
,CEILING((DATEDIFF(MONTH, [YEAR], ACTUALDATE) + 1)/3.0) AS
QUARTEROFYEAR
,'QUARTER ' + CONVERT(VARCHAR, CEILING((DATEDIFF(MONTH,
[YEAR], ACTUALDATE) + 1)/3.0)) AS QUARTEROFYEARNAME

FROM
(
SELECT
CONVERT(CHAR(8), D, 112) AS CALENDARKEY
,M AS FISCALSTARTMONTH
,CASE
WHEN
M = 1 THEN 'GREGORIAN'
ELSE CONVERT(VARCHAR, DATENAME(MONTH, DATEADD
(MONTH, (M-1), 0)))
+
' BASED FISCAL'
END AS CALENDARNAME
,D AS ACTUALDATE
,DATENAME(DW, D)+ ', ' + DATENAME(MM, D) + ' ' + RIGHT
(
'00' + CONVERT(VARCHAR(2), DATEPART(D, D)), 2) + ' ' + CONVERT
(
CHAR(4), DATEPART(YYYY, D)) AS [DATENAME]
,CASE
WHEN
DATEPART(MONTH, D) >= M THEN DATEADD(MONTH,
(
M-1), DATEADD(YEAR, DATEDIFF(YEAR, 0, D), 0))
WHEN DATEPART(MONTH, D) < M THEN DATEADD(MONTH, (M-
1)-12, DATEADD(YEAR, DATEDIFF(YEAR, 0, D), 0))

END AS [YEAR]
FROM
DATES
CROSS JOIN
MONTHS
) DATES

OPTION (MAXRECURSION 10000)

;
WITH DATES( D ) AS
(
SELECT @STARTDATE AS D
UNION ALL
SELECT DATEADD(DAY, 1, D)
FROM DATES
WHERE D < @ENDDATE
)

INSERT INTO TTB_DIM_DATE (DATE_KEY, ACTUAL_DATE)
SELECT
CONVERT(INT, CONVERT(CHAR(8), D, 112)) AS DATEKEY,
D AS ACTUALDATE
FROM
DATES
OPTION (MAXRECURSION 10000)

INSERT INTO [TTB_DIM_DATE_CALENDAR] (DATE_KEY, CALENDAR_KEY)
SELECT
D.DATE_KEY, C.CALENDAR_KEY
FROM
TTB_DIM_DATE D
INNER JOIN TTB_DIM_CALENDAR C ON
D.DATE_KEY = CONVERT(INT, RIGHT(C.CALENDAR_DATE_ID, 8))

At this point if I assume you have just populated one year entries into the calendar dimension this is how number of entries should be in the calendar dimension table -
image


Add calendar and set dimension properties as under -


image


Set additional calendar attribute properties as under -
image


Next build hierarchy and set attribute relationship as under -


image


image


Add Date dimension and leave everything to default value. I however changed the key column to Date from Date_Key. Here is the snap of the Date dimension -


image


Now we will add a fact table (actual fact) for sales. Fact table will be linked to calendar and date dimensions.





CREATE TABLE [DBO].[TTB_FACT_SALES](
[DATE_KEY] [INT] NOT NULL REFERENCES
[DBO].[TTB_DIM_DATE]([DATE_KEY]),
[CALENDAR_KEY] [INT] NOT NULL REFERENCES
[DBO].[TTB_DIM_CALENDAR]([CALENDAR_KEY]),
[SALES_AMOUNT] DECIMAL(10,2)
)
Add above fact table to data source view and it should look like this at this point - 

image


Create a measure from the bridge table. This will act as intermediate fact table when we join with sales fact through calendar dimension (using many to many relationship and will be discussed later). It is nothing but the fact less fact table and we will not be using this for anything. Let’s add count measure on Date_key column. Here is what I mean -


image


image


Populate sales fact table as under. I just created for all the possible date and calendar key combination for us to be able to test with some data.





INSERT INTO [DBO].[TTB_FACT_SALES]([DATE_KEY], [CALENDAR_KEY], [SALES_AMOUNT]) 
SELECT DATE_KEY, CALENDAR_KEY, RIGHT(DATE_KEY, 2) FROM [DBO].[TTB_DIM_DATE_CALENDAR]
Next add sales measure to the cube and leave everything else to default. This is how cube structure should look like.

image


Set the dimension usage tab as under. Link DATE CALENDAR measure group with Calendar and Date dimension using Regular relationship type. This measure group we will be using when linking with actual fact which is sales in this case.


Link sales fact with Date dimension using Regular relationship type and with calendar dimension using many to many relationship type through bridge table measure group (above one). Find below dimension usage tab and many to many relationship snap for your to connect well.


image 


image


Process the cube and analyze what we got in here. Drag sales in the measure area and calendar hierarchy. This is what we get, all the values are same for the Calendar name because we had set IsAggregatable = False.


image




What we gonna see next is interesting. In Gregorian calendar March falls in Q1 and in March based fiscal again it will fall in Q1 but in April based fiscal April will fall in Q1. Similarly in Gregorian calendar May falls in Q2 but in April based fiscal it will fall in Q1 etc. So what I am driving here is, sales value will be accrued to different quarter in a year based on what is the starting month of a fiscal.


image


image


Rest you browse the cube and check the sales value distribution. Hope this will help you design time dimension when you have to support different calendar, fiscal etc. Thank you and bye for now!