MDX statements


MDX statements are what SQL queries are to relational databases, but instead acting on OLAP (multidimensional) databases. In the case of TM1, we will use them to query a dimension and its members. It is possible to make a set of dimension elements based on filters. Or just the lowest level elements. Well …, a lot is possible.


Use cases
Referencing elements
Using an existing subset in an expression
Elements at different levels
Descendants and Ascendants
More on hierarchical structures
Planning Analytics hierarchies
Union and Intersect
Cube values
Filtering on attributes
Filtering over dimensions
Filtering with wildcards
More on sorting
Subsets based on other objects
Distinct elements
Orphans and top consolidations
TM1 users and groups
Application and control objects
Temporary subsets


Writing MDX statements will not be that easy, certainly not in the beginning. Where can you find help ?

  1. This very page you are looking at ! In all honesty and humbleness, it contains very good material.
  2. This high-quality overview for more details
  3. Microsoft documentation on the topic. Also, see member properties and several related pages.
  4. TM1 has a 'Record expression' functionality:

  5. Record the relevant steps, much like the Macro recorder in MS Office applications - Visual Basic for Applications or VBA for short.

MDX use cases

As a TM1 developer, we mainly use MDX statements in the following cases:

  • to make dynamic subsets rather than static subsets. For example, a cubeview showing the sales of the customers who bought last month as well (and only those customers);
  • to create custom views in a Turbo Integrator (TI) process. We will often zero out (part of) a cube, this is clearing cells at the lowest level elements in certain dimensions. This list of elements can be generated with no need to loop through the dimension ourselves.
  • Active forms in Excel have dynamic row subsets, you can expand and contract consolidations. The list of elements in the rows can again be an MDX statement, even quite complex statements.

Mind the syntax, it is crucial to get that straight. If not, TM1 won‘t like it… Some of the statements can be generated in the Subset Editor: turn on the Properties Window (menu View), then choose Tools > Record Expression. Record the expression while you do the steps manually. Stop recording when you are finished.

Often used MDX statements

