Custom Analytical Queries in ABAP Cloud: Eigene analytische Auswertungen

kategorie
CDS
Veröffentlicht
autor
Johannes

Custom Analytical Queries ermöglichen dir, eigene analytische Auswertungen direkt in ABAP Cloud zu erstellen. Mit CDS Views und speziellen Annotations baust du Cubes, Queries und Dashboards für Business Intelligence Szenarien.

Was sind Analytical CDS Views?

Analytical CDS Views sind speziell annotierte Views für OLAP-Szenarien (Online Analytical Processing). Sie unterscheiden sich von transaktionalen Views durch:

  • Dimensionen: Gruppierungsmerkmale (Kunde, Produkt, Zeit)
  • Measures: Kennzahlen mit Aggregationslogik (Umsatz, Menge)
  • Hierarchien: Drill-Down Strukturen (Jahr → Quartal → Monat)
  • Aggregationen: Automatische Verdichtung (SUM, AVG, COUNT)

Embedded Analytics vs. BW

AspektEmbedded AnalyticsSAP BW
OrtDirekt im ABAP SystemSeparates Data Warehouse
EchtzeitdatenJaNein (ETL-Prozess)
KomplexitätGeringHoch
AnwendungsfallOperative ReportingStrategische Analyse
DatenvolumenMittelSehr hoch

Embedded Analytics ist ideal für operatives Reporting direkt aus den Transaktionsdaten.


Architektur analytischer Views

Die analytische Schichtung folgt dem Virtual Data Model (VDM):

┌─────────────────────────────────────────────────┐
│ Analytical Query │
│ @Analytics.dataCategory: #DIMENSION │
│ @Analytics.query: true │
│ (ZC_SalesQuery) │
└─────────────────────┬───────────────────────────┘
┌─────────────────────▼───────────────────────────┐
│ Cube View │
│ @Analytics.dataCategory: #CUBE │
│ (ZI_SalesCube) │
└─────────────────────┬───────────────────────────┘
┌─────────────────────▼───────────────────────────┐
│ Dimension Views │
│ @Analytics.dataCategory: #DIMENSION │
│ (ZI_Customer, ZI_Product, ZI_Time) │
└─────────────────────┬───────────────────────────┘
┌─────────────────────▼───────────────────────────┐
│ Basic Views │
│ (Transaktionsdaten) │
└─────────────────────────────────────────────────┘

Cube vs. Query: Der Unterschied

