Anyway, as I know your time is precious, I will try to sum up its purpose for you: “The Pearson Correlation Coefficient calculates the correlation between two variables over a given set of items. The result is a number between -1 and 1. A value higher than 0.5 (or lower than –0.5) indicate a strong relationship whereas numbers towards 0 imply weak to no relationship.”

The two values we want to correlate are our axes, whereas the single dots represent our set of items. The PCC calculates the trend within this chart represented as an arrow above.

The mathematical formula that defines the Pearson Correlation Coefficient is the following:

The PCC can be used to calculate the correlation between two measures which can be associated with the same customer. A measure can be anything here, the age of a customer, it’s sales, the number of visits, etc. but also things like sales with red products vs. sales with blue products. As you can imagine, this can be a very powerful statistical KPI for any analytical data model. To demonstrate the calculation we will try to correlate the order quantity of a customer with it’s sales amount. The order quantity will be our [MeasureX] and the sales will be our [MeasureY], and the set that we will calculate the PCC over are our customers. To make the whole calculation more I split it up into separate measures:

- MeasureX := SUM(‘Internet Sales’[Order Quantity])
- MeasureY := SUM(‘Internet Sales’[Sales Amount])

Based on these measures we can define further measures which are necessary for the calculation of our PCC. The calculations are tied to a set if items, in our case the single customers:

- Sum_XY := SUMX(VALUES(Customer[Customer Id]), [MeasureX] * [MeasureY])
- Sum_X2 := SUMX(VALUES(Customer[Customer Id]), [MeasureX] * [MeasureX])
- Sum_Y2 := SUMX(VALUES(Customer[Customer Id]), [MeasureY] * [MeasureY])
- Count_Items := DISTINCTCOUNT(Customer[Customer Id])

Now that we have calculated the various summations over our base measures, it is time to create the numerator and denominator for our final calculation:

- Pearson_Numerator :=
- ([Count_Items] * [Sum_XY]) – ([MeasureX] * [MeasureY])
- Pearson_Denominator_X :=
- ([Count_Items] * [Sum_X2]) – ([MeasureX] * [MeasureX])
- Pearson_Denominator_Y :=
- ([Count_Items] * [Sum_Y2]) – ([MeasureY] * [MeasureY])
- Pearson_Denominator :=
- SQRT([Pearson_Denominator_X] * [Pearson_Denominator_Y])

Having these helper-measures in place the final calculation for our PCC is straight forward:

- Pearson := DIVIDE([Pearson_Numerator], [Pearson_Denominator])

This [Pearson]-measure can then be used together with any attribute in our model – e.g. the Calendar Year in order to track the changes of the Pearson Correlation Coefficient over years:

For those of you who are familiar with the Adventure Works sample DB, this numbers should not be surprising. In 2005 and 2006 the Adventure Works company only sold bikes and usually a customer only buys one bike – so we have a pretty strong correlation here. However, in 2007 they also started selling Clothing and Accessories which are in general cheaper than Bikes but are sold more often.

This has impact on our Pearson-value which is very obvious in the screenshots above.

As you probably also realized, the Grand Total of our Pearson calculation cannot be directly related to the single years and may also be the complete opposite of the single values. This effect is called Simpson’s Paradox and is the expected behavior here.

[MeasuresX] and [MeasureY] can be exchanged by any other DAX measures which makes this calculation really powerful. Also, the set of items over which we want to calculated the correlation can be exchanged quite easily. Below you can download the sample Excel workbook but also a DAX query which could be used in Reporting Services or any other tool that allows execution of DAX queries.

Sample Workbook (Excel 2013): Pearson.xlsx

DAX Query: Pearson_SSRS.dax

For those of you who are new to the concept of PASS SQL Saturdays, this is a series of free-of-charge events all around the globe where experienced speakers talk about all topics around the Microsoft SQL Server platform and beyond. As I said, its free, you just need to register in time in order to get a ticked so better be fast before all slots are taken!

I will do a session together with my colleague Markus Begerow (b, t) on “Power BI on SAP HANA” – two technologies I got to work a lot with recently. We are going to share our experience on how to use Power BI to extract data from SAP HANA, the different interfaces you can use and the advantages and drawbacks of each. Even tough it is considered a general sessions, we will also do a lot of hands on and elaborate on some of the technical details you need to be aware of, for both, the Power BI side and also for SAP HANA.

