Defining Calculated Measures

Objective

After completing this lesson, you will be able to define a Calculated Measure

Simple Calculated Measures

Calculated measures are processed in the following way. At first the operands used in the formula, which are measures from the cube or restricted measures, are aggregated. Then, the formula is applied on the aggregated values. It is important to be aware of this behavior because SQL does not follow this approach. In SQL, calculations are performed at the individual row-level.

The annotation @Aggregation.default: #FORMULA is used to indicate a formula.

Here is an example of a code snippet that presents two simple calculated measures: Free Seats and Occupation Rate.

Code Snippet
12345678910
@AnalyticsDetails.query.axis: #COLUMNS MaximumSeats, @AnalyticsDetails.query.axis: #COLUMNS OccupiedSeats, @Aggregation.default: #FORMULA @EndUserText.label: 'Free Seats' $projection.maximumseats - $projection.occupiedseats as FreeSeats, @Aggregation.default: #FORMULA @EndUserText.label: 'Occupation Rate' $projection.occupiedseats / $projection.maximumseats as OccupationRate

Below, we see the result of the two calculated measures.

The query results showing two calculated measures: free seats and occupation rate

In the row AA/Totals we see the value 4,520 for the calculated measure Free Seats which is maximum seats minus occupied seats.

The Occupation Rate is calculated as 0,7188180. This is the result of the calculated measure 11,555 / 16,075. The value 11,555 is the aggregated number of occupied seats and 16,075 is the number of maximum seats.

Currencies and Units of Measurement

Measures such as amount sold are meaningless unless they are associated with a currency or a unit of measure.

Let's look at how we define calculated measures that require references to units or currencies:

Code Snippet
1234567891011121314151617181920
CurrencyCode, DistanceUnit, // Measures @Semantics.amount.currencyCode: 'CurrencyCode' @AnalyticsDetails.query.axis: #COLUMNS curr_to_decfloat_amount(SalesAmount) as SalesAmount, @Semantics.quantity.unitOfMeasure: 'DistanceUnit' @AnalyticsDetails.query.axis: #COLUMNS Distance, virtual CalcUnitPrice: dd_cds_calculated_unit, @AnalyticsDetails.query.axis: #COLUMNS @Aggregation.default: #FORMULA @EndUserText.label: 'Price' @Semantics.quantity.unitOfMeasure: 'CalcUnitPrice' $projection.SalesAmount / $projection.distance as Price

In our example, the field SalesAmount is of type CURR and data is stored with 2 digits for all currencies. The currency shift is modeled with the function curr_to_decfloat_amount (SalesAmount). This function was explained in a previous lesson.

The annotation @Semantics.quantity.unitOfMeasure is not necessary for the field Distance. That annotation is inherited from the cube view. But reference field DistanceUnit is required in the analytical query view.

The unit of the formula for the field Price is derived by the analytical engine. Therefore, the reference @Semantics.quantity.unitOfMeasure is needed, but the referenced field can be virtual using the data element DD_CDS_CALCULATED_UNIT to indicate that it is a complex unit.

Notice that the field $projection.SalesAmount in the formula for Price references the field SalesAmount in the analytical query definition. It is of type DECFLOAT. Fields of type CURR are not allowed in this formula. The other operand $projection.distance could be replaced by the field distance from the cube view.

Let's look at the results when using these calculated measures against the dimension AirlineID on the rows axis:

Results of a query that shows measures with individual currencies

Notice the SalesAmount now displays with a currency read from the field CurrencyCode. If different currencies are aggregated, the result doesn't make sense. In this case, an * is displayed.

Also, notice the formula Price is calculated with the unit and currency references taken into account.

Implementing Currency Conversions

Sometimes your measures are in different currencies or units of measurement, as in our example above. Let's replace the definition of SalesAmount with this new code:

