Nov 032008
 

Few months ago, while on vacation, I saw an interesting post on MSDN forum where a user asked for rank of members placed on rows, irrespective of hierarchy. Furthermore, that was supposed to be achieved using only one calculated measure, nothing else. Intrigued by this puzzle, I made an example of such calculated measure on Adventure Works database using Axis() function. Here it is:

WITH
MEMBER [Measures].[Sales Amount Rank] AS
     iif(
         IsEmpty([Measures].[Sales Amount]),
        
null,
         Rank(
              Axis(1).Item(0).Item(Axis(1).Item(0).Count – 1
                                  ).Hierarchy.CurrentMember,
              Order(
                    Extract(
                            Axis(1),
                            Axis(1).Item(0).Item(
                                            Axis(1).Item(0).Count – 1
                                               
).Hierarchy
                           ),
                   [Measures].[Sales Amount],
                    BDESC

                   )
             )
        )
SELECT
{
    [Measures].[Sales Amount],
    [Measures].[Sales Amount Rank]
}

ON AXIS(0),
NON EMPTY
    [Product].[Category].[Category].MEMBERS *
    [Sales Territory].[Sales Territory Country].
                     
[Sales Territory Country].MEMBERS
ON AXIS(1)
FROM
    [Adventure Works]
 

Here’s how it works.

We should test for empty values first. Empty values of the measure on which we do ranking (Sales Amount in this example). This is done in order not to influence the original result, the one without the rank measure. Why? Because Rank() function returns numbers, never nulls. Normally, NON EMPTY serves to filter rows for which Sales Amount in the above example is null.  If we introduce the second measure that is never null, those rows will suddenly appear too. And this is not a wanted behavior.

Rank is made by using current member of the last hierarchy on rows, as requested. Ordered set is made by extracting that hierarchy and ordering its members by measure Sales Amount. The trick is that Order() function returns a different set of members for each row. In other words, current context (tuple) influences ordering of members. On each Category (first hierarchy), another ordered set of Countries (last hierarchy) might appear, because countries will be ordered per current product category. The same would work for any number of hierarchies on rows.

Recently, I was asked to make it even more universal (general) in a way to avoid ranking of members from different hierarchy levels at the same time. Meaning, if we place .ALLMEMBER, we don’t want [All] member to be ranked as 1, while all the others as second, third, and so on.

Another puzzle, another solution. This time I named it Goldfish Rank since it fulfills any wishes you might have about ranking in general. Here it is:

WITH
MEMBER [Measures].[Goldfish Rank] AS
    iif(
        IsEmpty(Axis(0).Item(0).Item(0))
        — making it absolutely general
        — using first measure on columns


        /*
        — un-comment this block if you want to get rank
        — only for lowest level members in you last column
        OR
        Axis(1).Item(0).Item(Axis(1).Item(0).Count – 1
                            ).Hierarchy.CurrentMember.Level.Ordinal <
        Max(
            Extract(
                    Axis(1),
                    Axis(1).Item(0).Item( Axis(1).Item(0).Count – 1
                                        ).Hierarchy
                   ),
            Axis(1).Item(0).Item(Axis(1).Item(0).Count – 1
                                ).Hierarchy.CurrentMember.Level.Ordinal
           )
        */

        ,
        null,

        Rank(
            
Axis(1).Item(0).Item(Axis(1).Item(0).Count – 1
                                 ).Hierarchy.CurrentMember,
             Intersect(
                       Order(
                             Extract(
                                     Axis(1),
                                     Axis(1).Item(0).Item(
                                             Axis(1).Item(0).Count – 1
                                                         ).Hierarchy
                                    ),
                             Axis(0).Item(0).Item(0),
                             — making it absolutely general
                             DESC
                             — BDESC would be redundant,
                             —
since we work per level
                            ),

                       iif(
                           Axis(1).Item(0).Item(
                                                Axis(1).Item(0).Count – 1
                                               ).Hierarchy.CurrentMember
                                               
.Level.Ordinal = 0,
                           Axis(1).Item(0).Item(
                                                Axis(1).Item(0).Count – 1
                                               ).Hierarchy.CurrentMember,

                          
NonEmpty(
                                    Axis(1).Item(0).Item(
                                            Axis(1).Item(0).Count – 1
                                                        ).Hierarchy.CurrentMember
                                                        
.SIBLINGS,
                                  { Axis(0).Item(0).Item(0) }
                                    — making it absolutely fast
                                   )
                          )
                       */

                       — /*
                       — this is the second block, referred above
                       — comment NonEmpty() function
                       — if you un-comment the block above

                       NonEmpty(
                                Axis(1).Item(0).Item(
                                                Axis(1).Item(0).Count – 1
                                                    ).Hierarchy.CurrentMember
                                                    
.Level.MEMBERS,
                              { Axis(0).Item(0).Item(0) }
                                — making it absolutely fast
                               )
                       — end of second block
                       — */

                      )
            )
       )
SELECT
{
    [Measures].[Sales Amount],
    [Measures].[Goldfish Rank]
}

ON AXIS(0),
NON EMPTY
{
    [Product].[Category].ALLMEMBERS
    * [Sales Territory].[Sales Territory].ALLMEMBERS
}

    — un-comment lower part and comment the line above
    — if you find it difficult to compare results
    — when the last column is hierarchized
    — and when applying rank on level members, not siblings