In Bratislava I will speak about Lessons Learned: SSAS Tabular in the real world where I will present the technical and non-technical findings I made in the past when implementing SSAS Tabular models at larger scales for various customers. I will cover the whole process from choosing SSAS Tabular as your engine (or not choosing it), things to consider during implementation and also shed some light on the administrative challenges once the solution is in production.

I think both are really interesting sessions and I would be happy to see a lot of you there and have some interesting discussions!

]]>- Sales ForeCast :=
- IF (
- NOT ( ISBLANK ( [Sales] ) ),
- [Sales],
- CALCULATE (
- [Sales ForeCast],
- DATEADD ( 'Date'[Calendar], –1, MONTH )
- ) * 1.05
- )

However, in DAX you would end up with the following error:

A circular dependency was detected: ‘Sales'[Sales ForeCast],’Sales'[Sales ForeCast]. |

This makes sense as you cannot reference a variable within its own definition – e.g. X = X + 1 cannot be defined from a mathematical point of view (at least according to my limited math skills). MDX is somehow special here where the SSAS engine takes care of this recursion by taking the IF() into account.

So where could you possible need a recursive calculation like this? In my example I will do some very basic forecasting based on monthly growth rates. I have a table with my actual sales and another table for my expected monthly growth as percentages. If I do not have any actual sales I want to use my expected monthly growth to calculate my forecast starting with my last actual sales:

This is a very common requirement for finance applications, its is very easy to achieve in pure Excel but very though to do in DAX as you probably realized on your own what finally led you here

In Excel we would simply add a calculation like this and propagate it down to all rows:

(assuming column C contains your Sales, D your Planned Growth Rate and M is the column where the formula itself resides)

In order to solve this in DAX we have to completely rewrite our calculation! The general approach that we are going to use was already explained by Mosha Pasumansky some years back, but for MDX. So I adopted the logic and changed it accordingly to also work with DAX. I split down the solution into several steps:

1) find the last actual sales – April 2015 with a value of 35

2) find out with which value we have to multiply our previous months value to get the current month’s Forecast

3) calculate the natural logarithm (DAX LN()-function) of the value in step 2)

4) Sum all values from the beginning of time until the current month

5) Raise our sum-value from step 4) to the power of [e] using DAX EXP()-function

6) do some cosmetic and display our new value if no actual sales exist and take care of aggregation into higher levels

**Note:** The new Office 2016 Preview introduces a couple of new DAX functions, including PRODUCTX() which can be used to combine the Steps 3) to 5) into one simple formula without using any complex LN() and EXP() combinations.

**Step 1:**We can use this formula to get our last sales:

- Last Sales :=
- IF (
- ISBLANK (
- CALCULATE (
- [Sales],
- DATEADD ( 'Date'[DateValue], 1, MONTH )
- )
- ),
- [Sales],
- 1
- )

It basically checks if there are no [Sales] next month. If yes, we use the current [Sales]-value as our [Last Sales], otherwise we use a fixed value of 1 as a multiplication with 1 has no impact on the final result.

**Step 2:**

Get our multiplier for each month:

- MultiplyBy :=
- IF (
- ISBLANK ( [Last Sales] ),
- 1 + [Planned GrowthRate],
- [Last Sales]
- )

If we do not have any [Last Sales], we use our [Planned GrowthRate] to for our later multiplication/summation, otherwise take our [Last Sales]-value.

**Step 3 and 4:**As we cannot use “Multiply” as our aggregation we first need to calculate the LN and sum it up from the first month to the current month:

- Cumulated LN :=
- CALCULATE (
- SUMX ( VALUES ( 'Date'[Month] ), LN ( [MultiplyBy] ) ),
- DATESBETWEEN (
- 'Date'[DateValue],
- BLANK (),
- MAX ( 'Date'[DateValue] )
- )
- )

**Step 5 and 6:**If there are no actual sales, we display our calculated Forecast:

- Sales ForeCast :=
- SUMX (
- VALUES ( 'Date'[Month] ),
- IF ( ISBLANK ( [Sales] ), EXP ( [Cumulated LN] ), [Sales] )
- )

Note that we need to use SUMX over our Months here in order to also get correct subtotals on higher levels, e.g. Years. That’s all the SUMX is necessary for, the IF itself should be self-explaining here.