Cube (@Analytics.dataCategory: #CUBE)

Der Cube ist das Datenmodell mit allen Dimensionen und Measures:

@Analytics.dataCategory: #CUBE
@ObjectModel.supportedCapabilities: [#ANALYTICAL_QUERY]
define view entity ZI_SalesCube
as select from vbak as order
inner join vbap as item on order.vbeln = item.vbeln
inner join kna1 as customer on order.kunnr = customer.kunnr
{
-- Dimensionen
@Analytics.dimension: true
@ObjectModel.foreignKey.association: '_Customer'
customer.kunnr as CustomerId,
@Analytics.dimension: true
@ObjectModel.foreignKey.association: '_Product'
item.matnr as ProductId,
@Analytics.dimension: true
@Semantics.calendar.yearMonth: true
concat(substring(order.erdat, 1, 4), substring(order.erdat, 5, 2)) as CalendarYearMonth,
@Analytics.dimension: true
@Semantics.calendar.year: true
substring(order.erdat, 1, 4) as CalendarYear,
@Analytics.dimension: true
order.vkorg as SalesOrg,
@Analytics.dimension: true
item.werks as Plant,
-- Measures
@Analytics.measure: true
@Aggregation.default: #SUM
@Semantics.amount.currencyCode: 'Currency'
item.netwr as Revenue,
@Analytics.measure: true
@Aggregation.default: #SUM
@Semantics.quantity.unitOfMeasure: 'Unit'
item.kwmeng as Quantity,
@Analytics.measure: true
@Aggregation.default: #SUM
cast(1 as abap.int4) as OrderCount,
@Semantics.currencyCode: true
order.waerk as Currency,
@Semantics.unitOfMeasure: true
item.vrkme as Unit,
-- Assoziationen fuer Texte
_Customer,
_Product
}

Eigenschaften eines Cubes:

  • Enthält alle Dimensionen und Measures
  • Definiert Aggregationsverhalten
  • Wird nicht direkt für UI verwendet
  • Grundlage für mehrere Queries

Query (@Analytics.query: true)

Die Query definiert eine konkrete Auswertung auf dem Cube:

@Analytics.query: true
@ObjectModel.supportedCapabilities: [#ANALYTICAL_QUERY]
@EndUserText.label: 'Umsatzanalyse nach Kunde'
define view entity ZC_SalesAnalysis
as projection on ZI_SalesCube
{
-- Ausgewaehlte Dimensionen
@AnalyticsDetails.query.axis: #ROWS
@AnalyticsDetails.query.displayHierarchy: #FILTER
CustomerId,
@AnalyticsDetails.query.axis: #ROWS
CalendarYearMonth,
@AnalyticsDetails.query.axis: #FREE
SalesOrg,
-- Aggregierte Measures
@AnalyticsDetails.query.axis: #COLUMNS
@AnalyticsDetails.query.totals: #SHOW
Revenue,
@AnalyticsDetails.query.axis: #COLUMNS
Quantity,
@AnalyticsDetails.query.axis: #COLUMNS
OrderCount,
-- Berechnete Kennzahlen
@AnalyticsDetails.query.axis: #COLUMNS
@EndUserText.label: 'Durchschnittlicher Bestellwert'
division(Revenue, OrderCount, 2) as AverageOrderValue,
Currency
}

Eigenschaften einer Query:

  • Definiert Achsen (Rows, Columns, Free)
  • Bestimmt Aggregationsebene
  • Kann berechnete Kennzahlen enthalten
  • Wird fuer UI/Reporting verwendet

Dimensionen erstellen

Dimensionen sind die Gruppierungsmerkmale deiner Analyse.

Einfache Dimension

@Analytics.dataCategory: #DIMENSION
@ObjectModel.representativeKey: 'CustomerId'
@EndUserText.label: 'Kundenstamm'
define view entity ZI_CustomerDimension
as select from kna1
{
@ObjectModel.text.element: ['CustomerName']
key kunnr as CustomerId,
name1 as CustomerName,
@ObjectModel.text.element: ['CountryName']
land1 as Country,
ort01 as City,
brsch as Industry,
_CountryText.landx as CountryName
}

Zeit-Dimension

@Analytics.dataCategory: #DIMENSION
@EndUserText.label: 'Zeitdimension'
define view entity ZI_TimeDimension
as select from I_CalendarDate
{
@Semantics.calendar.date: true
key CalendarDate,
@Semantics.calendar.year: true
CalendarYear,
@Semantics.calendar.yearQuarter: true
CalendarYearQuarter,
@Semantics.calendar.yearMonth: true
CalendarYearMonth,
@Semantics.calendar.yearWeek: true
CalendarYearWeek,
@Semantics.calendar.month: true
CalendarMonth,
@Semantics.calendar.quarter: true
CalendarQuarter,
@Semantics.calendar.dayOfMonth: true
CalendarDay,
@Semantics.calendar.dayOfWeek: true
DayOfWeek,
WeekDay
}

Dimension mit Hierarchie

@Analytics.dataCategory: #DIMENSION
@Hierarchy.parentChild: [{
recurse: { parent: 'ParentOrgUnit', child: 'OrgUnit' },
siblingsOrder: [{ by: 'OrgUnit', direction: #ASC }]
}]
@EndUserText.label: 'Organisationseinheiten'
define view entity ZI_OrgUnitDimension
as select from zorg_unit
{
@ObjectModel.text.element: ['Description']
key org_unit as OrgUnit,
parent_unit as ParentOrgUnit,
description as Description,
org_type as OrgType
}

Measures und Aggregationen

Measures sind Kennzahlen mit definiertem Aggregationsverhalten.

Verfuegbare Aggregationstypen

AggregationBeschreibungAnwendungsfall
#SUMSummeUmsatz, Menge, Anzahl
#AVGDurchschnittPreise, Ratings
#MINMinimumFruehestes Datum
#MAXMaximumHoechstwert
#COUNTAnzahlDatensaetze zaehlen
#COUNT_DISTINCTAnzahl eindeutiger WerteKunden pro Region

Measure-Definition

-- Summe
@Analytics.measure: true
@Aggregation.default: #SUM
@Semantics.amount.currencyCode: 'Currency'
netwr as Revenue,
-- Durchschnitt
@Analytics.measure: true
@Aggregation.default: #AVG
unit_price as AveragePrice,
-- Zaehler
@Analytics.measure: true
@Aggregation.default: #SUM
cast(1 as abap.int4) as LineItemCount,
-- Minimum
@Analytics.measure: true
@Aggregation.default: #MIN
order_date as FirstOrderDate,
-- Maximum
@Analytics.measure: true
@Aggregation.default: #MAX
order_date as LastOrderDate

Berechnete Measures

-- Im Cube: Basis-Measures definieren
@Analytics.measure: true
@Aggregation.default: #SUM
netwr as Revenue,
@Analytics.measure: true
@Aggregation.default: #SUM
menge as Quantity,
-- In der Query: Berechnungen durchfuehren
@EndUserText.label: 'Durchschnittspreis'
division(Revenue, Quantity, 2) as AverageUnitPrice,
@EndUserText.label: 'Umsatzanteil %'
division(Revenue, sum(Revenue over ()), 4) * 100 as RevenueSharePercent

Referenz-Aggregation (Exception Aggregation)

Fuer Bestandswerte wie Lagerbestand braucht man spezielle Aggregation:

@Analytics.measure: true
@Aggregation.default: #SUM
@Aggregation.referenceElement: 'CalendarDate'
@Aggregation.exception: #LAST
stock_quantity as EndingInventory

Das bedeutet: Summiere normalerweise, aber bei Aggregation ueber Zeit nimm den letzten Wert.


Vollstaendiges Beispiel: Verkaufsanalyse

1. Dimension Views

-- Kunden-Dimension
@Analytics.dataCategory: #DIMENSION
@ObjectModel.representativeKey: 'CustomerId'
define view entity ZI_Customer_Dim
as select from kna1
{
@ObjectModel.text.element: ['CustomerName']
key kunnr as CustomerId,
name1 as CustomerName,
land1 as Country,
ort01 as City,
brsch as Industry
}
-- Produkt-Dimension
@Analytics.dataCategory: #DIMENSION
@ObjectModel.representativeKey: 'ProductId'
define view entity ZI_Product_Dim
as select from mara
inner join makt on mara.matnr = makt.matnr
and makt.spras = $session.system_language
{
@ObjectModel.text.element: ['ProductName']
key mara.matnr as ProductId,
makt.maktx as ProductName,
mara.mtart as ProductType,
mara.matkl as ProductGroup
}

2. Cube View

@Analytics.dataCategory: #CUBE
@ObjectModel.supportedCapabilities: [#ANALYTICAL_QUERY]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Verkaufscube'
define view entity ZI_Sales_Cube
as select from vbak as header
inner join vbap as item
on header.vbeln = item.vbeln
association [1..1] to ZI_Customer_Dim as _Customer
on $projection.CustomerId = _Customer.CustomerId
association [1..1] to ZI_Product_Dim as _Product
on $projection.ProductId = _Product.ProductId
{
-- Dimensionen mit Fremdschluessel-Beziehung
@Analytics.dimension: true
@ObjectModel.foreignKey.association: '_Customer'
header.kunnr as CustomerId,
@Analytics.dimension: true
@ObjectModel.foreignKey.association: '_Product'
item.matnr as ProductId,
@Analytics.dimension: true
@Semantics.calendar.year: true
substring(header.erdat, 1, 4) as CalendarYear,
@Analytics.dimension: true
@Semantics.calendar.yearMonth: true
concat(substring(header.erdat, 1, 4), substring(header.erdat, 5, 2)) as CalendarYearMonth,
@Analytics.dimension: true
header.vkorg as SalesOrg,
@Analytics.dimension: true
header.vtweg as DistributionChannel,
@Analytics.dimension: true
item.werks as Plant,
-- Measures
@Analytics.measure: true
@Aggregation.default: #SUM
@Semantics.amount.currencyCode: 'Currency'
item.netwr as Revenue,
@Analytics.measure: true
@Aggregation.default: #SUM
@Semantics.amount.currencyCode: 'Currency'
item.mwsbp as TaxAmount,
@Analytics.measure: true
@Aggregation.default: #SUM
@Semantics.quantity.unitOfMeasure: 'Unit'
item.kwmeng as Quantity,
@Analytics.measure: true
@Aggregation.default: #SUM
cast(1 as abap.int4) as LineItems,
@Semantics.currencyCode: true
header.waerk as Currency,
@Semantics.unitOfMeasure: true
item.vrkme as Unit,
-- Assoziationen
_Customer,
_Product
}

3. Analytical Query

@Analytics.query: true
@ObjectModel.supportedCapabilities: [#ANALYTICAL_QUERY]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Verkaufsanalyse nach Kunde und Produkt'
define view entity ZC_Sales_Query
as projection on ZI_Sales_Cube
{
-- Zeilen-Dimensionen
@AnalyticsDetails.query.axis: #ROWS
@AnalyticsDetails.query.totals: #SHOW
@AnalyticsDetails.query.display: #KEY_TEXT
CustomerId,
@AnalyticsDetails.query.axis: #ROWS
@AnalyticsDetails.query.display: #KEY_TEXT
ProductId,
-- Filter-Dimensionen
@AnalyticsDetails.query.axis: #FREE
@Consumption.filter: { selectionType: #SINGLE, mandatory: false }
CalendarYear,
@AnalyticsDetails.query.axis: #FREE
@Consumption.filter: { selectionType: #SINGLE, mandatory: false }
SalesOrg,
@AnalyticsDetails.query.axis: #FREE
DistributionChannel,
-- Spalten (Kennzahlen)
@AnalyticsDetails.query.axis: #COLUMNS
@AnalyticsDetails.query.totals: #SHOW
Revenue,
@AnalyticsDetails.query.axis: #COLUMNS
Quantity,
@AnalyticsDetails.query.axis: #COLUMNS
LineItems,
-- Berechnete Kennzahlen
@AnalyticsDetails.query.axis: #COLUMNS
@EndUserText.label: 'Bruttoumsatz'
Revenue + TaxAmount as GrossRevenue,
@AnalyticsDetails.query.axis: #COLUMNS
@EndUserText.label: 'Durchschnittspreis'
division(Revenue, Quantity, 2) as AveragePrice,
Currency,
Unit,
_Customer,
_Product
}

4. Service Binding

-- Service Definition
@EndUserText.label: 'Sales Analytics Service'
define service ZSB_SALES_ANALYTICS {
expose ZC_Sales_Query as SalesAnalysis;
expose ZI_Customer_Dim as Customer;
expose ZI_Product_Dim as Product;
}

Integration mit Embedded Analytics

Fiori Analytical List Page (ALP)

Die Analytical List Page kombiniert Chart und Table in einer App:

@Analytics.query: true
@UI.chart: [{
chartType: #BAR,
dimensions: ['CustomerId'],
measures: ['Revenue'],
dimensionAttributes: [{
dimension: 'CustomerId',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Revenue',
role: #AXIS1
}]
}]
@UI.presentationVariant: [{
visualizations: [{
type: #AS_CHART
}, {
type: #AS_LINEITEM
}]
}]
define view entity ZC_SalesALP
as projection on ZI_Sales_Cube
{
@UI.lineItem: [{ position: 10 }]
@AnalyticsDetails.query.axis: #ROWS
CustomerId,
@UI.lineItem: [{ position: 20 }]
@AnalyticsDetails.query.axis: #ROWS
ProductId,
@UI.lineItem: [{ position: 30 }]
@AnalyticsDetails.query.axis: #COLUMNS
Revenue,
@UI.lineItem: [{ position: 40 }]
@AnalyticsDetails.query.axis: #COLUMNS
Quantity,
Currency
}

KPI Tiles im Fiori Launchpad

Fuer analytische Kacheln im Launchpad:

@Analytics.query: true
@UI.headerInfo.title.value: 'Revenue'
@UI.kpi: [{
id: 'RevenueKPI',
selectionVariantQualifier: 'CurrentYear',
detail: {
semanticObject: 'SalesAnalysis',
action: 'display'
}
}]
define view entity ZC_RevenueKPI
as projection on ZI_Sales_Cube
{
@AnalyticsDetails.query.axis: #ROWS
@Consumption.filter: {
selectionType: #SINGLE,
defaultValue: '2026'
}
CalendarYear,
@AnalyticsDetails.query.axis: #COLUMNS
@UI.dataPoint: {
criticalityCalculation: {
improvementDirection: #MAXIMIZE,
toleranceRangeLowValue: 1000000,
deviationRangeLowValue: 500000
}
}
Revenue,
Currency
}

Best Practices

1. Cube-Design

-- RICHTIG: Nur Key-Felder und Measures im Cube
@Analytics.dimension: true
@ObjectModel.foreignKey.association: '_Customer'
customer_id as CustomerId, -- Nur ID, Text kommt via Assoziation
-- FALSCH: Text-Felder direkt im Cube
customer_id as CustomerId,
customer_name as CustomerName -- Macht Cube unnoetig gross

2. Performance-Optimierung

-- Aggregations-Verhalten explizit angeben
@Analytics.measure: true
@Aggregation.default: #SUM
revenue as Revenue,
-- Waehrungsreferenz fuer korrekte Aggregation
@Semantics.amount.currencyCode: 'Currency'
revenue as Revenue,
@Semantics.currencyCode: true
waerk as Currency

3. Wiederverwendbarkeit

-- Ein Cube, mehrere Queries
ZI_Sales_Cube
├── ZC_Sales_ByCustomer -- Kundenanalyse
├── ZC_Sales_ByProduct -- Produktanalyse
├── ZC_Sales_ByRegion -- Regionalanalyse
└── ZC_Sales_Trending -- Zeitreihenanalyse

4. Access Control beachten

@EndUserText.label: 'Sales Cube Authorization'
@MappingRole: true
define role ZI_Sales_Cube_Auth {
grant select on ZI_Sales_Cube
where (SalesOrg) = aspect pfcg_auth(V_VBAK_VKO, VKORG, ACTVT='03');
}

Haeufige Fehler und Loesungen

Fehler: “Aggregation not allowed”

-- PROBLEM: Text-Feld ohne Dimension-Annotation
customer_name as CustomerName,
-- LOESUNG: Als Dimension markieren oder aus Query entfernen
@Analytics.dimension: true
customer_name as CustomerName,

Fehler: “Currency conversion required”

-- PROBLEM: Verschiedene Waehrungen werden addiert
@Aggregation.default: #SUM
revenue as Revenue, -- EUR, USD, CHF gemischt
-- LOESUNG: Waehrungskonvertierung verwenden
@Aggregation.default: #SUM
@Semantics.amount.currencyCode: 'TargetCurrency'
currency_conversion(
amount => revenue,
source_currency => currency,
target_currency => 'EUR',
exchange_rate_date => $session.system_date
) as RevenueInEUR,
cast('EUR' as waers) as TargetCurrency

Fehler: “Hierarchy not found”

-- PROBLEM: Hierarchie-View nicht korrekt definiert
@Hierarchy.parentChild: [{ ... }]
-- LOESUNG: Sicherstellen dass Parent/Child-Beziehung korrekt ist
@Hierarchy.parentChild: [{
recurse: {
parent: 'ParentId', -- Muss existieren
child: 'NodeId' -- Muss Key-Feld sein
}
}]

Zusammenfassung

KonzeptAnnotationBeschreibung
Cube@Analytics.dataCategory: #CUBEDatenmodell mit allen Dimensionen/Measures
Query@Analytics.query: trueKonkrete Auswertung auf Cube
Dimension@Analytics.dimension: trueGruppierungsmerkmal
Measure@Analytics.measure: trueKennzahl mit Aggregation
Achse@AnalyticsDetails.query.axisPlatzierung in Query (#ROWS, #COLUMNS, #FREE)

Analytical CDS Views sind maechtige Werkzeuge fuer operative Analytik direkt im ABAP System. Mit Cubes definierst du das Datenmodell, mit Queries die konkreten Auswertungen fuer Fiori Apps und Dashboards.

Verwandte Themen