<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>The Frog-Blog &#187; OLAP</title>
	<atom:link href="http://www.purplefrogsystems.com/blog/index.php/tag/olap/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.purplefrogsystems.com/blog</link>
	<description>Purple Frog-Blog for all that is Business Intelligence</description>
	<lastBuildDate>Mon, 06 Sep 2010 21:20:51 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.1</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Calculate Run Rate (Full Year Projection) in MDX</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2010/08/calculate-run-rate-full-year-projection-in-mdx/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2010/08/calculate-run-rate-full-year-projection-in-mdx/#comments</comments>
		<pubDate>Wed, 18 Aug 2010 17:26:18 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[Calculated Member]]></category>
		<category><![CDATA[Cube]]></category>
		<category><![CDATA[Finance]]></category>
		<category><![CDATA[MDX]]></category>
		<category><![CDATA[OLAP]]></category>
		<category><![CDATA[SSAS]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=152</guid>
		<description><![CDATA[This post explains how to create an MDX calculated member that will take a value from the cube and project it forward to the end of the year. This provides a simple mechanism for calculating what your expected total will be at year end, based upon current performance.
To do this more accurately you should use [...]]]></description>
			<content:encoded><![CDATA[<p>This post explains how to create an MDX calculated member that will take a value from the cube and project it forward to the end of the year. This provides a simple mechanism for calculating what your expected total will be at year end, based upon current performance.</p>
<p>To do this more accurately you should use time series data mining models in SSAS and use DMX expressions to query the results, but this method is very simple and requires little effort, and will be pretty accurate so long as the data you&#8217;re modelling is fairly linear. Please note though that the more cyclical and seasonal your data is the less effective this will be.</p>
<p>The basic idea is that we take what we have done so far (i.e. year to date sales), look at how far through the year we are, and extrapolate the value of future months (or days/weeks/etc.) based upon values so far.</p>
<p>i.e. If we&#8217;re at March month end and we&#8217;ve sold 100 widgets so far this year, we&#8217;re 1/4 of the way through the year so we multiply 100 by 4 and get a prejected yearly total of 400.</p>
<p><img src="http://www.purplefrogsystems.com/blog/wp-content/uploads/2010/08/RunRateAt6Months.png" alt="" title="RunRateAt6Months" width="462" height="220" class="alignleft size-full wp-image-153" /><br />
This chart shows the concept of what we&#8217;re doing, and shows the full year prejections calculated in March (with 3 months of available data) and June (6 months of data). The projections obviously get more accurate the further you are through the year.</p>
<p>One of the points to note is that when creating a calculation like this, based upon a time dimension, the calculation should always work with any level of the dimension hierarchy selected. i.e. The user shouldn&#8217;t care whether they&#8217;re looking at a month, week, quarter or a day, the calculation should always work the same. To achieve this we simply use the .currentmember of the time hierarchy.</p>
<p>The following examples are based upon projecting the Internet Sales Amount measure found within the SQL Server 2008 Adventure Works DW sample cube.</p>
<p><b>Step 1 &#8211; What are our total sales so far this year?</b></p>
<p>MDX helpfully provides us with the YTD function which takes care of this for us.</p>
<pre><span style="color: #008000;">
  MEMBER [Measures].[YTD Sales] AS
    AGGREGATE(
      YTD([Date].[Calendar].CurrentMember)
      ,[Measures].[Internet Sales Amount])
</span></pre>
<p>This takes the current member of the Calendar hierarchy, and creates a set of all dates before it (this year) using YTD. It then aggregates (in this case sums) the Internet Sales Amount for all of these dates to calculate YTD Sales.</p>
<p><b>Step 2 &#8211; Which period are we in?</b></p>
<p>Here we&#8217;ll use the same YTD function to create a set of all dates so far this year, but in this case we&#8217;ll count the number of resulting members. Note that because we&#8217;re using the .CurrentMember of the hierarchy, it doesn&#8217;t matter if we&#8217;re looking at a date, week or month, the MDX will work. i.e. If we&#8217;re looking at 21 Jan it will return 21. If we&#8217;re looking at Q3 it will return 3, August will return 8 etc.</p>
<pre><span style="color: #008000;">
  MEMBER [Measures].[CurPeriod] AS
    COUNT(
      YTD([Date].[Calendar].CurrentMember)
      ,INCLUDEEMPTY)
</span></pre>
<p><b>Step 3 &#8211; How many periods are in the year?</b></p>
<p>If we coded this to only work with months then we could hard code this to 12 however we need to keep it generic to all levels of the hierarchy. So, we have to count all the cousins of the current time member [within this year]. Unfortunately there isn&#8217;t a Cousins function in MDX, and Siblings will only return other members within the same parent. i.e. siblings of May 4th would include May 1 through to May 31. To get around this we find the year of the current member by using the Ancestor function.</p>
<pre><span style="color: #008000;">
  ANCESTOR([Date].[Calendar].CurrentMember
  , [Date].[Calendar].[Calendar Year])
</span></pre>
<p>Then we find all of the descendants of the year, at the same level of the hierarchy (week/day/etc.) as the current member. We can then take a count as before.</p>
<pre><span style="color: #008000;">
  MEMBER [Measures].[TotalPeriods] AS
    COUNT(
      DESCENDANTS(
        ANCESTOR([Date].[Calendar].CurrentMember
          ,[Date].[Calendar].[Calendar Year])
        ,[Date].[Calendar].CurrentMember.level)
      ,INCLUDEEMPTY)
</span></pre>
<p><b>Step 4 &#8211; Calculate the Run Rate</b></p>
<p>Calculating the prejected yearly total (run rate) is then a simple calculation</p>
<pre><span style="color: #008000;">
  MEMBER [Measures].[Full Year Run Rate] AS
    [Measures].[YTD Sales]
    * ([Measures].[TotalPeriods]
       /[Measures].[CurPeriod])
</span></pre>
<p>You can then put the whole lot together and see the results&#8230;</p>
<pre><span style="color: #008000;">
WITH

  MEMBER [Measures].[YTD Sales] AS
    AGGREGATE(
      YTD([Date].[Calendar].CurrentMember)
      ,[Measures].[Internet Sales Amount])

  MEMBER [Measures].[CurPeriod] AS
    COUNT(
      YTD([Date].[Calendar].CurrentMember)
      ,INCLUDEEMPTY)

  MEMBER [Measures].[TotalPeriods] AS
    COUNT(
      DESCENDANTS(
        ANCESTOR([Date].[Calendar].CurrentMember
          ,[Date].[Calendar].[Calendar Year])
        ,[Date].[Calendar].CurrentMember.level)
      ,INCLUDEEMPTY)

  MEMBER [Measures].[Full Year Run Rate] AS
    [Measures].[YTD Sales]
    * ([Measures].[TotalPeriods]
       /[Measures].[CurPeriod])

SELECT
{
     [Measures].[Internet Sales Amount]
    ,[Measures].[YTD Sales]
    ,[Measures].[Full Year Run Rate]
    ,[Measures].[CurPeriod]
    ,[Measures].[TotalPeriods]
} ON 0,
{
    DESCENDANTS([Date].[Calendar].[CY 2003])
} ON 1
FROM [Direct Sales]
</span></pre>
<p>In my next blog I&#8217;ll be diong the same calculation in DAX for use with PowerPivot, stay tuned&#8230;</p>
<p>Frog-Blog Out</p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2010/08/calculate-run-rate-full-year-projection-in-mdx/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Data Warehouse Presentation</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2010/02/data-warehouse-presentation/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2010/02/data-warehouse-presentation/#comments</comments>
		<pubDate>Tue, 23 Feb 2010 23:26:23 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[OLAP]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=34</guid>
		<description><![CDATA[Purple Frog is presenting a session on data warehouse design concepts at the South Wales SQL Server User Group on Thursday 25th February 2010.
If you&#8217;re in the area and want to come along you can register for free here. Eversheds are hosting the event in their Cardiff office.
Adam Morton will be demonstrating an ETL control [...]]]></description>
			<content:encoded><![CDATA[<p>Purple Frog is presenting a session on data warehouse design concepts at the South Wales SQL Server User Group on Thursday 25th February 2010.</p>
<p>If you&#8217;re in the area and want to come along you can register for free <a href="http://sqlserverfaq.com/events/210/BI-Fundamentals-Building-an-Enterprise-ETL-Framework-in-SSIS-other-stuff-tbc.aspx" target="_blank">here</a>. Eversheds are hosting the event in their Cardiff office.</p>
<p>Adam Morton will be demonstrating an ETL control framework in SSIS, and Alex Whittles will be discussing the concepts of data warehousing and the fundamental differences in architecture between OLTP and OLAP systems.</p>
<p>Hope to see you there!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2010/02/data-warehouse-presentation/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Excel 2007 and SSAS 2008 Error</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2009/10/excel-2007-and-ssas-2008-error/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2009/10/excel-2007-and-ssas-2008-error/#comments</comments>
		<pubDate>Fri, 30 Oct 2009 16:56:06 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[Excel]]></category>
		<category><![CDATA[MDX]]></category>
		<category><![CDATA[OLAP]]></category>
		<category><![CDATA[Pivot]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=20</guid>
		<description><![CDATA[I was working on a new SSAS 2008 cube today, and came across an error which Google was unable to help with. I thought I&#8217;d post the solution here to help anyone else who may encounter it.
The cube in question will be primarily be accessed using Excel 2007, so I&#8217;d been dutifully testing it along [...]]]></description>
			<content:encoded><![CDATA[<p>I was working on a new SSAS 2008 cube today, and came across an error which Google was unable to help with. I thought I&#8217;d post the solution here to help anyone else who may encounter it.</p>
<p>The cube in question will be primarily be accessed using Excel 2007, so I&#8217;d been dutifully testing it along the way to ensure all was well. And then, after a number of changes the following error appeared when connecting to the cube from Excel to create a pivot table.</p>
<p><span style="color: #008000;">Excel was unable to get necessary information about this cube. The cube might have been reorganized or changed on the server.</p>
<p>Contact the OLAP cube administrator and, if necessary, set up a new data source to connect to the cube<br />
</span></p>
<p>Connecting and querying the cube via SSMS or BIDS worked without error (hense I didn&#8217;t spot the error sooner!).</p>
<p>A quick Google revealed a number of posts regarding this error, but they all related to attributes containing <a href="http://support.microsoft.com/kb/210806" target="_blank">invalid characters</a> when accessed from Excel 2000 Or problems with <a href="http://social.msdn.microsoft.com/Forums/en-US/sqlanalysisservices/thread/22796693-6060-483c-9071-84965d3536a7" target="_blank">translations and locale settings</a> in the .oqy file. Neither of these was the cause here, so I had to go back and recreate every change I had made step by step to track the problem.</p>
<p>Well, I&#8217;m please to report that in the end it was nothing more that a simple spelling mistake in a named set. One of the dynamic named sets in the cube calculations referred to a specific member of a dimension, which was spelled slightly incorrectly. (Simplified example..)</p>
<pre><span style="color: #008000;">CREATE DYNAMIC SET CURRENTCUBE.[Set1]
 AS {[Dimension].[Attribute].[Value1],
     [Dimension].[Attribute].[Value2WithTypo]
    };
</span></pre>
<p>When querying calculated measures through MDX in SSMS, the MDX parser just ignored the problem and only uses the valid members, however it appears as though Excel 2007 is slightly more picky with its cubes.</p>
<p>Useful to know, and even more useful when used as a tool to double check for any errors in the MDX calculations.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2009/10/excel-2007-and-ssas-2008-error/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Dynamic MDX in Reporting Services</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2008/09/dynamic-mdx-in-reporting-services/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2008/09/dynamic-mdx-in-reporting-services/#comments</comments>
		<pubDate>Thu, 18 Sep 2008 14:28:36 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[Reporting Services]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Dynamic]]></category>
		<category><![CDATA[MDX]]></category>
		<category><![CDATA[OLAP]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=18</guid>
		<description><![CDATA[After a couple of days off work last week with SQL Bits III and SQL 2008 Unleashed, it&#8217;s back to the grindstone this week; however before I get onto the serious stuff I&#8217;d like to say thank you to the organisers of both events. Watching you on stage headbanging to Rockstar &#8211; legendary! (You can [...]]]></description>
			<content:encoded><![CDATA[<p>After a couple of days off work last week with SQL Bits III and SQL 2008 Unleashed, it&#8217;s back to the grindstone this week; however before I get onto the serious stuff I&#8217;d like to say thank you to the organisers of both events. Watching you on stage headbanging to Rockstar &#8211; legendary! (You can see for yourself <a href="/images/blog/rockstar1.jpg" target="_blank">here</a> and <a href="/images/blog/rockstar2.jpg" target="_blank">here</a>&#8230;).</p>
<p>Anyway, back to MDX&#8230;</p>
<p>This post explains how you can build a dynamic MDX query in Reportins Services, customised to the users requirements. This can often bring some quite major performance benefits.</p>
<p>Lets say for example that you want to have a sales report grouped dynamically by either product, sales person, department or customer. Normally you would use a single static MDX query, and then add a dynamic grouping to the table in the report. This is fine, until you try it on a large dataset. If you only have 50 products, 2 salesmen, 5 departments and 100 customers, your MDX needs to return 50,000 records, the report then has to summarise all of this into the level of grouping you want. This renders the pre-calculated aggregations in OLAP pretty much worthless.</p>
<p>To get around this, you can generate your MDX dynamically, so that the query returns the data already grouped into the correct level. You can also use this to add extra filters to the query, but only when they are required.</p>
<p>To start with, lets see how we would do this normally with SQL. Assuming we&#8217;re working from a denormalised table such as this</p>
<table width="100%" align="center">
<tbody>
<tr>
<td align="center"><img src="/images/blog/DynamicMDXtable.png" alt="Dynamic MDX Table" /></td>
</tr>
</tbody>
</table>
<p>Dynamic SQL is pretty simple, instead of having your dataset query as</p>
<pre><span style="color: #008000;">  SELECT SalesPerson,
      Sum(Sales) AS Sales
   FROM tblData
   GROUP BY SalesPerson
</span></pre>
<p>you can add a report parameter called GroupBy,</p>
<table width="100%" align="center">
<tbody>
<tr>
<td align="center"><img src="/images/blog/DynamicMDXSQL.png" alt="Dynamic MDX SQL" /></td>
</tr>
</tbody>
</table>
<p>and then use an expression as your dataset</p>
<pre><span style="color: #008000;">  ="SELECT "
      + Parameters!GroupBy.Value + " AS GroupName,
      Sum(Sales) AS Sales
   FROM tblData
   GROUP BY " + Parameters!GroupBy.Value
</span></pre>
<p>However MDX queries don&#8217;t let you use an expression in the dataset, so we have to work around that quite major limitation. To do this we make use of the OpenRowset command. You need to enable it in the surface area config tool, but once it&#8217;s enabled you can fire off an OpenRowset command to SQL Server, which will then pass it on to the cube. As the datasource connnection is to SQL Server not Analysis Services, it allows you to use an expression in the dataset.</p>
<pre><span style="color: #008000;">  ="SELECT * FROM OpenRowset('MSOLAP',
    'DATA SOURCE=localhost; Initial Catalog=SalesTest;',
    'SELECT
      {[Measures].[Sales]} ON 0,
      NON EMPTY {[Product].[Product].[Product].Members} ON 1
      FROM Sales') "
</span></pre>
<p>You can then expand this to make it dynamic depending on the value of a parameter. Before we do this though, there are a couple of items I should point out.<br />
1) As the expression can get quite large, I find it much easier to create the query from a custom code function<br />
2) As SSRS can&#8217;t interpret the expression at runtime, you need to define the fields in your dataset manually (more on this later)</p>
<p>To use a custom code function, just change the dataset expression to</p>
<pre><span style="color: #008000;">  =Code.CreateMDX(Parameters)</span></pre>
<p>We pass in the parameters collection so that we can use the parameters to determine the query. Create a function called CreateMDX() in the code block</p>
<table width="100%" align="center">
<tbody>
<tr>
<td align="center"><img src="/images/blog/DynamicMDXCode.png" alt="Dynamic MDX Code" /></td>
</tr>
</tbody>
</table>
<p>You can then construct your MDX query within the code block.</p>
<pre><span style="color: #008000;">  Public Function CreateMDX(ByVal params as Parameters) as string

   Dim mdx as string 

   mdx = "SELECT * FROM OpenRowset("
   mdx += " 'MSOLAP', "
   mdx += " 'DATA SOURCE=localhost; Initial Catalog=SalesTest;', "
   mdx += " ' SELECT {[Measures].[Sales]} ON 0, "
   mdx += "    NON EMPTY {[Product].[Product].[Product].Members} ON 1 "
   mdx += "   FROM Sales ' "
   mdx += ")"

   return mdx

End Function
</span></pre>
<p>We&#8217;re almost there&#8230;<br />
The next problem is that the field names returned by the query are less than helpful. To fix this we just need to alias the fields in the query. I usually take the opportunity of casting the numerical fields so that the report treats them as such, rather than as a string.</p>
<pre><span style="color: #008000;">  Public Function CreateMDX(ByVal params as Parameters)
                  as string

   Dim mdx as string 

   mdx = "SELECT "
   mdx += "  ""[Product].[Product].[Product].[MEMBER_CAPTION]"" AS GroupName, "
   mdx += "   Cast(""[Measures].[Sales]"" AS int) AS Sales "
   mdx += " FROM OpenRowset("
   mdx += " 'MSOLAP', "
   mdx += " 'DATA SOURCE=localhost; Initial Catalog=SalesTest;', "
   mdx += " ' SELECT {[Measures].[Sales]} ON 0, "
   mdx += "    NON EMPTY {[Product].[Product].[Product].Members} ON 1 "
   mdx += "   FROM Sales ' "
   mdx += ")"

   return mdx

End Function
</span></pre>
<p>(please do watch out for the quotes, double quotes and double double quotes, it can get a little confusing!)<br />
We then need to tell the dataset which fields to expect from the query.</p>
<table width="100%" align="center">
<tbody>
<tr>
<td align="center"><img src="/images/blog/DynamicMDXFields.png" alt="Dynamic MDX Fields" /></td>
</tr>
</tbody>
</table>
<p>You can now use the dataset in your report.<br />
However, the original point of this was to make the query dynamic&#8230; All we need to do to achieve this is expand the VB.Net code accordingly.</p>
<pre><span style="color: #008000;">  Public Function CreateMDX(ByVal params as Parameters) as string

   Dim mdx as string 

   mdx = "SELECT "

IF params("GroupBy").Value.ToString()="Product" THEN
   mdx += "  ""[Product].[Product].[Product]"
ELSE IF params("GroupBy").Value.ToString()="SalesPerson" THEN
   mdx += "  ""[Sales Person].[Sales Person].[Sales Person]"
ELSE IF params("GroupBy").Value.ToString()="Customer" THEN
   mdx += "  ""[Customer].[Customer].[Customer]"
END IF

   mdx += ".[MEMBER_CAPTION]"" AS GroupName, " 

   mdx += "   Cast(""[Measures].[Sales]"" AS int) AS Sales "
   mdx += " FROM OpenRowset("
   mdx += " 'MSOLAP', "
   mdx += " 'DATA SOURCE=localhost; Initial Catalog=SalesTest;', "
   mdx += " ' SELECT {[Measures].[Sales]} ON 0, "

IF params("GroupBy").Value.ToString()="Product" THEN
   mdx += "  NON EMPTY {[Product].[Product].[Product]"
ELSE IF params("GroupBy").Value.ToString()="SalesPerson" THEN
   mdx += "  NON EMPTY {[Sales Person].[Sales Person].[Sales Person]"
ELSE IF params("GroupBy").Value.ToString()="Customer" THEN
   mdx += "  NON EMPTY {[Customer].[Customer].[Customer]"
END IF

   mdx += ".Members} ON 1 "

   mdx += "   FROM Sales ' "
   mdx += ")"

   return mdx

End Function
</span></pre>
<p>It&#8217;s certainly not that simple, and debugging can cause a few headaches, but you can benefit from a massive performance in complex reports if you&#8217;re prepared to put the work in.</p>
<table width="100%" align="center">
<tbody>
<tr>
<td align="center"><img src="/images/blog/DynamicMDXResults.png" alt="Dynamic MDX Results" /></td>
</tr>
</tbody>
</table>
<p>You can download the project files <a href="/Download/blog/DynamicMDX.zip"><strong>here</strong></a></p>
<p>As always, please let me know how you get on with it, and shout if you have any queries&#8230;</p>
<p>Alex</p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2008/09/dynamic-mdx-in-reporting-services/feed/</wfw:commentRss>
		<slash:comments>32</slash:comments>
		</item>
		<item>
		<title>Mosha&#8217;s MDX Studio</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2008/09/moshas-mdx-studio/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2008/09/moshas-mdx-studio/#comments</comments>
		<pubDate>Fri, 12 Sep 2008 16:03:38 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[Cube]]></category>
		<category><![CDATA[MDX]]></category>
		<category><![CDATA[Mosha]]></category>
		<category><![CDATA[OLAP]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=17</guid>
		<description><![CDATA[I almost feel embarrassed&#8230;, I&#8217;ve been writing this blog for over 9 months now, and I have yet to mention Mosha, although in my defence, there is a link to his blog in the links section to the right.
As many/most of you may know, Mosha Pasumansky is one the key brains behind designing the MDX [...]]]></description>
			<content:encoded><![CDATA[<p>I almost feel embarrassed&#8230;, I&#8217;ve been writing this blog for over 9 months now, and I have yet to mention Mosha, although in my defence, there is a link to his <a href="http://sqlblog.com/blogs/mosha/" target="_blank">blog</a> in the links section to the right.</p>
<p>As many/most of you may know, Mosha Pasumansky is one the key brains behind designing the MDX language and Analysis Services &#8211; nuff said?</p>
<p>Over the last year he has been working on a pet project, MDX Studio. It&#8217;s an MDX query tool which any self respecting OLAP developer should now be using on a regular basis. He has just released v0.4.6, which adds some really nifty features such as the dependency view.</p>
<p>If you&#8217;re just starting out with MDX, then the intellisense will be of massive benefit to you; even if you&#8217;re a seasoned pro, the performance monitoring is an essential tool on its own.</p>
<p>If you haven&#8217;t already tried it, have a look at <a href="http://sqlblog.com/blogs/mosha/archive/2008/09/07/inspecting-calculation-dependencies-with-mdx-studio.aspx" target="_blank">Mosha&#8217;s blog</a>, and get a copy &#8211; you won&#8217;t regret it.</p>
<p>And thanks for all your hard work Mosha &#8211; It&#8217;s much appreciated.</p>
<p>Alex</p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2008/09/moshas-mdx-studio/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ranking results from MDX queries</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2008/05/ranking-results-from-mdx-queries/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2008/05/ranking-results-from-mdx-queries/#comments</comments>
		<pubDate>Wed, 21 May 2008 20:36:08 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[MDX]]></category>
		<category><![CDATA[OLAP]]></category>
		<category><![CDATA[Ranking]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=14</guid>
		<description><![CDATA[This post explains how you can create a ranking of data from an OLAP MDX query. This will take the results from the query, and assign a ranking to each row. i.e. 1st, 2nd, 3rd best rows etc.
The first thing to do is to decide two things.
1) What measure do you want to rank by
2) [...]]]></description>
			<content:encoded><![CDATA[<p>This post explains how you can create a ranking of data from an OLAP MDX query. This will take the results from the query, and assign a ranking to each row. i.e. 1st, 2nd, 3rd best rows etc.</p>
<p>The first thing to do is to decide two things.<br />
1) What measure do you want to rank by<br />
2) What data set are you returning</p>
<p>Let&#8217;s assume we want to rank all stores by sales value. The basic non-ranked MDX query would be something like this</p>
<pre><span style="color: #008000;">  SELECT
    {[Measures].[Sales Value]} ON 0,
    {[Store].[Store Name].members} ON 1
  FROM
    [SalesCube]
</span></pre>
<p>So our measure is Sales Value, and our data set (granularity) is Store Name. We now want to create an ordered set of this data, ordered by Sales Value. We do this with the ORDER() function, which takes a set, a measure and either ascending or descending as its parameters. Note that by specifying the attribute twice we remove the [All] member from the set.</p>
<pre><span style="color: #008000;">  WITH SET [OrderedSet] AS
    ORDER([Store].[Store Name].[Store Name].MEMBERS,
	  [Measures].[Sales Value],
	  BDESC)
  SELECT
    {[Measures].[Sales Value]} ON 0,
    {[OrderedSet]} ON 1
  FROM
    [SalesCube]
</span></pre>
<p>The next stage is to apply a ranking to this ordered set. Helpfully, MDX provides us with a Rank() function, which takes a member and a set as its parameters. All it does is locate the member within the set and return its position. Because we have ordered the set, it will give us the ranking.</p>
<pre><span style="color: #008000;">  WITH SET [OrderedSet] AS
    ORDER([Store].[Store Name].[Store Name].MEMBERS,
	  [Measures].[Sales Value],
	  BDESC)
  MEMBER [Measures].[Rank] AS
    RANK([Store].[Store Name].CurrentMember,
	 [OrderedSet])
  SELECT
    {[Measures].[Rank], [Measures].[Sales Value]} ON 0,
    {[OrderedSet]} ON 1
  FROM
    [SalesCube]
</span></pre>
<p>You can now easily expand this to only show you the top x records, by using the Head() function on the ordered set. In this example we&#8217;re only showing the top 10. You could also use Tail() to find the bottom x records. Other functions you can use include TopPercent(), TopSum(), BottomPercent() and BottomSum().</p>
<pre><span style="color: #008000;">  WITH SET [OrderedSet] AS
    ORDER([Store].[Store Name].[Store Name].MEMBERS,
	  [Measures].[Sales Value],
	  BDESC)
  MEMBER [Measures].[Rank] AS
    RANK([Store].[Store Name].CurrentMember,
	 [OrderedSet])
  SELECT
    {[Measures].[Rank], [Measures].[Sales Value]} ON 0,
    {HEAD([OrderedSet], 10)} ON 1
  FROM
    [SalesCube]
</span></pre>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2008/05/ranking-results-from-mdx-queries/feed/</wfw:commentRss>
		<slash:comments>13</slash:comments>
		</item>
		<item>
		<title>Semi Additive Measures using SQL Server Standard</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2008/04/semi-additive-measures-using-sql-server-standard/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2008/04/semi-additive-measures-using-sql-server-standard/#comments</comments>
		<pubDate>Mon, 21 Apr 2008 12:54:36 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Cube]]></category>
		<category><![CDATA[MDX]]></category>
		<category><![CDATA[OLAP]]></category>
		<category><![CDATA[Semi Additive]]></category>

		<guid isPermaLink="false">http://www.purplefrogsystems.com/blog/?p=12</guid>
		<description><![CDATA[One of the most frustrating limitations of SQL Server 2005 Standard edition is that it doesn&#8217;t support semi additive measures in SSAS Analysis Services cubes. This post explains a work around that provides similar functionality without having to shell out for the Enterprise Edition.
What Are Semi Additive Measures?
Semi Additive measures are values that you can [...]]]></description>
			<content:encoded><![CDATA[<p>One of the most frustrating limitations of SQL Server 2005 Standard edition is that it doesn&#8217;t support semi additive measures in SSAS Analysis Services cubes. This post explains a work around that provides similar functionality without having to shell out for the Enterprise Edition.</p>
<p><strong>What Are Semi Additive Measures?</strong></p>
<p>Semi Additive measures are values that you can summarise across any related dimension except time.</p>
<p>For example, Sales and costs are fully additive; if you sell 100 yesterday and 50 today then you&#8217;ve sold 150 in total. You can add them up over time.</p>
<p>Stock levels however are semi additive; if you had 100 in stock yesterday, and 50 in stock today, you&#8217;re total stock is 50, not 150. It doesn&#8217;t make sense to add up the measures over time, you need to find the most recent value.</p>
<p><strong>Why are they important?</strong></p>
<p>Whether they are important to you or not depends entirely on what you are trying to do with your cube. If all of your required measures are fully additive then you really don&#8217;t need to worry about anything. However as soon as you want to include measures such as stock levels, salarys, share prices or test results then they become pretty much essential.</p>
<p><strong>Why are they not available in SQL Standard edition?</strong></p>
<p>Microsoft has to have some way of pursuading us to pay for the Enterprise edition!</p>
<p><strong>How can I get this functionality within SQL Standard?</strong></p>
<p>Firstly we need to understand what semi additive measures do. By far the most common aggregation used is the LastNonEmpty function, so we&#8217;ll stick with that as an example. This basically says that whatever time frame you are looking at, find the most recent value for each tuple. This really is a fantastically powerful function, which only really becomes apparent whan you don&#8217;t have it!</p>
<p>Lets say that you perform a stock take of different products on different days of the week. You will have a stock entry for product A on a Thursday and product B on a Friday. The LastNonEmpty function takes care of this for you, if you look at the stock level on Saturday it will give you the correct values for both A and B, even though you didn&#8217;t perform a physical stock take on the Saturday.</p>
<p>If you then add the time dimension into the query, SSAS will perform this function for each and every time attribute shown, and then aggregate the results up to any other dimensions used. i.e. Each month will then display the sum of all LastNonEmpty values for all products within that month, essentially the closing stock level for each and every month.</p>
<p>To replicate this in Standard Edition, we need to split the work up into two stages.<br />
1) Create daily values in the data warehouse<br />
2) Use MDX to select a single value from the time dimension.</p>
<p>Think of this as splitting up the LastNonEmpty function into two, &#8216;Last&#8217; and &#8216;Non Empty&#8217;. The &#8216;Non Empty&#8217; bit essentially fills in the blanks for us. If a value doesn&#8217;t exist for that particular day, it looks at the previous day&#8217;s value. The &#8216;Last&#8217; bit says that if we are looking at months in our query, find the value for the last day in that month. The same goes for years, or indeed any other time attribute.</p>
<p>To code up a full LastNonEmpty function ourselves in MDX would be too slow to query as soon as you get a cube of any reasonable size. One of the key benefits of a cube is speed of querying data and we don&#8217;t want to impact this too much, therefore we move some of the donkey work into the ETL process populating the datawarehouse. This leaves the cube to perform a simple enough calculation so as to not cause any problems.</p>
<p><strong>1) The &#8216;Non Empty&#8217; bit</strong></p>
<p>Lets say that have a table called tblStock, containing the following data</p>
<p style="text-align: center;"><img src="http://www.purplefrogsystems.com/images/blog/tblStock.png" alt="" /></p>
<p>We need to expand this into a new fact table that contains one record per day per product.</p>
<p style="text-align: center;"><img src="http://www.purplefrogsystems.com/images/blog/FactStock.png" alt="" /></p>
<p>There are a number of ways of doing this, I&#8217;ll describe one here that should suit most situations, although you may need to customise it to your own situation, and limit it to only updating changed/new records rather than re-populating the entire table, but you get the idea. I should point out that you would be much better off populating this as part of your ETL process, but I&#8217;m showing this method as it&#8217;s more generic.</p>
<p>You need a list of all available dates relevant to your data warehouse or cube. If you already have a time dimension table then use this, otherwise create a SQL function that returns you a list of dates, such as this one:</p>
<pre><span style="color: #008000;">
   CREATE FUNCTION [dbo].[FN_ReturnAllDates](
         @DateFrom DateTime, @DateTo DateTime)
         RETURNS @List TABLE (Date DateTime)
     BEGIN
     DECLARE @tmpDate DateTime
     SET @tmpDate = @DateFrom
     WHILE @tmpDate&lt;=@DateTo
       BEGIN
         INSERT INTO @List
           SELECT Convert(datetime,
                 Convert(Nvarchar,@tmpDate, 102), 102)
         SET @tmpDate = Dateadd(d,1,@tmpDate)
       END
     RETURN
   END
</span></pre>
<p>We need to perform a full outer join between the date dimension and any other relevant dimensions, in this case product. This will generate one record per product per date. We can then perform a sub query for each combination to find the stock level appropriate for that day. (Yes, this will be a slow query to run &#8211; I did say you should do it in your ETL process!)</p>
<pre><span style="color: #008000;">
     INSERT INTO FactStock
        (StockTakeDate, ProductID, StockLevel)
     SELECT D.Date, P.ProductID,
           ISNULL((SELECT TOP 1 StockLevel
              FROM tblStock
              WHERE ProductID = P.ProductID
                 AND StockTakeDate&lt;=D.Date
              ORDER BY StockTakeDate DESC),0)
        FROM FN_ReturnAllDates((SELECT Min(StockTakeDate)
                      FROM tblStock),GetDate()) D
           FULL OUTER JOIN
                    (SELECT ProductID FROM tblProduct) P ON 1=1
</span></pre>
<p><strong>2) The &#8216;Last&#8217; bit</strong></p>
<p>Now that we have a large fact table consisting of one record per product/date, we can load this into the cube.</p>
<p>If you just add the StockLevel field as a measure and browse the results, you&#8217;ll quickly see that if you view it by month, you will get each day&#8217;s stock level added together giving you a non-sensical value. To fix this we need to tell Analysis Services to only show one day&#8217;s value.</p>
<p>To do this we first need to find all descendents of the current time member at the day level, using something like this:</p>
<pre><span style="color: #008000;">     DESCENDANTS([Time].[Year Month Day].CurrentMember,
       [Time].[Year Month Day].[Day])
       --Please modify to suit your own date hierarchy! </span></pre>
<p>We can then find the last member (giving us the closing stock level) by using TAIL():</p>
<pre><span style="color: #008000;">     TAIL(DESCENDANTS([Time].[Year Month Day].CurrentMember,
          [Time].[Year Month Day].[Day]))</span></pre>
<p>You could aso use HEAD() if you wanted to find the opening stock instead of closing.</p>
<p>You should hide the actual StockLevel measure to prevent users from selecting it, I usually alias these with an underscore, as well as making them invisible, just for clarity. You can then add a calculated member with the following MDX:</p>
<pre><span style="color: #008000;">
     CREATE MEMBER CURRENTCUBE.[MEASURES].[Stock Level Close]
      AS SUM(TAIL(DESCENDANTS([Time].[Year Month Day].currentmember,
                    [Time].[Year Month Day].[Day])),
                    [Measures].[_Stock Level]),
     FORMAT_STRING = "#,#",
     VISIBLE = 1  ;
</span></pre>
<p>Or you can calculate the average stock over the selected period</p>
<pre><span style="color: #008000;">
     CREATE MEMBER CURRENTCUBE.[MEASURES].[Stock Level Avg]
      AS AVG(DESCENDANTS([Time].[Year Month Day].currentmember,
                   [Time].[Year Month Day].[Day]),
                   [Measures].[_Stock Level]),
     FORMAT_STRING = "#,#",
     VISIBLE = 1  ;
</span></pre>
<p>Or the maximum value</p>
<pre><span style="color: #008000;">
     CREATE MEMBER CURRENTCUBE.[MEASURES].[Stock Level Max]
      AS MAX(DESCENDANTS([Time].[Year Month Day].currentmember,
                   [Time].[Year Month Day].[Day]),
                   [Measures].[_Stock Level]),
     FORMAT_STRING = "#,#",
     VISIBLE = 1  ;
</span></pre>
<p>Or the mimimum value</p>
<pre><span style="color: #008000;">
     CREATE MEMBER CURRENTCUBE.[MEASURES].[Stock Level Min]
      AS MIN(DESCENDANTS([Time].[Year Month Day].currentmember,
                  [Time].[Year Month Day].[Day]),
                  [Measures].[_Stock Level]),
     FORMAT_STRING = "#,#",
     VISIBLE = 1  ;
</span></pre>
<p>And there you have it, <strong><span>semi additive measures in SQL Server 2005 Standard Edition!</span></strong></p>
<p>Even though this method does work well, it is still not as good as having the Enterprise edition. The built in functions of Enterprise will perform significantly better than this method, and it saves having to create the large (potentially huge) fact table. This process will also only work on a single date hierarchy. If you have multiple hierarchies (i.e. fiscal and calendar) you will need to enhance this somewhat.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2008/04/semi-additive-measures-using-sql-server-standard/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Excel Addin for Analysis Services</title>
		<link>http://www.purplefrogsystems.com/blog/index.php/2007/12/excel-addin-for-analysis-services/</link>
		<comments>http://www.purplefrogsystems.com/blog/index.php/2007/12/excel-addin-for-analysis-services/#comments</comments>
		<pubDate>Sat, 15 Dec 2007 21:00:58 +0000</pubDate>
		<dc:creator>Alex</dc:creator>
				<category><![CDATA[Analysis Services]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[Cube]]></category>
		<category><![CDATA[Excel]]></category>
		<category><![CDATA[OLAP]]></category>

		<guid isPermaLink="false">http://draft.purplefrogsystems.com/blog/?p=6</guid>
		<description><![CDATA[For any users of Analysis Services, if you haven&#8217;t already downloaded the Excel (2002/2003) addin you&#8217;re missing out.
It&#8217;s a free download from Microsoft which significantly expands Excel&#8217;s cube querying ability. Well recommended!
Get it here&#8230;
]]></description>
			<content:encoded><![CDATA[<p>For any users of Analysis Services, if you haven&#8217;t already downloaded the Excel (2002/2003) addin you&#8217;re missing out.</p>
<p>It&#8217;s a free download from Microsoft which significantly expands Excel&#8217;s cube querying ability. Well recommended!</p>
<p><a href="http://www.microsoft.com/downloads/details.aspx?FamilyId=DAE82128-9F21-475D-88A4-4B6E6C069FF0&amp;displaylang=en">Get it here&#8230;</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.purplefrogsystems.com/blog/index.php/2007/12/excel-addin-for-analysis-services/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