So here is the final result – check out the last column:

The calculation is flexible enough to handle missing sales. So if for example we would only have sales for January, our recursion would start there and use the [Planned GrowthRate] already to calculate the February Forecast-value:

Quite handy, isn’t it?

The sample-workbook (Excel 365) can be downloaded here: RecursiveCalculations.xlsx

]]>This produces the following MDX query:

- SELECT
- {
- (
- [Measures].[Internet Sales Amount],
- [Product].[Category].&[1]
- )
- } ON 0
- FROM [Adventure Works]
- CELL PROPERTIES VALUE, FORMAT_STRING, LANGUAGE, BACK_COLOR, FORE_COLOR, FONT_FLAGS

So it basically creates a tuple that contains everything you pass into the CUBEVALUE-Function as a parameter. Knowing this we can create a calculated measure to get the MDX UniqueName of this tuple using MDX StrToTuple()- and MDX AXIS()-function:

- MEMBER [Measures].[Excel TupleToStr] AS (
- TupleToStr(axis(0).item(0))
- )

Replacing the [Measures].[Internet Sales Amount] of our initial CUBEVALUE-function with this new measure would return this to Excel:

- ([Measures].[Internet Sales Amount],[Product].[Category].&[1])

Ok, so far so good but nothing really useful as you need to hardcode the member’s UniqueName into the CUBEVALUE-function anyway so you already know the UniqueName.

However, this is not the case if you are dealing with Pivot Table Page Filters and/or Slicers! You can simply refer to them within the CUBEVALUE-function but you never get the UniqueName of the selected item(s). Well, at least not directly! But you can use the approach described above, using an special MDX calculated measure, to achieve this as I will demonstrate on the next pages.

Calculated measures can only be created using the Pivot Table interface but can also be used in CUBE-functions. So first thing you need to do is to create a Pivot Table and add a new MDX Calculated Measure:

**!Caution!** some weird MDX coming **!Caution!**

You may wonder, why such a complex MDX is necessary and what it actually does. What it does is the following: Based on the example MDX query that Excel generates (as shown above) this is a universal MDX that returns the MDX UniqueName of any other member that is selected together with our measure using the CUBEVALUE-function. It also removes the UniqueName of the measure itself so the result can be used again with any other measure, e.g. [Internet Sales Amount]

The reason why it is rather complex is that Excel may group similar queries and execute them as a batch/as one query to avoid too many executions which would slow down the overall performance. So we cannot just reference the first element of our query as it may belong to any other CUBEVALUE-function. This MDX deals with all this kinds of issues.

The MDX above allows you to specify only two additional filters but it may be extended to any number of filters that you pass in to the CUBEMEMBER-function. This would be the general pattern:

- MID(
- IIf(axis(0).item(0).count > 0 AND
- NOT(axis(0).item(0).item(0).hierarchy IS [Measures]),
- "," + axis(0).item(0).item(0).hierarchy.currentmember.uniquename,
- "")
- + IIf(axis(0).item(0).count > 1 AND
- NOT(axis(0).item(0).item(1).hierarchy IS [Measures]),
- "," + axis(0).item(0).item(1).hierarchy.currentmember.uniquename,
- "")
- + IIf(axis(0).item(0).count > n AND
- NOT(axis(0).item(0).item(n).hierarchy IS [Measures]),
- "," + axis(0).item(0).item(n).hierarchy.currentmember.uniquename,
- "")
- , 2)

After creating this measure we can now use it in our CUBE-functions in combination with our filters and slicers:

You may noted that I had to use CUBERANKEDMEMBER here. This is because filters and slicers always return a set and if we would pass in a set to our CUBEVALUE function a different MDX query would be generated which would not allow us to extract the single UniqueNames of the selected items using the approach above (or any other MDX I could think of). So, this approach currently **only works with single selections**! I hope that the Excel team will implement a native function to extract the UniqueName(s) of the selected items in the future to make this workaround obsolete!

Once we have our UniqeName(s) we can now use them in e.g. a CUBESET-function to return the Top 10 days for a given group of product (filter) and the selected year (slicer):

And that’s it!

So why is this so cool?

