UPDATE 2020-11-10: You can find more complete detailed and optimized examples (with both Power Pivot and Power BI files) for semi-additive calculations in the DAX Patterns: Semi-additive calculations article+video on daxpatterns.com.

UPDATE 2014-02-03: fixed errors in ClosingNonBlank functions thanks to Franj Tonsen’s comment.

A semi-additive measure does not aggregate data over all attributes like a regular additive measure would. For example, measures like balance account and product inventory units can be aggregated over any attribute but time. Instead of considering the time period selected (one year, one month …) you only consider a particular moment in time related to the time period selected. It could be the first day, the last day, the last day that had transactions, and so on.

This condition is typical for tables containing snapshots over time such as products inventory or accounts balances. In the following table, you can see an excerpt of a Product Inventory table. The same product has a Units Balance value for each date. Yet you cannot sum this column for two different dates, for example to calculate an average over different dates. If you want to calculate the value of Units Balance for July 2005, you need to filter the row for the last day of that month, ignoring rows for all the other days. However, you have to use a regular aggregation for other measures such as Units In and Units Out, which are regular additive measures.

Product Name Date Key Units In Units Out Units Balance
Road-650 Red, 44 20050630 0 0 170
Road-650 Red, 44 20050701 0 103 67
Road-650 Red, 44 20050702 102 0 169
Road-650 Red, 62 20050630 0 0 185
Road-650 Red, 62 20050701 0 135 50
Road-650 Red, 62 20050702 129 0 179

In order to implement a semi-additive measure in DAX, you use a technique that is similar to the one used to compute aggregations and comparisons over time. You change the filter over the date in a CALCULATE statement, but in this case you limit the range of selected dates instead of extending it (like year-to-date) or moving it (like prior year).

First and Last Date

You can use LASTDATE to get the last day active in the current filter context, for a particular date column passed as an argument. The result is a table of one column and one value that you can use as a filter argument in a CALCULATE statement, as in the following definition:

Units LastDate := CALCULATE ( SUM ( Inventory[UnitsBalance] ), LASTDATE ( 'Date'[Date] ) )

The result shows that the total for each quarter corresponds to the value of the last month in the quarter. For example, the Q1 value is the same as March. Each month value corresponds to the value of the last day in that month, not represented here.

FIG_02_08

First and Last Non Blank

The Units LastDate calculation assumes that there are data for every year in every month. If the inventory is daily, this is not an issue. However, it could become a problem if the inventory is written only for working days: if a month were to end on a Saturday, you would see the entire month. The problem is evident for future dates. In the following picture you see what happens when using Units LastDate with an Inventory table whose rows only go until December 15, 2007: you do not see the total for year 2007, for Q4, and for December!

FIG_02_09

The reason is that the LASTDATE formula operates on dates available in the filter context; also, the Date table contains all the days for the year 2007. This is a best practice, otherwise other Time Intelligence functions would not work correctly. You can use another DAX function, LASTNONBLANK, which returns the last date that satisfies a non-blank condition for an expression passed as second argument.

Units LastNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        LASTNONBLANK ( 
            'Date'[Date],
            CALCULATE ( SUM ( Inventory[UnitsBalance] ) )
        )
    )

It is important that the second argument of the LASTNONBLANK apply the context transition using an implicit or explicit CALCULATE. Otherwise, you would apply the expression without filtering by each date in the time period, and the result would be identical to LASTDATE. You can see the result in the following picture, where December, Q4 and the total for 2007 are all displayed.

FIG_02_10

If you need the first date of a time period instead of the last date, you can use FIRSTDATE. You also have FIRSTNONBLANK to get the first date with any data, similar to what you do with LASTNONBLANK for the last date with any data. All these functions return a table of one column and one row: for this reason, you can use them in a filter argument of a CALCULATE call. A common mistake is assuming that LASTDATE and MAX would produce the same result. While this is true from a logical point of view, there is an important syntactic difference. You cannot write the following expression:

Units MaxDate :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ),
        MAX ( 'Date'[Date] )
    )

The MAX function returns a scalar value, and the filter argument of a CALCULATE function requires a table expression or a logical condition referencing only one column. Thus, you use MAX instead of LASTDATE as in the following definition:

Units MaxDate :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ),
        FILTER ( 
            ALL( 'Date'[Date] ), 
            'Date'[Date] = MAX ( 'Date'[Date] ) 
        ) 
    )

You can also simplify the syntax by storing the MAX value in a variable, so you can use the shorter predicate condition in the CALCULATE argument.

Units MaxDate :=
    VAR LastDayInSelection = MAX ( 'Date'[Date] ) 
    RETURN CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ),
        'Date'[Date] = LastDayInSelection 
    )

The best practice is using LASTDATE when you write a filter expression, whereas MAX is better when you are writing a logical expression in a row context. This is because LASTDATE implies a context transition that hides the external filter context.

Opening and Closing Time Periods

Other Time Intelligence functions are useful in semi-additive measures, for getting the first and last date of a period (year, quarter, or month). These are helpful whenever you need to extract a value of a selection that is smaller than the entire period considered. For example, looking at the month level which may be displayed in rows, you might want to also display the quarter end and year end values in that same row. You can see this in the following picture.

