Creating a Bit array (also known as Bitmap) is an useful technique when analyzing data. Especially (but not necessarily) in case of low cardinality (small number of distinct values in column compared to the number of records in a table). For example, a column having two distinct values (male/female or yes/no status) could be optimized in storage and represented using values 0 and 1 only (bits).
A collection of such bits forms a bit array or a bitmap. Bitmap indexes use those arrays performing bitwise operations on them.
Almost all DSS and analytical systems use bitmap indexes, and so does Analysis Services. An example where it can be seen is SQL Server Profiler. Query Subcube event represents the bitmap of hierarchies and attributes being used in a query. Yet, what’s fine for computers might not be for humans, hence there’s also Query Subcube Verbose event which explains that in plain English. Kind of.
Although SSAS engine intensively uses them, bitwise operations in MDX are in short supplies. In spite of that, the same idea can be borrowed and turned in action using string functions. After all, an alternate name for bit array is bitstring.
Recently I’ve participated in a case where a measure was expected to return the percentage of parent no matter which hierarchy is placed on an axis and no matter how many of them are there. Actually, a similar problem appeared on MSDN SSAS forum a year ago too. While the initial problem in this latter case was solved using special solution, general solutions emerged also. The offer ranged from heavy string manipulation to recursion and stored procedure. Taking a look at it now, from the perspective of this case, I believe bitmap indexes might have been an alternate MDX solution, a relatively fast approach that doesn’t require anything but a query and few calculated members aside.
Here’s a case study featuring the bitmap index as the key point in obtaining the general solution.
SITUATION
The idea was to have a general solution for percentage (ratio to parent). One calculated measure covering all scenarios, meaning, whatever combination of hierarchies appears in OWC (cube browser).
The problem that soon appeared was this – no matter how much improved the calculation in each iteration I posted in forum was (a previous thread and my blog article on that subject), there was always a case where it didn’t fit. Therefore I started focusing on getting as detailed explanation as possible. When I finally got it (as an Excel sheet) and analyze it, deduced the algorithm and describe it like this (as in integrams):
-
- if there was a non-root member in the rightmost hierarchy, show ratio to parent
- root member in the rightmost hierarchy combined with all non-root members from other hierarchies to the left should result in 100%
- if all hierarchies were on root member, the result should be 100%
- in all other situations, ratio to parent for non-root member should be the result
The idea in integrams (I couldn’t link for them in English, it’s an enigmatic game) is to mark certain events that hold true and other that can never be such. By doing so in iterations (reading the initial text over and over again, every time in new context), one should be able to deduce the final combination of entities in a puzzle and thereby solve it. The idea here was to list all possible combinations using a bitmap, where 0 represents non-root member, 1 root member and their sequence, well, …the sequence of hierarchies on axis. Translated, that would look like this (asterisk being any value):
CASE | BITMAP VALUE | REQUIRED RESULT |
**0 | 0, 2, 4, 6, … (even numbers) | parent of 3rd |
001 | 1 | parent of 3rd (self), special case for 100%, per request |
011 | 3 | parent of 1st |
101 | 5 | parent of 2nd |
111 | 7 | parent of 1st, special case for 100% |
Now, see how this looks more like a solvable case? And so it was. The only thing necessary at this stage is to code it using MDX expressions in the most optimized way.
SOLUTION
A general MDX for this could be far more simpler than the one below. However, the requirement was that it should work in OWC and OWC has its specifics when it comes to MDX, about which I blogged here. Therefore, I had to make a lot of additional things, like testing where OWC put measures and everything related to that in later calculations. Nevertheless, I managed to make it work as required.
Create Member CurrentCube.[Measures].[Where are measures in OWC] AS case when NOT IsError(Extract( Axis(0), Measures ).Count) then 0 when NOT IsError(Extract( Axis(1), Measures ).Count) then 1 when NOT IsError(Extract( Axis(2), Measures ).Count) then 2 else -1 -- should not be possible end , Visible = 0 ; Create Member CurrentCube.[Measures].[Ratio measure] AS -- specify a measure you want to have a ratio for, or -- use default measure, or -- use dynamic expression, that selects -- either first measure on an axis or -- the default measure in case no measure is selected /* -- option 1 */ [Measures].[Sales Amount] /* -- option 2 [Measures].DefaultMember */ /* -- option 3 iif(Extract( Axis( [Measures].[Where are measures in OWC] ), Measures ).Item(0).Item(0).UniqueName = '[Measures].[Universal ratio %]', [Measures].DefaultMember, Extract( Axis([Measures].[Where are measures in OWC]), Measures ).Item(0) ) */ , Visible = 0 ; Create Member CurrentCube.[Measures].[Test Empty] AS IsEmpty( [Measures].[Ratio measure] ) , Visible = 0 ; Create Member CurrentCube.[Measures].[Count of columns] AS iif( IsError( Axis(1).Count ), 0, iif( [Measures].[Where are measures in OWC] = 1, Axis(0).Item(0).Count, Axis(1).Item(0).Count ) ) , Visible = 0 ; Create Member CurrentCube.[Measures].[Bitmap Index as String] AS iif( [Measures].[Where are measures in OWC] = 1, Generate( Head(Measures.AllMembers, [Measures].[Count of columns] ) AS L, -(Axis(0).Item(0).Item(L.CurrentOrdinal - 1) .Hierarchy.CurrentMember.Level.Ordinal = 0) ), Generate( Head(Measures.AllMembers, [Measures].[Count of columns] ) AS L, -(Axis(1).Item(0).Item(L.CurrentOrdinal - 1) .Hierarchy.CurrentMember.Level.Ordinal = 0) ) ) , Visible = 0 ; Create Member CurrentCube.[Measures].[Reversed Bitmap Index as String] AS iif( [Measures].[Where are measures in OWC] = 1, Generate( Head(Measures.AllMembers, [Measures].[Count of columns] ) AS L, -(Axis(0).Item(0).Item([Measures].[Count of columns] - L.CurrentOrdinal) .Hierarchy.CurrentMember.Level.Ordinal = 0) ), Generate( Head(Measures.AllMembers, [Measures].[Count of columns] ) AS L, -(Axis(1).Item(0).Item([Measures].[Count of columns] - L.CurrentOrdinal) .Hierarchy.CurrentMember.Level.Ordinal = 0) ) ) , Visible = 0 ; Create Member CurrentCube.[Measures].[Rightmost non root position] AS InStr([Measures].[Reversed Bitmap Index as String], '0') , Visible = 0 ; Create Member CurrentCube.[Measures].[Inner 100%] AS CInt([Measures].[Bitmap Index as String]) = 1 , Visible = 0 ; Create Member CurrentCube.[Measures].[Universal ratio %] AS iif( [Measures].[Test Empty], null, iif( [Measures].[Rightmost non root position] = 0 OR [Measures].[Inner 100%], 1, iif( [Measures].[Where are measures in OWC] = 1, -- check division by zero iif( ( Axis(0).Item(0) .Item([Measures].[Count of columns] - [Measures].[Rightmost non root position]) .Hierarchy.CurrentMember.Parent, [Measures].[Ratio measure] ) = 0, 0, [Measures].[Ratio measure] / ( Axis(0).Item(0) .Item([Measures].[Count of columns] - [Measures].[Rightmost non root position]) .Hierarchy.CurrentMember.Parent, [Measures].[Ratio measure] )), -- check division by zero iif( ( Axis(1).Item(0) .Item([Measures].[Count of columns] - [Measures].[Rightmost non root position]) .Hierarchy.CurrentMember.Parent, [Measures].[Ratio measure] ) = 0, 0, [Measures].[Ratio measure] / ( Axis(1).Item(0) .Item([Measures].[Count of columns] - [Measures].[Rightmost non root position]) .Hierarchy.CurrentMember.Parent, [Measures].[Ratio measure] ) ) ) ) ), FORMAT_STRING = "percent" ;
The first measure detects axis that has measures when using OWC.
The second measure is our ratio measure. That is, the measure we want to calculate the ratio for. I provided the syntax for several scenarios. The first one, of course, is the obvious and easiest one – using any measure we want and hard-coding it there. The second option is more general one – the use of cube’s default measure. Meaning, it could work for any cube. Finally, the third and the most complex option is to use the first measure on axis, whatever it might be. That’s even more general solution. In that scenario, it is up to an end-user to put a measure of his choice as the first measure and to see ratios based on that measure. It should work in any cube/report/query. The calculation features a handle for no measures at all (except that ratio measure) in which case the default measure is taken.
Next we test whether the original, ratio measure is empty. We’ll use the result of this in the following calculations, in order to preserve empty rows.
Count of columns gives us count of hierarchies being placed on an axis of interest (that is rows or columns, depending on where measures are not).
Next thing we do is generate a bitmap, and right next to it – a reversed bitmap. Innermost logical test returns either –1 or 0 (True or False), while minus in front of it turns -1 into 1. Looping is performed on a set of measures (which might be a week point – it is up to BI designer to provide something better in case there are only few measures). From which axis is bitmap formed? Depends on measures position in OWC. Additionally, in the reversed bitmap, the reversed order is obtained as it is usually being done in MDX – using a combination of two MDX functions: Count and CurrentOrdinal.
Why do we needed reversed bitmap? It becomes obvious once we come to the next calculated measure – Rightmost non root position. Analysis should be performed backwards, from right to left. That is the order in which we search patterns in bitmaps and therefore we needed reversed bitmap (string search is left based). This new measure returns us the position of the first zero value. In the table featuring 5 distinct cases, those positions are: 1, 2, 3, 2, 0.
Next measure is a Boolean type measure that detects a special case (see the table above) and that special case is 001 combination, where, per request, the value should be 100%.
Finally, we come to the last and the only visible measure – Universal Ratio %. There, the first thing we should do is test for emptiness of original ratio measure. Then only we handle the cases in our table. After covering cases with 100% result, based on where the measures are, we enter one branch and there test denominator for zero. Finally, we perform the ratio calculation. Only by now it is in utmost relative form, covering wide varieties of cases withonly one expression (notice the use of Rightmost non root position measure inside the innermost calculation). That’s the power and beauty of having and using bitmap indexes.
BENEFITS
It’s worth reminding again that the final formula looks complicated only because of two reasons: OWC is being used as a front-end and, there were special cases in the bitmap table. Without them, the calculation would come down to always necessary checks for emptiness and division by zero, following by the core ratio expression. In case you’re using a client that keeps measures on the axis you’ve dropped them on in your pivot, you can consider stripping down all unnecessary calculations or part of it, as mentioned in the beginning of this article.
I also made tests with real bitmaps. Meaning, I tried to use numbers, 2 powered to N, where N was the number of hierarchies on axis. Yet that turned out to be not only a dead-end (because the reverse operation is not easy), but also time consuming, prolonging the query time significantly. String operations on the other hand were very fast and manipulations with them very easy. Relatively, of course.
All in all, bitmaps and their power in being flexible pointers proved to be an interesting approach in providing universal MDX solutions, calculations that, did I mention it before, work in any cube.
I am doing research for my university paper, thanks for your brilliant points, now I am acting on a sudden impulse.
– Laura
You’re the man!
Any ideas on how to create a calculation like this that would work in Excel 2010? Multiselect doesn’t filter the calculation properly there and it’s driving me nuts!
The answer can be found here:
http://social.msdn.microsoft.com/Forums/en/sqlanalysisservices/thread/db54aeb3-8ec4-4057-b223-49b5ee923264
This is incredible. 2 days of searching to find something that would account for the Excel multi-select. Outstanding work. As far as getting it to function with more than one hierarchy on rows, that doesn’t sound like it would be easy, correct?
Thanks!
This works with multiple hierarchies by design. Take a look at the screenshot in the end of the post.
Oh, I was going by your answer to Deb, you pointed her here:
The answer can be found here:
http://social.msdn.microsoft.com/Forums/en/sqlanalysisservices/thread/db54aeb3-8ec4-4057-b223-49b5ee923264
At the bottom of which you said:
“This will only work for one hierarchy on rows. In order for it to work on multiple hierarchies, a proper dimensionality of sets in Intersect should be created, using other hierarchies’ All member. You would crossjoin them with these siblings, on the right place (probably before the siblings). In other words, A*B*C must match in order in both sets in Intersect.”
Let me refresh my memory … I will be back to you.
Thank you sir.
Any news on that ?