- It works with SSAS (multidimensional and tabular) and Power Pivot as Excel still uses MDX to query all those sources. It may also work with SAP HANA’s ODBO connector but I have not tested this yet!
- It does not require any VBA which would not work in Excel Services – this solution does!
- The calculation is stored within the Excel Workbook so it can be easily shared with other users!
- There is no native Excel functionality which would allow you to create a simple Top 10 report which works with filters and slicers as shown above or any more complex dynamic report/dashboard with any dynamic filtering.

So no more to say here – Have fun creating your interactive Excel web dashboards!

Download sample Workbook: Samples.xlsx

** Note:** You may also rewrite any TOPCOUNT expression and use the 4th and 5h parameter of the CUBESET-function instead. This is more native and does not require as much MDX knowledge:

However, if you are not familiar with MDX, I highly recommend to learn it before you write any advanced calculations as show above as otherwise the results might be a bit confusing in the beginning! Especially if you filter and use TOPCOUNT on the same dimension!

1) the formula was quite complex and not easy to understand or implement

2) the performance was not really great with bigger datasets

When the first of those comments flew in, I started investigating into a new, advanced formula. At about the same time Marco Russo and Alberto Ferrari published their ABC Classification pattern – a static version using calculated columns – at www.daxpatterns.com. When I had my first dynamic version ready I sent it to Marco and Alberto and asked if they are interested in the pattern and if I can publish it on their website. Long story short – this week the new pattern was released and can now be found here:

It got some major performance improvements and was also designed towards reusability with other models. The article also contains some detailed explanations how the formula actually works but its still very hard DAX which will take some time to be fully understood. The pattern also features some extended versions to address more specific requirements but I think its best to just read the article on your own.

Hope you enjoy it!

]]>Below you can find a short overview followed by some more details:

What? |
When? |
Where? |
Am I there? |

German SQL Server Conference | February 3-5th | Darmstadt, Germany | Yes, I am speaking! |

SQL Saturday Vienna | February 27-28th | Vienna, Austria | Maybe, but not speaking |

SQLRally Nordic Copenhagen | March 2-4th | Copenhagen, Denmark | Yes, I am speaking! |

SQLBits XIV Superheroes | March 4-7th | London, UK | Yes, I am speaking! |

__UPDATE 2014-01-13:__

I just received a confirmation that my session “Power BI on SAP HANA” was accepted for SQL Bits XIV!

It’s also the first time that I will do a session together with a Co-Speaker, my colleague Markus Begerow (b)

It starts with the German SQL Server Conference 2015 on February 3-5th

I am also very happy that my session “Load testing Analysis Services” was selected and I will be speaking the second time in a row at this awesome conference which is also the biggest German SQL Server Conference out there. And now worries, there are also a lot of English session in case you do not speak German

Up next is a true marathon of conferences starting with the SQL Saturday #374 in Vienna on 28th of February.

This year also featuring Pre-Cons on 27th of February!

Reza Rad (t, b) and Leila Etaati (t, b) will be speaking on “Power BI from Rookie to Rockstar” and Dejan Sarka (t, b) on “Advanced Data Modeling”

Last year it was fully booked pretty soon and we had a long waiting list so better do your reservation now!

The schedule can be found here and features 20 sessions of well-know SQL Server professionals.

Directly after the SQL Saturday in Vienna the PASS SQLRally Nordic opens its doors in Copenhagen again on March 2-4.

The official schedule was just released today and can be found here (full PDF)!

I will deliver my session “Deep-Dive to Analysis Services Security” on Wednesday 4th.

Last but definitely not least is SQLBits Conferences, Europe’s biggest SQL Server conference, which is taking place the 13st time now on March 4-7 in London. (don’t get confused just because its SQL Bits XIV, Microsoft also skipped Windows 9 ). This year its all about Superheroes and a lot of SQL Server Superheroes will be there!

A schedule is not available yet but will be made public within the next days I guess so stay tuned!

** UPDATE 2014-01-13:**The official schedule will soon be available here. Our Session is very likely to be on Friday.

Hope to see you there!

]]>However, I recently had a slightly different requirement where I needed to calculate the Events-In-Progress for Time Periods – e.g. the Open Orders in a given month – and not only for a single day. The calculations shown in the white-paper only work for a single day so I had to come up with my own calculation to deal with this particular problem.

