Figure 14-11. The results of Exercise 14-2
in this exercise, you wrote and executed an MDX query that returned four tuples. Let’s continue our coverage of the MDX language by looking at various aspects of dimensional members and levels.
Calculated Members
You can add calculated members to your cube from Visual Studio, and you can also add calculated members to the results of an MDX query. The difference is that MDX’s calculated member code query is not stored within the cube. It is added as an expression each time you write a query.
Listing 14-17 is an example of a query that includes a calculated member expression. Notice that the query begins with the WITH MEMBER clause and then indicates which dimension the member is to be placed within.
Listing 14-17. Adding a Calculated Member -- Adding a Calculated Member
With Member Measures.[test]
As
"test data" --String data (uses Double Quotes) -- Note: using Single Quotes causes an Error Select
{ [Measures].AllMemebers } On Columns From [CubePubsSales];
Note
■ Many developers expect the calculated member syntax to indicate the end of an expression. it is clear that the expression begins after the As keyword, but there is no clear indicator of where it ends. Because of this, many new developers try including curly braces to encompass the expression. Doing so causes an error.
Calculated members are evaluated as either string or numeric data. Use double quotes to indicate that the value is a string. This is different from SQL programming that uses a single quote to indicate a string. Use literal values for numeric data.
For mathematical expressions, specify the literal values and operator as you would any other programming language. It is good practice to include parentheses around the expressions, but they are not strictly necessary.
Although it may seem odd, you can use single quotes to surround an expression. Remember that in MDX, double quotes indicate a string of characters, not single quotes.
The three examples in Listing 14-18 highlight this calculated member syntax. The first query and third queries return the same result (shown in Figure 14-13).
Figure 14-12. The results of the example in Listing 14-17
Listing 14-18. Expression Syntax
--1) You can use parentheses to surround expressions With Member Measures.[AddStuff]
As
(5 + 7) -- Numeric data
SELECT Measures.[AddStuff] On Columns From [CubePubsSales]
--2) However, you CANNOT use Braces to surround your expression With Member Measures.[AddStuff]
As
{ 5 + 7 } -- Causes an #Error
SELECT Measures.[AddStuff] On Columns From [CubePubsSales]
--3) Oddly, you CAN use Single Quotes to surround your expression With Member Measures.[AddStuff]
As '5 + 7'
SELECT Measures.[AddStuff] On Columns From [CubePubsSales]
Calculated members are most commonly placed on the Measures dimension, but this is not a requirement.
They can be placed on any dimension. Listing 14-19 gives some examples using the Date dimension for our calculated member instead of the Measures dimension. The result of this query is the total sales quantity for the combined years of 1992 and 1993. Figure 14-14 displays these results.
Figure 14-13. The results of the first and last examples from Listing 14-18
Listing 14-19. Placing Members within Dimensions
-- Calculated Members do not have to be on the Measures dimension With Member [DimDates].[Year].[1992 And 1993]
As
[DimDates].[Year-Qtr-Month-Day].[1992]
+
[DimDates].[Year-Qtr-Month-Day].[1993]
Select
{ [Measures].[SalesQuantity] } On Columns, { [DimDates].[Year].[1992 And 1993] } On Rows From [CubePubsSales];
Member Properties
MDX uses a number of properties and functions to return a set of one or more tuples. Two common properties are the Members property and the AllMembers property. The Members property returns standard members. The AllMembers property returns standard members as well as any calculated members. Listing 14-20 gives an example of each. Figure 14-15 displays the query results.
Figure 14-14. The results of the example in Listing 14-19
Listing 14-20. The Members and AllMembers Properties With Member Measures.[test]
As "test data"
Select {
[Measures].AllMembers
} On Columns -- includes calculated members From [CubePubsSales];
GO
With Member Measures.[test]
As "test data"
Select {
[Measures].Members
} On Columns -- Does NOT Include Calculated Members From [CubePubsSales];
Important
■ when using a function or property that returns multiple tuples, your MDX code does not require braces, but most developers include them anyway. This MDX statement: Select [Measures].Members On Columns From [CubePubsSales]; is equivalent to this MDX statement with curly braces: Select { ( [Measures].Members ) } On Columns From [CubePubsSales];.
Figure 14-15. The results of the example in Listing 14-20
Members and Levels
Each dimension consists of members. These members can be grouped into user-defined hierarchies. Each user- defined hierarchy consists of two or more levels, with the highest level being the All level. (The only exception to this rule is the Measures dimension, which is completely flat and has no hierarchical structure of this type).
To display every member of the hierarchy or level, specify its name and use the AllMembers function. To specify the members of a level, include the dimension, hierarchy, and level. The Analysis Server will implicitly use the AllMembers function for you. Listing 14-21 outlines these concepts. Figure 14-16 displays the query results.
Figure 14-16. The results of the examples from Listing 14-21
Listing 14-21. Specifying the Hierarchy and a Level Implicitly Returns All Members Select
{ [Measures].[SalesQuantity] } On Columns,
{ −- Dimension - Hierarchy - Level - Property or Function [DimTitles].[TitlesByType].[Title].AllMembers
} On Rows
From [CubePubsSales];
Select
{ [Measures].[SalesQuantity] } On Columns, { −- Dimension - Hierarchy - Level
Because SSAS considers each attribute a hierarchy of its own (remember the warning in the designer about
“hiding” these?), using the name of an attribute and a level also returns a SET of tuples. Listing 14-22 shows an example of this, and Figure 14-17 displays the results.
Figure 14-17. The results of the example in Listing 14-22
Listing 14-22. Specifying an Attribute Hierarchy Does Not Implicitly Return All Members Select
{ [Measures].[SalesQuantity] } On Columns, { −- Dimension - Attribute Hierarchy [DimTitles].[Title]
} On Rows
From [CubePubsSales];
The NonEmpty Function
Sometimes when you execute a query, a lot of null values are included in the results. For example, in the Pubs database, many publishers have no sales associated with them. Therefore, the sales quantity for that publisher is displayed as null, which in MDX is equivalent to an empty set. The query in Listing 14-23 demonstrates this. The results are displayed in Figure 14-18.
Listing 14-23. A Query That Produces Many Null Values Select
{ [Measures].[SalesQuantity] } On Columns, { [DimTitles].[Publisher].[Publisher] } On Rows From [CubePubsSales];
If you do not want null values displayed, use the NonEmpty function to remove nulls from the result set.
Listing 14-24 shows an example of this function. See Figure 14-19 for the results.
Figure 14-18. The results of the example in Listing 14-23
Listing 14-24. Using the NonEmpty Function Select
{ [Measures].[SalesQuantity] } On Columns,
{ NonEmpty( [DimTitles].[Publisher].[Publisher] ) } On Rows From [CubePubsSales];
Compare the results in Figure 14-19 to the results in Figure 14-18. Notice how the NonEmpty function has removed the null values.
The Non Empty Clause
Nulls can also be removed from a result set using the Non Empty clause in MDX statements. Both the NonEmpty function and the Non Empty clause can be combined to remove null values from your results. Listing 14-25 shows an example of this option. See Figure 14-20 for the results.
Note
■ The NonEmpty function in MDX uses parentheses similar to most programming languages.
The Non Empty clause does not. Additionally, as you may have noticed, there is no space in the name of the NonEmpty function, but there is a space in the Non Empty clause!
Listing 14-25. Using the Non Empty Clause and the NonEmpty Function Select -- Start with Lots of nulls in eight columns and six rows { [DimDates].[Year-Qtr-Month-Day].[Year].AllMembers } On Columns, { [DimTitles].[TitlesByType].[TitleType].AllMembers } On Rows From [CubePubsSales]
GO
Select Non Empty -- Now has less nulls (5 columns removed)
{ [DimDates].[Year-Qtr-Month-Day].[Year].AllMembers } On Columns, { [DimTitles].[TitlesByType].[TitleType].AllMembers } On Rows From [CubePubsSales]
GO
Select Non Empty -- Even Less nulls (1 more row removed)
{ NonEmpty( [DimDates].[Year-Qtr-Month-Day].[Year].AllMembers ) } On Columns, { NonEmpty( [DimTitles].[TitlesByType].[TitleType].AllMembers ) } On Rows From [CubePubsSales]
Member and Level Paths
Developers may find it challenging that MDX code can be written with many variations. For example, to locate a particular member or level, you provide its name, like June, or a full path, like DimTime.1992.June. And the path can include various combinations of the dimension, hierarchy, level, member, and various functions.
Unfortunately, when you incorrectly indicate a path to a member, SSAS does not return an error. It returns an empty result set. This oddity makes developers scratch their head all the time, wondering what exactly went wrong.
One way to avoid path issues is to memorize typical patterns used to access a member or level.
The pattern to memorize is this: Dimension[Optional].Hierarchy[Optional].Level[Optional].
Member. < ChildMember > . < ChildMember>
The code in Listing 14-26 displays examples of what the different paths may look like. Notice that each statement in this listing returns the same result, as shown in Figure 14-21.
Listing 14-26. Various Paths to Access a Member or Level -- Starts like this. . .
Select
{ [Measures].[SalesQuantity] } On Columns,
{ −- Dim.Hierarchy.Level.Member.Member (long path)
[DimTitles].[TitlesByPublisher].[Publisher].[New Moon Books].[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
GO
-- Can change to this. . . Select
{ [Measures].[SalesQuantity] } On Columns, { −- Hierarchy.Level.Member.Member
[TitlesByPublisher].[Publisher].[New Moon Books].[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
GO
-- Or this. . . Select
{ [Measures].[SalesQuantity]} On Columns, { −- Dim.Member.Member
[DimTitles].[New Moon Books].[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
GO
-- Or this. . .
Figure 14-21. The results are the same for all the code in Listing 14-26.
{ −- Dim.Member
[DimTitles].[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
GO
-- Or even like this. . . Select
{ [Measures].[SalesQuantity] } On Columns, { −- Member
[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
In Listing 14-27, we provide some examples of what not to do when typing MDX member paths. The following list describes what is happening in the examples in this listing:
1. A hierarchy is left out of the path and goes straight to the level and members.
2. A path to a member is broken by skipping over a level.
3. A path of members fails to be properly chained together because the level is in front of the members.
Listing 14-27. MDX Errors to Avoid -- 1. Skipping the hierarchy Select
{ [Measures].[SalesQuantity]} On Columns, { −- Dim. < Skipped hierarchy > .Level.Level.Member -- DOES NOT WORK (but does NOT give an error!) [DimTitles].[Publisher].[Title].[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
-- 2. Skipping over a level breaks the path to the member Select
{ [Measures].[SalesQuantity]} On Columns, { −- Dim.Level. < Skipped Level >.Member
-- DOES NOT WORK (but does not give an error either!) [DimTitles].[All].[Is Anger the Enemy?]
} On Rows
From [CubePubsSales];
-- 3. Multiple members after a level Select
{ [Measures].[SalesQuantity]} On Columns, { −- Dim.Level.Member.Member
-- DOES NOT WORK (and of course no error either!)
[DimTitles].[Publisher].[New Moon Books].[Is Anger the Enemy?]
One interesting pattern that happens to work is the use of the All level followed by a path of multiple members. An example is shown in Listing 14-28.
Listing 14-28. An Exception to the Rule -- This works!
Select
{ [Measures].[SalesQuantity]} On Columns,
{ −- Dim.Member.Member.Member (and not Dim.Level.Member.Member) [DimTitles].[All].[New Moon Books].[Is Anger the Enemy?]
-- Works because [All] is both a member and a level.
} On Rows
From [CubePubsSales];
What makes this last example seem so odd is that the All level is both a level and a member, so it looks like you are getting away with using an incorrect path of Dimension. < Skipped hierarchy > .Level.Member.Member, when you are actually using Dimension.Member.Member.Member. Isn’t MDX fun?!
Common Functions
Like any programming language, MDX has a number of useful functions. Let’s take a look at some of the ones you are likely to encounter.
Tip
■ in Microsoft’s MDX documentation, what other programming languages refer to as methods, properties, or operators are all called functions. Consider these terms interchangeable here.
PrevMember and NextMember Functions
These two functions return the previous or next member from the same level. For example, the three MDX examples in Listing 14-29 all return the same results, as shown in Figure 14-22.
Listing 14-29. PrevMember and NextMember Functions Select
{ [Measures].[SalesQuantity] } On Columns, {
[DimDates].[Year].[1993]
, [DimDates].[Year].[1993].PrevMember } On Rows
From [CubePubsSales];
GO Select
{ [Measures].[SalesQuantity] } On Columns, {
[DimDates].[Year].[1992].NextMember , [DimDates].[Year].[1992]
} On Rows
From [CubePubsSales];
GO
-- You can even chain these functions together Select
{ [Measures].[SalesQuantity] } On Columns, {
[DimDates].[Year].[1991].NextMember.NextMember Figure 14-22. The results of the examples in Listing 14-29
The Children Function
To use the Children function, indicate the coordinates of a member that represents the parent of the child members to be returned.
You can use this function multiple times in the same statement to access a set of child tuples from different members in the same dimension. For example, Listing 14-30 is using the Date dimension to identify the children of the years 1993 and 1992. The results are shown in Figure 14-23.
Figure 14-23. The results of the first example in Listing 14-30
Listing 14-30. The Children Function Select
{ [Measures].[SalesQuantity] } On Columns, {
[DimDates].[Year-Qtr-Month-Day].[Year].[1992].Children , [DimDates].[Year-Qtr-Month-Day].[Year].[1993].Children } On Rows
From [CubePubsSales];
The Parent Function
The Parent function allows you to specify a member and receive its parent members’ value. In Listing 14-31 we
Listing 14-31. The Parent Function Select
{ [Measures].[SalesQuantity] } On Columns, {
[OrderDate].[Year-Qtr-Month-Day].[Q1 - 1993].Parent ,[OrderDate].[Year-Qtr-Month-Day].[Q1 - 1993]
} On Rows
From [CubePubsSales];
The CurrentMember Function
The CurrentMember function returns values based on the member of a dimension currently under focus. To understand this, consider how SSAS must resolve a query. Each time you ask for a given member of a dimension, the SSAS query engine must check each member to see whether it is a match for your request. If it is not, it moves to the next member until it finds all the members you requested in your MDX statement. Each time the SSAS engine checks a member, that member is in focus and represents the current member of that dimension.
The CurrentMember function is rarely required for most situations in your MDX code. Listing 14-32 gives two examples; the first uses the CurrentMember function, and the second does not. Note that both queries return the exact same results, as shown in Figure 14-25.
Figure 14-24. The results of the example in Listing 14-31
Listing 14-32. The CurrentMember Function Select
{ [Measures].[SalesQuantity] } On Columns, { [DimDates].[Year].CurrentMember } On Rows From [CubePubsSales];
GO
-- The CurrentMember function is implied Select
{ [Measures].[SalesQuantity] } On Columns, { [DimDates].[Year] } On Rows
From [CubePubsSales];
Occasionally, when you are working with MDX expressions in Visual Studio or in a reporting application such as SSRS, the CurrentMember function must be explicitly typed out for an expression to work properly. Both of these tools usually let you create the MDX expression with a designer interface that programs the MDX code for you, but not always. Therefore, keep this option in mind when creating MDX expressions in either one of these programs.
Figure 14-25. The results of the examples in Listing 14-32
Tip
■ Do not worry about when this will or will not occur. Just add the CurrentMember function to an expression whenever you get an error about an expression being ambiguous, and this will often resolve the issue. it is not very scientific, but it is easy to remember.
The Order Function
Ordering results is a common task in SQL programming. This is also true of MDX programming. Many of the client software applications that work with SSAS do the sorting for you after receiving the results of an MDX query. But, on occasion, you may want to sort the results beforehand. You can do so by using the Order function.
The Order function in MDX works slightly different from the Order By clause in SQL programming because of the hierarchical nature of a cube. To understand this, let’s first take a look at the results of a query with no Order function, as shown in Listing 14-33. See Figure 14-26 for some of the results.
Figure 14-26. The partial results of the example in Listing 14-33 Listing 14-33. An MDX Query Without the Order Function Select
{ [Measures].[SalesQuantity] } On Columns,
{ [DimTitles].[TitlesByPublishers].AllMembers } On Rows From [CubePubsSales];
In the Pubs database, New Moon Books and Binnet & Hardley are publishers. Figure 14-26 shows that the results of the code in Listing 14-28 have been automatically grouped based on the publishers that the books fall under. Currently, the order the titles appear in is based on the position of each member’s key value. The unsorted
If you want to order the results based on SalesQuantity’s numeric values, you can do so with the query shown in Listing 14-34.
The first order function argument indicates the members to be sorted. The second argument indicates the values to sort by. And the third argument indicates whether you want to sort them in ascending or descending order.
Listing 14-34. The Order Function
-- Ordered by Sales Quantity, descending Select
{ [Measures].[SalesQuantity] } On Columns, {
Order( [DimTitles].[TitlesByPublisher].AllMembers , [Measures].[SalesQuantity]
, Desc ) } On Rows
From [CubePubsSales];
Figure 14-27 shows the results of the second query. As you can see, the values are now ordered sequentially and in descending order, but they are still grouped based on each publisher.
If you do not want your values to be grouped based on hierarchy, you can use the Break Hierarchy version of the Order function. To do this, specify in the third argument that you want to use break ascending or break descending, as shown in Listing 14-35. Figure 14-28 shows the results.
Figure 14-28. The results of the last example in Listing 14-35 Listing 14-35. The Break Hierarchy Argument
Select
{ [Measures].[SalesQuantity] } On Columns, {
Order( [DimTitles].[TitlesByPublisher].AllMembers , [Measures].[SalesQuantity]
, BDesc ) } On Rows
From [CubePubsSales];
The CrossJoin Operator (*) and Function
As expected, there will be times when you would like your data to include subtotals based on a combination of multiple dimensions. For example, you can use the CrossJoin function to request the subtotals of sales quantity for all years and all titles. The CrossJoin function can be represented either with the * operator or with a function call. Listing 14-36 shows an example of both the cross join * operator and the function call. The results returned are the same no matter which version you use and are displayed in Figure 14-29.
Figure 14-29. The results of the Listing 14-36
Listing 14-36. The CrossJoin Operator and Function Select
{ [Measures].[SalesQuantity] } On Columns, { NonEmpty(
[DimDates].[Year].AllMembers
* -- This symbol is the cross join operator
From [CubePubsSales];
GO Select
{ [Measures].[SalesQuantity] } On Columns, { NonEmpty(
CrossJoin( [DimDates].[Year].AllMembers
, [DimTitles].[TitlesByType].[Title].AllMembers )
) } On Rows
From [CubePubsSales];
Tip
■ naturally, a number of null values will be returned when using this function. For instance, if a title did not sell in a particular year, the results returned will be a null value. Because of this, it is common to combine a cross join operator with the NonEmpty function as we did in Listings 14-31 and 14-32.
Joining More than Two Dimensions
It is possible to combine results from more than two dimensions at a time. In Listing 14-37 we combine results from the Years, Titles, and Stores dimensions. The code results are shown directly after the listing in Figure 14-30.
Both queries return the same result.