-->

MDX - TopCount plus 'Other' or 'The Re

2020-04-19 07:07发布

问题:

I've got requirement to display top 5 customer sales by customer group, but with other customers sales within the group aggregated as 'Others'. Something similar to this question, but counted separately for each of customer groups.

According to MSDN to perform TopCount, over a set of members you have to use Generate function.

This part works ok:

with 

set [Top5CustomerByGroup] AS
GENERATE
( 
    [Klient].[Grupa Klientow].[Grupa Klientow].ALLMEMBERS,
    TOPCOUNT
    (
        [Klient].[Grupa Klientow].CURRENTMEMBER * [Klient].[Klient].[Klient].MEMBERS
        , 5
        , [Measures].[Przychody ze sprzedazy rzeczywiste wartosc]
    )
)

SELECT 
{ [Measures].[Przychody ze sprzedazy rzeczywiste wartosc]} ON COLUMNS,
{
[Klient].[Grupa Klientow].[Grupa Klientow].ALLMEMBERS * [Klient].[Klient].[All], --for drilldown purposes
[Top5CustomerByGroup]
}
ON ROWS
FROM 
(
  SELECT ({[Data].[Rok].&[2013]} ) ON COLUMNS
      FROM [MyCube]
)

however I've got problem with 'Others' part.

I think I was able to construct set with other customers by group (data looks good) as:

set [OtherCustomersByGroup] AS
GENERATE
( 
    [Klient].[Grupa Klientow].[Grupa Klientow].ALLMEMBERS,
    except
    (
        {[Klient].[Grupa Klientow].CURRENTMEMBER * [Klient].[Klient].[Klient].MEMBERS},
        TOPCOUNT
        (
            [Klient].[Grupa Klientow].CURRENTMEMBER * [Klient].[Klient].[Klient].MEMBERS
            , 5
            , [Measures].[Przychody ze sprzedazy rzeczywiste wartosc]
        )
    )
)

however I don't have idea how to aggregate it with grouping.

Doing this as in this question

member [Klient].[Klient].[tmp] as
aggregate([OtherCustomersByGroup])

produces one value, which is logical.

I think I need list of sets with 'other' customers in each group instead of single [OtherCustomersByGroup] set, but don't have idea how to construct them.

Does anyone have any ideas or suggestions?

UPDATE:

There is some misunderstanding of my needs. I need Top n customers within each of customer group by sales with sales of other customers in this group aggregated to one position (let's say called Others).

For example for this simplified input:

| Group  | Client   | Sales  |
|--------|----------|--------|
| Group1 | Client1  |    300 |
| Group1 | Client2  |      5 |
| Group1 | Client3  |    400 |
| Group1 | Client4  |    150 |
| Group1 | Client5  |    651 |
| Group1 | Client6  | null   |
| Group2 | Client7  |     11 |
| Group2 | Client8  |     52 |
| Group2 | Client9  |     44 |
| Group2 | Client10 |     21 |
| Group2 | Client11 |    201 |
| Group2 | Client12 |    325 |
| Group2 | Client13 |    251 |
| Group3 | Client14 |     15 |

I need such output (here is top 2):

| Group  | Client   | Sales  |
|--------|----------|--------|
| Group1 | Client5  |    651 |
| Group1 | Client3  |    400 |
| Group1 | Others   |    455 |
| Group2 | Client12 |    325 |
| Group2 | Client13 |    251 |
| Group2 | Others   |    329 |
| Group3 | Client14 |     15 |
| Group3 | Others   |  null  | <- optional row

Sorting isn't required, we are going to process it by client side.

回答1:

The following is against AdvWrks and uses a technique I saw on Chris Webb's blog which he outlines here:
https://cwebbbi.wordpress.com/2007/06/25/advanced-ranking-and-dynamically-generated-named-sets-in-mdx/

The section of the script that creates the set MyMonthsWithEmployeesSets I find very difficult to get my head around - maybe @AlexPeshik could shed a little more light on what is happening in the following script.

WITH 
  SET MyMonths AS 
    TopPercent
    (
      [Date].[Calendar].[Month].MEMBERS
     ,20
     ,[Measures].[Reseller Sales Amount]
    ) 
  SET MyEmployees AS 
    [Employee].[Employee].[Employee].MEMBERS 
  SET MyMonthsWithEmployeesSets AS 
    Generate
    (
      MyMonths
     ,Union
      (
        {[Date].[Calendar].CurrentMember}
       ,StrToSet
        ("
             Intersect({}, 
             {TopCount(MyEmployees, 10, ([Measures].[Reseller Sales Amount],[Date].[Calendar].CurrentMember))
             as EmployeeSet"
            + 
              Cstr(MyMonths.CurrentOrdinal)
          + "})"
        )
      )
    ) 
  MEMBER [Employee].[Employee].[RestOfEmployees] AS 
    Aggregate
    (
      Except
      (
        MyEmployees
       ,StrToSet
        (
          "EmployeeSet" + Cstr(Rank([Date].[Calendar].CurrentMember,MyMonths))
        )
      )
    ) 
  MEMBER [Measures].[EmployeeRank] AS 
    Rank
    (
      [Employee].[Employee].CurrentMember
     ,StrToSet
      (
        "EmployeeSet" + Cstr(Rank([Date].[Calendar].CurrentMember,MyMonths))
      )
    ) 
SELECT 
  {
    [Measures].[EmployeeRank]
   ,[Measures].[Reseller Sales Amount]
  } ON 0
 ,Generate
  (
    Hierarchize(MyMonthsWithEmployeesSets)
   ,
      [Date].[Calendar].CurrentMember
    * 
      {
        Order
        (
          Filter
          (
            MyEmployees
           ,
            [Measures].[EmployeeRank] > 0
          )
         ,[Measures].[Reseller Sales Amount]
         ,BDESC
        )
       ,[Employee].[Employee].[RestOfEmployees]
      }
  ) ON 1
FROM [Adventure Works];

Edit - solution for Alex's third attempt:

WITH 
  SET [AllCountries] AS [Country].[Country].MEMBERS 
  SET [AllStates]    AS [State-Province].[State-Province].MEMBERS 
  SET [Top2States] AS 
    Generate
    (
      [AllCountries]
     ,TopCount
      (
        (EXISTING 
          [AllStates])
       ,3
       ,[Measures].[Internet Order Count]
      )
    ) 
  MEMBER [State-Province].[All].[RestOfCountry] AS 
    Aggregate({(EXISTING {[AllStates]} - [Top2States])}) 
SELECT 
  {[Measures].[Internet Order Count]} ON COLUMNS
 ,{
      [AllCountries]
    * 
      {
        [Top2States]
       ,[State-Province].[All].[RestOfCountry]
       ,[State-Province].[All]
      }
  } ON ROWS
FROM [Adventure Works];


回答2:

Yes, you've got the main idea by using SET for Others, but several minor additions are required to complete the task.

I'll use my test DBs, but this can easily be transformed to yours.

  • [Report Date] - date dimension ([Klient] analogue)
  • [REPORT DATE Y] - years hierarchy ([Grupa Klientow])
  • [REPORT DATE YM] - months hierarchy ([Klient].[Klient])
  • [Measures].[Count] - measure for TopCount ([Measures].[Przychody ze sprzedazy rzeczywiste wartosc])

I also used top 3 just to show result image here.

And here's the code:

with

/* first, add empty [Other] member to the group level */
member [Report Date].[REPORT DATE Y].[Other] as null

/* second, copy measure by fixing the lowest level */
member [Measures].[Count with Other Groups] as ([Report Date].[REPORT DATE YM],[Measures].[Count])

/* third, create top 10 by group */
set [Report Date Top 10 Groups] as
Generate([Report Date].[REPORT DATE Y].Children
,TopCount([Report Date].[REPORT DATE Y].CurrentMember
 * [Report Date].[REPORT DATE YM].Children,3,[Measures].[Count with Other Groups]))

/* this is the part for Other group mapping */
set [Report Date Other Groups] as
[Report Date].[REPORT DATE Y].[Other]
 * ([Report Date].[REPORT DATE YM].Children
    - Extract([Report Date Top 10 Groups],[Report Date].[REPORT DATE YM]))

select {[Measures].[Count],[Measures].[Count with Other Groups]} on 0
,
{
[Report Date Top 10 Groups],[Report Date Other Groups]}
on 1
from 
[DATA]

And here is the result:

..all members till the last one (which is 201606) are on the Other group.

Hope this helps, bardzo dziękuję!

Update: code was optimized by removing one multiplying in Report Date Other Groups calculation.

Update-2: (not solved yet, but in progress)

(use 'Other' member under each group)

IMPORTANT! We need additional hierarchy: Group->Client ([Report Date].[REPORT DATE] with Year->Month is my case) to be able to determine parent for each low level member.

with

/* create top 10 by group */
set [Report Date Top 10 Groups] as
Generate([Report Date].[REPORT DATE Y].Children
,TopCount([Report Date].[REPORT DATE Y].CurrentMember
 * [Report Date].[REPORT DATE].Children,3,[Measures].[Count]))

/* this is the part for Other group the lowest level non-aggregated members */
set [Report Date Other Members] as
[Report Date].[REPORT DATE Y].Children
* ([Report Date].[REPORT DATE].[Month].AllMembers
    - [Report Date].[REPORT DATE].[All])
- [Report Date Top 10 Groups]

/* add empty [Other] member to the group level, HERE IS AN ISSUE */
member [Report Date].[REPORT DATE].[All].[Other] as null

set [Report Date Other Groups] as
[Report Date].[REPORT DATE Y].[All].Children
* [Report Date].[REPORT DATE].[Other]

member [Measures].[Sum of Top] as
IIF([Report Date].[Report Date].CurrentMember is [Report Date].[REPORT DATE].[Other]
,null /* HERE SHOULD BE CALCULATION, but only
 {[Report Date].[Report Date Y].[All].[Other]}
 is shown, because 'Other' is added to the entire hierarchy */
,SUM([Report Date].[REPORT DATE Y].CurrentMember
        * ([Report Date].[Report Date].CurrentMember.Parent.Children
            - Extract([Report Date Other Members],[Report Date].[REPORT DATE]))
    ,[Measures].[Count]))

member [Measures].[Sum of Group] as
([Report Date].[Report Date].CurrentMember.Parent,[Measures].[Count])

select {[Measures].[Count],[Measures].[Sum of Group],[Measures].[Sum of Top]} on 0
,
Order(Hierarchize({[Report Date Top 10 Groups]
,[Report Date Other Groups]}),[Measures].[Count],DESC)

on 1
from 
[DATA]

And here is the intermediate result:

I need to move this result here, but have no idea how to do it.

I also tried using flat hierarchies of each level. Other member is shown correctly, but not able to calculate SUM, because both levels are independent. Maybe we can add a property like 'Group_Name' and use unlinked levels, but again - it decreases performance drastically. All this IIF([bla-bla-bla low level member].Properties("Group_Name")=[bla-bla-bla group level].Member_Name are extremely slow.

Update-3 (AdvWorks version of code above)

with

/* create top 10 by group */
set [Top 10 Groups] as
Generate([Customer].[Country].Children
,TopCount([Customer].[Country].CurrentMember
 * [Customer].[Customer Geography].Children,3,[Measures].[Internet Order Count]))

/* this is the part for Other group the lowest level non-aggregated members */
set [Other Members] as
[Customer].[Country].Children
* ([Customer].[Customer Geography].[State-Province].AllMembers
    - [Customer].[Customer Geography].[All])
- [Top 10 Groups]

/* add empty [Other] member to the group level */
member [Customer].[Customer Geography].[All].[Other] as
([Customer].[Country],[Measures].[Internet Order Count])

set [Other Groups] as
[Customer].[Country].[All].Children
* [Customer].[Customer Geography].[Other]

member [Measures].[Sum of Top] as
IIF([Customer].[Customer Geography].CurrentMember is [Customer].[Customer Geography].[Other]
,null
,SUM([Customer].[Country].CurrentMember
        * ([Customer].[Customer Geography].CurrentMember.Parent.Children
            - Extract([Other Members],[Customer].[Customer Geography]))
    ,[Measures].[Internet Order Count]))

member [Measures].[Sum of Group] as
([Customer].[Customer Geography].CurrentMember.Parent,[Measures].[Internet Order Count])

select {[Measures].[Internet Order Count],[Measures].[Sum of Group],[Measures].[Sum of Top]} on 0
,
Order(Hierarchize({[Top 10 Groups],[Other Groups]}),[Measures].[Internet Order Count],DESC) on 1
from [Adventure Works]

Update-4 (with a solution in year/month example)

Amazing solution of @whytheq helped to do what I want:

WITH 
  SET [All Grupa Klientow]  AS ([Report Date].[Report Date Y].Children) 
  SET [All Klient] AS ([Report Date].[Report Date YM].Children)
  SET [Top N Members] AS 
    Generate
    (
      [All Grupa Klientow]
     ,TopCount
      (
        (EXISTING 
          [All Klient])
       ,3
       ,[Measures].[Count]
      )
    ) 
  MEMBER [Report Date].[Report Date YM].[Other] AS 
    Aggregate({(EXISTING {[All Klient]} - [Top N Members])}) 
SELECT 
  {[Measures].[Count]} ON 0
 ,{
      [All Grupa Klientow]
    * 
      {
        [Top N Members]
       ,[Report Date].[Report Date YM].[Other]
      }
  } ON 1
FROM [DATA];

And the image:

Task is solved, but please mark not this answer, but @whytheq's!



标签: ssas mdx olap