Before we can start we need to identify which orders we actually want to count if a Time Period is selected. Basically we have to differentiate between 6 types of Orders for our calculation and which of them we want to filter or not:

Order | Definition |

Order1 (O1) | Starts before the Time Period and ends after it |

Order2 (O2) | Starts before the Time Period and ends in it |

Order3 (O3) | Starts in the Time Period and ends after it |

Order4 (O4) | Starts and ends in the Time Period |

Order5 (O5) | Starts and ends after the Time Period |

Order6 (O6) | Starts and ends before the Time Period |

For my customer an order was considered as “open” if it was open within the selected Time Period, so in our case we need to count only Orders O1, O2, O3 and O4. The first calculation you would usually come up with may look like this:

- [MyOpenOrders_FILTER] :=
- CALCULATE (
- DISTINCTCOUNT ( 'Internet Sales'[Sales Order Number] ),
- FILTER (
- 'Internet Sales',
- 'Internet Sales'[Order Date]
- <= CALCULATE ( MAX ( 'Date'[Date] ) )
- ),
- FILTER (
- 'Internet Sales',
- 'Internet Sales'[Ship Date]
- >= CALCULATE ( MIN ( 'Date'[Date] ) )
- )
- )

We apply custom filters here to get all orders that were __ordered on or before the last day__ and were also __shipped on or after the first day__ of the selected Time Period. This is pretty straight forward and works just fine from a business point of view. However, performance could be much better as you probably already guessed if you read Alberto’s white-paper.

So I integrate his logic into my calculation and came up with this formula (Note that I could not use the final Yoda-Solution as I am using a DISTINCTCOUNT here):

- [MyOpenOrders_TimePeriod] :=
- CALCULATE (
- DISTINCTCOUNT ( 'Internet Sales'[Sales Order Number] ),
- GENERATE (
- VALUES ( 'Date'[Date] ),
- FILTER (
- 'Internet Sales',
- CONTAINS (
- DATESBETWEEN (
- 'Date'[Date],
- 'Internet Sales'[Order Date],
- 'Internet Sales'[Ship Date]
- ),
- [Date], 'Date'[Date]
- )
- )
- )
- )

To better understand the calculation you may want to rephrase the original requirement to this: “An open order is an order that was open on at least one day in the selected Time Period”.

I am not going to explain the calculations in detail again as the approach was already very well explained by Alberto and the concepts are the very same.

An alternative calculation would also be this one which of course produces the same results but performs “different”:

- [MyOpenOrders_TimePeriod2] :=
- CALCULATE (
- DISTINCTCOUNT ( 'Internet Sales'[Sales Order Number] ),
- FILTER (
- GENERATE (
- SUMMARIZE (
- 'Internet Sales',
- 'Internet Sales'[Order Date],
- 'Internet Sales'[Ship Date]
- ),
- DATESBETWEEN (
- 'Date'[Date],
- 'Internet Sales'[Order Date],
- 'Internet Sales'[Ship Date]
- )
- ),
- CONTAINS ( VALUES ( 'Date'[Date] ), [Date], 'Date'[Date] )
- )
- )

I said it performs “different” as for all DAX calculations, performance also depends on your model, the data and the distribution and granularity of the data. So you should test which calculation performs best in your scenario. I did a simple comparison in terms of query performance for AdventureWorks and also my customer’s model and results are slightly different:

Calculation (Results in ms) | AdventureWorks | Customer’s Model |

[MyOpenOrders_FILTER] | 58.0 | 1,094.0 |

[MyOpenOrders_TimePeriod] | 40.0 | 390.8 |

[MyOpenOrders_TimePeriod2] | 35.5 | 448.3 |

As you can see, the original FILTER-calculation performs worst on both models. The last calculation performs better on the small AdventureWorks-Model whereas on my customer’s model (16 Mio rows) the calculation in the middle performs best. So it’s up to you (and your model) which calculation you should prefer.

The neat thing is that all three calculations can be used with any existing hierarchy or column in your Date-table and of course also on the Date-Level as the original calculation.

]]>Dynamic Named Sets are usually used if you want a set to be re-evaluated in the current context opposed to Static Named Sets which are only evaluated once during creation. For example you can create a Dynamic Named Set for your Top 10 customers. Changing the Year or Country would cause a re-evaluation of the set and different customers are returned for your filter selections. These type of calculated sets is usually not a problem.