Below, I will present you the MDX statements I need most of the time. This page will therefore serve as a kind of code library: we can copy/paste and adjust only names of cubes/dimensions/elements. You will notice [] around dimension names and element names. They could be left out, unless (for instance) the name contains a space. Then we must enclose the name with []. The expressions are most of the time case-insensitive. Except for function names, the expressions are also space-insensitive.

    Referencing elements

  • 1 hard-coded element (dimension name and element name follow):
    {[Project_type].[All types]}

  • In a Turbo Integrator process, we often use a parameter for the year or scenario:
    SubsetCreatebyMDX('Subset_Years', '{[FIN_Year].[2021]}');
    SubsetCreatebyMDX('Subset_Years', '{[FIN_Year].['|pYear|']}');

    The function EXPAND in TI can be used to substitute variable names to their values at run-time: SubsetCreatebyMDX('Subset_Years', EXPAND('{[FIN_Year].[%pYear%]}'));

  • 2 hard-coded elements:
    {[FIN_Year].[2021], [FIN_Year].[2022]}

  • Another example of EXPAND:
    SubsetCreateByMDX( vSubset, 'Descendants( TM1FilterByPattern( TM1SubsetAll([ ' | Dim1 | ']), ' | vConso | ' ))' );
    compared to:
    SubsetCreateByMDX( vSubset, Expand( 'Descendants( TM1FilterByPattern( TM1SubsetAll([%Dim1%]), %vConso% ))' ));

  • If you want to use MDX statements in Excel or TM1 Web, you can use cell references in the following way:
    ="{[FIN_Year].[" & E15 & "],[FIN_Year].[" & E16 & "]}"

    This allows you to customize your reports to a great extent. For example, the user can sort the rows ascending or descending, where these 2 options are a selection in a cell. The choice of the user is then picked up in the MDX expression at hand.

  • Getting the element on a certain index position in the dimension / hierarchy:

    This gives us the 6th account, index-wise, since we start counting at 0.
  • Using an existing subset in an expression

  • The function TM1SubsetToSet can be really useful (information). It allows us to use a different subset (name-based) into another expression. No need to duplicate the same logic twice or even more. Also, the referenced subset could be maintained by users and can change over time - even static subsets. The referenced subset can be public or private, if unspecified through the argument then a private subset will win over the public subset with the same name.
    {TM1SubsetToSet([Period].[Period], "Current Period", "public")}

    This expression uses the contents of the "Current period" subset within the Period hierarchy of the Period dimension.

    The public/private character of the referenced subset was submitted by George Tonkin.
  • Elements at different levels

  • All lowest level elements in a dimension:
    {TM1FilterByLevel( TM1SubsetAll( [Project_type] ), 0)}

  • All elements at certain levels:
    {TM1FilterByLevel( TM1SubsetAll( [Project_type] ), 1,2,3 )}

  • All consolidated elements in a dimension:
    { Except( TM1SubsetAll( [Project_type] ), { TM1FilterByLevel( TM1SubsetAll( [Project_type] ), 0) } )}

  • All consolidated elements in a dimension - hierarchical representation:
    { Except( Hierarchize( TM1SubsetAll( [Project_type] )), { TM1FilterByLevel( TM1SubsetAll( [Project_type] ), 0) } )}

  • Instead of TM1SubsetAll, you can also use Members:

    HOWEVER, TM1SubsetAll and Members do not necessarily lead to the same output ! Members can have more elements in the resulting subset, since it counts unique paths towards an element whereas TM1SubsetAll only takes unique elements. So: if you use certain elements multiple times in the dimension, it can lead to different results.

  • All lowest level elements in a dimension, except one element:
    { Except( TM1FilterByLevel( TM1SubsetAll( [Work_Customers] ), 0), {[Work_Customers].[Office]} ) }

  • We can use the concept of levels as follows. Given a simple dimension called Period_MDX like this, the following statements all work - depending on the entries in the }HierarchyProperties cube where we can give meaningful names to levels:

    Filter( Descendants( [Period_MDX].[All months]), [Period_MDX].CurrentMember.Level.Ordinal < 2 )
    Intersect( Descendants( [Period_MDX].[All months]), [Period_MDX].[Year].Members )
    Filter( Descendants( [Period_MDX].[All months]), [Period_MDX].CurrentMember.Level.Name = "Year" ), OR:
    Filter( Descendants( [Period_MDX].[All months]), [Period_MDX].CurrentMember.Level.Name = "level001" )

    Filter( Descendants( [Period_MDX].[All months]), [Period_MDX].CurrentMember.Level.Ordinal = [Period_MDX].[Year].Ordinal ), OR:
    Filter( Descendants( [Period_MDX].[All months]), [Period_MDX].CurrentMember.Level.Ordinal = [Period_MDX].[level001].Ordinal )

    For this to work, you should refer to the picture below for the level names:

    Run the little TI process to update the hierarchies in the different dimensions (every time you are done making changes to that particular cube) !

  • Sorting

  • All lowest level elements in a dimension, sorted alphabetically:
    {TM1Sort( TM1FilterByLevel( TM1SubsetAll( [Project_type] ), 0), Asc)}

    Next to Sort, Order is also supported.

  • If you would like to Order elements based on an alias or other attribute:
    Order({[Month].Members}, [Month].[Short monthnr], Asc)

    Short monthnr is an alias in this case.

  • Sorting the elements on an attribute:
    {Order( TM1SubsetAll( [Month] ), [Month].CurrentMember.Properties("Long name"), Asc)}

  • Another example for customers:
    {Order( Descendants( [Client].[Paid projects] ), [Client].CurrentMember.Properties("Description"), Asc)}

  • Sorting customers based on Sales value in a cube, leaving out sales values that are almost 0:
    {[Customer].[Total Customer]} + {Order( Filter( TM1FilterByLevel( Descendants( [Customer].[Total Customer]), 0), [Sales].([Product].[Total Product], [Year].[Total Year], [Period].[Total Period], [Currency].[EUR], [Sales_Msr].[Sales]) < -0.01 OR [Sales].([Product].[Total Product], [Year].[Total Year], [Period].[Total Period], [Currency].[EUR], [Sales_Msr].[Sales]) > 0.01 ), [Sales].([Product].[Total Product], [Year].[Total Year], [Period].[Total Period], [Currency].[EUR], [Sales_Msr].[Sales]), Desc) }

    Note how we have 3 times the same information in the expression regarding element selections. For some dimensions, it would be better to leave them out and use the DefaultMember property in the }HierarchyProperties control cube.

    I did not manage to use a function like Absolute value in an MDX query. In case anyone knows this: please let me know !

  • Order can be nested, so if you want to sort on 3 measures, in order (no pun intended), you can put Order inside a second Order inside a third Order:
    Order(Order(Order({[Employee].Members}, [Employee].[Department], Asc), [Employee].[Age], Asc), [Employee].[Salary], Asc)

    This expression sorts employees first on their Salary, then on their Age, lastly on their Department. Thank you Keith Would for sending an email on this expression. Note that using BAsc and BDesc is probably preferred over Asc and Desc, respectively.

    Keith also pointed out that you can sort on the concatenated attribute values by using, for example:

    Order(Order({[Employee].Members}, [Employee].[Department]+[Employee].[Age], Asc), [Employee].[Salary], Asc)

  • Descendants and Ascendants

  • An element and its descendants all the way down the hierarchy:
    {Descendants([FIN_Account].[EBIT]) }

  • All Descendants of an element, without that element:
    {Except( Descendants([FIN_Account].[EBIT]), {[FIN_Account].[EBIT]} )}

  • Leaf-level Descendants of a given element:
    {TM1FilterByLevel( Descendants([FIN_Account].[EBIT]), 0)}
    {TM1FilterByLevel( TM1DrilldownMember( [FIN_Account].[EBIT], ALL, Recursive), 0)}

    You can easily sort the returned elements (here, ascending):
    {TM1Sort( TM1FilterByLevel( Descendants([FIN_Account].[EBIT]), 0), Asc)}

    Leaving out the filter by level will return the consolidated element(s) as well. So, Descendants and TM1DrilldownMember both include the parent element that was drilled upon.

  • A typical month selection where the user could choose a consolidated element - parameter pMonth:
    SubsetCreateByMDX( vSubset, '{TM1FilterByLevel( Descendants( [Month].[' | pMonth | ']), 0)}', 'Month' );
  • All lowest level elements in a dimension except those elements below a certain consolidation:
    { Except( TM1FilterByLevel( TM1SubsetAll( [Fct_Product] ), 0), Descendants( [Fct_Product].[My consolidated product] ) )}

  • An element and its descendants all the way down the hierarchy, but omitting level 0:
    {Except( Descendants([Customer].[Total Customer]), {TM1FilterByLevel( Descendants([Customer].[Total Customer]), 0 )} )}

  • Leaf-level descendants of 2 consolidated elements:
    {TM1FilterByLevel( Union ( Descendants( [FIN_Account].[EBIT] ), Descendants( [FIN_Account].[Taxes] )), 0)}

  • Leaf-level descendants of a given consolidated element, that consolidation included:
    {Union( {FIN_Account.[EBIT]}, TM1FilterByLevel( Descendants(FIN_Account.[EBIT]), 0) )}
    {FIN_Account.[EBIT], TM1FilterByLevel( Descendants(FIN_Account.[EBIT]), 0) }
    Note: The Union function can be shortened to the + operator (an element is listed only once):
    {FIN_Account.[EBIT]} + {TM1FilterByLevel( Descendants(FIN_Account.[EBIT]), 0)}
    or, now EBIT is at the bottom of the list, the Order of the elements in the expression is retained left to right:
    {{TM1FilterByLevel( Descendants(FIN_Account.[EBIT]), 0)}, FIN_Account.[EBIT] }

    Next to Union, Intersect is also supported. Sidenote: if you want to avoid the removal of duplicate elements, use a comma instead of a plus sign. Example: [Fct_Product].[A], [Fct_Product].[B], [Fct_Product].[C], TM1FilterByLevel( Descendants([Fct_Product].[C]), 0)}

  • Filtering out Profit and Loss (P&L) and Balance Sheet accounts:
    TM1Sort( Union( TM1FilterByPattern( TM1FilterByLevel( TM1SubsetAll( [St_Account] ), 0), "6*"), TM1FilterByPattern( TM1FilterByLevel( TM1SubsetAll( [St_Account] ), 0), "7*") ), Asc )

  • Filtering out Balance Sheet accounts as the result of what remains when P&L accounts are removed:
    Except( TM1FilterByLevel( TM1SubsetAll( [St_Account] ), 0), TM1SubsetToSet( [PL_Account], "P&L Accounts" ))

    Notice that the subset "P&L accounts" should exist too: we filter out elements that appear in a certain subset.

  • The parent elements of an element including the element itself, can be retrieved with Ascendants:

    Note that only the parents in the first dimension rollup are retrieved, which can often be a limitation.

  • Much better would be to list all parent elements of an element in the entire dimension. The element drilled upon in the example below is 'INPUT Sales third parties' in the dimension 'Rp_Cost_Element'.
    Filter( Except(TM1SubsetAll( [Rp_Cost_Element] ), TM1FilterByLevel( TM1SubsetAll( [Rp_Cost_Element] ), 0 )), TM1TupleSize( Intersect({[Rp_Cost_Element].[INPUT Sales third parties]}, Descendants([Rp_Cost_Element].CurrentMember)).Item(0)) > 0)

    Item(0) gives us the first element in the result set.
    Thank you Ty Cardin

  • The top parent of an element would then be:
    {Tail( {Ascendants([Fct_Customer].[111522])}, 1 )}

    In TM1 an element may have multiple parents within a hierarchy so if there are multiple parent branches you may need the intersect between the ancestors and all top node elements in the dimension:
    {Intersect( {Ascendants([Fct_Customer].[111522])}, {Filter( {TM1SubsetAll( [Fct_Customer] )}, [Fct_Customer].CurrentMember.Parent.Name = "")})}

    I would understand it if you prefer levels (0 is the highest level in a dimension/hierarchy): {Intersect( {Ascendants([Fct_Customer].[111522])}, [Fct_Customer].Levels(0).Members )}
  • More on hierarchical structures

  • The immediate children (leaf level or consolidated) of a given element:
    {[Project_type].[All Types].Children}

  • An element together with its immediate children (leaf level or consolidated):
    {DrilldownLevel( {[Project_type].[All Types]} )}

  • Alternatively:
    {TM1DrillDownMember( {[Project_type].[All Types]}, ALL )}

  • Sometimes we require a consolidated element with all its Descendants, together with a different consolidation with only that one's immediate children:
    { Descendants([Fct_Assumption].[Total Assumption]) } + { DrilldownLevel( [Fct_Assumption].[Subtotals]) }

  • Similar to DrilldownLevel, but you limit the list to the first x elements:
    {DrilldownLevelTOP( [Project_type].[All Types], 3)}

    DrilldownLevelBOTTOM is similar to this function.

  • FIRSTCHILD gives us the first child of the element:

    … will return Apr. LASTCHILD gives us Jun.

  • FIRSTSIBLING gives us the first child of the shared parent:

    … will return Q1. LASTSIBLING gives us Q4.

  • HEAD gives us the first element of a set:
    {HEAD( {[Month].[Q1].Children}

    … will return Jan.

  • TAIL gives us the last element of a set:
    {TAIL( {[Month].[Q1].Children}

    … will return Mar.

  • LAG returns the element that is a specified number of positions along the element’s level:

    … will return Q1.

  • LEAD is the inverse of the function LAG:

    … will return Q4.

    Lag(1) is equivalent to LEAD(-1), since the functions are inverse. Both functions can take on positive, negative or 0 values.
  • PREVMEMBER is equal to LAG(1) and NEXTMEMBER is LEAD(1):

    … will return Q2.

  • What should we do if we want to retrieve the next or previous member (index-wise) in any dimension ? The added challenge is that the current element is stored in a subset, rather than entered in a cube or as an attribute. For the solution, please refer to my posts here where I demonstrate Named levels in TM1.
    { ParallelPeriod( [Week].[Week_L0], -1, TM1Member( [Week].[Current week].Item(0), 0) )}

    Note that I also use the Week_L0 'level' element in the }HierarchyProperties cube. Pay attention to use e.g. 'Week_L0' if you fill out the cube cell, however, if you did not specify anything you should use level002 (within the given dimension/hierarchy). You need to refresh when edits are made to this cube, but other than that, it's a good way to make your TM1 models more dynamic and 'readable' by humans. Refreshing can be done using the TI function RefreshMDXHierarchy( '' ); for all dimensions / hierarchies, or RefreshMDXHierarchy( 'dimension or hierarchy' ); for a given dimension/hierarchy.

    I used the dimension for 'Week' as an example but the above function also works on dimensions that do not have a time concept, like companies.

    Notice the defaultMember element too, we will discuss it later on in the document.

    For more information on Named levels, refer to: IBM documentation. The idea is also that statements like [Customer].[Country].Members or [Customer].[Country].Members work, certainly in for dimensions/hierarchies that are not ragged - here, we have a Customer dimension where a rollup exists for customers into countries (the result is the list of distinct countries).

  • We also have functions to roll up from elements to their parent(s), for example Parent, Ancestors or TM1Rollup:
    {TM1Rollup( {[Month].[Jan]}, {[Month].[Jan]} )}

    returns the parents of the month of January in the Month dimension. This expression mimicks the rollup button in the Subset Editor, by the way. It can be similar to a loop over Elpar from 1 to ElparN.

  • Ancestor can be useful to ask for the parents of an element, going from parent to grandparent to grand-grandparent, etc:
    {Ancestor([Rp_Account].[600000], 0)},
    {Ancestor([Rp_Account].[600000], 1)},
    {Ancestor([Rp_Account].[600000], 2)},
    {Ancestor([Rp_Account].[600000], 3)}, …

    will return the element itself, then the parent, grandparent, parent of the grandparent.

    The Ancestors function can be worthwile too:
    {Ancestors([Rp_Account].[600000], 1)}
  • LASTPERIODS returns the n elements preceding and including the selected element along the element’s level
    {LastPeriods( 3, [Month].[Q3])}

    … will return Q1, Q2, Q3.

    Please note that LastPeriods is not restricted to time dimensions. It can be utilized in a similar fashion on any dimension irrespective to what element names are used.

  • LEVEL returns the elements at the same level as the chosen element:

    … will return Q1, Q2, Q3, Q4.

  • PERIODSTODATE is an interesting one. It returns the set of sibling elements (so at the same level) starting with the first element, up until the chosen/last element. Ty Cardin gave a nice example where cube data is filtered from beginning of the year until a certain period:
    PERIODSTODATE( [Manad].[Level000], TM1MEMBER( TAIL( FILTER( [Manad].[Total Year].children, [cubename].([z.TestRowEntryList].[R01]) > 0), 1).Item(0), 0))

    This solution works if the Level000 name is adequate (it can be swapped for something meaningful). It stands for the parent level of the elements to be selected. Look at the }HierarchyProperties cube and a TI process with the command RefreshMDXHierarchy.

  • Should you prefer to work with the name of the consolidated element, rather than the level name, then use the Level function:
    PERIODSTODATE( [Manad].[Total Year].Level, TM1MEMBER( TAIL( FILTER( [Manad].[Total Year].children, [cubename].([z.TestRowEntryList].[R01]) > 0), 1).Item(0), 0))

    The PERIODSTODATE formula can also be used in dimensions (hierarchies) that are not dealing with Time or Periods. So Accounts, Cost Centers, Companies, ... though of course it still has to make sense.
  • MEMBERS allows us to select a range of contiguous elements (following dimension indexes) from the same level specifying the first and last element of the set with a colon between them:

    … will return Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct.

      Please note the 2 following requirements:
    • Both elements should be at the same level in the dimension
    • Index-wise, no dimension elements at a different level, can be between the 2 listed elements

  • A more advanced example of this range operator:
    TM1SortByIndex( TM1Member( TM1SubsetToSet([Months].[Months], "subset1", "public").Item(0), 0):TM1Member( TM1SubsetToSet([Months].[Months], "subset1", "public").Item(0), 0).LEAD(5), Asc)

    Here we retrieve a number of months, based on the contents of a public subset called subset1 (its first element). That element and the next 5 elements are taken, 6 months in total. Note that this technique is very powerful in continuous time dimensions, crossing years. Your 12 month moving windows becomes child's play !

    Also note that the previous expression can be greatly simplified to:
    TM1SortByIndex( LastPeriods( -6, TM1Member( TM1SubsetToSet( [Months], "subset1", "public").Item(0), 0 )), Asc)

    Finally on this topic, consider "jumping" to a different month by advancing 5 months:

    {TM1Member( TM1SubsetToSet( [Months], "subset1", "public").Item(0), 0).Lead(5)}

    Planning Analytics hierarchies

    Since some time now we can use hierarchies in IBM Planning Analytics. Any dimension can become a container of 1 or more so called hierarchies. These hierarchies can be used in cube views and reports in an orthogonal fashion, such that one can analyze the data in a much richer way than before. The so-called Leaves hierarchy is automatically maintained by the software, containing a copy of all level 0 elements in the entire dimension (over hierarchies). Here is a dynamic subset on the }Dimensions dimension to identify all dimensions that contain hierarchies: Filter( TM1SubsetAll( [}Dimensions] ), Instr( [}Dimensions].CurrentMember.Lead(1).Name, [}Dimensions].CurrentMember.Name + ":" ) = 1 )

  • In some cases, though, there could be a mismatch between elements in the Main hierarchy of a dimension, and the Leaves hierarchy of the same dimension. This MDX allows you to filter them out from the Leaves hierarchy:
    Except( TM1FilterByLevel( TM1SubsetAll( [Customer].[Leaves]), 0 ), Generate( TM1FilterByLevel( TM1SubsetAll( [Customer].[Customer]), 0 ), Filter( TM1FilterByLevel(TM1SubsetAll([Customer].[Leaves]) , 0), [Customer].[Leaves].CurrentMember.Name = [Customer].[Customer].CurrentMember.Name )))

    Submitted by George Tonkin, thanks for this.

  • Union and Intersect

  • Union can be interesting if you have a subset that should "expand above" but there are other elements that should be shown at the top:
    {Union( {[Account].[Earnings Before Taxes] }, {[Account].[Subset Exp Above] })}

    Here the subset 'Subset Exp Above' is a second (public) subset on the same dimension: { Except( {TM1DrillDownMember( {[Account].[Earnings Before Taxes] }, ALL, RECURSIVE )}, { [Account].[Earnings Before Taxes] })}
    Thanks to Paul Simon for posting.

  • Intersect is useful to evaluate two sets of elements:
    {Intersect( Filter( TM1SubsetAll( [Month] ), [Filter].([Filter].[FilterOrder]) <> 0), Descendants(Month.[Q1]) )}

  • Another example on Intersect:
    {Intersect( TM1SubsetToSet( [Account], "Cost accounts" ), TM1SubsetToSet( [Account], "Filter by level 0" ) )}

    The above expression would be more efficient with TM1FilterByLevel, however, but this is just an illustration.

  • We can automate the previous subset creation in Turbo Integrator as follows:
    vDim = 'Account';
    vSubset1 = 'Cost accounts';
    vSubset2 = 'Filter by level 0';
    vSubset3 = 'Input cost accounts';
    # Create MDX
    vMdx = '{Intersect( TM1SubsetToSet( [' | vDim | '], "' | vSubset1 | '" ), TM1SubsetToSet( [' | vDim | '], "' | vSubset2 | '" ) )}';
    # Or, in a more readable format:
    vMdx = Expand( '{Intersect( TM1SubsetToSet( [%vDim%], "%vSubset1%" ), TM1SubsetToSet( [%vDim%], "%vSubset2%" ) )}' );
    SubsetCreateByMDX( vSubset3, vMDX, vDim );
  • Cube values

  • Selecting elements based on values in a cube (we do not have to specify every dimension in the cube):
    {Filter( TM1FilterByLevel( TM1SubsetAll( [PL_Account] ), 0), [BalanceSheet].([FIN_Year].[2021],[FIN_Period].[P07],[FIN_Scenario].[Actual],[BS_Measures].[Amount]) <> 0)}

    If we do not specify an element in a dimension of the cube, TM1 will use the element with the lowest index of all elements at the highest level in the dimension, unless we specify a defaultMember in the }HierarchyProperties control cube. So watch out and make sure this is what you need. Note that we also have a DEFAULT_HIERARCHY property in the }DimensionProperties cube.
    The defaultMember property of a dimension/hierarchy can be retrieved with:
    { [SalesPerson].DefaultMember }
    The }HierarchyProperties control cube is useful to turn your TM1 models into more dynamic models which are 'readable' by developers that come after you !
    Topcount, bottomcount, topsum, bottomsum, toppercent and bottompercent could be done in a similar fashion:
    {TopCount( TM1FilterByLevel( TM1SubsetAll( [SalesPerson] ), 0), 5, [Sales].([Sales_Measures].[Amount]))}

    A practical use case for Order and topcount:
    {Order( TopCount( TM1FilterByLevel( TM1SubsetAll( [SalesPerson] ), 0), 10, [Sales].([Sales_Measures].[Amount])), [Sales].([Sales_Measures].[Amount]), BDesc)} Statements like TopCount or TopSum can be recorded with the interface, as well as sorting elements alphabetically:

    In the above expression please take out the dreaded TM1SubsetBasis (cf. supra). By the way, I discovered a nice trick to quickly see in which cubes a dimension is used: Go to the dimension's Subset Editor, then Tools > Filter... and the dropdown will show you the cubes containing the dimension.

    As we noticed earlier, the HEAD function can be used to limit a subset to, say, its first 5 elements.

  • Selecting customers that have an outstanding balance in the current month:
    Filter({TM1FilterByLevel( {TM1SubsetAll( [Customer] )}, 0)}, [Invoice].(TM1Member([Time].[Invoice Month].Item(0),0),[Invoice_measures].[Outstanding balance]) > 0)

  • A practical example is an MDX statement to replace the spacer trick. This method allows you to show the next 5 open entries ('rows') for the user. Think about vacancies to budget new hires, or entering CAPEX details.

  • {Head( Filter( [Data Entry Row].[All Rows].Children, [Entry Cube].([Version].[Forecast], [Entry Cube Measure].[Entry]) = "" ), 5 )}

    Important notes:
    • This method requires a refresh of the sheet/dashboard to get the extra rows to appear (but not after every row that was input).
    • For the HEAD function to work properly the element ordering and leveling in the dimension is important !
    • If your Entry measure is of type Numeric, replace the double quotes with a 0 in the expression and make sure that that all rows that are filled in, are not equal for that measure.
    • You could still add [Data Entry Row].CurrentMember, in the Filter expression but you don't have to
    Thank you Lotsaram

    To convert text in a cube to valid elements, we typically use: TM1FilterByPattern, StrToMember or LookUpCube. We will show examples of the syntax below.

  • TM1FilterByPattern:
    {TM1FilterByPattern( TM1SubsetAll([Account_PL]), [Prm_Account].([Prm_Account_dim1].[Pattern], [Prm_Account_Msr].[Text]))}

  • LookUpCube is more advanced but it also allows us to filter on elements following information in a cube:
    {TM1FilterByPattern( TM1SubsetAll([Account_PL]), LookUpCube ( "Prm_Account", "([Prm_Account_dim1].[Pattern], [Prm_Account_Msr].[Text])" ) )}

    … will filter all elements in the Account_PL dimension following the pattern mentioned in the lookup cube Prm_Account.

    LookUpCube could also grab from a cube an entire MDX expression, not just element names or certain selections. For example:
    {StrToMember( LookUpCube( "Sys_CFG", "[Sys_CFG].[MDX current month], [Sys_CFG_Msr].[Text]" ))}

    whereby the targete string cell in the system configuration cube holds: [Period].[June]
  • A more useful business example would be to return in a subset the month name that is entered in a lookup cube. For instance, which month and year are subject to being closed financially ?
    {TM1FilterByPattern( TM1SubsetAll([Month]), [Prm_Finance].([Prm_Fin_Param].[Month of close], [Prm_Finance_Msr].[Text]))}

  • But now it gets interesting ! To the best of my knowledge, there aren't many ways to have a dynamic subset on a Months dimension return for example both a month and its year to date (YTD) counterpart. Like, "Nov" and "YTD_Nov" where "Nov" is input in a lookup cube.

    This can be done, however, with similar syntax:
    {Union( TM1FilterByPattern( TM1SubsetAll([Month]), [Prm_Finance].([Prm_Fin_Param].[Month of close], [Prm_Finance_Msr].[Text])), TM1FilterByPattern( TM1SubsetAll([Month]), "YTD_" + [Prm_Finance].([Prm_Fin_Param].[Month of close], [Prm_Finance_Msr].[Text])))}

    Alternatively you can again use the LookUpCube function:
    {Union( TM1FilterByPattern( TM1SubsetAll([Month]), LookUpCube( "Prm_Finance", "([Prm_Fin_Param].[Month of close], [Prm_Finance_Msr].[Text])" )), TM1FilterByPattern( TM1SubsetAll([Month]), "YTD_" + LookUpCube( "Prm_Finance", "([Prm_Fin_Param].[Month of close], [Prm_Finance_Msr].[Text])" )))}

    Syntax 1 definitely has my vote, since it is somewhat shorter, but most of all because we avoid the double quote character ".

    You can of course use Turbo Integrator to create/maintain subsets but honestly, this MDX-based dynamic solution certainly has merits, and is something I would add to TM1 models to increase the usefulness to the TM1 user community.

    You could use a wildcard to cut down on the length of the expression, but beware that this could lead to additional elements in the subset which might not be wanted:
    {TM1FilterByPattern( TM1SubsetAll([Month]), "*" + [Prm_Finance].([Prm_Fin_Param].[Month of close], [Prm_Finance_Msr].[Text]))}

    Lastly, and apologies for the spoiler to the Generate function which is discussed below: Generate( Filter( TM1FilterByLevel( TM1SUBSETALL( [Period] ), 0), [Period].CurrentMember.Properties("YTD") <> ""), {StrToMember("[Period].[" + [Period].CurrentMember.Properties("YTD") + "]")})

    Here we filter out all lowest-level Period elements that have an attribute value (textual) for their YTD name. Then the MDX returns those YTD elements instead of the level 0 elements. Think of Generate as swapping elements for the result of a subquery that uses each element (in turn). More on this below !

  • Let's now introduce the StrToMember function to get the reporting year for Actuals from a parameter cube:
    {StrToMember("[Rp_Year].[" + [Rp_Irp_Parameter].([Rp_Scenario].[Actual],[Rp_Irp_Parameter].[Reporting Year],[Rp_Irp_Parameter_Msr].[Value]) + "]") }

  • The same result but using the LookUpCube function to get the cube value:
    {StrToMember("[Rp_Year].[" + LookUpCube( "Rp_Irp_Parameter", "[Rp_Scenario].[Actual],[Rp_Irp_Parameter].[Reporting Year],[Rp_Irp_Parameter_Msr].[Value]" ) + "]") }

  • This means that we can filter years based on the input of a given year in a system cube:
    {LastPeriods( 4, StrToMember("[Year].[" + [System].([System_Prm].[Year],[System_Msr].[Text]) + "]") )}

    Here we want to retrieve the year from the system cube along with its 3 previous years. Make sure the Year dimension indexes are correct.

  • Going further, a way to filter elements in a dimension based on cube data, where the definition of the cube cells is stored in a parameter cube:
    {Filter( TM1FilterByLevel( TM1SubsetAll( [Period] ), 0), [Periods 2D].( StrToMember("[Year].[" + [System].([System_Prm].[Year], [System_Msr].[Text]) + "]")) > 0 )}

    Hereby, we filter elements in the dimension 'Period' based on data in the cube 'Periods 2D'. In that cube, we select cells based on a year that is stored in the cube 'System'.

  • A real-world example, taken from the TM1 forum. We want to dynamically filter projects for the logged on user, whereby a cube stores the access of users to sites. Site is an attribute of Project:
    {Filter( {Filter({TM1SubsetAll([Project])}, [Project].[Primary Site] <> "" )}, [z_Security_Site].( StrToMember( "[}Clients].["+USERNAME+ "]"), StrToMember("[Site].[" + [Project].[Primary Site] + "]"), [z_Security_Control_Measure].[Write_Flag]) > 0 )}

  • Next to StrToMember, functions like StrToSet also exist. The idea is, like with converting a string to a valid dimension member, converting a string to a valid set of elements:
    StrToSet( "{Filter( {[Year].Members}, " + IIF( COUNT( {Filter( {[Year].Members}, [YM].([Year].CurrentMember, [Maand].[tekst]) = "test" )} ) > 0, "[YM].([Year].CurrentMember, [Maand].[tekst]) = """ + "test" + """", "[Year].CurrentMember.Name = """ + "total" + """" ) + ")}" )

    If certain elements satisfy the condition of having a certain input text in a 2D cube, then take that list. If not, take a dummy element. Code adapted from Declan Rodger's posting here. Added benefit: this example teaches you the functions IIF and COUNT (see below).

  • Another example with IIF and COUNT:
    StrToSet( "{Filter(Descendants([z_test].[Total)," + IIF( COUNT( {Filter(Descendants([z_test].[Total), [z_test].[Type] = 'A')}) > 0, "[z_test].[Type] = 'A'", IIF( COUNT( {Filter(Descendants([z_test].[Total]), [z_test].[Type] = 'B')}) > 0, "[z_test].[Type] = 'B'", "[z_test].[Type] = 'C'" )) + ")}" )

  • Intermezzo; rather than testing if a cell / input is not empty by using <> "", you can use IsEmpty:
    {Filter( {Filter({TM1SubsetAll([Project])}, Not( IsEmpty( [Project].[Primary Site] )) )}, [z_Security_Site].( StrToMember( "[}Clients].["+USERNAME+ "]"), StrToMember("[Site].[" + [Project].[Primary Site] + "]"), [z_Security_Control_Measure].[Write_Flag]) > 0 )}

  • Filtering those elements whose cells in a cube contain a certain text:
    Filter( TM1FilterByLevel( TM1SubsetAll( [CostCenter] ), 0), Instr([Prm_CostCenter].([CostType].[No type], [Prm_CostCenter_Msr].[Manager Name]), 'jones' ) > 0)

  • Filtering those elements whose values in a cube are lower than the average over all elements. For instance, the customers who bought more than the average of all customers:
    {Filter( TM1FilterByLevel( TM1SubsetAll( [Customer] ), 0), ([Sales].[Sales_Msr].[Amount] > Avg( TM1FilterByLevel( TM1SubsetAll( [Customer] ), 0), [Sales].([Sales_Msr].[Amount])) ))}

  • Selecting elements based on values in a cube and sort on the cube values:
    {[Customers].[All customers]} + {Order( Filter( TM1FilterByLevel( TM1SubsetAll( [Customers] ), 0), [Sales].([Sales_Measures].[Amount]) <> 0 ), [Sales].([Sales_Measures].[Amount],[Sales_Year].[2021]), Asc) }

  • Filtering elements in rows or columns can also be done on selections in other dimensions, provided that the latter are part of the title section of a view or report. For example, let's filter out models based on the region:

    Suppose that each region/country holds the name of the (consolidated) model that can be sold in the region/country:

    Then we can get the relevant consolidated model element based on the region selection in the titles section:

    1. The MDX is like this:
    {StrToMember( "[model].[" + [region].CurrentMember.Properties("Model selection") + "]")}

    2. Alternatively: We can also use the Filter function as with Declan Rodger here
    Filter( TM1SubsetAll( [model] ), [model].CurrentMember.Name = [region].[Model selection])

    3. This is somewhat longer, in case the attribute name could/would coincide with an element name in the dimension:
    Filter( TM1SubsetAll( [model] ), [model].CurrentMember.Name = [region].CurrentMember.Properties("Model selection"))

    If we want to have the consolidated element and all its lowest-level descendants (for data entry for example):
    {StrToMember( "[model].[" + [region].CurrentMember.Properties("Model selection") + "]")} + TM1Sort( TM1FilterByLevel( Hierarchize( Descendants( {StrToMember( "[model].[" + [region].CurrentMember.Properties("Model selection") + "]")} )), 0 ), Asc )

  • We can also apply it to a more interesting case like the following. Suppose that we have companies and each company sells within a main product category.
    For budget entry we can then have a template with companies as a title selection and in the rows we have the main product category for that company, as well as the level 0 descendants:
    {StrToMember( "[Rp_ProductGroup].[" + [Rp_Company].CurrentMember.Properties("Product Group parent member") + "]")} + TM1Sort( TM1FilterByLevel( Hierarchize( Descendants( {StrToMember( "[Rp_ProductGroup].[" + [Rp_Company].CurrentMember.Properties("Product Group parent member") + "]")} )), 0 ), Asc )

  • Ty Cardin posted on an MDX query where a filter to cube values uses an element that does not exist in a dimension. You can read a short discussion here.

  • Quite often we need to generate subsets that are to be used by input or reporting areas of the TM1 model. It's convenient to generate these subsets as MDX-based dynamic subsets, but obviously you can also turn them into the static variants. Following is a concrete example of a company dimension that holds an attribute of the product group sold. The idea is to create 1 dynamic subset for each company, containing the lowest-level products within the given product group. An example for 1 company:
    TM1Sort( TM1FilterByLevel( Descendants( {StrToMember("[Rp_ProductGroup].[" + [Rp_Company].[C90290].Properties("Product Group parent member") + "]") }] ), 0 ), Asc )

    How do we generate the subsets in 1 TI process ? There are a lot of interesting learning tips in this code so please be sure to look in detail, if needed. A double Expand function is just 1 area of interest.
    # Process to (re)generate subsets on the product dimension, where every company has its own (dynamic) subset of products sold
    sDim_Comp = 'Rp_Company';
    sDim_Prod = 'Rp_ProductGroup';
    sAttr_For_Prod = 'Product Group parent member';
    # MDX query to get the products linked to a company
    sMDX = 'TM1Sort( TM1FilterByLevel( Descendants( {StrToMember("[%sDim_Prod%].[" + [%sDim_Comp%].[%sElem_Code%].Properties("%sAttr_For_Prod%") + "]") }] ), 0 ), Asc )';
    # loop over companies and (re)generate the dynamic subsets
    c = 1;
    While( c <= Dimsiz( sDim_Comp ) );
       sElem_Name = Dimnm( sDim_Comp, c );
       sElem_Code = Attrs( sDim_Comp, sElem_Name, 'Code' );
       If( sElem_Code @<> '' );
       If( Ellev( sDim_Comp, sElem_Name ) = 0 );
       If( Elisanc( sDim_Comp, 'Total Company', sElem_Name ) = 1 );
          vSubset = '}Products_' | sElem_Code;
          If( SubsetExists( sDim_Prod, vSubset ) = 0 );
              SubsetCreate( sDim_Prod, vSubset );
          SubsetMDXSet( sDim_Prod, vSubset, Expand( sMDX ));
          SubsetAliasSet( sDim_Prod, vSubset, 'Description' );
       c = c + 1;

  • Filtering on attributes

  • Filtering elements on a String attribute (alias or text attribute) could be achieved with a Property:
    {Filter( TM1SubsetAll( [FIN_Account] ), [FIN_Account].CurrentMember.Properties("Type") = "PL")}

  • The same result can be obtained as follows, in a shorter way:
    {Filter( TM1SubsetAll( [FIN_Account] ), [FIN_Account].[Type] = "PL")}

    The drawback is that this syntax could be ambiguous, if 'Type' is an element or subset or attribute of the dimension/hierarchy. The previous syntax is more robust and therefore better.

  • Another variation, here to retrieve versions that were created by a certain user:
    {Filter( {[version].members}, [version].[created by user] = StrToMember( "[}Clients].[" + UserName + "]").Properties('}TM1_DefaultDisplayValue'))}
    Double quotes around }TM1_DefaultDisplayValue are allowed too.

  • Filtering elements on a Numeric attribute is best done through the control cube for element attributes:
    TM1SortByIndex( Filter( TM1FilterByLevel( TM1SubsetAll( [Article] ), 0 ), [}ElementAttributes_Article].([}ElementAttributes_Article].[Weight]) = 0 ), Asc)

    Here we filter out articles that have a weight equal to 0 and sort the resulting articles (if any).
  • Combining (string) information from a lookup cube with a numeric comparison (of dates) involving an attribute - the goal is to filter out periods based on dates:
    Filter( TM1FilterByLevel( TM1SubsetAll([Periods-Months]), 0), [Periods-Months].[TM1 Last Day Serial Date] >= StrToValue( StrToMember( "[Periods-Months].[" + [Prm_Finance].( [Prm_Fin_Param].[Value], [Prm_Finance_Msr].[Text] ) + "]" ).Properties("TM1 Last Day Serial Date")))
    See here.

  • The lookup value for the attribute can also be stored in a cube:
    {Filter( TM1SubsetAll( [FIN_Account] ), [FIN_Account].[Type] = [Prm_Account].([Prm_Account].[Parameter_Value],[Prm_Account_Measures].[Account type]))}

  • Filtering elements on a Numeric attribute:
    {Filter( TM1SubsetAll( [FIN_Account] ), [FIN_Account].[Conversion] = 1)}

  • Filtering those elements at any level that contain an alias:
    {Filter( TM1SubsetAll( [FIN_Account] ), [FIN_Account].[Description] <> "" )}

  • Filtering those elements where an attribute contains a certain text:
    {Filter( TM1SubsetAll( [FIN_Account] ), Instr( UCase([FIN_Account].[Description]), "AMORTIZATION" ) > 0 )}

    Note that the MDX function Instr is case-sensitive. Hence the use of the function UCase. More information can be obtained here

  • Filtering the currency dimension when the user has a currency attribute filled in:
    { Filter( {TM1SubsetAll([Dim_Currency])}, [}ClientProperties].( StrToMember("[}Clients].["+USERNAME+"]"), [}ClientProperties].[currency]) = [Dim_Currency].CurrentMember.Name )}
    • Dim_Currency is the dimension of currencies, it could be anywhere in the view, active form, ...
    • currency is an attribute added to the }ClientProperties cube, it should match the currencies in the dimension Dim_Currency

  • Filtering the organisation to which the currently logged in client belongs:
    {Filter({[Organisation].Members}, [Organisation].Currentmember.Name = StrToMember ( "[}Clients].[" + UserName + "]" ).Properties("Department"))}
  • Filtering the level 0 descendants of elements marked with a certain attribute:
    {TM1FilterByLevel( Descendants( Filter( TM1SubsetAll( [CostCenter] ), [CostCenter].[Overhead CC] = "Y" ) ), 0)}

  • Filtering over dimensions

  • A very useful trick involving Instr is the following. You can apply it to views in the Cube viewer, Planning Analytics, Active forms, and so on. Well, you can filter (e.g.) accounts in the rows of a view in the cube viewer, when the account prefix equals the company name. Company is another dimension in the cube:
    {Filter( TM1SubsetAll( [St_Account] ), Instr( [St_Account].CurrentMember.Name, [Mp_Std_Company].([Mp_Std_Company_Msr].[Company]) ) = 1 )}
    • St_Account is the dimension of accounts in the rows.
    • Mp_Std_Company is the cube where the companies are mapped (a 2D cube with companies and a measure called "Company"). Below, China Guangzhou is mapped in the lookup cube to "C010", which filters the prefix in the accounts.
    It appears that in this setup the element selection for the dimension in the titles must be "retrieved" via a cube, such as Mp_Std_Company. A good alternative can be to use the }ElementAttributes control cube for said dimension.

    This is based on the information in this topic.

  • Above we see a typical mapping cube. In practice we need to find the source elements that are mapped to a certain target:
    {TM1Sort( Filter( TM1FilterByLevel( TM1SubsetAll( [St_Account] ), 0), Instr([Mp_Std_Account].([Mp_Std_Account_Msr].[Reporting Account]), "710000" ) = 1 ), Asc )}

    Here we generate a list of source accounts that are mapped to account 710000. Combine it with the OR function for greater flexibility.

  • Filtering with wildcards

  • Filtering using wildcards:
    {TM1FilterByPattern( TM1FilterByLevel( TM1SubsetAll( [FIN_Account] ), 0), "613?2*")}

  • Filtering using AND and OR:
    { Filter( TM1SubsetAll( [FIN_Account] ), ( ([FIN_Account].[Type] = "PL" AND [FIN_Account].[Conversion] = 1) OR ([FIN_Account].[Description] <> "" ) ) ) }

  • More on sorting

  • Sorting the hierarchy following its consolidation structures:
    { Hierarchize( TM1SubsetAll( [CostCenter] ))}

  • Sorting the elements by index (all elements have an index):
    {TM1SortByIndex( TM1SubsetAll( [PL_Account] ), Asc)}

    {Order( TM1SubsetAll( [PL_Account] ), [PL_Account].CurrentMember.Properties("Index"), Asc)}
    Submitted by George Tonkin, thanks.

  • Months 'smaller' (earlier) than a threshold (variable) month (for example, copy Actuals to Budget scenario) (element names like 202009):
    {TM1Sort( Filter( TM1FilterByLevel( TM1SubsetAll( [Time] ), 0), [Time].CurrentMember.Name <= "' | pLastMonthOfActuals | '" ), Asc)}
    or (Code is an alias on the Time dimension):
    {TM1Sort( Filter( TM1FilterByLevel( TM1SubsetAll( [Time] ), 0), [Time].[Code] <= "' | pLastMonthOfActuals | '" ), Asc)}
    or (selecting a range of months, possibly spanning multiple years):
    {TM1Sort( TM1FilterByLevel( [Time].[202001]:[Time].[202009], 0), Asc)}

  • Selecting Products that have revenue but where an attribute is missing (entered by users or loaded from a source system):
    {TM1Sort( Filter( Filter( TM1FilterByLevel( TM1SubsetAll( [Product] ), 0), [Rpt_Sales].([Year_Month].[Total 2021], [Region].[Total Region], [Sales_Msr].[Revenue]) <> 0), [Product].[Sales_Rep] = "" ), Asc )}

    Note that this kind of dynamic subset is an ideal candidate to present in a view to users - one can see immediately for which products the attribute should be completed.

  • Generate

  • Generate can be tricky but very useful. It allows to apply an operation on every element in a first selection/Set. Like in: select all level 1 customers (consolidated customers) and for each of them, find the customer with the highest revenue:
    {Generate( {TM1FilterByLevel( {TM1SubsetAll( [Customers] )}, 1)}, TopCount( Descendants([Customers].CurrentMember, 1), 1, Act_Sales.([Act_Sales_Msr].[Revenue])))}

    Please note that with Generate, the second clause is the operative dimension in which you create the subset.

  • A second example is provided below. You have a typical month structure but the fiscal year starts in April while the calendar year starts in January (obviously there are multiple ways to tackle this and this is not necessarily the best method):
    Generate( Order([Month_YTD_FY_Offset].[Total Year].Children, [Month_YTD_FY_Offset].[FY order], Asc), Descendants( {StrToMember("[Month_YTD_FY_Offset].[" + [Month_YTD_FY_Offset].CurrentMember.Name + "]")}))

    A numeric attribute is used to capture the order of the months in the fiscal year. The output is summarized for clarity, not all days are shown.


  • A third example if you like. List all managers of employees in a subset. Manager names are part of the same dimension and they are entered against the employee name in a 3D-cube called "Employee Planning":
    {Generate( Filter( TM1SubsetAll( [Employee] ), [Employee Planning].([Scenario].[RP 2021], [Employee Planning Measures].[Manager ID]) <> ""), {StrToMember("[" + [Employee Planning].([Scenario].[RP 2021], [Employee].CurrentMember, [Employee Planning Measures].[Manager ID]) + "]")})}

  • To return all elements in a dimension (dim_1) that also exist in a second dimension (dim_2):
    Generate( TM1SubsetAll([Dim_1]), Filter( TM1SubsetAll([Dim_2]), [Dim_2].CurrentMember.Name = [Dim_1].CurrentMember.Name))

    In this example, the subset will be created in the dimension Dim_2. Adapted from here.

  • We can use this function to compare elements in dimensions. For clean up purposes, we would like to identify all elements that are not part of a second dimension. Real use case: a dimension containing balance sheet accounts got polluted and needs to be validated against a dimension containing all accounts (all correct accounts).
    Except( TM1FilterByLevel(TM1SubsetAll([Balance Sheet Account]),0), Generate( TM1FilterByLevel(TM1SubsetAll([_S-General Ledger Account]),0), Filter( TM1FilterByLevel(TM1SubsetAll([Balance Sheet Account]),0), [Balance Sheet Account].CurrentMember.Name = [_S-General Ledger Account].CurrentMember.Name)))

    Yes, that's correct, it's lengthy. And a Dimix function in a TI process could do the same. But still useful as MDX serves different purposes. Submitted by George Tonkin, thanks for that.

  • Last example: you have a dimension ('A') with elements. These elements can occur as an alias (called Alt_Name) in a second dimension ('B'). You would like to create a subset containing all dimension A elements that occur in dimension B:
    Generate( Filter( TM1SubsetAll( [B] ), Len([B].[Alt_Name]) > 0), {StrToMember("[A].[" + [B].[Alt_Name] + "]")})

    Caveat: if the Alt_Name alias is filled in in dimension B, that name has to exist as an element in dimension A ! If it does not, TM1 will throw up an error in the best case, some TM1 versions will even experience a crash ! Thank you Ty Cardin

  • To list all security groups that 1 or more users are part of:
    Hierarchize( Generate( TM1SubsetToSet([}Clients], "users" ), Filter( TM1SubsetAll([}Groups]), [}ClientGroups].([}Clients].CurrentMember) <> "" )))

    "users" is a (public) subset containing the users you would like to evaluate. Most probably this subset will only contain 1 element but you are free to have multiple users. The result of the MDX expression, group names, can be copy/pasted elsewhere - for example in other security cubes.
    You can also have other logic instead of SubsetToSet, adjust that part of the expression to suit.

    With a Turbo Integrator process to automate the task:
    m = 'Test users';
    n = Expand( 'Groups linked to subset %m%' );
    If( SubsetExists( '}Clients', m ) = 0 );
       SubsetCreate(  '}Clients', m );
    If( SubsetExists( '}Groups', n ) = 0 );
       SubsetCreateByMDX( n, Expand( 'Hierarchize( Generate( TM1SubsetToSet([}Clients], "%m%" ), Filter( TM1SubsetAll([}Groups]), [}ClientGroups].([}Clients].CurrentMember) <> "" )))'), '}Groups' );

    Below you will find similar code.

  • Subsets based on other objects

  • To quickly populate a subset in TI with elements from another subset, use TM1SubsetToSet:
    SubsetCreateByMDX( 'Target subset', 'TM1SubsetToSet( [Account], "Source subset" )' );

    The source subset will probably be dynamic but that's not a requirement. After the previous line to create the subset, you can still add or remove elements in the target subset such that, as an added benefit, the target subset becomes static.

    [Continue reading on this in the next paragraph.]

    Or retrieve all elements of that dimension:
    SubsetCreateByMDX( 'Target subset', 'TM1SubsetAll( [Account] )' );

    Or all elements in a dimension Account2 that also exist in the dimension Account:
    SubsetCreateByMDX( 'Target subset', 'Generate( TM1SubsetAll([Account]), Filter( TM1SubsetAll([Account2]), [Account].CurrentMember.Name = [Account2].CurrentMember.Name))' );

    Lastly, all elements in a dimension Account2 that also exist in the subset 'Filter by level 0' in the dimension Account:
    SubsetCreateByMDX( 'Target subset', 'Generate( TM1SubsetToSet([Account], "Filter by level 0"), Filter( TM1SubsetAll([Account2]), [Account].CurrentMember.Name = [Account2].CurrentMember.Name))' );

    The latter expression is a winner, it saves you from writing a loop over subset elements.
  • Distinct

  • Generating a list of distinct element names:
    { Distinct( TM1SubsetAll( [PL_Account] ))}

    or based on an existing subset:
    { Distinct( TM1SubsetToSet( [PL_Account], "YOUR_SUBSET_NAME" ))}
  • Orphans and top consolidations

  • Orphans in the dimension (definition: level 0 elements without parent consolidation), but ONLY n type elements:
    {Filter( TM1FilterByLevel( TM1SubsetAll([Accounts]), 0), [Accounts].CurrentMember.Parent.Name = "" )}

    {Filter( TM1FilterByLevel( TM1SubsetAll([Accounts]), 0), Count([Accounts].CurrentMember.Ancestors) = 0)}

    {Filter( Filter( TM1SubsetAll([Accounts]), [Accounts].CurrentMember.Parent.Name=""), ISLEAF( [Accounts].CurrentMember ))}

  • Orphans in the dimension (definition: level 0 elements without parent consolidation), both n elements and consolidations:
    {Filter( TM1SubsetAll([Accounts]), [Accounts].CurrentMember.Parent.Name = "" )}

  • Top level consolidations or the root elements
    In Turbo Integrator, have a look at the DimensionTopElementName function too. {Except( Filter( TM1SubsetAll( [Accounts] ), [Accounts].CurrentMember.Parent.Name = "" ), {TM1FilterByLevel( TM1SubsetAll( [Accounts] ), 0 )} )}
    Wrapping this statement in an TM1SortByIndex function can be useful.

    Alternative shorter expression:

    Here I used the levels of a dimension/hierarchy, as discussed above.
    This works too:
    [Accounts].[level000].Members or even [Accounts].[Top accounts].Members if you gave that specific level000 a clear name (which is good practice in TM1 by the way !). Please refer to the IBM site

    Should a subset exist with the same name as a named level, then here the named level takes precedence.

  • Top level consolidations containing a certain element (account 600000):
    Filter( Filter( TM1SubsetAll([Accounts]), [Accounts].CurrentMember.Parent.Name = ''), TM1TupleSize(Intersect({[Accounts].[600000]}, Descendants({[Accounts].CurrentMember})).Item(0)) > 0 )

    This expression features the function TM1TupleSize to return the number of members in a tuple.

  • Consolidations without children:
    {Filter( TM1FilterByLevel( TM1SubsetAll([Accounts]), 1), ISLEAF( [Accounts].CurrentMember ))}

    {Filter( TM1FilterByLevel( TM1SubsetAll([Accounts]), 1), [Accounts].CurrentMember.FirstChild.Name = "")}

    {Filter( TM1FilterByLevel( TM1SubsetAll([Accounts]), 1), Count([Accounts].CurrentMember.Children) = 0)}

    {Filter( TM1FilterByLevel( TM1SubsetAll([Accounts]), 1), TM1TupleSize( ([Accounts].CurrentMember.Children).Item(0)) = 0)}

    With the TM1 REST API:
    /api/v1/Dimensions('Accounts')/Hierarchies('Accounts')/Members?$select=Name&$expand=Element($select=Type),Children/$count&$filter=Element/Type eq 3 and Children/$count eq 0

  • TM1 users and groups

  • The current TM1 username in the }Clients dimension (call this subset whoami):
    {StrToMember("[}Clients].[" + USERNAME + "]")}

    {TM1FilterByPattern( TM1SubsetAll( [}Clients] ), USERNAME )}

  • Using this notion of a username we can also use for the user (given a subset whoami as defined above):

    TM1SubsetToSet([}Clients], "whoami")


  • The list of security groups to which a client (vUser) has been assigned:
    Filter( [}Groups].Members, [}ClientGroups].( [}Clients].[' | vUser | '] ) <> "" )
    Note that the resulting subset could be empty.

  • The list of security groups to which the logged in user has been assigned:
    Filter( [}Groups].Members, [}ClientGroups].( StrToMember("[}Clients].[" + USERNAME + "]")) <> "" )
    Note that the resulting subset could be empty.

  • Let me take this (a whole lot) further with a TI process to create a custom view with dynamic subsets in the }ClientGroups cube:
    # Make sure the }ClientPropertiesSyncInterval=5 is present in the TM1s.cfg file
    c0 = 'Active clients security';
    vCube = '}ClientGroups';
    vDim_1 = '}Clients';
    vDim_2 = '}Groups';
    ## start from scratch
    If( ViewExists( vCube, c0 ) > 0 );
       ViewDestroy( vCube, c0 );
       SubsetDestroy( vDim_1, c0 );
       SubsetDestroy( vDim_2, c0 );
    # create subsets in the relevant dimensions
    SubsetCreateByMDX( c0, Expand( 'TM1Sort( Filter( TM1SubsetAll( [%vDim_1%] ), [}ClientProperties].([}ClientProperties].[STATUS]) = "ACTIVE" ), Asc )' ), vDim_1 );
    SubsetCreateByMDX( c0, Expand( 'TM1Sort( Hierarchize( Generate( Filter( TM1SubsetAll( [%vDim_1%] ), [}ClientProperties].([}ClientProperties].[STATUS]) = "ACTIVE" ), Filter( TM1SubsetAll( [%vDim_2%] ), [%vCube%].([%vDim_1%].CurrentMember) <> "" ))), Asc )' ), vDim_2 );
    # apply an Alias
    If( Dtype( '}ElementAttributes_}Clients', '}TM1_DefaultDisplayValue' ) @= 'AA' );
       SubsetAliasSet( vDim_1, c0, '}TM1_DefaultDisplayValue' );
    If( Dtype( '}ElementAttributes_}Groups', '}TM1_DefaultDisplayValue' ) @= 'AA' );
       SubsetAliasSet( vDim_2, c0, '}TM1_DefaultDisplayValue' );
    # create a view
    ViewCreate( vCube, c0 );
    # add subsets to the view
    ViewSubsetAssign( vCube, c0, vDim_1, c0 );
    ViewSubsetAssign( vCube, c0, vDim_2, c0 );
    # the view layout
    ViewRowDimensionSet(  vCube, c0, vDim_1, 1 );
    ViewRowDimensionSet(  vCube, c0, vDim_2, 2 );
    ViewSuppressZeroesSet( vCube, c0, 1 );
  • The list of users that are ACTIVE (logged on, at least) in TM1:
    TM1Sort( Filter( TM1SubsetAll( [}Clients] ), [}ClientProperties].([}ClientProperties].[STATUS]) = 'ACTIVE' ), Asc )

    Precondition is that in the TM1s.cfg file, we add a property: ClientPropertiesSyncInterval=5 (a dynamic property, no need to restart TM1)

  • TM1 stores the output in the }ClientProperties cube in a measure called "STATUS"
    5 is the number of seconds at which the list of active users is refreshed.
    To disable it, use 0 or leave out the property.
  • Combine the previous MDX to end up with a view in the }ClientGroups cube containing the groups of the active users:
    Filter( [}Groups].Members, [}ClientGroups].( StrToMember("[}Clients].[" + USERNAME + "]")) <> "" )
    Note that the resulting subset could be empty.

  • Application and control objects

  • All application cubes in the TM1 model:
    TM1Sort( Except( TM1SubsetAll( [}Cubes] ), TM1FilterByPattern( TM1SubsetAll( [}Cubes] ), "}*" )), Asc)
    Likewise for processes, dimensions, … TM1Sort( Except( TM1SubsetAll( [}Cubes] ), TM1FilterByPattern( TM1SubsetAll( [}Dimensions] ), "}*" ) ), Asc)

  • All control cubes in the TM1 model:
    TM1Sort( {M1FilterByPattern( TM1SubsetAll( [}Cubes] ), "}*" ), Asc)

    Likewise for processes, dimensions, …

When you look at the Microsoft links I mentioned above, you will find a lot more functions. Including functions for information on the dimension structure: FirstSibling, Parent, Children, FirstChild / LastChild, Ancestors, and so on. Many other options for creating dynamic subsets are explained on that webpage. It is a must read!

Temporary subsets

Needless to say, but these subset definitions using MDX syntax can be used in the function SubsetCreatebyMDX. That function can also create temporary subsets as of TM1 10.2.2 FP4 (see here).


A few caveats apply. If you create a dynamic subset in TI (the function is called SubsetCreatebyMDX('NAME OF THE SUBSET', 'MDX EXPRESSION'), make sure you always have at least 1 element that matches. If not, the process errors out. Unless you use TM1 10.1 or later, where an undocumented 3rd parameter to the function equal to the dimension name, will avoid the error in case no elements satisfy the expression. As an alternative, in that case you should loop over the dimension elements and insert the matching elements in a static subset SubsetCreate('DIMENSION NAME', 'SUBSET NAME') and SubsetElementInsert('DIMENSION NAME', 'SUBSET NAME', 'ELEMENT NAME', POSITION). The latter technique will always work (unless you are stuck in an endless loop like me on some Monday mornings), whichever you will use depends on your liking and coding skills.

Please do NOT use the function TM1SubsetBasis() (unless you know what you are doing). Even though the MDX expression recorder generates such code, it is in fact not to be used. It refers to a relative selection of elements ('at the time of using the function') rather than an absolute selection of elements ('it will always be the same selection/result, all else being equal').


In Excel spreadsheets, we can work with MDX queries to return a subset: for example in active forms. There, you could store the MDX query in a (hidden) cell. The query expression could be chopped up to include cell references. For instance, the user can choose a Product family in cell H10, and the active form returns the data on the lowest-level children of that Product family. Mind that there could be a 256 characters limit for the MDX expression for older versions of TM1. You COULD leave out the dimension names BUT then the element names must be unique throughout the ENTIRE TM1 model (over all dimensions).


Section contents

About Wim

Wim Gielis is a Business Intelligence consultant and Excel expert

Other links