/*
*

{
    [Sales Territory].[All],
    [Sales Territory].[Group].MEMBERS,
    [Sales Territory].[Country].MEMBERS,
    [Sales Territory].[Region].MEMBERS
}
*/

ON AXIS(1)
FROM
    [Adventure Works]
 

This query has [All] members on rows. From both hierarchies. That serves for testing purposes of newly created calculated measure for rank.

Calculated measure used in previous example was enhanced in a way that it sorts only members that have the same level (or parent) as current one (current member from the last hierarchy). This is achieved using Intersect() function. Members on each level (or siblings, depends on the switch you make with blocks of code) will be ranked only according to equal to themselves. By the term equal, I assume those from the same level (or of the same parent). Metaphorically, having the same chance to be ranked as 1.

Second argument of Intersect() function should have been simple .MEMBERS from particular level (or .SIBLINGS), but that was optimized using NonEmpty() function and the same measure we rank by. Optimization means we reduce potentially large set of members by applying NonEmpty() function on them, so that only those having result for that measure will survive.

Goldfish Rank has several options how to deal with higher levels.

Goldfish Rank in level members      Goldfish Rank in siblings

First commented block enables user to rank only on lowest level members, while providing nulls for the rest. The second block enables user to avoid ranking the [All] member, since it will most probably always have rank 1. And finally, if you find it hard to match members from different levels in the example above, I provided commented block of several levels for the second user hierarchy on axis 1. This way results will not be hierarchized, so it will be easier for you to compare them and see them in action.

Another way of ranking is to rank among siblings. For that purpose, I provided a commented block inside Intersect() function. Using that instead of the block below (un-commented) will give us results as in right picture.

While there can be other variations of ranking, I assume the code above will suffice as a template for each of those variations. If you encounter any problems during that, remember to ask the Goldfish!

In the end, it’s worth saying that the forum user’s wish was fulfilled.

– If you like this post, be sure to drop by occasionally, since there will be more interesting MDX queries in the days to come.

Oct 212008
 

My name is Tomislav Piasevoli. I’m a Business Intelligence Specialist located in Croatia (Europe) working for SoftPro Tetral company.

I’ve been involved in SSAS for more than 5 years, doing mostly nothing but pure BI projects while providing occasional help to developers of our company’s OLAP clients (SoftPro Manager and later CubePlayer).

I managed to gather a solid practical knowledge about SSAS while working on approximately 30 still-in-use local BI projects. I never got certified because: a) I wasn’t very interested in that; and b) I witnessed you can achieve that with zero-or-so finished real-life projects which created an aversion towards that in me. Presenting solutions, designing BI systems, deployment and later support of BI project, in order for them to live and grow, was always my primary concern. The practice as we could say, not theory.

During a 5-months maternity leave (yes, you can do that in Croatia), I changed my mind. I guess when you have the time, you can look things from different perspective. I decided to go for it, so I ordered 70-445 Training Kit as soon as I returned to work. While reading it, I started to explore Internet for other resources that might be useful.

One of the things I encountered was MSDN SSAS Forum which seemed to me like a good playground to practice my knowledge and learn new things. I registered on 9th of June and soon began to post answers to various questions. In the very beginning it was more like a challenge, but later I began to like spending time there because it was the only place so far where I could discuss about SSAS with people that deal with it daily. Many people. And that’s not something you find around the corner. Moreover, that’s something I missed previous years. I mean, it’s not like I didn’t know about forums and similar places – I was too busy working on projects (we had an exhausting delivery rate per year), no time was left even for thoughts about other activities.

Pretty soon I got hooked up to that forum. When I saw I can enter the Top 10 list, I set myself my first goal – to reach No1 in less than a month. Few days before the deadline I reached it. Then I formed another one – to stay in Top 10 (preferably Top 5) until the end of the year. That one is still not reached, but – so far so good. And, as time passed, I set another one in between – 3 stars in less than 3 month. Achieved in first days of September. As I said, I became addicted to it and all those goals were and still are just an excuse to spend more time on it.

Good thing is that in its core it’s actually a 2-way process: you give yet also you gain. While providing answers I learned a lot. The beauty of it is that you can directly apply it in your daily work. And you make many people happy.

Obviously, I neglected the Training Kit. I mean, interaction with people is much better than reading a book, don’t you agree?

And then there comes this blog.

If you’d asked me few months ago why don’t I have a blog, I’d say to you I didn’t see a point in one. Yet, after few weeks of activity in forum I realized that all those good and interesting things I provided should be gathered in one place. A different kind of place. What I had in mind was some kind of knowledge base, which a blog is not. But, that’s too complicated, so I’ll settle with a blog for now. Seems like almost everyone is having one these days.

I see it’s good features also. It’s another win-win. Readers will benefit from it but so shall I, eventually.

After deciding to open a blog, I postponed my first post for the same reason – you spend even more time online once you start blogging. It’s a commitment you take on yourself. Yet, the amount of good material and various documents that might contribute to other individuals working with SSAS is growing each day and that has pushed me forward. I’ve decided – tonight’s the night. I’ll start blogging.

Being so, there’s something I’ve always wanted to scream: “Hello World!“. There you go.

Enough said. Let the adventure begin!