Core MDX Functionality

advertisement
INTRODUCTION TO MDX QUERIES
TABLE OF CONTENTS
Getting Started with MDX .......................................................................................................................................................................... 2
Slicer Specifications............................................................................................................................................................................ 4
Core MDX Functionality ............................................................................................................................................................................. 4
Calculated Members and Named Sets ................................................................................................................................................... 4
Hierarchical Navigation .......................................................................................................................................................................... 6
Time Series Functions ............................................................................................................................................................................ 9
Tuples and CROSSJOIN ......................................................................................................................................................................... 11
Filtering and Sorting ............................................................................................................................................................................. 11
Top and Bottom Performance Analysis ............................................................................................................................................... 13
Numeric Functions ............................................................................................................................................................................... 14
User Defined Functions ........................................................................................................................................................................ 15
Member Set and Logical Functions ...................................................................................................................................................... 16
References ........................................................................................................................................................................................... 17
Microsoft SQL Server™ Analysis Services 2008 provides an architecture for access to multidimensional data. This data is summarized,
organized, and stored in multidimensional structures, called cubes, for rapid response to user queries. Analysis Services supports
MDX functions as a full language implementation for creating and querying cube data. It is the querying capabilities of this language
that will be the focus of this paper. To demonstrate these capabilities the paper will, whenever possible, utilize simple real world
sample expressions based on the Adventure Works 2008 Database. The latest samples for SQL Server 2008 can be downloaded from
the following location: http://www.codeplex.com/MSFTDBProdSamples/
These MDX expressions can then be run to view the actual output. A brief outline of the dimensions and measure that will make up
the samples is outlined in below.
In describing MDX it will be assumed that one is familiar with multidimensional, data warehousing, and online analytical processing
(OLAP) terms. If not one should spend some time reading about these topics.
GETTING STARTED WITH MDX
Let’s start by outlining one of the simplest forms of a MDX expression, bearing in mind this is for an expression returning two cube
dimensions:
SELECT
axis specification ON COLUMNS,
axis specification ON ROWS
FROM cube_name
WHERE slicer_specification
The axis specification can also be thought of as the member selection for the axis. If a single dimension is required, using this
notation, COLUMNS must be returned. For more dimensions the named axes would be PAGES, CHAPTERS, and finally SECTIONS. If one
desires more generic axes terms, over the use named terms, one can use the AXIS(index) naming convention. The index will be a
zero based reference to the axis.
The slicer specification on the WHERE clause is actually optional. If not specified the returned measure will be the default for the
cube. Unless one actually queries the Measures dimension (as will the first few expressions) one should always specify a slicer
specification. This defines the slice of the cube to be viewed, hence the term. More will be said on this later.
The simplest form of an axis specification or member selection is to take the Members of the required dimension, including that of
the special Measures dimension:
SELECT
Measures.Members ON COLUMNS,
[Product].[Product Categories].Members ON ROWS
FROM [Adventure Works]
This expression satisfies the requirement to query the recorded measures for each Product Category along with a summary at every
defined summary level. Alternatively, it displays the measures for the “Product Categories” hierarchy. In running this expression one
will see a row member named “All Products”. The “All” member is generated by default and becomes the default member for the
dimension.
The square brackets are optional but are required for identifiers with embedded spaces. The axis definition can be enclosed in curly
brackets, which are used to denote sets. There use is needed only when enumerating sets.
In addition to taking the members of a dimension, a single member of a dimension can be selected. If desired an enumeration of the
required members can be returned on a single axis:
SELECT
Measures.Members ON COLUMNS,
{[Product].[Product Categories].[Category].[Bikes], [Product].[Product Categories].[Category].[Components]} ON ROWS
FROM [Adventure Works]
This expression queries the measures for the product categories summarized for the “Bikes” and “Components”. To actually query
the measures for the members making up both these categories, one would query the Children of the required members:
SELECT
Measures.Members ON COLUMNS,
{[Product].[Product Categories].[Category].[Bikes].Children,
[Product].[Product Categories].[Category].[Components].Children} ON ROWS
FROM [Adventure Works]
When running this expression, it is interesting to note that the row set could be expressed by either of the following expressions:
[Product].[Product Categories].[Category].[Bikes].Children
[Product].[Product Categories].[Bikes].Children
[Product Categories].[Bikes].Children
The expression uses fully qualified or unique names. Fully qualified member names include their dimension, and the parent member
of the given member at all levels. When member names are uniquely identifiable fully qualified members names are not required.
Currently the unique name property is defined as the fully qualified name. To remove any ambiguities in formulating expressions,
the use of unique names should always be adopted.
At this point one should be comfortable with the concept of both Members and Children. To define these terms, Members will return
the members for the specified dimension or dimension level and Children will return the child members for a particular member
within the dimension. Both functions are used often in formulating expressions, but do not provide the ability to drill down to a
lower level within the hierarchy. For this a function called DESCENDANTS is required. This function allows one to go to any level in
depth. The syntax for the Descendants function is:
DESCENDANTS(member, level [, flags])
By default, only members at the specified level will be included. By changing the value of the flag one can include or exclude
descendants or children before and after the specified level. Using DESCENDANTS it becomes possible to query the cube for
information at the individual product level, in addition to providing a summary at the product category and subcategory. Using Bikes
as an example:
SELECT
Measures.Members ON COLUMNS,
{[Product].[Product Categories].[Category].[Bikes],
DESCENDANTS([Product].[Product Categories].[Category].[Bikes], [Product].[Product Categories].[Product])} ON ROWS
FROM [Adventure Works]
The value of the optional flag can be SELF (the default value whose value can be omitted), BEFORE, AFTER, BEFORE_AND_AFTER,
SELF_AND_AFTER, SELF_AND_BEFORE, SELF_BEFORE_AFTER, or LEAVES.
This statement would return all members for “Bikes” down to the “Product” level.
DESCENDANTS([Product].[Product Categories].[Category].[Bikes], [Product].[Product Categories].[Product], SELF_AND_BEFORE)
In running the queries one may see empty cells; null returned value’s for measures for which there is no value. These can easily be
excluded from the result set using the NON EMPTY specification; this being a simple form of filtering which is discussed later:
SELECT
NON EMPTY Measures.Members ON COLUMNS,
{[Product].[Product Categories].[Category].[Bikes],
DESCENDANTS([Product].[Product Categories].[Category].[Bikes], [Product].[Product Categories].[Product])} ON ROWS
FROM [Adventure Works]
One final function does require mentioning for calculated members, that of ADDCALCULATEDMEMBERS. Calculated members are not
enumerated if one requests the dimensions members. They have to be explicitly requested by using the ADDCALCULATEDMEMBERS
function:
SELECT
NON EMPTY ADDCALCULATEDMEMBERS(Measures.Members) ON COLUMNS,
Descendants([Product].[Product Categories].[Category].[Bikes], [Product].[Product Categories].[Product],
SELF_AND_BEFORE) ON ROWS
FROM [Adventure Works]
SLICER SPECIFICATIONS
The WHERE clause is where one defines the slicer specification, outlining the slice of the cube to be viewed. Usually, the WHERE clause
is used to define the measure that is being queried. As the cube's measures are just another dimension, selecting the desired
measure is achieved by selecting the appropriate slice of the cube.
If one was required to query the internet sales amounts for the products summarized at the product line level, cross referenced
against the customer geography of the sales, a slicer specification would have to be defined. This requirement could be expressed by
the following expression:
SELECT
{[Product].[Product Model Lines].[Product Line].Members} ON COLUMNS,
{[Customer].[Customer Geography].[State-Province].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Internet Sales Amount])
As stated the slicer specification in the WHERE clause is actually a dimensional slice of the cube. Thus the WHERE clause can, and often
is, extended to other dimensions. If one was only interested in the sales amounts for the fiscal year 2004 the WHERE clause would be
written as:
WHERE (Measures.[Internet Sales Amount], [Date].[Fiscal Year].[FY 2004]
Alternatively it could also be written defining the member key rather than the member name:
WHERE (Measures.[Internet Sales Amount], [Date].[Fiscal Year].&[2004]
It is important to note that slicing is not the same as filtering. Slicing does not affect selection of the axis members, but rather the
values that go into them. This is different to filtering, since filtering will reduce the number of axis members.
CORE MDX FUNCTIONALITY
Although the basics of MDX are enough to provide simple queries against the multidimensional data, there are many features of the
MDX implementation that make MDX a rich and powerful query tool. These features will allow more useful analysis of the cube data
and will be the focus of the remainder of this paper.
CALCULATED MEMBERS AND NAMED SETS
An important concept when working with MDX expressions is that of Calculated Members and Named Sets. Calculated members
allow one to define formulas and treat the formula as a new member of a specified parent. Within a MDX expression, the syntax for
a calculated member is to put the following construction in front of the SELECT statement:
WITH MEMBER parent.name AS ‘expression’
Here, parent refers to the parent of the new calculated member name. Since dimensions are organized as a hierarchy, when defining
calculated members their position inside the hierarchy must be defined.
Similarly for named sets the syntax is:
WITH SET set_name AS ‘expression’
If one needs to have named sets and calculated members available for the life of a session, and visible to all queries in that session,
one can use the CREATE statement with a SESSION scope. As this is part of the cube definition syntax this paper will only concentrate
on query specific calculated members and named sets.
The simplest use of calculated members is in defining a new measure that relates already defined measures. This is a common
practice for such questions as percentage profit for sales, by defining the calculated measure “Reseller Profit Percent”.
WITH
MEMBER Measures.[Reseller Profit Percent] AS
(Measures.[Reseller Sales Amount] - Measures.[Reseller Total Product Cost]) /
(Measures.[Reseller Total Product Cost]), FORMAT_STRING = '#.00%'
In defining calculated members there are two properties that one will need to learn; FORMAT_STRING and SOLVE_ORDER.
FORMAT_STRING merely informs the MDX expression of the display format to use for the new calculated member. The format
expression takes the form of the Visual Basic format function. The use of the percent symbol (%) states that the calculation returns a
percentage and should be treated as such, including the multiplication by a factor of 100. With FORMAT_STRING string one can
alternatively use designated named words such as Fixed, Standard, Percent and Currency.
The SOLVE_ORDER property is used when multiple calculated members or named sets are being defined. This property helps decide
the order in which to perform the evaluations. The calculated member or named set with the highest solve order value will be the
first to be evaluated. The default value for the solve order is zero.
With calculated members one can easily define a new time member to compare the summer and fall periods:
MEMBER [Date].[Calendar].[Summer Months 2003] AS
[Date].[Calendar].[Month].[June 2003] + [Date].[Calendar].[Month].[July 2003]
+ [Date].[Calendar].[Month].[August 2003]
MEMBER [Date].[Calendar].[Fall Months 2003] AS
[Date].[Calendar].[Month].[September 2003] + [Date].[Calendar].[Month].[October 2003]
+ [Date].[Calendar].[Month].[November 2003]
Using all this, if one was required to display the product line sales percentage profit for each define period the MDX expression
would read:
WITH
MEMBER Measures.[Reseller Profit Percent] AS
(Measures.[Reseller Sales Amount] - Measures.[Reseller Total Product Cost]) /
(Measures.[Reseller Total Product Cost]),
FORMAT_STRING = 'Percent', SOLVE_ORDER = 1
MEMBER [Date].[Calendar].[Summer Months 2003] AS
[Date].[Calendar].[Month].[June 2003] + [Date].[Calendar].[Month].[July 2003]
+ [Date].[Calendar].[Month].[August 2003]
MEMBER [Date].[Calendar].[Fall Months 2003] AS
[Date].[Calendar].[Month].[September 2003] + [Date].[Calendar].[Month].[October 2003]
+ [Date].[Calendar].[Month].[November 2003]
SELECT
{[Date].[Calendar].[Summer Months 2003], [Date].[Calendar].[Fall Months 2003]} ON COLUMNS,
{[Product].[Product Model Lines].[Product Line].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Profit Percent])
The use of the SOLVE_ORDER deserves explanation. As the SOLVE_ORDER for the calculated measure, profit percent, is defined as to
be greater than zero, its value will be evaluated first. In evaluating the percentage profit over the new calculated time members,
summer 2003 and fall 2003, the values for sales amount and product cost over these time periods will be calculated. The calculation
for profit percent will then be correctly based on these values the new calculated time members.
If the solve order was defined to evaluate the new calculated date members first the profit percent would be calculated for each
season prior to adding the season’s results. This will have the affect of adding together the two percentages for each season rather
than calculating the percent for the whole defined time period. This would obviously yield incorrect results.
In all these expressions the new calculated member has been directly related to a dimension. This doesn’t have to be the case;
calculated members can also be related to a member within the hierarchy, as the next sample shows:
WITH
MEMBER [Date].[Calendar].[CY 2003].[Summer Months 2003] AS
[Date].[Calendar].[Month].[June 2003] + [Date].[Calendar].[Month].[July 2003]
+ [Date].[Calendar].[Month].[August 2003]
MEMBER [Date].[Calendar].[CY 2003].[Fall Months 2003] AS
[Date].[Calendar].[Month].[September 2003] + [Date].[Calendar].[Month].[October 2003]
+ [Date].[Calendar].[Month].[November 2003]
SELECT
{[Date].[Calendar].[CY 2003].[Summer Months 2003], [Date].[Calendar].[CY 2003].[Fall Months 2003]} ON COLUMNS,
{[Product].[Product Model Lines].[Product Line].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Sales Amount])
In defining the calculated members one again could use the member keys rather than the member names:
WITH MEMBER [Date].[Calendar].&[2003].[Summer Months 2003] AS
[Date].[Calendar].[Month].&[2003]&[6] + [Date].[Calendar].[Month].&[2003]&[7]
+ [Date].[Calendar].[Month].&[2003]&[8]
The definition of named sets follows the exact same syntax as that for calculated members. A named set could be defined that
contains the first half of each fiscal year, within the date dimension. Using this one can display and compare sales amounts for the
first half of each fiscal period:
WITH
SET [Fiscal.FirstHalves] AS
GENERATE({[Date].[Fiscal].[Fiscal Year].Members}, {[Date].[Fiscal].CurrentMember.FirstChild})
SELECT
[Fiscal.FirstHalves] ON COLUMNS,
{[Product].[Product Model Lines].[Product Line].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Sales Amount])
The function FirstChild takes the first child of the specified member, in this case the first half of each fiscal period. A similar
function called LastChild also exist which will take the last child of a specified member.
More will be said about calculated members and named sets in the following sections, including the use of the CurrentMember
function. In using calculated members and named sets, it is the ability to perform hierarchical navigation that really extends their
usage. It is this ability that will be covered in the next section.
HIERARCHICAL NAVIGATION
In constructing MDX expressions it is often necessary for one to relate a current member value’s to others in the hierarchy. MDX has
many methods that can be applied to a member to traverse this hierarchy. The most commonly used ones are PrevMember,
NextMember, CurrentMember, and Parent. Others do exist such as FirstChild and LastChild.
Consider the common business requirement of calculating the sales for a product as a percentage of the sales for that product
within its product subcategory. To satisfy this requirement one will need to calculate the percentage of the sales for the current
product, or member, to that of its parent. This can be achieved using the CurrentMember and Parent functions:
WITH
MEMBER Measures.[Parent Internet Sales] AS
([Product].[Product Categories].CurrentMember.Parent, Measures.[Internet Sales Amount]),
FORMAT_STRING = 'Currency'
MEMBER Measures.[Percentage Sales] AS
([Product].[Product Categories].CurrentMember, Measures.[Internet Sales Amount]) /
([Product].[Product Categories].CurrentMember.Parent, Measures.[Internet Sales Amount]),
FORMAT_STRING = 'Percent'
SELECT
{Measures.[Internet Sales Amount], Measures.[Parent Internet Sales], Measures.[Percentage Sales]} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Product].Members} ON ROWS
FROM [Adventure Works]
The CurrentMember function returns the current member along a dimension during an iteration. The Parent function returns the
parent of a member.
In this expression the Parent of the CurrentMember of the product will be the product subcategory. If one needed to rewrite this to
query product sales by model as a percentage of the sales within the product category, one could define the calculated measures as:
WITH
MEMBER Measures.[Parent Internet Sales] AS
([Product].[Product Categories].CurrentMember.Parent.Parent, Measures.[Internet Sales Amount]),
FORMAT_STRING = 'Currency'
MEMBER Measures.[Percentage Sales] AS
([Product].[Product Categories].CurrentMember, Measures.[Internet Sales Amount]) /
([Product].[Product Categories].CurrentMember.Parent.Parent, Measures.[Internet Sales Amount]),
FORMAT_STRING = 'Percent'
The repeated use of the Parent function can be replaced by calculating the appropriate ancestor of the CurrentMember. The
appropriate function for this is Ancestor, which returns the ancestor of a member at the specified level:
WITH
MEMBER Measures.[Parent Internet Sales] AS
(Ancestor([Product].[Product Categories].CurrentMember, [Product].[Product Categories].[Category]),
Measures.[Internet Sales Amount]),
FORMAT_STRING = 'Currency'
MEMBER Measures.[Percentage Sales] AS
([Product].[Product Categories].CurrentMember, Measures.[Internet Sales Amount]) /
(Ancestor([Product].[Product Categories].CurrentMember, [Product].[Product Categories].[Category]),
Measures.[Internet Sales Amount]),
FORMAT_STRING = 'Percent'
If this analysis was needed for reseller sales around reseller promotions, there could be an issue with the fact that a promotion
member exists that represents sales for which no promotion discount was applied. In this case the use of named sets and the
function Except will easily allow an expression to be formulated that shows the percentage of sales for each promotion compared
only to other promotions:
WITH
SET [Promotion Sales] AS
Except({[Promotion].[Promotions].[Type].Members}, {[Promotion].[Promotions].[Category].[No Discount]})
MEMBER Measures.[Percentage Sales] AS
([Promotion].[Promotions].CurrentMember, Measures.[Reseller Sales Amount]) /
Sum([Promotion Sales], Measures.[Reseller Sales Amount]),
FORMAT_STRING = 'Percent'
SELECT
{Measures.[Reseller Sales Amount], Measures.[Percentage Sales]} ON COLUMNS,
NON EMPTY {[Promotion Sales]} ON ROWS
FROM [Adventure Works]
The Except function finds the difference between two sets, optionally retaining duplicates. The syntax for this function is:
Except(set1, set2 [, ALL])
Duplicates are eliminated from both sets prior to finding the difference. The optional ALL flag retains duplicates. One could also
define the set definition using the except operator (the minus operand):
WITH
SET [Promotion Sales] AS
({[Promotion].[Promotions].[Type].Members} - {[Promotion].[Promotions].[Category].[No Discount]})
The concept of taking the current member within a set is also useful when using the Generate function. The Generate function
iterates through all the members of a set, using a second set as a template for the resultant set.
Consider the requirement to query unit sales for promotions for each fiscal year, breaking down the fiscal year information into its
corresponding quarter details; excluding the intermediary semester dimension. Not knowing what years there are to display, one
needs to generate the axis information based on the members of the time dimension for the fiscal year level along with the
corresponding members of the quarterly level:
SELECT
NON EMPTY {[Promotion].[Promotions].[Type].Members} ON COLUMNS,
NON EMPTY Generate({[Date].[Fiscal].[Fiscal Year].Members}, {[Date].[Fiscal].CurrentMember,
Descendants([Date].[Fiscal].CurrentMember, [Date].[Fiscal].[Fiscal Quarter])}) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount])
Similarly, if one needed to display the internet sales amounts for all product categories and customer cities within the US state of
Washington, one would need to enumerate all the cities for each state. The Generate function though could be utilized to allow new
states to be easily added or removed from the expression:
SELECT
NON EMPTY {[Product].[Product Categories].[Category].Members} ON COLUMNS,
Generate({[Customer].[Customer Geography].[State-Province].&[WA]&[US]},
{[Customer].[Customer Geography].CurrentMember,
Descendants([Customer].[Customer Geography].CurrentMember, [Customer].[Customer Geography].[City])}) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Internet Sales Amount])
Another common business problem is the requirement to show growth over a time period. This is where the PrevMember function
can be used. If one needed to display sales profit and the incremental change from the previous time member for all months in the
year 2003, the MDX expression would read:
WITH
MEMBER Measures.[Profit Growth] AS
(Measures.[Gross Profit]) - (Measures.[Gross Profit], [Date].[Calendar].PrevMember)
SELECT
{Measures.[Gross Profit], Measures.[Profit Growth]} ON COLUMNS,
{Descendants([Date].[Calendar].[Calendar Year].&[2003], [Date].[Calendar].[Month])} ON ROWS
FROM [Adventure Works]
Using NextMember in this expression would show sales for each month compared with those of the following month. There is also a
function called Lead that returns the member that is a specified number of positions following a specified member along the
member's dimension. The syntax for the Lead function is:
member.Lead(number)
If the number is negative a prior member is returned, and if zero the current member is returned. This allows the PrevMember,
NextMember, and CurrentMember navigation to all be replaced with the more generic Lead(-1), Lead(1), and Lead(0). A similar
function called Log exist such that Lag(n) is equivalent Lead(-n).
Putting this all together the following query will show gross profit and growth for each calendar year and each month within the
calendar year:
WITH
MEMBER Measures.[Profit Growth] AS
(Measures.[Gross Profit]) CoalesceEmpty((Measures.[Gross Profit], [Date].[Calendar].Lag(1)), Measures.[Gross Profit])
SELECT
{Measures.[Gross Profit], Measures.[Profit Growth]} ON COLUMNS,
Generate({[Date].[Calendar].[Calendar Year].Members},
{[Date].[Calendar].CurrentMember, {Descendants([Date].[Calendar].Lead(0), [Date].[Calendar].[Month])}}) ON ROWS
FROM [Adventure Works]
The CoalesceEmpty function is used to ensure that the growth is shown as zero for the first calendar year and month within this
calendar year; as the actual metrics are not recorded. The CoalesceEmpty function coalesces an empty cell value to a number or a
string and returns the coalesced value; namely first (from the left) non-empty value expression in the list of value expressions. If all
are empty then it returns the empty cell value.
If one needed to rewrite this to display the growth percent the usage of the IIf function could be used ensure the growth for the
first periods on which there is reporting is correctly rendered as 0%; assuming this was actually the first month of recorded sales:
WITH
MEMBER Measures.[Percentage Growth] AS
IIf(IsEmpty((Measures.[Gross Profit], [Date].[Calendar].PrevMember)), 0,
((Measures.[Gross Profit]) - (Measures.[Gross Profit], [Date].[Calendar].PrevMember))
/ (Measures.[Gross Profit], [Date].[Calendar].PrevMember)),
FORMAT_STRING = 'Percent'
SELECT
{Measures.[Gross Profit], Measures.[Percentage Growth]} ON COLUMNS,
Generate({[Date].[Calendar].[Calendar Year].Members},
{[Date].[Calendar].CurrentMember, {Descendants([Date].[Calendar].Lead(0), [Date].[Calendar].[Month])}}) ON ROWS
FROM [Adventure Works]
TIME SERIES FUNCTIONS
This last sample expression brings up an important part of data analysis, time period analysis; how did we do this month compared
to the same month last year; how did we do this quarter compared to last quarter. MDX provides a powerful set of time series
functions for time period analysis.
Even though these functions are called time series functions, they work equally well with any other dimension. In fact, in many
cases, there exist scenarios where they can be useful on other dimensions, but their most common use is with the Time dimension.
Exceptions to this are the Xtd (Ytd, Mtd, Qtd, Wtd) functions, which are applicable to the Time dimension only. These functions refer
to Year, Quarter, Month, and Week to date periods. These will be discussed later in this section.
Including the Xtd functions, the important time series functions that will be demonstrated will be ParallelPeriod, ClosingPeriod,
OpeningPeriod, and PeriodsToDate.
Continuing on this theme of time period analysis, ParallelPeriod allows one to easily compare member values from a prior period
in the same relative position as a specified member. The prior period is the prior member value at a higher specified level in the
hierarchy. An example would be to compare values with the same relative month in the previous year. The previous expression,
using the PrevMember function, compared growth with the previous month; using ParallelPeriod it becomes easy to compare
growth with the same period in the previous quarter:
WITH
MEMBER Measures.[Profit Growth] AS
(Measures.[Gross Profit]) - (Measures.[Gross Profit], ParallelPeriod([Date].[Calendar].[Calendar Quarter])),
FORMAT_STRING = 'Currency'
SELECT
{Measures.[Gross Profit], Measures.[Profit Growth]} ON COLUMNS,
{[Date].[Calendar].[Month].Members} ON ROWS
FROM [Adventure Works]
If one runs this expression, for the first quarter the profit growth will be equivalent to the profit. In these situations, an appropriate
value of zero is used for parallel periods beyond the cubes range.
The exact syntax for ParallelPeriod is:
ParallelPeriod(level_expression, index, member_expression)
All parameters are optional. The index allows one to specify how many periods one wishes to go back. With this, one could just as
easily write the previous expression to traverse back to the same month in the previous half year:
ParallelPeriod([Date].[Calendar].[Calendar Quarter], 2)
The functions OpeningPeriod and ClosingPeriod have similar syntax:
OpeningPeriod(level, member)
ClosingingPeriod(level, member)
Their purpose is to return the first or last sibling among the descendants of a member at a specified level. All function parameters
are optional. If no member is specified, the default is [Time].CurrentMember. If no level is specified, it is the level below that of
member that will be assumed.
For seasonal sales businesses it could be important to see how much profit increases after the first month in the season. Using the
quarters to represent the season one can measure the gross profit difference for each month compared with the opening month of
the quarter:
WITH
MEMBER Measures.[Profit Difference] AS
(Measures.[Gross Profit]) - (Measures.[Gross Profit],
OpeningPeriod([Date].[Calendar].[Month], [Date].[Calendar].CurrentMember.Parent))
SELECT
{Measures.[Gross Profit], Measures.[Profit Difference]} ON COLUMNS,
{[Date].[Calendar].[Month].Members} ON ROWS
FROM [Adventure Works]
In deriving the calculated member “Profit Difference”, the opening period at the month level is taken for the quarter in which the
month resides. Replacing OpeningPeriod with ClosingPeriod will show sales based on the final month of the specified season.
The final set of time series functions to be mentioned will be the Xtd functions. Prior to describing these the functions the
PeriodsToDate function needs to be mentioned, as the Xtd functions are merely special cases of this function.
PeriodsToDate returns a set of periods (members) from a specified level starting with the first period and ending with a specified
member. This becomes very useful when combined with functions such as Sum, as will be quickly shown. The syntax for the
PeriodsToDate function is:
PeriodsToDate(level, member)
If member is not specified then the member [Time].CurrentMember is assumed. As a simple example, to define a set of all the
months up to and including the month of June for the year 2003 the following definition could be used:
PeriodsToDate([Date].[Calendar].[Calendar Year], [Date].[Calendar].[Month].&[2003]&[6])
Before continuing with this discussion the Sum function warrants a brief description. This function returns the sum of a numeric
expression evaluated over a set. As a brief example, one can easily display the sum of internet profits for customers in the states of
California and Washington with the simple expression:
Sum({[Customer].[Customer Geography].[State-Province].&[WA]&[US],
[Customer].[Customer Geography].[State-Province].&[CA]&[US]}, Measures.[Internet Gross Profit])
More will be said about Sum and other numeric functions later. Using the functions Sum and PeriodsToDate it becomes easy to
define a calculated member that displays year to date information. Take for example the requirement to query monthly year to date
sales amounts for each product category in 2003. The measure to be displayed will be the sum of the current time member over the
year level:
PeriodsToDate([Date].[Calendar].[Calendar Year], [Date].[Calendar].CurrentMember)
This is easily abbreviated by the expression Ytd():
WITH
MEMBER Measures.[Ytd Sales] AS
Sum(Ytd(), Measures.[Sales Amount])
SELECT
{Descendants([Date].[Calendar].[Calendar Year].&[2003], [Date].[Calendar].[Month])} ON COLUMNS,
{[Product].[Product Categories].[Category].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Ytd Sales])
Calculating Quarter to date information is easily achieved by using the Qtd instead of Ytd function. It can also be achieved by using
the PeriodsToDate function with the level defined as [Date].[Calendar].[Calendar Quarter]. Similar rules apply for using the
Mtd and Wtd functions. Using PeriodsToDate may be more exasperating but does offer greater flexibility in the definition of the set
of periods.
In addition PeriodsToDate allows one to perform operations over fiscal periods. Say one wanted to show year 2004 fiscal year to
date sales amounts for each fiscal quarter, the PeriodsToDate could be used:
WITH
MEMBER Measures.[Fiscal Ytd Sales] AS
Sum(PeriodsToDate([Date].[Fiscal].[Fiscal Year], [Date].[Fiscal].CurrentMember), Measures.[Sales Amount])
SELECT
{Descendants([Date].[Fiscal].[Fiscal Year].&[2004], [Date].[Fiscal].[Fiscal Quarter])} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Subcategory].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Fiscal Ytd Sales])
It is also worth noting that PeriodsToDate can also be used with non time dimensions.
TUPLES AND CROSSJOIN
In many cases a combination of members from different dimensions will be enclosed in brackets. This is known as a tuple, and is
used to display multiple dimensions onto a single axis. In the case of a single member tuple, the brackets can be omitted.
The main advantage of tuple’s becomes apparent when more than two axes are required. Say one needed to query reseller sales
amounts for the product categories to each reseller state/province for each fiscal quarter. This is easily expressed by the following
MDX expression, but unless one is mapping the data into a 3D graph, impossible to display.
SELECT
[Product].[Product Categories].[Category].Members ON COLUMNS,
[Geography].[Geography].[State-Province].Members ON ROWS,
[Date].[Fiscal].[Fiscal Quarter].Members ON PAGES
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount])
To allow this to be viewed in a matrix format one would need to combine the geographic and time dimensions onto a single axis.
This is a tuple, a combination of dimension members coming from different dimensions. The syntax for a tuple is:
(member_of_dim_1, member_of_dim_2, ..., member_of_dim_n)
The only problem here would be to enumerate all the possible combinations of customer cities and yearly quarters. Fortunately
MDX supports this operation with the CrossJoin function. This function produces all combinations of two sets. The previous
expression can now be rewritten to display the results on two axes as follows:
SELECT
{[Product].[Product Categories].[Category].Members} ON COLUMNS,
NON EMPTY
CrossJoin([Geography].[Geography].[State-Province].Members, [Date].[Fiscal].[Fiscal Quarter].Members) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount])
A word of caution here; performing cross join operates is a Cartesian product such that the number of rows returned from the cross
join operation is the product of the number of members of each member dimension.
FILTERING AND SORTING
As mentioned earlier the concept or slicing and filtering are very distinct. As expected, filtering will actually reduce the number of
members on an axis. All slicing does is affect the values that go into the axis members, and not actually reduce their number.
Quite often an MDX expression will retrieve axis members when there are no values to be placed into them. This brings up the
easiest form of filtering, removing empty members from the axis. This is achieved by the use of the NON EMPTY clause; as discussed
previously.
For more specific filtering MDX offers the Filter function. This function returns the set resulting from filtering the set based on the
specified search condition. The format of the Filter function is:
Filter(set, search_condition)
Consider the simple expression comparing reseller sales in 2003 for each region against the reseller type:
SELECT
{[Reseller].[Reseller Type].[Business Type].Members, [Reseller].[Reseller Type].[All Resellers]} ON COLUMNS,
{[Geography].[Geography].[State-Province].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount], [Date].[Fiscal].[Fiscal Year].&[2003])
If one was only interested in viewing reseller regions into which a delivery is made more than once a month, defined by those whose
unit orders exceed 12, a filter may be defined as follows:
SELECT
{[Reseller].[Reseller Type].[Business Type].Members, [Reseller].[Reseller Type].[All Resellers]} ON COLUMNS,
Filter({[Geography].[Geography].[State-Province].Members},
([Measures].[Reseller Order Count], [Date].[Fiscal].[Fiscal Year].&[2003]) > 12) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount], [Date].[Fiscal].[Fiscal Year].&[2003])
This can easily be extended to query those reseller region’s whose profit percent is less than that for the resellers within their
country; which reseller region profit margins are falling behind the country average based on the reseller type (the query also
showing the reseller sales for each country):
WITH
MEMBER Measures.[Reseller Profit Percent] AS
(Measures.[Reseller Sales Amount] - Measures.[Reseller Total Product Cost]) /
(Measures.[Reseller Total Product Cost]),
FORMAT_STRING = 'Percent'
SELECT
{[Reseller].[Reseller Type].[Business Type].Members, [Reseller].[Reseller Type].[All Resellers]} ON COLUMNS,
NON EMPTY Generate({[Geography].[Geography].[Country].Members}, {[Geography].[Geography].CurrentMember,
Filter({[Geography].[Geography].CurrentMember.Children},
([Measures].[Reseller Profit Percent], [Date].[Fiscal].[Fiscal Year].&[2003]) <
([Measures].[Reseller Profit Percent], [Date].[Fiscal].[Fiscal Year].&[2003],
Ancestor([Geography].[Geography].CurrentMember, [Geography].[Geography].[Country])))}
) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount], [Date].[Fiscal].[Fiscal Year].&[2003])
During cube queries all the members in a dimension have a natural order. This can be seen when one utilizes the inclusion operator,
a colon. Consider the simple expression displaying measures for the internet order cities:
SELECT
{[Measures].[Internet Sales Amount], [Measures].[Internet Total Product Cost]} ON COLUMNS,
{[Customer].[Customer Geography].[City].Members} ON ROWS
FROM [Adventure Works]
In reviewing the latter part of the output one will see a sequence of Racine, Casper, Cheyenne, and then Rock Springs. The natural
order is not very apparent, as one does not know the member from the parent level. If one was only interested in the cities listed
between Birmingham and Rock Spring (those within the USA) one could write:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Total Product Cost]} ON COLUMNS,
NON EMPTY {[Customer].[Customer Geography].[City].&[Birmingham]&[AL]:
[Customer].[Customer Geography].[City].&[Rock Springs]&[WY]} ON ROWS
FROM [Adventure Works]
What if one wanted all the stores in this listed sorted by the city name, a common requirement for reporting. MDX provides this
functionality through the Order function. The full syntax for this function is:
Order(set, expression [, ASC | DESC | BASC | BDESC])
The expression can be numeric or a string expression. The default sort order is ASC. The “B” prefix indicates that the hierarchical
order can be broken. Hierarchized ordering first arranges members according to their position in the hierarchy then it orders each
level. The non-hierarchized ordering arranges members in the set without regard to the hierarchy.
Working on the previous sample to order the set regardless of the hierarchy:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Total Product Cost]} ON COLUMNS,
NON EMPTY Order({[Customer].[Customer Geography].[City].&[Birmingham]&[AL]:
[Customer].[Customer Geography].[City].&[Rock Springs]&[WY]},
[Customer].[Customer Geography].CurrentMember.Name, BASC) ON ROWS
FROM [Adventure Works]
Here a property called Name is being used. This returns the name of a level, dimension, member, or hierarchy. A similar property
called UniqueName exists which return the corresponding unique name.
More often than not the actual ordering will be based on an actual measure. The same query can easily be changed to display the all
city information based on the internet order count; query cities’ sales information ordered by their sales order performance:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count]} ON COLUMNS,
NON EMPTY Order({[Customer].[Customer Geography].[City].&[Birmingham]&[AL]:
[Customer].[Customer Geography].[City].&[Rock Springs]&[WY]},
Measures.[Internet Order Count], BDESC) ON ROWS
FROM [Adventure Works]
One could again extend this to only show those cities where the number of orders is greater that 100:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count]} ON COLUMNS,
Order(Filter({Descendants([Customer].[Customer Geography].[Country].&[United States],
[Customer].[Customer Geography].[City])},
Measures.[Internet Order Count] > 100), Measures.[Internet Order Count], BDESC) ON ROWS
FROM [Adventure Works]
In addition to the Order function there is one called Hierarchize that orders the members of a set into a hierarchy. More will be
said on this in the next section but if one wanted to show resellers sales for each reseller type against the countries and
corresponding regions, the Hierarchize function will ensure that each country is correctly followed by the corresponding regions:
SELECT
{[Reseller].[Reseller Type].[Business Type].Members, [Reseller].[Reseller Type].[All Resellers]} ON COLUMNS,
Hierarchize({[Geography].[Geography].[Country].Members, [Geography].[Geography].[State-Province].Members}) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Amount])
TOP AND BOTTOM PERFORMANCE ANALYSIS
When displaying information such as the best selling cities, based on unit sales, it would be beneficial to limit the query to say the
top dozen. MDX support can support this operation using a function called Head. This function is very simple and returns the first
members in the set based on the number that one requests. A similar function called Tail exists that returns a subset from the end
of the set. Taking the previous best selling stores as an example the top performing cities can be queried by the expression:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count]} ON COLUMNS,
Head(Order({Descendants([Customer].[Customer Geography].[Country].&[United States],
[Customer].[Customer Geography].[City])}, Measures.[Internet Order Count], BDESC), 12) ON ROWS
FROM [Adventure Works]
As expected, since this is such as common request, MDX supports a function called TopCount to perform such a task. The syntax for
the TopCount function is:
TopCount(set, count, numeric_expression)
The previous expression can easily be rewritten:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count]} ON COLUMNS,
TopCount({Descendants([Customer].[Customer Geography].[Country].&[United States],
[Customer].[Customer Geography].[City])}, 12, Measures.[Internet Order Count]) ON ROWS
FROM [Adventure Works]
This expression is very simple, but it doesn’t have to be. A MDX expression can be calculated that displays the top 50 cities, based on
sales amounts, and how much all the other cities combined have sold. This expression also shows another use of the Sum function in
addition to named sets and calculated members:
WITH
SET [Top US Cities] AS
TopCount({Descendants([Customer].[Customer Geography].[Country].&[United States],
[Customer].[Customer Geography].[City])},
50, Measures.[Internet Sales Amount])
MEMBER [Customer].[Customer Geography].[Other Cities] AS
'([Customer].[Customer Geography].[Country].&[United States], Measures.CurrentMember)
- Sum([Top US Cities], Measures.CurrentMember)'
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count]} ON COLUMNS,
{[Top US Cities], [Customer].[Customer Geography].[Other Cities]} ON ROWS
FROM [Adventure Works]
Other functions exist for the top filter processing. They are TopPercent, returning the top elements whose cumulative total is at
least a specified percentage, and TopSum, returning the top elements whose cumulative total is at least a specified value. As
expected there are a series of Bottom functions, returning the bottom items in the list.
The expression above can easily be modified to display the list of worldwide cities whose sales count accounts for 50% of all sales:
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count]} ON COLUMNS,
TopPercent({[Customer].[Customer Geography].[City].Members}, 50, Measures.[Internet Order Count]) ON ROWS
FROM [Adventure Works]
In all these expressions the row axis has been defined as the members of the Measures dimension. Again, this does not have to be
the case. One can easily formulate an expression that shows the breakdown of the sales counts for the product categories:
SELECT
NON EMPTY {[Product].[Product Model Lines].[Product Line].Members} ON COLUMNS,
TopPercent({[Customer].[Customer Geography].[City].Members}, 50, Measures.[Internet Order Count]) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Internet Sales Amount])
As mentioned earlier a function called Hierarchize that orders the members of a set into a hierarchy. Consider the requirements to
show the top 12 performing product sub-categories based on internet sales. If one also wanted to show the corresponding
information for the categories into which the sub-categories are members then the query would be:
WITH SET [Top Products] AS
Hierarchize(Generate(
TopCount([Product].[Product Categories].[Subcategory].Members, 12, [Measures].[Internet Sales Amount]),
{Ancestor([Product].[Product Categories].CurrentMember,[Product].[Product Categories].[Category]),
[Product].[Product Categories].CurrentMember}))
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Gross Profit]} ON COLUMNS,
{[Top Products]} ON ROWS
FROM [Adventure Works]
In this case one creates the set of performing sub-categories and the uses the Generate function to define the set consisting of the
categories and sub-categories. The Hierarchize function ensures that the rendering occurs in the natural member order with
categories before sub-categories. An optional POST parameter can also be used such that the category will be rendered after all
corresponding sub-categories.
NUMERIC FUNCTIONS
MDX supports many numeric functions. The Sum function is an example of one that has already been used in this paper. Another
important function is Count. This simply counts the number of tuples in a set.
The Count function has two options, including and excluding empty cells. One can also use the DistinctCount function it exclude
empty cells. Count is very useful for such operations as deriving the number of distinct customers that purchased products within a
particular product category. Looking at sales, the number of customers that purchased products can be derived by counting the
number of tuples of sales and customers names. Excluding empty cells is necessary to restrict the count to those customers for
which there are sales within the product category:
WITH MEMBER Measures.[Distinct Customers] AS
DistinctCount(CrossJoin({[Customer].[Customer Geography].[Customer].Members},
{[Measures].[Internet Sales Amount]})), FORMAT_STRING = '###,##0'
SELECT
{Measures.[Internet Sales Amount], Measures.[Internet Order Count], Measures.[Distinct Customers]} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Category]} ON ROWS
FROM [Adventure Works]
Other numeric functions exist for such operations as calculating the average, median, maximum, minimum, variance and standard
deviation of tuples in a set based on a numeric value. As expected, these functions in turn are Avg, Median, Max, Min, Var, and
Stdev; amongst others. The format for all these functions is the same:
Function(set, numeric_value_expression)
Taking the Avg function as an example, one can easily analyze each product category to see what the average internet sales for a
month period was for each calendar year:
WITH
MEMBER Measures.[Average Monthly Sales] AS
Avg(Descendants([Date].[Calendar].CurrentMember, [Date].[Calendar].[Month]), Measures.[Internet Sales Amount]),
FORMAT_STRING='$#,##0;($#,##0);;-'
SELECT
{[Date].[Calendar].[Calendar Year].Members} ON COLUMNS,
NON EMPTY {[Product].[Product Categories].[Category].Members} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Average Monthly Sales])
As the AVG functions is dependent on a count for the members for which the average is being taken, an important point to note is
that the function excludes empty cells. Replacing Avg with Max would give the maximum monthly sales for the product category
within the calendar year.
Taking this previous sample further, if one needed not only to view the sales amounts for each calendar year and the average for the
month, but in addition to the sales count during the month for which the average is derived, the MDX expression would be
formulated as:
WITH
MEMBER Measures.[Total Yearly Sales] AS
Measures.[Internet Sales Amount],
FORMAT_STRING='$#,##0;($#,##0);;-'
MEMBER Measures.[Average Monthly Sales] AS
Avg(Descendants([Date].[Calendar].CurrentMember, [Date].[Calendar].[Month]), Measures.[Internet Sales Amount]),
FORMAT_STRING='$#,##0;($#,##0);;-'
MEMBER Measures.[Total Sales Count] AS
DistinctCount(CrossJoin({Descendants([Date].[Calendar].CurrentMember, [Date].[Calendar].[Month])},
{Measures.[Internet Sales Amount]})), FORMAT_STRING='#,##0;(#,##0);-;-'
SELECT
NON EMPTY {DrilldownLevel({[Product].[Product Categories].[All Products]})} ON COLUMNS,
CrossJoin({[Date].[Calendar].[Calendar Year].Members},
{Measures.[Total Yearly Sales], Measures.[Average Monthly Sales], Measures.[Total Sales Count]}) ON ROWS
FROM [Adventure Works]
As one is showing 3 measures for each calendar year it is necessary to perform a CrossJoin operation. The FORMAT_STRING is used
in this case to ensure all empty and zero cells are represented consistently.
USER DEFINED FUNCTIONS
In addition to built in functions MDX allows you to create and register your own functions that operate on multidimensional data.
These functions, called “user-defined functions” (UDFs), can accept arguments and return values in the MDX syntax. UDF's in
Analysis Services supports both COM and CLR assemblies. CLR assemblies are recommended because of the enhanced security
available to CLR assemblies. In addition to this, MDX supports many functions in the Microsoft Visual Basic® for Applications
Expression Services library. If Microsoft Office Excel is installed on the server, Excel functions are also available.
Take for example the VBA function InStr. This compares two strings to return the position of the second string within the first.
Using this function one can query the measures for the store types where the actual the store type contains the word
“supermarket”; stores defined as a type of supermarket:
WITH SET [Tire Products] AS
Filter({[Product].[Product Model Lines].[Model].Members},
VBAMDX!InStr(1, [Product].[Product Model Lines].CurrentMember.Name, "Tire") > 0)
SELECT
NON EMPTY {[Date].[Fiscal].[Fiscal Year].Members} ON COLUMNS,
{[Tire Products]} ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Sales Amount])
Here the use of the VBAMDX clause is not needed. The purpose of this is to fully qualify the origin of the function; namely the
assembly. This is only needed when the same function exists in multiple declared libraries.
MEMBER SET AND LOGICAL FUNCTIONS
There are many functions that operate and return sets, many of which have already been mentioned. A few more that warrant
mentioning are the DrillDown and DrillUp functions, StrTo functions, and Ascendants.
The DrilldownLevel function allows one to easily define a set that consists of members that constitute the set in addition to those
at a level below that represented in the set. Other functions exist such as DrilldownLevelTop and DrilldownLevelBottom that
allow one to select the number of members of the set to be returned.
Corresponding Drillup functions also exist. The following query uses the DrillupLevel function to enable one to show all
customer geography information where more than 125 orders have been placed, regardless of the hierarchy, but limited to the city
level as the lowest member hierarchy.
SELECT
{[Measures].[Internet Sales Amount], [Measures].[Internet Gross Profit],
[Measures].[Internet Order Count]} ON COLUMNS,
Hierarchize(
DrillupLevel(Filter([Customer].[Customer Geography].Members, [Measures].[Internet Order Count] > 125),
[Customer].[Customer Geography].[City])
) ON ROWS
FROM [Adventure Works]
There are several functions that exist to assist in the definition of sets, members, and tuples from string values; and vice-versa.
These are called StrToMember, StrToSet, SetToTuple, and StrToValue. The format for all this functions is:
Function(expression [,CONSTRAINED] )
The optional CONSTRAINED parameter prevents the result from resolving to an MDX expression.
Before showing the usage of the StrToMember function it is work first mentioned the Ascendants function. The Ascendants
function returns the set of the ascendants of a specified member, including the member itself. This is useful for enabling queries that
show a member and its associated ancestors; such as internet product sales for this month along with the corresponding sales for
the quarter, semester, and year.
SELECT
(Ascendants(StrToMember(
'[Date].[Calendar].[Month].&[' + CStr(Year("2004/03/01")) + ']&[' + CStr(Month("2004/03/01")) + ']'
, CONSTRAINED)) - [Date].[Calendar].[All]) ON COLUMNS,
{[Product].[Product Categories].[Product].Members} ON ROWS
FROM [Adventure Works]
WHERE ([Measures].[Internet Sales Amount])
This example also demonstrates the usage of further VBA functions to define a member that represent the current calendar date. In
this case I have hard-coded a date to ensure results are returned but one could easily have defined the date using the Now()
function.
An example of a logical function that has already been used in this paper is the IsEmpty function. Several other functions exist that
are used to determine a conditional state about a specified member; such as IsAncestor, IsGeneration, IsLeaf, IsSibling. These
conditional expressions all return a true/false value.
To demonstrate the usage of one of these functions consider let’s look at the IsAncestor function. Say for resellers there was a
competition for specialty bikes store where a weighting system was used; a point for each $100 sale for bikes and for $80 for all
other items (as selling bikes is easier and they are more expensive). We would need to define a measure that was based on each
product sale and dependant on whether the sale item was a descendant of the bike category:
WITH MEMBER Measures.[Reseller Sales Score] AS
Sum(Descendants([Product].[Product Categories].CurrentMember,, LEAVES),
IIf(IsAncestor([Product].[Product Categories].[Category].[Bikes], [Product].[Product Categories].CurrentMember),
Int(Measures.[Reseller Sales Amount] / 100), Int(Measures.[Reseller Sales Amount] / 80))),
FORMAT_STRING = '#,#00'
SELECT
NON EMPTY {[Product].[Product Categories].[All], [Product].[Product Categories].[Category].Members} ON COLUMNS,
TopCount({[Reseller].[Reseller Type].[Business Type].&[Specialty Bike Shop].Children},
25, Measures.[Reseller Sales Score]) ON ROWS
FROM [Adventure Works]
WHERE (Measures.[Reseller Sales Score], [Date].[Fiscal].[Fiscal Year].&[2004])
In this case the top 25 performing stores are returned for fiscal year 2004. Again, a VBA function, specifically Int, is used to
determine the number of points allocated to each sale.
REFERENCES
MSDN MDX Introduction: http://msdn.microsoft.com/en-us/library/aa216767(SQL.80).aspx
Analysis Services Stored Procedure Project CodePlex: http://www.codeplex.com/ASStoredProcedures
MSDN MDX Language Reference: http://msdn.microsoft.com/en-us/library/ms145595.aspx
MSDN MDX Function Reference: http://msdn.microsoft.com/en-us/library/ms145970.aspx
MSDN VBScript Function Reference: http://msdn.microsoft.com/en-us/library/3ca8tfek(VS.85).aspx
ADOMD.Net Class Library: http://msdn.microsoft.com/en-us/library/cc281460.aspx
Download