12 November 2013

Review “MDX with SSAS 2012 Cookbook” by Tomislav Piasevoli and Sherry Li

“MDX with SSAS 2012 Cookbook” has a good chapter and topic organization which starts simply, but not too simply, and builds in complexity. “Elementary MDX” sets the stage for the expected level of knowledge with the other chapters building from that knowledge base. I particularly liked the chapters on “Working with Time”, and “Business Analytics” because the regression and non-allocated expenses are tough problems for MDX newbies to solve. Tomislav does a nice job with practical regression and expense allocation solutions providing a well thought out solution rather than a simple “this is how the function works” which is too often what we get from other documentation.

I recommend this book to anyone who needs a reference for good solutions to practical problems. It’s much more than a simple functional reference. I use “MDX with SSAS 2012 Cookbook” frequently as a reference and I’ve been coding MDX since SSAS was released by Microsoft back in 1998 with the release of SQL Server 7. You can never stop learning better ways to get answers with MDX and this book really helps move you along that path.

See this link to learn more about the book: http://bit.ly/19l8zZH 

12 May 2010

Year-Qtr-Month MDX Set definition for page filtering

I ran across a need for a Performance Point Server (PPS)page filter using the Date hierarchy which was a bit of a head scratcher. Here's the story and the solution. Hope you can make use of it. And I'm sure there are about 6 ways to solve this problem - the one presented here is what I came up with at the time.

Problem: I have multiple SSRS charts on a PPS page which use the same date page filter. The user requirement is to allow selection of the year, quarter, or month which is then consumed by each of the charts. The order of the date elements is to start at the most recent year and go back 5 years. If the current year is 2010 the presentation is 2010, 2009, 2008... As you expand each year (using the native tree display functionality of a PPS filter) you should see quarter, then expand quarters and see months. Sounds simple enough.

(keep in mind that the query samples below execute as standalone queries in SQL Manager. The PPS filter definition is just the set which is defined on the rows axis in each query).

The first pass at the filter definition shows the years in the range I want:Here are the results:
We can see the years are listed on the Rows, but they are not in descending order and do not display the quarters or months. I revised the above query to sort the years in descending order using the MDX ORDER() funtion. Luckily I could sort by the intrinsic member property MEMBER_KEY.

Here's the query:

And the result:

Well, the year ordering looks right, but now the problem is how do I show the quarter and month? I'll use the MDX GENERATE() function to use each year as CURRENT_MEMBER and get the descendants for each Year (which is CURRENT_MEMBER) to the month level. here's the quesry using the GENERATE(). The first set specified in the GENERATE() is the same year range set I used in the earlier queries. The second set in the GENERATE() is the DESCENDANTS() of CURRENT_MEMBER which is each Year


Here's the query:


Here's the result:
This is much closer to what I need. But in a user review they voiced a legitimate concern. They said, "I understand the descending years, but when I look at quarters and months I expect to see them in their normal calendar order." I guess this is a need you don't recognize until you see the months listed backwards. It made sense but was a bit of a puzzle to solve. The answer is to change when the ORDER() is done compared to the GENERATE(). The trick is to ORDER the year set within the first set passed to the GENERATE() function, and keep the DESCENDANTS() function (second set passed to the GENERATE() function) outside the ORDER().

Here's the query:




Here's the result:
Ok, that's it. Standby for next weeks post which shows how to replace the hardcoded begin and end years in the above set with a dynamially generated range of years.














































07 April 2008

Tab delimited ouptut from Oracle/SQLPlus

OK - It's been a looong tim since I made a posting. Sorry to anyone (is there anyone who reads this?) who might checks this BLOG for updates. I promise posts will get more frequent.

Now, to the heart of this topic. I had a situation where I NEEDED to get a copy of a table from an Oracle9 system into my SQL Server 2005 datamart. The extract in my SSIS package was working fine until our IT infrastructure team decided to monkey with the firewalls between offices. They had all the best intentions, but their 'monkeying' wasn't planned for completion until 6 months from now. Not a good situation considering I needed a copy of the Oracle table EVERY NIGHT!

Without the correct firewall access for the OLE DB Extract directly into a SQL Server table I resorted to the old school method of executing a SQL query in the command tool SQLPlus. It should have been simple, but I wanted a TAB DELIMITED output and the SET commands for SQLPlus didn't seem to have any setting for tab delimitation.

