THE SQL Server Blog Spot on the Web

Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | Join | Help
in Search

Microsoft OLAP by Mosha Pasumansky

Finding lowest common ancestor in set

This thread on MSDN forums asks seemingly easy question – given a multiple selection of elements in the parent-child hierarchy – how to find lowest common ancestor among them ? While it is fairly easy to do using stored procedure, it turned out to be far from trivial using pure MDX. Here is my solution to this puzzle, it isn’t fully optimized, I cared more to keep it simple to understand instead.

Below is my solution, applied to the example of set

{
  [Employee].[Employees].&[289]
 ,[Employee].[Employees].&[281]
 ,[Employee].[Employees].&[296]
}

The right answer for this example is Brian Welcker (of Reporting Services fame). If you have simpler or more elegant solution – I invite the competition ! My solution follows:

WITH 
  // This is example set 
  SET SelectedEmployees AS 
    {
      [Employee].[Employees].&[289]
     ,[Employee].[Employees].&[281]
     ,[Employee].[Employees].&[296]
    }
  // We compute the highest level in the set, since
  // lowest common ancestor has to be at or above this level
  MEMBER HighLevel AS 
    Min
    (
      SelectedEmployees
     ,[Employee].[Employees].CurrentMember.Level.Ordinal
    ) 
  // Set of all common ancestors
  SET CommonAncestors AS 
    Filter
    (
      // go over ancestors of the first element in the set
      Ascendants(SelectedEmployees.Item(0)) AS iter
     ,
        // discard those below lowest possible common level
        [Employee].[Employees].CurrentMember.Level.Ordinal <= HighLevel
      AND 
        // Check that all the ancestors at given level
        // are the same
          Generate
          (
            SelectedEmployees
           ,Ancestors
            (
              [Employee].[Employees].CurrentMember
             ,iter.Current.Item(0).Level
            )
          ).Count
        = 1
    )
  // Pick lowest common ancestor among all common ancestors
  SET LowestCommonAncestor AS 
    // get first element
    Head
    (
      // ...of the reversed hierarchization
      Hierarchize
      (
        CommonAncestors
       ,POST
      )
     ,1
    )
SELECT 
  LowestCommonAncestor ON 0
FROM [Adventure Works];

Update: Gerhard Brueckl has offered another, more elegant solution. It has clever use of the fact that EXISTING over the parent-child hierarchy is going to bring all of the ancestors too, so there is no need to do extra Generate(…, Ascendants()). Here is Gerhard solution:

WITH 
  SET selected_employees AS 
    {(EXISTING [Employee].[Employees].MEMBERS)}
  SET common_ancestors AS 
    {
      Filter
      (
        selected_employees
       ,
          Intersect
          (
            [Employee].[Employees].CurrentMember.Level.MEMBERS
           ,selected_employees
          ).Count
        = 1
      )
    }
  MEMBER [Measures].LCA AS 
    Tail
    (
      common_ancestors
     ,1
    ).Item(0).Name 
SELECT 
  [Measures].LCA ON COLUMNS
FROM [Adventure Works]
WHERE 
  {
    [Employee].[Employees].&[289]
   ,[Employee].[Employees].&[281]
   ,[Employee].[Employees].&[296]
  };

Published Tuesday, September 02, 2008 10:02 PM by mosha

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Turkey said:

great an article, I'm very thank you, I think really usefull, I'm following  comments, thanks

September 10, 2008 1:28 PM
 

CALIN said:

Hello everyone , great blog

Think I 've found a slight variation of Common Ancestor exercise..

Hope  it works

WITH

 SET selected_employees AS

  {

   [Employee].[Employees].&[289]

  ,[Employee].[Employees].&[281]

  ,[Employee].[Employees].&[296]

 }

 SET ALL_ANCESTORS AS GENERATE (selected_employees,

ASCENDANTS ([Employee].[Employees].CURRENTMEMBER))

SET FLT_ANCESTORS AS { FILTER (

ALL_ANCESTORS

,INTERSECT (DESCENDANTS ([Employee].[Employees].CURRENTMEMBER ) ,

selected_employees ).COUNT = DISTINCTCOUNT (selected_employees)  ) }

SET MIN_LEVEL AS TOPCOUNT ( FLT_ANCESTORS , 1 , [Employee].[Employees].CURRENTMEMBER.LEVEL.ORDINAL)

select MIN_LEVEL on 0

from [Adventure Works]

March 17, 2009 12:49 PM
 

Reed said:

Hi Mosha,

Gerhard's solution may be elegant, but it also returns the wrong answer. It assumes that the lowest level where the intersection coung is 1 is the LCA. If you're selecting several members all from the same level, then that is true, but if you select members from different levels--and the lowest level happens to have only one member, then that member gets returned as the LCA, which it is not.

Yours works--and the simplification that Existing with a Parent-Child does return all the Ascendants is valid. But there's other stuff you do in yours to check the top & bottom levels. Still trying to grok it all.

Cheers,

Reed

April 2, 2009 7:08 PM

Leave a Comment

(required) 
(optional)
(required) 
Submit
Powered by Community Server (Commercial Edition), by Telligent Systems
  Privacy Statement