Appearance
Lesson 7 — CDS Aggregation, GROUP BY, HAVING & Code Pushdown
Until now, most CDS views in this course returned one result row for one source row.
For example, /DMO/FLIGHT contains individual flights:
text
Carrier + Connection + Flight Date
↓
one flight rowBut many backend requirements ask different questions:
text
How many flights exist per connection?
How many seats are available in total?
What is the average occupancy count?
What is the smallest or largest value in a group?
Which connections have at least two flights?Those are aggregation questions.
Instead of moving all detailed rows to ABAP and calculating the answer in a loop, the database can perform the grouping and calculations first.
text
Detailed database rows
↓
GROUP BY
↓
COUNT / SUM / AVG / MIN / MAX
↓
HAVING
↓
Small summarized result
↓
ABAP consumerThis lesson completes the CDS foundation before the course moves to your own persistence and RAP.
Lesson information
| Duration | About 1–2 course days |
| Environment | SAP BTP ABAP Trial |
| Development tool | Eclipse with ABAP Development Tools (ADT) |
| Prerequisites | Lessons 1–6 |
| Guided source | /DMO/FLIGHT |
| Aggregate CDS | ZI_FLIGHT_STATS_### |
| HAVING CDS | ZI_BUSY_CONNECTION_### |
| Reader | ZCL_FLIGHT_STATS_READER_### |
| Runner | ZCL_CDS_L07_### |
| Practice source | /DMO/BOOKING |
Replace ### with your own student/group suffix.
Learning objectives
After completing this lesson, you should be able to:
- explain what aggregation does;
- use
COUNT,SUM,AVG,MIN, andMAXin CDS; - explain why
AVGrequires an explicit result type in ABAP CDS; - use
GROUP BYcorrectly; - explain why non-aggregated selected fields belong in
GROUP BY; - explain why aliases from the current select list are not used in
GROUP BY; - distinguish
WHEREfromHAVING; - use an aggregate expression in
HAVING; - understand that
COUNT( * )counts rows in each group; - preserve amount/currency semantics when aggregating monetary values;
- explain why values from different currencies must not be blindly summed together;
- explain code pushdown in practical terms;
- consume an aggregated CDS View Entity from ABAP;
- recognize when aggregation belongs in CDS/database processing rather than an ABAP loop.
1. Start by looking at the detail data
Open:
text
/DMO/FLIGHTUse Data Preview.
The table contains fields including:
text
carrier_id
connection_id
flight_date
price
currency_code
plane_type_id
seats_max
seats_occupiedThe key is:
text
carrier_id
+
connection_id
+
flight_dateSo these rows represent individual flights.
2. A detail query answers a row-level question
A normal query could return:
text
Carrier Connection Date Seats Max Occupied
LH 0400 ... ... ...
LH 0400 ... ... ...
LH 0401 ... ... ...Each source flight remains visible separately.
That is useful when the requirement is:
Show me the individual flights.
But suppose the requirement changes to:
Give me one summary row per carrier and connection.
Now we need aggregation.
3. What aggregation does
Aggregation reduces multiple input rows into calculated results.
Example source:
text
LH 0400 100 occupied
LH 0400 120 occupied
LH 0401 80 occupiedGrouped by:
text
CarrierId + ConnectionIdcould become:
text
LH 0400 FlightCount = 2 OccupiedSeats = 220
LH 0401 FlightCount = 1 OccupiedSeats = 80The detailed rows still exist in the database.
The CDS query returns a summary result.
4. The five aggregate functions in this lesson
We will use:
text
COUNT
SUM
AVG
MIN
MAXCOUNT
Counts rows or non-null values.
abap
count( * )means:
Count the rows in the current group.
SUM
Adds numeric values.
abap
sum( Flight.seats_occupied )AVG
Calculates an average.
abap
avg( Flight.seats_occupied as abap.dec( 15, 2 ) )ABAP CDS requires the result type to be specified for AVG.
MIN
Returns the smallest value in the group.
abap
min( Flight.seats_occupied )MAX
Returns the largest value in the group.
abap
max( Flight.seats_occupied )5. Experiment first in SQL Console
Before creating CDS, open the SQL Console and try an aggregated ABAP SQL query.
Use:
abap
SELECT FROM /dmo/flight
FIELDS carrier_id,
connection_id,
COUNT( * ) AS flight_count,
SUM( seats_occupied ) AS occupied_seats
GROUP BY carrier_id,
connection_id
ORDER BY carrier_id,
connection_idThe exact output depends on the current demo data in your system.
The important change is the shape of the result:
text
many flight rows
↓
one row per carrier/connection group6. Why GROUP BY is necessary
Look at this result request:
text
CarrierId
ConnectionId
FlightCountFlightCount is aggregated:
abap
count( * )but:
text
CarrierId
ConnectionIdare normal selected fields.
SQL needs to know which rows belong together before it can calculate the aggregate.
So we say:
abap
group by
Flight.carrier_id,
Flight.connection_id7. The core GROUP BY rule
When a CDS select list mixes:
text
aggregate expressions
+
normal fieldsthe normal selected source fields must be included in GROUP BY.
Conceptually:
text
Selected field
↓
Is it inside an aggregate function?
├─ Yes → do not GROUP BY that aggregate result
└─ No → it belongs in GROUP BYFor our model:
text
CarrierId → GROUP BY
ConnectionId → GROUP BY
FlightCount → aggregate
TotalSeats → aggregate
OccupiedSeats → aggregate
AverageOccupied → aggregate
MinimumOccupied → aggregate
MaximumOccupied → aggregate8. Create the aggregate CDS View Entity
Create a new Data Definition.
Use:
text
Name: ZI_FLIGHT_STATS_###
Description: Flight StatisticsChoose a CDS View Entity template.
9. Define ZI_FLIGHT_STATS_###
Use:
abap
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Flight Statistics'
define view entity ZI_Flight_Stats_###
as select from /dmo/flight as Flight
{
key Flight.carrier_id as CarrierId,
key Flight.connection_id as ConnectionId,
count( * ) as FlightCount,
sum( Flight.seats_max ) as TotalSeats,
sum( Flight.seats_occupied )
as OccupiedSeats,
avg( Flight.seats_occupied as abap.dec( 15, 2 ) )
as AverageOccupiedSeats,
min( Flight.seats_occupied )
as MinimumOccupiedSeats,
max( Flight.seats_occupied )
as MaximumOccupiedSeats
}
group by
Flight.carrier_id,
Flight.connection_idReplace ###.
10. Read the view from top to bottom
The source is:
abap
as select from /dmo/flight as FlightThe grouping dimensions are:
text
CarrierId
ConnectionIdThe measures/calculated summaries are:
text
FlightCount
TotalSeats
OccupiedSeats
AverageOccupiedSeats
MinimumOccupiedSeats
MaximumOccupiedSeatsThe output grain is now:
one row per CarrierId + ConnectionId
That phrase — output grain — is useful when reasoning about grouped queries.
11. Why FlightDate disappeared
The source table key includes:
text
FlightDatebut the aggregate view does not expose it.
That is intentional.
If you grouped by:
text
CarrierId
ConnectionId
FlightDateyou would move back toward one group per individual flight.
The requirement is instead:
summarize all flights belonging to the same carrier/connection.
So FlightDate is not part of the grouping dimensions.
12. CDS keys after aggregation
The view declares:
abap
key CarrierId
key ConnectionIdBecause those are exactly the grouping dimensions, the result has at most one row for each carrier/connection combination.
Remember the rule from Lesson 2:
A CDS key expresses the semantic identity of the CDS result. It does not create or change a database-table primary key.
13. Why GROUP BY uses source-field names
The select list defines aliases:
abap
Flight.carrier_id as CarrierId
Flight.connection_id as ConnectionIdBut the GROUP BY uses:
abap
Flight.carrier_id,
Flight.connection_idnot:
text
CarrierId
ConnectionIdFor the current CDS View Entity, the aliases created in the select list are not used as replacements in the same view's GROUP BY clause.
Use the source fields.
14. ADT can help generate GROUP BY
A useful ADT workflow is:
- write the element list containing aggregates;
- run syntax check;
- place the cursor where ADT reports the missing grouping;
- press:
text
Ctrl + 1- if offered, choose the Quick Fix to add the
GROUP BYclause; - inspect the generated fields rather than accepting them blindly.
This is also useful certification practice: use the IDE productively, then verify the result.
15. Check and activate
Run:
text
Ctrl + F2
Ctrl + F3Then open Data Preview:
text
F8You should now see summarized rows.
The number of result rows should normally be smaller than the number of /DMO/FLIGHT detail rows because multiple flight dates can belong to one carrier/connection group.
16. Understand COUNT( * )
This element:
abap
count( * ) as FlightCountcounts the rows in each group.
If a carrier/connection group contains two flight rows:
text
FlightCount = 2If another group contains five:
text
FlightCount = 5In current ABAP, COUNT aggregate results use an integer result type suitable for row counts; you should treat the result as a count, not as one of the source business fields.
17. Understand SUM
This:
abap
sum( Flight.seats_max ) as TotalSeatsmeans:
Add
seats_maxacross all flights in the current group.
This:
abap
sum( Flight.seats_occupied ) as OccupiedSeatsmeans:
Add occupied seats across the same group.
Example:
text
Flight 1: 180 occupied
Flight 2: 160 occupied
SUM = 34018. Understand AVG
This:
abap
avg( Flight.seats_occupied as abap.dec( 15, 2 ) )
as AverageOccupiedSeatscalculates the average occupied-seat count per flight in the group.
For example:
text
100
120
80average:
text
100.00ABAP CDS requires you to specify the desired result type inside AVG.
That is why we use:
abap
as abap.dec( 15, 2 )inside the aggregate call.
19. Why the AVG result type is explicit
An average can produce decimal places even when the source values are integers.
For example:
text
100 + 101
---------
2
= 100.5The explicit type makes the required result representation clear.
Do not confuse:
abap
avg( field as abap.dec( 15, 2 ) )with a normal CDS CAST expression.
This is the result-type syntax of AVG in ABAP CDS.
20. Understand MIN and MAX
These elements:
abap
min( Flight.seats_occupied ) as MinimumOccupiedSeats,
max( Flight.seats_occupied ) as MaximumOccupiedSeatsanswer:
text
What was the smallest occupied-seat value in this group?
What was the largest occupied-seat value in this group?They do not tell you which flight date produced that value.
That distinction matters.
Aggregation summarizes values; it does not automatically return the full source row associated with the minimum or maximum.
21. Filtering before vs after aggregation
Now consider two requirements.
Requirement A:
Before grouping, ignore certain individual flight rows.
Requirement B:
After grouping, keep only groups whose calculated
FlightCountmeets a threshold.
These require different clauses.
text
WHERE
↓
filters source rows before grouping
HAVING
↓
filters groups after aggregation22. WHERE happens before grouping
Conceptually:
text
Source rows
↓
WHERE
↓
remaining rows
↓
GROUP BY
↓
aggregate functionsA WHERE clause does not filter based on a result such as:
text
FlightCountbecause that aggregate result does not exist until after the rows have been grouped.
23. HAVING happens after grouping
Conceptually:
text
Source rows
↓
GROUP BY
↓
COUNT / SUM / AVG / ...
↓
HAVING
↓
remaining groupsHAVING can therefore contain aggregate expressions.
For example:
abap
having count( * ) >= 2means:
Return only groups that contain at least two source flight rows.
24. Create a second CDS using HAVING
Create:
text
ZI_BUSY_CONNECTION_###Description:
text
Busy Flight ConnectionsUse:
abap
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Busy Flight Connections'
define view entity ZI_Busy_Connection_###
as select from /dmo/flight as Flight
{
key Flight.carrier_id as CarrierId,
key Flight.connection_id as ConnectionId,
count( * ) as FlightCount,
sum( Flight.seats_occupied )
as OccupiedSeats
}
group by
Flight.carrier_id,
Flight.connection_id
having count( * ) >= 2Activate and preview it.
25. Why repeat count( * ) in HAVING?
The select list contains:
abap
count( * ) as FlightCountbut the filtering condition uses:
abap
having count( * ) >= 2This directly expresses the aggregate condition used to keep or remove a group.
Think of it as:
text
calculate the count for each group
then keep groups whose count is at least 226. WHERE vs HAVING summary
| Clause | Filters | Evaluation stage | Aggregate expressions? |
|---|---|---|---|
WHERE | individual source rows | before grouping | No |
HAVING | grouped result | after grouping | Yes |
A useful question is:
Am I filtering rows, or am I filtering a calculated group?
27. Could we use CDS-on-CDS instead?
Yes.
You already learned layered CDS modeling in Lesson 3.
You could define:
text
ZI_FLIGHT_STATS_###
↓
second CDS view
↓
WHERE FlightCount >= 2Once FlightCount is a normal element of the lower CDS entity, an upper view can filter it with WHERE.
That is a valid layered design.
This lesson uses HAVING because the requirement can be expressed directly in the same aggregation query.
So there are two different modeling choices:
text
same aggregation query
→ HAVING
higher CDS layer over an aggregated CDS
→ WHERE on the already-calculated elementChoose the design that gives the clearest reusable model.
28. Code pushdown
Now compare two approaches.
Approach A — retrieve detail rows and calculate in ABAP
text
Database
↓
all flight rows
↓
ABAP application server
↓
LOOP
↓
manual grouping/counting/summingApproach B — aggregate in CDS
text
Database
↓
GROUP BY / aggregates
↓
summary rows only
↓
ABAP application serverApproach B demonstrates code pushdown.
The calculation is expressed close to the data using CDS/SQL capabilities so the database can execute set-oriented processing.
29. Why pushdown matters
Potential benefits include:
text
less detail data transferred to ABAP
set-oriented processing
reuse of the calculation in multiple consumers
simpler ABAP consumer code
use of SAP HANA/database query processingThe principle is not:
Put every possible calculation in CDS.
The better principle is:
Perform data-intensive filtering, joining, grouping, and calculation in the database layer when that is the appropriate place for the requirement.
Business behavior and transactional rules will later belong in RAP/ABAP behavior logic.
30. A poor ABAP approach
Imagine reading every flight first:
abap
SELECT FROM /dmo/flight
FIELDS *
INTO TABLE @DATA(flights).and then writing application-server logic to:
text
group by carrier
then connection
count rows
sum seats
find min
find max
calculate averageABAP can perform such calculations.
But if the requirement is fundamentally a database aggregation, retrieving all detail rows only to reduce them again in ABAP is usually unnecessary.
31. CDS gives the consumer a reusable summary model
Now any ABAP consumer can read:
text
ZI_FLIGHT_STATS_###without knowing how the aggregates are calculated.
Architecture:
text
Runner
↓
Reader
↓
ABAP SQL
↓
ZI_FLIGHT_STATS_###
↓
GROUP BY + aggregate functions
↓
/DMO/FLIGHTThis is the same separation you have used throughout the course.
32. Create the Reader
Create:
text
ZCL_FLIGHT_STATS_READER_###Use:
abap
CLASS zcl_flight_stats_reader_### DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES tt_stats TYPE STANDARD TABLE OF zi_flight_stats_###
WITH EMPTY KEY.
TYPES tt_busy TYPE STANDARD TABLE OF zi_busy_connection_###
WITH EMPTY KEY.
CLASS-METHODS get_all
RETURNING VALUE(stats) TYPE tt_stats.
CLASS-METHODS get_busy
RETURNING VALUE(stats) TYPE tt_busy.
ENDCLASS.
CLASS zcl_flight_stats_reader_### IMPLEMENTATION.
METHOD get_all.
SELECT FROM zi_flight_stats_###
FIELDS *
ORDER BY CarrierId,
ConnectionId
INTO TABLE @stats.
ENDMETHOD.
METHOD get_busy.
SELECT FROM zi_busy_connection_###
FIELDS *
ORDER BY CarrierId,
ConnectionId
INTO TABLE @stats.
ENDMETHOD.
ENDCLASS.Replace ###.
33. The CDS entity is a global ABAP type
This line:
abap
TYPES tt_stats TYPE STANDARD TABLE OF zi_flight_stats_###
WITH EMPTY KEY.works because an activated CDS View Entity can be used as a global structured type in ABAP.
You already used this technique in earlier CDS lessons.
The difference is that the structure now contains aggregate results rather than detailed source-row fields.
34. Create the Runner
Create:
text
ZCL_CDS_L07_###Implement:
abap
CLASS zcl_cds_l07_### DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_cds_l07_### IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA(all_stats) =
zcl_flight_stats_reader_###=>get_all( ).
out->write( 'Flight statistics by connection' ).
out->write( all_stats ).
DATA(busy_stats) =
zcl_flight_stats_reader_###=>get_busy( ).
out->write( 'Connections with at least two flights' ).
out->write( busy_stats ).
ENDMETHOD.
ENDCLASS.Activate and run:
text
F935. Notice what the Runner does not contain
The Runner does not contain:
text
LOOP AT flights
manual counters
manual sums
manual averages
manual grouping tablesIt asks for the summary model.
text
ABAP receives the result
rather than rebuilding the database calculationThat is one of the practical outcomes of code pushdown.
36. SQL aggregation is not the same as an analytical CDS model
This lesson uses SQL-style aggregation directly in a CDS View Entity:
text
GROUP BY
COUNT / SUM / AVG / MIN / MAX
HAVINGYou may later encounter analytical annotations such as:
abap
@Aggregation.default: #SUMThose belong to analytical modeling, where a consumer can request aggregation dynamically from an analytical model.
That is a different topic.
Here, the grouping grain is fixed by the CDS query itself:
text
CarrierId + ConnectionIdand the database returns an already-grouped result.
Do not add analytical annotations to this lesson's views.
37. Aggregating money requires semantic care
Seats are easy to add:
text
180 seats + 200 seatsMoney is different:
text
100 EUR + 100 USDA raw numeric total of:
text
200has no meaningful single currency.
So when aggregating amount fields, the currency dimension must be handled correctly.
38. Correct approach for same-currency groups
Suppose a source contains:
text
CarrierId
FlightPrice
CurrencyCodeA safe basic grouping is:
text
CarrierId + CurrencyCodeThen:
text
AA + USD
LH + EUR
LH + USDbecome different groups.
Within each group, the amount values have the same currency context.
39. Amount semantics still matter after SUM
An aggregated monetary element should preserve the amount/currency relationship.
Conceptually:
abap
@Semantics.amount.currencyCode: 'CurrencyCode'
sum( Booking.flight_price ) as TotalBookingValue,
Booking.currency_code as CurrencyCodeand:
text
CurrencyCodemust also participate in the grouping.
You will implement this in the Practice assignment.
40. Grouping by currency is not currency conversion
This:
text
GROUP BY CurrencyCodeprevents different currencies from being mixed in one total.
It does not convert currencies.
If the requirement were:
Show one total in EUR across bookings stored in several currencies.
then the design would need an explicit currency-conversion strategy before or as part of aggregation.
That is a different requirement.
41. Common mistake: adding every source field to GROUP BY
Suppose you begin with:
text
CarrierId
ConnectionId
FlightDateand add all three to GROUP BY just to make the syntax checker happy.
You may accidentally produce:
text
one group per flightinstead of:
text
one group per connectionThe correct question is not:
Which fields can I add until the error disappears?
The correct question is:
What should one output row represent?
Then choose the grouping dimensions accordingly.
42. Common mistake: using DISTINCT instead of aggregation
DISTINCT and GROUP BY solve different problems.
DISTINCT removes duplicate result rows.
Aggregation calculates values across groups.
Do not write:
text
DISTINCTas a substitute for:
text
COUNT
SUM
GROUP BY43. Common mistake: trying to use WHERE on an aggregate
This idea is wrong inside the same aggregation query:
text
WHERE FlightCount >= 2because FlightCount has not yet been calculated at the row-filtering stage.
Use:
abap
having count( * ) >= 2or create a higher CDS layer and apply WHERE to the already-calculated lower-view element.
44. Common mistake: grouping by the current select-list alias
Inside the same CDS View Entity, do not write:
abap
group by CarrierId,
ConnectionIdwhen those names are aliases introduced by:
abap
Flight.carrier_id as CarrierIdUse the source fields:
abap
group by Flight.carrier_id,
Flight.connection_id45. Common mistake: forgetting the AVG type
This is incomplete for ABAP CDS:
abap
avg( Flight.seats_occupied )Specify the result type:
abap
avg( Flight.seats_occupied as abap.dec( 15, 2 ) )46. Common mistake: summing amounts across currencies
Do not create a single:
text
TotalBookingValueacross rows that may contain:
text
EUR
USD
GBPunless the amounts have first been converted to a common currency.
For the basic practice exercise, group by currency.
47. Certification connection
SAP's current ABAP data-modeling learning content explicitly includes:
text
aggregations
GROUP BY
COUNT
MIN
MAX
SUM
AVG
code pushdownand teaches aggregation as part of processing data sets in CDS views.
This lesson therefore closes an important gap before persistence and RAP.
A practical assessment may not ask:
Define code pushdown.
It may instead give you a requirement where the correct implementation is:
text
one CDS query with aggregationrather than:
text
read everything + calculate manually in ABAP48. Knowledge check
1. What does aggregation do?
Show answer
It calculates summary values from multiple source rows, optionally separately for groups defined by GROUP BY.
2. What does COUNT( * ) count?
Show answer
The rows in the current result/group.
3. Why do CarrierId and ConnectionId belong in GROUP BY?
Show answer
They are selected as normal fields outside aggregate functions and define the intended output grain: one summary row per carrier/connection.
4. Why is FlightDate not grouped in ZI_FLIGHT_STATS_###?
Show answer
The requirement is to combine multiple flights/dates into one connection summary. Grouping by flight date would create finer groups and defeat that summary requirement.
5. What is the difference between WHERE and HAVING?
Show answer
WHERE filters source rows before grouping; HAVING filters groups after aggregation and can use aggregate expressions.
6. Why does AVG specify a result type?
Show answer
ABAP CDS requires an explicit result type for AVG, and an average can require decimal precision that differs from the source value.
7. What does code pushdown mean here?
Show answer
The grouping and calculations are expressed in CDS so they are executed in database query processing rather than retrieving all detail rows and manually reproducing the same set operation in ABAP.
8. Why must currency be considered when summing amounts?
Show answer
A numeric sum across different currencies has no single monetary meaning. Basic aggregation should keep currencies separate or explicitly convert amounts to a common target currency.
49. Lesson summary
You extended your CDS knowledge from row-level modeling to set-level summaries.
You learned:
text
COUNT
SUM
AVG
MIN
MAX
GROUP BY
HAVING
WHERE vs HAVING
output grain
aggregate result typing
amount/currency grouping
code pushdownThe architecture is:
text
/DMO/FLIGHT
↓
GROUP BY Carrier + Connection
↓
COUNT / SUM / AVG / MIN / MAX
↓
ZI_FLIGHT_STATS_###
↓
Reader
↓
RunnerFor filtered groups:
text
/DMO/FLIGHT
↓
GROUP BY
↓
COUNT
↓
HAVING count(*) >= 2
↓
ZI_BUSY_CONNECTION_###The central idea is:
When a requirement asks for a database summary, model the set operation close to the data instead of automatically moving all detail rows to ABAP and rebuilding the calculation there.
50. What comes next?
The first seven lessons used persistence owned by SAP's /DMO/ reference scenario.
Next, that changes.
Lesson 8 introduces:
text
your own Domain
↓
your own Data Element
↓
your own database table
↓
controlled INSERT / UPDATE / DELETEThat persistence will then become the foundation for your first managed RAP Business Object.