Code Snippet
12345678910
virtual CurrEUR: abap.cuky, @Aggregation.default: #FORMULA @AnalyticsDetails.query.axis: #COLUMNS @Semantics.amount.currencyCode: 'CurrEUR' currency_conversion( amount => curr_to_decfloat_amount(SalesAmount), source_currency => CurrencyCode, target_currency => abap.cuky'EUR', exchange_rate_date => abap.dats'20241104', exchange_rate_type => 'M' ) as SalesAmount,

Notice we use the function currency_conversion that takes the input measure and the currency that is bound to that measure and coverts it to the fixed target currency EUR. Also notice the date of the currency exchange rate and the rate type are fixed. Often, these settings are parameterized so that the business user can provide these values at run-time. In particular, the date of the exchange rate could be provided by the session parameter $session.system_date to give you the current system date, or you could use$session.user_date which is the date of the user in their time-zone.

We now see the results below that show the SalesAmount in the currency of EUR.

Now we have converted SalesAmount for all airlines to a single currency, it is possible to calculate the total. Also notice we now have Price in EUR/KM.

Note

For unit of measurement conversions there is the function unit_conversion available.

Percentage Calculations

Let's look at how we define a formula that calculates a percentage value:

Code Snippet
123456789101112131415161718192021
// Measures virtual CurrencySalesAmountCurrent : abap.cuky, @AnalyticsDetails.query.axis: #COLUMNS @Semantics.amount.currencyCode: 'CurrencySalesAmountCurrent' @EndUserText.label: 'Sales Jul 2024' case when FlightYearMonth = '202407' then curr_to_decfloat_amount(SalesAmount) end as SalesAmountCurrent, virtual CurrencySalesAmountPrev : abap.cuky, @AnalyticsDetails.query.axis: #COLUMNS @Semantics.amount.currencyCode: 'CurrencySalesAmountPrev' @EndUserText.label: 'Sales Jun 2024' case when FlightYearMonth = '202406' then curr_to_decfloat_amount(SalesAmount) end as SalesAmountPrev, abap.unit'%' as Percent, @Aggregation.default: #FORMULA @AnalyticsDetails.query.axis: #COLUMNS @EndUserText.label: '% Deviation' @Semantics.quantity.unitOfMeasure: 'Percent' deviation_ratio( portion => $projection.SalesAmountCurrent, total => $projection.SalesAmountPrev ) *100 as deviation

You define abap.unit'%' as Percent and reference the formula to this unit. This is only possible if the result of the formula is a plain figure. The result of the deviation ratio is a figure without a reference to unit.

Then we multiply by 100 so that the result is between 1 and 100.

Query results showing a percentage calculation

Boolean Operators

By implementing boolean logic, we can define a condition where the result is either true or false. We can then perform an action based on whether the result is true or false. This is a useful technique used in formulas.

For boolean operations, the CASE - WHEN - THEN statement is used. Only measures, restricted measures or calculated measures are allowed in this statement.

Let's look at the code that defines a boolean operation:

Code Snippet
123456789101112131415161718
@AnalyticsDetails.query.axis: #COLUMNS MaximumSeats, @AnalyticsDetails.query.axis: #COLUMNS OccupiedSeats, abap.unit'%' as Percent, @Aggregation.default: #FORMULA @AnalyticsDetails.query.axis: #COLUMNS @EndUserText.label: 'Occupation Rate' @Semantics.quantity.unitOfMeasure: 'Percent' ratio_of( portion => OccupiedSeats, total => MaximumSeats ) *100 as OccupationRate, @Aggregation.default: #FORMULA @EndUserText.label: 'Number of poor utilized Flights' @AnalyticsDetails.query.axis: #COLUMNS case when $projection.OccupationRate <= abap.decfloat34'66' then NumberOfFlights else 0 end as NumberOfPoorUtilizedFlights, NumberOfFlights

For our analytical query, we have defined five measures. Pay special attention to the measure Number of poor utilized Flights which is based on a boolean operation. Here we count the number of flights where the Occupation Rate is less than or equal to 66% which is the threshold that we have chosen to define a poorly utilized flight.

Query results showing the a measure Number of Poor Utilized Flight that is based on a boolean formula