Another reason to used Dynamic Named Sets is to deal with Sub-Selects. Some client tools, foremost Microsoft Excel Pivot Tables, use Sub-Selects to deal with multiple selections on the same attribute. Lets do a little example on how to use Dynamic Named Sets here. Assume you need to calculate the average yearly sales for the selected years. This may sound very trivial at first sight but can be very tricky. In order to calculated the yearly average we first need to calculated how many years are in the currently selected:

- CREATE MEMBER CURRENTCUBE.[Measures].[CountYears_EXISTING] AS (
- COUNT(EXISTING [Date].[Calendar Year].[Calendar Year].members)
- );

However, this does not work if Sub-Selects are used in the query:

The calculated member returns “6” (the overall number of years) instead of “2” (the actually selected number of years). The issue here is that the calculation is not aware of any Sub-Select or filters within the Sub-Select as it is executed only outside of the Sub-Select.

To work around this issue you can create a Dynamic Name Set and refer to it in your calculated member:

- CREATE DYNAMIC SET [ExistingYears] AS {
- EXISTING [Date].[Calendar Year].[Calendar Year].members
- };
- CREATE MEMBER CURRENTCUBE.[Measures].[CountYears_DynamicSet] AS (
- COUNT([ExistingYears])
- );

Now we get the correct results for our Count of Years calculation and could simply divide our Sales Amount by this value to get average yearly sales. The reason for this is that Dynamic Named Sets are also evaluated within the Sub-Select and therefore a COUNT() on it returns the correct results here.

So this technique is quite powerful and is also the only feasible workaround to deal with this kind of problem. But as I initially said, this can also cause some performance issues!

To illustrate this issue on Adventure Works simply add these two calculations to your MDX Script:

- CREATE DYNAMIC SET [ExpensiveSet] AS {
- Exists(
- [Product].[Category].[Category].members,
- Filter(
- Generate(
- Head([Date].[Calendar].[Month].MEMBERS, 30),
- CrossJoin(
- {[Date].[Calendar].CURRENTMEMBER},
- Head(
- Order(
- [Customer].[Customer].[Customer].MEMBERS,
- [Measures].[Internet Sales Amount],
- BDESC),
- 10000))),
- [Measures].[Internet Order Quantity] > -1
- ),
- 'Internet Sales'
- )
- };
- CREATE MEMBER CURRENTCUBE.[Measures].[UnusedCalc] AS (
- COUNT([ExpensiveSet])
- );

The [ExpensiveSet] is just a Dynamic Named Set which needs some time to be evaluated and the [UnusedCalc] measure does a simple count over the [ExpensiveSet]. Having these calculations in place you can now run __any query__ against your cube and will notice that even the simplest query now takes some seconds to execute even if the new calculated member is not used:

- SELECT
- [Measures].[Internet Sales Amount] ON 0
- FROM [Adventure Works]

I am quite sure that this behavior is related to how Dynamic Named Sets are evaluated and why they also work for Sub-Selects. However, I also think that calculations that are not used in a query should not impact the results and/or performance of other queries!

I just created a bug on Connect in case you want to vote it:

https://connect.microsoft.com/SQLServer/feedback/details/1049569

I know that Dynamic Named Sets in combination with calculated members is a not a widely used technique as I guess most developers are not even aware of its power. For those who are, please keep in mind that these kind of calculations get evaluated __for every query__ which can be crucial if your Dynamic Named Set is expensive to calculate! This has also impact on meta-data queries!

The tool is currently in a beta state and this is the first official release – and also my first written tool that I share publicly so please don’t be too severe with your feedback – just joking every feedback is good feedback!

Below is a little screenshot which shows the results after the reference query is executed. The green lines are effectively used by the query whereas the others do not have any impact on the values returned by the query.

A list of all features, further information and also the source code can be found at the project page on codeplex:

https://mdxscriptdebugger.codeplex.com/

Also the download is available from there:

https://mdxscriptdebugger.codeplex.com/releases

Looking forward to your feedback and hope it helps you to track down performance issues of your MDX Scripts!

]]>There are already a lot of whitepapers out there which describe how to set things up correctly. Here are just some examples:

– MSDN: http://msdn.microsoft.com/en-us/library/gg492140.aspx