FIG_02_11

The definitions of the ClosingMonth, ClosingQuarter, ClosingYear, OpeningMonth, OpeningQuarter and OpeningYear measures used in the previous pivot table are the following:

ClosingMonth := CLOSINGBALANCEMONTH ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
ClosingQuarter := CLOSINGBALANCEQUARTER ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
ClosingYear := CLOSINGBALANCEYEAR ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
OpeningMonth := OPENINGBALANCEMONTH ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
OpeningQuarter := OPENINGBALANCEQUARTER ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
OpeningYear := OPENINGBALANCEYEAR ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )

The previous measures correspond to the following ones defined using CALCULATE and using the filter provided by the ENDOFMONTH, ENDOFQUARTER, ENDOFYEAR, STARTOFMONTH, STARTOFQUARTER and STARTOFYEAR functions, respectively:

ClosingEOM := CALCULATE ( SUM ( Inventory[UnitsBalance] ), ENDOFMONTH ( 'Date'[Date] ) )
ClosingEOQ := CALCULATE ( SUM ( Inventory[UnitsBalance] ), ENDOFQUARTER ( 'Date'[Date] ) )
ClosingEOY := CALCULATE ( SUM ( Inventory[UnitsBalance] ), ENDOFYEAR ( 'Date'[Date] ) )

StartingSOM :=
CALCULATE ( 
    SUM ( Inventory[UnitsBalance] ), 
    DATEADD ( STARTOFMONTH ( 'Date'[Date] ), -1, DAY )
)

StartingSOQ := 
CALCULATE ( 
    SUM ( Inventory[UnitsBalance] ), 
    DATEADD ( STARTOFQUARTER ( 'Date'[Date] ), -1, DAY )
)

StartingSOY := 
CALCULATE ( 
    SUM ( Inventory[UnitsBalance] ), 
    DATEADD ( STARTOFYEAR ( 'Date'[Date] ), -1, DAY )
)

No functions for opening and closing periods consider the non-blank condition. You can see the behavior of the previous closing measures for the year 2007, where data is only available until December 15, in the following picture.

FIG_02_12

Instead of the OPENING/CLOSING functions, you should use the LASTNONBLANK function as filter in a CALCULATE statement, applying an extension of the time period considered using the PARALLELPERIOD function. Here are the corresponding definitions:

OpeningMonthNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], -1, MONTH ) 
        ) 
    )

OpeningQuarterNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], -1, QUARTER ) 
        ) 
    )

OpeningYearNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], -1, YEAR ) 
        ) 
    )

ClosingMonthNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], 0, MONTH ) 
        ) 
    )

ClosingQuarterNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], 0, QUARTER ) 
        ) 
    )

ClosingYearNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], 0, YEAR ) 
        ) 
    )

The following is the final result using these measures for the year 2007.

FIG_02_13

The filter calculation might be different according to the logic you want to implement, but the pattern for a semi-additive measure is to filter a single date based on the initial selection of dates in the filter context. This logic is usually in a filter argument of a CALCULATE function call – unless a special time intelligence function is used, hiding the internal calculation that is always applied on a CALCULATE statement.

You can hear me explain a few aspects of semi-additive measures in the DAX Time Intelligence video.

CALCULATE
Context transition

Evaluates an expression in a context modified by filters.

CALCULATE ( <Expression> [, <Filter> [, <Filter> [, … ] ] ] )

LASTDATE
Context transition

Returns last non blank date.

LASTDATE ( <Dates> )

LASTNONBLANK
Context transition

Returns the last value in the column for which the expression has a non blank value.

LASTNONBLANK ( <ColumnName>, <Expression> )

FIRSTDATE
Context transition

Returns first non blank date.

FIRSTDATE ( <Dates> )

FIRSTNONBLANK
Context transition

Returns the first value in the column for which the expression has a non blank value.

FIRSTNONBLANK ( <ColumnName>, <Expression> )

MAX

Returns the largest value in a column, or the larger value between two scalar expressions. Ignores logical values. Strings are compared according to alphabetical order.

MAX ( <ColumnNameOrScalar1> [, <Scalar2>] )

ENDOFMONTH
Context transition

Returns the end of month.

ENDOFMONTH ( <Dates> )

ENDOFQUARTER
Context transition

Returns the end of quarter.

ENDOFQUARTER ( <Dates> )

ENDOFYEAR
Context transition

Returns the end of year.

ENDOFYEAR ( <Dates> [, <YearEndDate>] )

STARTOFMONTH
Context transition

Returns the start of month.

STARTOFMONTH ( <Dates> )

STARTOFQUARTER
Context transition

Returns the start of quarter.

STARTOFQUARTER ( <Dates> )

STARTOFYEAR
Context transition

Returns the start of year.

STARTOFYEAR ( <Dates> [, <YearEndDate>] )

PARALLELPERIOD
Context transition

Returns a parallel period of dates by the given set of dates and a specified interval.

PARALLELPERIOD ( <Dates>, <NumberOfIntervals>, <Interval> )