MDX to count common members - EXISTS alternative - ssas

Let me describe the issue giving example from [Adventure Works] cube.
Following MDX returns count of 17473
SELECT NON EMPTY { [Measures].[Internet Order Count] } ON COLUMNS,
[Internet Sales Order Details].[Sales Order Number] on ROWS
FROM [Adventure Works])
WHERE ( [Sales Reason].[Sales Reason].&[1] -- price
and following returns count of 3515
SELECT NON EMPTY { [Measures].[Internet Order Count] } ON COLUMNS ,
[Internet Sales Order Details].[Sales Order Number] on ROWS
FROM [Adventure Works]
WHERE ( [Sales Reason].[Sales Reason].&[2]) -- on promotion
I would like to count [Sales Order Number] which are common in [Sales Reason].&1 and [Sales Reason].&[2]
SQL equivalent would be:
select count(distinct f.SalesOrderNumber)
from FactInternetSales f
join FactInternetSalesReason fs
on f.SalesOrderNumber = fs.SalesOrderNumber and f.SalesOrderLineNumber = fs.SalesOrderLineNumber
where fs.SalesReasonKey = 1 and fs.SalesOrderNumber in
(select SalesOrderNumber from FactInternetSalesReason fs1 where fs1.SalesReasonKey = 2)
-- sales reason 1 = 17473
-- sales reason 2 = 3515
-- common 1689
I got common count using following mdx:
WITH MEMBER [Measures].[common] AS count
( exists ( exists ([Internet Sales Order Details].[Sales Order Number].[Sales Order Number].Members,
[Sales Reason].[Sales Reason].&[1],"Internet Orders"
),
[Sales Reason].[Sales Reason].&[2],"Internet Orders"
)
)
SELECT NON EMPTY [Measures].[common] ON COLUMNS
FROM [Adventure Works]
-- 1689
But use of EXISTS is rather slow for my requirement. Please suggest an alternative.
Also please see related thread here
Thank you

Please try adding both reasons to the WHERE clause as a single set:
SELECT
NON EMPTY
{[Measures].[Internet Order Count]} ON COLUMNS
,[Internet Sales Order Details].[Sales Order Number] ON ROWS
FROM [Adventure Works]
WHERE
{
[Sales Reason].[Sales Reason].&[2]
,[Sales Reason].[Sales Reason].&[1]
};
Here is an alternative that runs faster, only looks at the common orders, and does not use the EXISTS function:
WITH
SET [AllOrders] AS
[Internet Sales Order Details].[Sales Order Number].[Sales Order Number].MEMBERS
SET [OrdersIntersection] AS
Intersect
(
NonEmpty
(
[AllOrders]
,{
(
[Sales Reason].[Sales Reason].&[1]
,[Measures].[Internet Order Count]
)
}
)
,NonEmpty
(
[AllOrders]
,{
(
[Sales Reason].[Sales Reason].&[2]
,[Measures].[Internet Order Count]
)
}
)
)
MEMBER [Measures].[commonCount] AS
[OrdersIntersection].Count
SELECT
//NON EMPTY //<<not needed
[Measures].[commonCount] ON COLUMNS
FROM [Adventure Works];

Related

MDX SSAS Rank rownumber function

I have an existing SSRS report out of T-SQL query and I am trying to recreate it using MDX queries on SSAS cube. I am stuck with rewriting Row num and rank logic to MDX.
It is written as:
SELECT ceil((ROW_NUMBER() OVER (PARTITION BY PRODUCT ORDER BY YEARMONTH))/12)
Rank1 in the SQL. Can someone tell me if this can be done using MDX? In the cube, PRODUCT and YEARMONTH are coming from separate dimensions.
Thank you for your help!
There is the Rank() function. For example:
with
Dynamic Set OrderedSet as
Order(
NonEmptyCrossJoin(
[Date].[Year].[Year].Members,
[Product].[Product].[Product].Members,
[Measures].[Invoice Count],
2
),
[Measures].[Invoice Count],
BDESC
)
Member [Measures].[Rank] as
Rank(
([Client].[Client].Currentmember,[Date].[Year].CurrentMember),
OrderedSet
)
select {[Measures].[Invoice Count],[Measures].[Rank]} on 0,
non empty OrderedSet on 1
from [BI Fake]
where ([Date].[Day].&[20160120])
You can read in details about it from my blog post.
You can use Generate to repeat ranks like this:
WITH
SET [SalesRank] AS
Generate
(
[Customer].[Customer Geography].[State-Province]
,Order
(
NonEmpty
(
(
[Customer].[Customer Geography].CurrentMember
,[Product].[Product Categories].[Subcategory]
)
,{[Measures].[Internet Sales Amount]}
)
,[Measures].[Internet Sales Amount]
,BDESC
)
)
MEMBER [Measures].[CategoryRank] AS
Rank
(
(
[Customer].[Customer Geography].CurrentMember
,[Product].[Product Categories].CurrentMember
)
,Exists
(
[SalesRank]
,[Product].[Product Categories].CurrentMember
)
)
SELECT
{
[Measures].[Internet Sales Amount]
,[Measures].[CategoryRank]
} ON 0
,[SalesRank] ON 1
FROM [Adventure Works];
It results in this:

Ranking of multiple dimensions, restarting for every year

I have a measure, Sales Amount. I want to rank customers within a divison by year for that measure. I need to also display that rank as a measure. The rank needs to start over every year. I am able to do customers by year and customers by division, but I can't seem to figure out how to combine them both so it iterates over both dimensions properly. Below is what I have for the customers by year. I have tried adding another Division set, creating another named set that I GENERATE with the YearsWithCustomers set, and RANK using that new named set. I seem to be super close to figuring this out but I think I am putting something in the wrong place. I got the idea to iterate over a set from one of Chris Webb's blogs, located here.
WITH
SET Years AS
TopPercent
(
[Sales and Forecast Date].[Calendar Year].[Year Number].MEMBERS
,100
,[Measures].[Sales Amount]
)
SET Customers AS
Filter
(
[Customer].[Customer Number].[Customer Number].MEMBERS
,
[Measures].[Sales Amount] > 0
)
SET YearsWithCustomers AS
Generate
(
Years
,Union
(
{[Sales and Forecast Date].[Calendar Year].CurrentMember}
,StrToSet
("
Intersect({},
{order(Customers,([Sales Amount],[Sales and Forecast Date].[Calendar Year].CurrentMember),desc)
as CustomerSet"
+
Cstr(Years.CurrentOrdinal)
+ "})"
)
)
,ALL
)
MEMBER [Measures].[Customer Rank] AS
Rank
(
[Customer].[Customer Number].CurrentMember
,StrToSet
("CustomerSet"
+
Cstr
(
Rank
(
[Sales and Forecast Date].[Calendar Year].CurrentMember
,Years
)
)
)
)
SELECT
{
[Customer Rank]
,[Measures].[Sales Amount]
} ON 0
,Order
(
Filter
(
(
YearsWithCustomers
,Customers
)
,
[Sales Amount] > 0
)
,[Sales Amount]
,desc
) ON 1
FROM [OrdersAndBudgets];
Here is what I currently have. I would expect to see 1, 2, 3, etc for the Rank measure. It should reset for each division for every year.
I like this sort of pattern:
WITH
SET [AllCountries] AS
[Country].[Country].MEMBERS
SET [AllProds] AS
[Product].[Product].[Product].MEMBERS
SET [Top5Prods] AS
Generate
(
[AllCountries] AS a
,{
(
a.CurrentMember
,[Product].[Product].[All]
)
+
//The top x prods
a.CurrentMember
*
TopCount
(
[AllProds]
,5
,[Measures].[Internet Sales Amount]
)
}
)
MEMBER [Product].[Product].[All].[Other Products] AS
Aggregate
(
[Country].CurrentMember * [Product].[Product].[Product].MEMBERS
-
[Top5Prods]
)
SELECT
{[Measures].[Internet Sales Amount]} ON COLUMNS
,Hierarchize(
{
[Top5Prods]
,[AllCountries] * [Product].[Product].[All].[Other Products]
}
) ON ROWS
FROM [Adventure Works];
It returns the following:
There is quite an extensive thread here: Top X of Top Y with RestOf member where X and Y are hierarchies from different dimensions

MDX Start and End Time per transaction

I hope you can help i have tried so many way to try get this right with no luck. I am trying to get out the player account number the date and start and end date time and maybe calculate the play duration between the start and end times.
I would like the output to look something like this.
PlayerAccount | GamingDate | StartTime | EndTime | PlayDuration | ActualWin
I always seem to return the start and end time for the whole day and not Per account.
WITH
SET [MySet] AS
[Customer].[Player Account Number].Children*
Head
(
NonEmpty
(
[Start Time].[Hour].Children
,[Measures].[Actual Win]
)
,1
)*
Tail
(
NonEmpty
(
[End Time].[Hour].Children
,[Measures].[Actual Win]
)
,1
)
SELECT
{[Measures].[Actual Win]} ON 0
,{[MySet]} ON 1
FROM
(
SELECT
[Customer].[Player Account Number].&[1040002184]
:
[Customer].[Player Account Number].&[1040002198] ON 0
FROM Ratings
)
WHERE
{[Gaming Date].[Full Date].&[20150101]};
Usually first date and last date would be measures. If you only want results for those two player then use the WHERE clause rather than a sub-select:
WITH
MEMBER [Measures].[fDate] AS
Head
(
NonEmpty
(
[Start Time].[Hour].MEMBERS
,[Measures].[Actual Win]
)
).Item(0).Item(0).Member_Caption
MEMBER [Measures].[lDate] AS
Tail
(
NonEmpty
(
[End Time].[Hour].MEMBERS
,[Measures].[Actual Win]
)
).Item(0).Item(0).Member_Caption
SELECT
{
[Measures].[fDate]
,[Measures].[lDate]
,[Measures].[Actual Win]
} ON 0
,{[Customer].[Player Account Number].Children} ON 1
FROM Ratings
WHERE
([Gaming Date].[Full Date].&[20150101],
{ [Customer].[Player Account Number].&[1040002184]
,[Customer].[Player Account Number].&[1040002198]});
This is a working AdvWrks script which does the sort of thing you're trying to achieve:
WITH
MEMBER [Measures].[firstDate] AS
Head
(
NonEmpty
(
[Date].[Date].[Date].MEMBERS
,[Measures].[Internet Sales Amount]
)
).Item(0).Item(0).Member_Caption
MEMBER [Measures].[lastDate] AS
Tail
(
NonEmpty
(
[Date].[Date].[Date].MEMBERS
,[Measures].[Internet Sales Amount]
)
).Item(0).Item(0).Member_Caption
SELECT
{
[Measures].[Internet Sales Amount]
,[Measures].[firstDate]
,[Measures].[lastDate]
} ON 0
,NON EMPTY {[Promotion].[Promotion].MEMBERS} ON 1
FROM [Adventure Works];
If you'd rather pull back a member instead of using Measures I think the GENERATE function will work like this AdvWrks example:
WITH
SET [aSet] AS
Generate
(
[Promotion].[Promotion].MEMBERS
,
[Promotion].[Promotion].CurrentMember
*
Head
(
NonEmpty
(
[Date].[Date].[Date].MEMBERS
,[Measures].[Internet Sales Amount]
)
)
)
SELECT
{[Measures].[Internet Sales Amount]} ON 0
,NON EMPTY
{[aSet]} ON 1
FROM [Adventure Works];
Thanks a million whytheq it seems like that .Item(0).Item(0).Member_Caption made all the difference the results seem to be come out right now. The final solution looks like this now..
with set [MySet]
as
[Casino Hierarchy].[Casino Key].children *
[Gaming Date].[Date].currentmember *
[Customer].[Player Account Number].children *
[Start Time].[Hour].children
member [measures].[Mindate]
as
HEAD(nonempty([End Time].[Hour].children,[Measures].[Actual Win]),1).Item(0).Item(0).Member_Caption
member [measures].[Maxdate]
as
TAIL(nonempty([Start Time].[Hour].children,[Measures].[Actual Win]),1).Item(0).Item(0).Member_Caption
select non empty
{
[Measures].[Actual Win],[measures].[Mindate],[measures].[Maxdate]
} on 0, non empty
{
[Customer].[Player Account Number].children
} on 1
from
(
select ([Customer].[Player Account Number].&[1040002184]:[Customer].[Player Account Number].&[1040002198]) on 0 from Ratings
)
where
{
[Gaming Date].[Full Date].&[20150101]
}
You are having a static set(which always return the same members). Use the EXISTING keyword to evaluate the Start and end time in the current context.
with member [Measures].[StartDate] as
EXISTING
HEAD(
nonempty(
[Start Time].[Hour].Children,[Measures].[Actual Win]
) ,1
).ITEM(0).ITEM(0).MEMBERVALUE
member [Measures].[EndDate] as
EXISTING
TAIL(
nonempty(
[End Time].[Hour].children,[Measures].[Actual Win]
) ,1
).ITEM(0).ITEM(0).MEMBERVALUE
select
{
[Measures].[Actual Win],
[Measures].[StartDate],
[Measures].[EndDate]
} on 0,
[Customer].[Player Account Number].children on 1
from
(
select (
[Customer].[Player Account Number].&[1040002184]:
[Customer].[Player Account Number].&[1040002198]
) on 0
from Ratings
)
where {[Gaming Date].[Full Date].&[20150101]}
Have rearranged the [Customer].[Player Account Number] so that dates are evaluated in the context of each player account. Hope that's fine!
Edit.
A simplified AdWrks version of this approach is as follows: still not quite working:
WITH
SET [aSet] AS
(EXISTING
Head
(
NonEmpty
(
[Date].[Date].[Date].MEMBERS
,[Measures].[Internet Sales Amount]
)
))
SELECT
{[Measures].[Internet Sales Amount]} ON 0
,NON EMPTY
{[Promotion].[Promotion].MEMBERS * [aSet]} ON 1
FROM [Adventure Works];

Using GENERATE to concatenate Rank and Member Name

The following is from MSDN: http://msdn.microsoft.com/en-us/library/ms144726.aspx
WITH
SET OrderedCities AS Order
([Geography].[City].[City].members
, [Measures].[Reseller Sales Amount], BDESC
)
MEMBER [Measures].[City Rank] AS Rank
([Geography].[City].CurrentMember, OrderedCities)
SELECT {[Measures].[City Rank],[Measures].[Reseller Sales Amount]} ON 0
,Order
([Geography].[City].[City].MEMBERS
,[City Rank], ASC)
ON 1
FROM [Adventure Works]
Would it be possible to use the GENERATE function to concatenate the rank and the city into a single column. So results would look like the following:
EDIT
So the following must be getting closer:
WITH
SET OrderedCities AS Order
([Geography].[City].[City].members
, [Measures].[Reseller Sales Amount], BDESC
)
MEMBER [Measures].[City Rank] AS Rank
([Geography].[City].CurrentMember, OrderedCities)
MEMBER [Measures].[memberName] AS
'[Geography].[City].CurrentMember.name'
MEMBER [Measures].[memberValue] AS
'[Measures].[City Rank].value'
MEMBER [Measures].[concat] AS
// [Measures].[memberName] + [Measures].[memberValue]//<<this errors
// '[Measures].[memberName] & [Measures].[memberValue]'//<<this errors
'[Measures].[memberName] + [Measures].[memberValue]'//<<this errors
SELECT
{[Measures].[City Rank],
[Measures].[memberName],
[Measures].[memberValue],
[Measures].[concat],
[Measures].[Reseller Sales Amount]} ON 0
,Order
([Geography].[City].[City].MEMBERS
,[City Rank], ASC)
ON 1
FROM [Adventure Works]
the following should work:
MEMBER [Measures].[concat] AS
cstr(RANK( [Geography].[City].currentmember, orderedcities)) +'.'+
[Geography].[City].currentmember.name
You will still have to use the presentation programm to get rid of the first redundant column.
Philip,

Elegant way to ignore null when applying RANK

This article gives a method for ignoring null when applying RANK : http://www.bidn.com/blogs/CraigLove/ssas/2617/mdx-walkthrough
Is there a more elegant way of doing this than using CASE ?
WITH
MEMBER [Measures].[City Rank]
AS
CASE
WHEN
NOT ISEMPTY
(
(
[Geography].[City].CurrentMember
,[Measures].[Reseller Sales Amount]
)
)
THEN
RANK
(
[Geography].[City].CurrentMember
,[Geography].[City].AllMembers
,[Measures].[Reseller Sales Amount]
)
ELSE
NULL
END
SET [OrderedCity]
AS
ORDER
(
[Geography].[City].AllMembers
,[Measures].[Reseller Sales Amount]
,DESC
)
SELECT
{
[Measures].[City Rank]
,[Measures].[Reseller Sales Amount]
} ON COLUMNS
,NON EMPTY [OrderedCity] ON ROWS
FROM [Adventure Works];
You could apply the NonEmpty function within OrderedCity:
WITH
SET [OrderedCity]
AS
NonEmpty(
ORDER
(
[Geography].[City].AllMembers
,[Measures].[Reseller Sales Amount]
,DESC
)
)
MEMBER [Measures].[City Rank]
AS
RANK
(
[Geography].[City].CurrentMember
,[OrderedCity]
)
SELECT
{
[Measures].[City Rank]
,[Measures].[Reseller Sales Amount]
} ON COLUMNS
,[OrderedCity]
ON ROWS
FROM [Adventure Works];
This way the NON EMPTY is applied on the set on the rows before it it put there, so there is no need to apply it on the rows themselves. Furthermore, re-using the set within the definition of City Rank allows Analysis Services to calculate the sorting only once, and cache the ordered set. Otherwise, the Rank would require the ordering to be re- calculated for each row.