– MSBI Academy (great video!): http://msbiacademy.com/?p=5711 by Rob Kerr

They provide very useful information and you should be familiar with the general setup before proceeding here or using the final PowerShell script.

The PowerShell script basically performs the following steps:

- Create a local folder as base for your WebSite in IIS
- Copy SSAS ISAPI files (incl. msmdpump.dll) to the folder
- Create and Configure an IIS AppPool
- Create and Configure a IIS WebSite
- Add and enable an ISAPI entry for msmdpump.dll
- Configure Authentication
- Configure Default Document
- Update connection information to SSAS server

I tested it successfully with a clean installation of IIS 8.0 (using applicationhost.config.clean.install). In case you already have other WebSites running you may still consider doing the steps manually or adopting the script if necessary. The script is written not to overwrite any existing Folders, WebSites, etc. but you never know.

So here is my final script:

- #Import Modules
- Import-Module WebAdministration
- # change these settings
- $iisSiteName = "OLAP"
- $iisPort = "8000"
- $olapServerName = "server\instance"
- # optionally also change these settings
- $isapiFiles = "c:\Program Files\Microsoft SQL Server\MSAS11.MSSQLSERVER\OLAP\bin\isapi\*"
- $iisAbsolutePath = "C:\inetpub\wwwroot\" + $iisSiteName
- $iisAppPoolName = $iisSiteName + "_AppPool"
- $iisAppPoolUser = "" #default is ApplicationPoolIdentity
- $iisAppPoolPassword = ""
- $iisAuthAnonymousEnabled = $false
- $iisAuthWindowsEnabled = $true
- $iisAuthBasicEnabled = $true
- $olapSessionTimeout = "3600" #default
- $olapConnectionPoolSize = "100" #default
- if(!(Test-Path $iisAbsolutePath -pathType container))
- {
- #Creating Directory
- mkdir $iisAbsolutePath | Out-Null
- #Copying Files
- Write-Host -NoNewline "Copying ISAPI files to IIS Folder … "
- Copy -Path $isapiFiles -Destination $iisAbsolutePath -Recurse
- Write-Host " Done!" -ForegroundColor Green
- }
- else
- {
- Write-Host "Path $iisAbsolutePath already exists! Please delete manually if you want to proceed!" -ForegroundColor Red
- Exit
- }
- #Check if AppPool already exists
- if(!(Test-Path $("IIS:\\AppPools\" + $iisAppPoolName) -pathType container))
- {
- #Creating AppPool
- Write-Host -NoNewline "Creating ApplicationPool $iisAppPoolName if it does not exist yet … "
- $appPool = New-WebAppPool -Name $iisAppPoolName
- $appPool.managedRuntimeVersion = "v2.0"
- $appPool.managedPipelineMode = "Classic"
- $appPool.processModel.identityType = 4 #0=LocalSystem, 1=LocalService, 2=NetworkService, 3=SpecificUser, 4=ApplicationPoolIdentity
- #For details see http://www.iis.net/configreference/system.applicationhost/applicationpools/add/processmodel
- if ($iisAppPoolUser -ne "" -AND $iisAppPoolPassword -ne "") {
- Write-Host
- Write-Host "Setting AppPool Identity to $iisAppPoolUser"
- $appPool.processmodel.identityType = 3
- $appPool.processmodel.username = $iisAppPoolUser
- $appPool.processmodel.password = $iisAppPoolPassword
- }
- $appPool | Set-Item
- Write-Host " Done!" -ForegroundColor Green
- }
- else
- {
- Write-Host "AppPool $iisAppPoolName already exists! Please delete manually if you want to proceed!" -ForegroundColor Red
- Exit
- }
- #Check if WebSite already exists
- $iisSite = Get-Website $iisSiteName
- if ($iisSite -eq $null)
- {
- #Creating WebSite
- Write-Host -NoNewline "Creating WebSite $iisSiteName if it does not exist yet … "
- $iisSite = New-WebSite -Name $iisSiteName -PhysicalPath $iisAbsolutePath -ApplicationPool $iisAppPoolName -Port $iisPort
- Write-Host " Done!" -ForegroundColor Green
- }
- else
- {
- Write-Host "WebSite $iisSiteName already exists! Please delete manually if you want to proceed!" -ForegroundColor Red
- Exit
- }
- #Ensuring ISAPI CGI Restriction entry exists for msmdpump.dll
- if ((Get-WebConfiguration "/system.webServer/security/isapiCgiRestriction/add[@path='$iisAbsolutePath\msmdpump.dll']") -eq $null)
- {
- Write-Host -NoNewline "Adding ISAPI CGI Restriction for $iisAbsolutePath\msmdpump.dll … "
- Add-WebConfiguration "/system.webServer/security/isapiCgiRestriction" -PSPath:IIS:\\ -Value @{path="$iisAbsolutePath\msmdpump.dll"}
- Write-Host " Done!" -ForegroundColor Green
- }
- #Enabling ISAPI CGI Restriction for msmdpump.dll
- Write-Host -NoNewline "Updating existing ISAPI CGI Restriction … "
- Set-WebConfiguration "/system.webServer/security/isapiCgiRestriction/add[@path='$iisAbsolutePath\msmdpump.dll']/@allowed" -PSPath:IIS:\\ -Value "True"
- Set-WebConfiguration "/system.webServer/security/isapiCgiRestriction/add[@path='$iisAbsolutePath\msmdpump.dll']/@description" -PSPath:IIS:\\ -Value "msmdpump.dll for SSAS"
- Write-Host " Done!" -ForegroundColor Green
- #Adding ISAPI Handler to WebSite
- Write-Host -NoNewline "Adding ISAPI Handler … "
- Add-WebConfiguration /system.webServer/handlers -PSPath $iisSite.PSPath -Value @{name="msmdpump"; path="*.dll"; verb="*"; modules="IsapiModule"; scriptProcessor="$iisAbsolutePath\msmdpump.dll"; resourceType="File"; preCondition="bitness64"}
- Write-Host " Done!" -ForegroundColor Green
- #enable Windows and Basic Authentication
- Write-Host -NoNewline "Setting Authentication Providers … "
- #need to Unlock sections first
- Set-WebConfiguration /system.webServer/security/authentication/anonymousAuthentication MACHINE/WEBROOT/APPHOST -Metadata overrideMode -Value Allow
- Set-WebConfiguration /system.webServer/security/authentication/windowsAuthentication MACHINE/WEBROOT/APPHOST -Metadata overrideMode -Value Allow
- Set-WebConfiguration /system.webServer/security/authentication/basicAuthentication MACHINE/WEBROOT/APPHOST -Metadata overrideMode -Value Allow
- Set-WebConfiguration /system.webServer/security/authentication/anonymousAuthentication -PSPath $iisSite.PSPath -Value @{enabled=$iisAuthAnonymousEnabled}
- Set-WebConfiguration /system.webServer/security/authentication/windowsAuthentication -PSPath $iisSite.PSPath -Value @{enabled=$iisAuthWindowsEnabled}
- Set-WebConfiguration /system.webServer/security/authentication/basicAuthentication -PSPath $iisSite.PSPath -Value @{enabled=$iisAuthBasicEnabled}
- Write-Host " Done!" -ForegroundColor Green
- #Adding Default Document
- Write-Host -NoNewline "Adding Default Document msmdpump.dll … "
- Add-WebConfiguration /system.webServer/defaultDocument/files -PSPath $iisSite.PSPath -atIndex 0 -Value @{value="msmdpump.dll"}
- Write-Host " Done!" -ForegroundColor Green
- #Updating OLAP Server Settings
- Write-Host -NoNewline "Updating OLAP Server Settings … "
- [xml]$msmdpump = Get-Content "$iisAbsolutePath\msmdpump.ini"
- $msmdpump.ConfigurationSettings.ServerName = $olapServerName
- $msmdpump.ConfigurationSettings.SessionTimeout = $olapSessionTimeout
- $msmdpump.ConfigurationSettings.ConnectionPoolSize = $olapConnectionPoolSize
- $msmdpump.Save("$iisAbsolutePath\msmdpump.ini")
- Write-Host " Done!" -ForegroundColor Green
- Write-Host "Everything done! "
- Write-Host "The SSAS server can now be accessed via http://$env:computername`:$iisPort"

The script can also be downloaded here.

The process of setting up HTTP connectivity is the same for Analysis Services Multidimensional and Tabular so the script works for both scenarios, just change the server name accordingly.

]]>