I did find an obscure posting from 2002 (wow, that's old) which showed hardcoding characters inbetween the column specifications of the SQL query using commas. I thought, "hey maybe it'll work the same if I substitute the TAB character instead of the comma?" Well, sure enough it worked. Here's a snippet of the SQLPlus code. I cut out a lot of the column names because there were 100+ column references. Note the output is spooled to a text file which I then FTP to my SQL Server Box and import the text file into SQl server. The character inbetween the single quotes in the query ('') is a TAB character. Crude - BUT IT WORKS!

SET LINESIZE 8000
set trimspool on
SET FEEDBACK OFF
SET TERMOUT OFF
SET HEAD OFF
SET SPACE 1
SET PAGES 0

SPOOL TabdelimitOuput.txt

SELECT
JOBNUMBER
' ' ENTRYNUMBER
' ' THEJOURNALNUMBER
' ' JOURNALLINENUMBER
' ' POSTINGDATE
' 'VOUCHERNUMBER
' 'DATEOFENTRY
' 'ACTIVITYNUMBER
' 'ACTIVITYTYPE
' 'TEXT
' 'EMPLOYEENUMBER
' 'NUMBERHOURSREGISTERE
' 'NUMBEROFHOURSINVOICE
' 'CURRENCY
' 'COSTPRICE
' 'COSTPRICETOTAL
FROM WIRE.JOBENTRY;

SPOOL OFF
run
exit

29 May 2007

LinRegPoint() MDX Function Sample in AdventureWorks


Thanks to Mosha Pasumansky (no surprise) for the real workings of this sample. Mosha provides a sample of the LinRegPoint() MDX function on the SQL 2000 Foodmart sample database at this link: http://sqljunkies.com/WebLog/mosha/archive/2004/12/21/5689.aspx
More importantly, Mosha explains the usage of the parameters and why the use of the MDX RANK() function really helps in arguments to LinRegpoint. Please see the above link for a detailed explanation on how the sample included here works.

This sample works against the 2005 AdventureWorks sample. It shows the [Internet Sales Amount] compared to the values produced from the LinRegPoint function for the months in calendar year 2003. Here's the MDX:

WITH
MEMBER Measures.[Internet Sales Forecast] AS
LinRegPoint(
Rank([Date].[Calendar].CurrentMember, [Date].[Calendar].CurrentMember.LEVEL.MEMBERS),
Descendants([Date].[Calendar].[Calendar Year].&[2003],[Date].[Calendar].CurrentMember.LEVEL),
[Measures].[Internet Sales Amount],
Rank([Date].[Calendar].CurrentMember, [Date].[Calendar].CurrentMember.LEVEL.MEMBERS))
,FORMAT_STRING="$#,0.00"

SELECT DESCENDANTS([Date].[Calendar].[Calendar Year].&[2003], [Date].[Calendar].[Month]) ON COLUMNS,
{[Measures].[Internet Sales Amount], Measures.[Internet Sales Forecast]} ON ROWS
FROM [Adventure Works]

The graph at the top shows the results in Excel 2007. An image any business savvy individual can value when interested in forecasting:





12 October 2006

Getting a SUM() of Members Independent of a Measure

A simple, yet common, problem for MDX reports is combining non-measure members which are query specific. The requirement to combine the members is a reporting requirement which means it doesn't belong in the cube. Here's hte problem statement and two solutions.

I want to select not only [Aus - New South Wales], but also [Canada - Alberta], and show both as one sum, not 2 separate columns. Normally, the WHERE clause needs a tuple, and each dimension can only appear once in a tuple. The following MDX illustrates the use of a single reference to the customer dimension in the WHERE clause.

SELECT
NON EMPTY {Measures.[Internet Sales Amount], Measures.[Internet Tax Amount]} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Category].ALLMEMBERS} ON ROWS
FROM [Adventure Works]
WHERE ([Date].[Calendar].[Calendar Year].&[2004],[Customer].[Customer Geography].[State-Province].&[NSW]&[AU])

One solution is creating a calculated member which SUM()s the values for NSW and Alberta, but the trick is placing the new calculated memebr as part of the Customer dimension which makes it independent of the Measure selected. Most MDX beginners assume you can only create new Measures. That's not the case. A Calculated member can belong in any Dimension.Hierarchy. Here's the solution with the calculated member which is then referneced in the WHERE clause to slice by the SUM() of the two country members.

WITH
MEMBER [Customer].[Customer Geography].[Test SUM] AS
'
SUM({[Customer].[Customer Geography].[State-Province].&[NSW]&[AU], [Customer].[Customer Geography].[State-Province].&[AB]&[CA]}, Measures.CurrentMember)
'