Notice that airline AZ has two flight numbers, AZ/788 andAZ/789. For each of these flight numbers, there are 14 flights in the result (let's imagine there is one flight per day and our analytical query result is restricted to two weeks). We can see from the column Number of poor utilized Flights that flight number AZ/789 is poorly utilized with 14 flights.

But what does the value 14 represent?

Firstly, remember that, by default, a calculation is always performed on the aggregated result of the dimensions. So in our case, the boolean logic is applied on the aggregated result of the combination of airline and flight. Notice the occupation rate for airline AZ and flight 789 is 63%. This means that this flight falls into the trueside of the boolean condition as it is less that 66% . So in this case, the column Number of poor utilized Flights is defined using the analytical query measure NumberOfFlights, which is 14. The column label Number of poor utilized Flights is misleading because it appears that every one of the 14 flights is poorly utilized. But this is not the case, and in fact only 7 of the 14 flights for AZ/789 are poorly utilized. But how would we discover that? We would need to add a drill-down by flight date so this would become clear.

Watch the video below to learn how we implement boolean logic within a formula.

We saw how the count of poorly utilized flights is executed at the aggregated level of airline / flight number. This is not incorrect, but based on the label of the column Number of Poor Utilized Flights this is misleading. The business user would expect to see the true number of flights and might not know to drill down the flight date to reveal this information. What we should do is adjust the calculation so that it executes at the more granular level of airline / flight / flight date. To do this we must implement an exception aggregation which forces the calculation to execute at a specific level of aggregation, which might not be the level of aggregation that is displayed in the analytical query result. We will cover exception aggregation in the next section.

Exception Aggregation

In the previous section, we learned that the boolean example did not provide the expected result. This was because the evaluation of the boolean expression was executed at the incorrect level of aggregation. To resolve this, we must force the boolean expression to execute at a specific aggregation level.

We must implement exception aggregation in our analytical query definition.

In this section, we first introduce exception aggregation, and then later we improve our earlier example by combining a boolean operation with exception aggregation, so we can finally identify the individual poorly utilized flights.

Example of exception aggregation

Before we review the code required to define the boolean formula using exception aggregation, let's first learn about exception aggregation using a simple example.

Code Snippet
12345
@Aggregation.default: #FORMULA @EndUserText.label: '# Planetypes' @AnalyticsDetails.exceptionAggregationSteps: [{ exceptionAggregationBehavior: #SUM, exceptionAggregationElements: [ 'planetype' ] }] abap.int4'1' as NumberOfAirplains

Above we see the code required to define exception aggregation. You define a formula and use the annotation @AnalyticsDetails.exceptionAggregationSteps:

There are two key settings:

  • Aggregation behavior (SUM, MIN, MAX etc.)
  • Level of aggregation (which dimensions are part of the aggregation?)

Watch the video to learn how to implement an exception aggregation:

Exception Aggregation using a Boolean Operation

Now that we have learned how to implement an exception aggregation, let's learn how to identify the individual poorly utilized flights using a boolean operation.

A flight is defined as a tuple of AirlineID, ConnectionID, and a FlightDate. If you want to count the number of flights with poor seat utilization, the boolean operation has to be evaluated on the level of AirlineID, ConnectionID and FlightDate. In our query, we have defined the level of aggregation as AirlineID and ConnectionID. FlightDate is not included in the aggregation.

Below we see the code that is added to the boolean example where we already specified NumberOfPoorUtilizedFlights.

Code Snippet
1234567
@Aggregation.default: #FORMULA @EndUserText.label: 'Correct Number of poor utilized Flights' @AnalyticsDetails.query.axis: #COLUMNS @AnalyticsDetails.exceptionAggregationSteps: [{ exceptionAggregationBehavior: #SUM, exceptionAggregationElements: [ 'AirlineID' , 'ConnectionID', 'FlightDate' ] }] $projection.NumberOfPoorUtilizedFlights as CorrectNumber

Watch the video to show the different results for the same formula when specifying an exception aggregation.

Log in to track your progress & complete quizzes