SELECT
NON EMPTY {Measures.[Internet Sales Amount], Measures.[Internet Tax Amount]} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Category].ALLMEMBERS} ON ROWS
FROM [Adventure Works]
WHERE ([Date].[Calendar].[Calendar Year].&[2004],[Customer].[Customer Geography].[Test SUM])

Another solution (thanks to Deepak Puri) is using a SET reference in the WHERE clause which combines the two members as a set, and CROSSJOINs the set with the other members in the WHERE clause. This a nice trick, and not obvious - even to experienced MDX coders.

SELECT
NON EMPTY {Measures.[Internet Sales Amount], Measures.[Internet Tax Amount]} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Category].ALLMEMBERS} ON ROWS
FROM [Adventure Works]
WHERE CROSSJOIN({[Customer].[Customer Geography].[State-Province].&[NSW]&[AU], [Customer].[Customer Geography].[State-Province].&[AB]&[CA]}, {[Date].[Calendar].[Calendar Year].&[2004]})

13 September 2006

Percent Contribution for ALL Hierarchies

I ran across an interesting question from the MSDN forum which challenged my MDX ability making it worth sharing. The question posed on the forum was, "The user requirement is ability to get cross tabulations among the dimensions. For example, they may want the proportion of cases in each year at a given location or the proportion of cases in each year by product? In this case, I'd need a generalized contribution precentage formula because I'll never be sure which dimension(s) users may cross-tab."

This type of "generic" contribution precentage calculation is useful for reports where a users experience is "guided", however, I'd caution against using this type of percentage calculation for ad-hoc users because they'll likely end up with slices of the data which affect percentages displayed not realizing all the slices in effect.

The tricks to this calculated member are: (1) Avoid division by zero error, and (2) Use the .DefaultMember when the .CurrentMember is the highest member in the hierarchy.

Trick (1) is taken care of in the IIF() statement which returns NULL if ANY (.CurrentMember, Measure) tuple returns empty. Trick (2) is handled in the denominator of the percentage calculation by detecting the .CurrentMember level and using DefaultMember when appropriate.

WITH MEMBER Measures.[Percent Customer Count] AS
'
IIF(ISEMPTY([Measures].[Customer Count]), NULL,
IIF([Product].[Product Categories].CurrentMember.Level.Ordinal <> 0 AND
ISEMPTY(([Product].[Product Categories].CurrentMember.Parent, Measures.[Customer Count])), NULL,
IIF([Date].[Calendar].CurrentMember.Level.Ordinal <> 0 AND
ISEMPTY(([Date].[Calendar].CurrentMember.Parent, Measures.[Customer Count])), NULL,
IIF([Customer].[Customer Geography].CurrentMember.Level.Ordinal <> 0 AND
ISEMPTY(([Customer].[Customer Geography].CurrentMember.Parent, Measures.[Customer Count])), NULL,
// Do the calculation
([Product].[Product Categories].CurrentMember
,[Date].[Calendar].CurrentMember
,[Customer].[Customer Geography].CurrentMember
,[Measures].[Customer Count])
/
(IIF([Product].[Product Categories].CurrentMember.Level.Ordinal = 0, [Product].[Product Categories].DefaultMember, [Product].[Product Categories].CurrentMember.Parent)
,IIF([Date].[Calendar].CurrentMember.Level.Ordinal = 0, [Date].[Calendar].DefaultMember, [Date].[Calendar].CurrentMember.Parent)
,IIF([Customer].[Customer Geography].CurrentMember.Level.Ordinal = 0, [Customer].[Customer Geography].DefaultMember, [Customer].[Customer Geography].CurrentMember.Parent)
,[Measures].[Customer Count])
)
)
)
)
' ,SOLVE_ORDER=10, FORMAT_STRING="#,0.0%"


SELECT
{[Measures].[Customer Count], Measures.[Percent Customer Count]} ON COLUMNS
,{[Product].[Product Categories].[All]
,DESCENDANTS([Product].[Product Categories].[All], [Product].[Product Categories].[Category])} ON ROWS
FROM [Adventure Works]
WHERE(
[Date].[Calendar].[Calendar Year].&[2003]
,[Customer].[Customer Geography].[Country].&[United States]
)

The above example works with the AdventureWorks DW database, but only on three of the hierarchies. Which implies the biggest drawback of this implementation technique - if you have more than 5-6 hierarchies the nesting of the IIF() and maintenance of the MDX is tedious and error prone. However, if you're diligent it will work.

07 September 2006

Officially - I'm a Nerd (though barely)

OK - I took the test. Nerd score of 59. Not much. Looks like I need some work.

What's your nerd score?

I am nerdier than 59% of all people. Are you nerdier? Click here to find out!