CDS Analíticas no SAP: como montar dimension, cube e analytical query do jeito certo
Por que uma query analítica não é um SELECT?
O maior mal-entendido sobre CDS analíticas é tratá-las como uma view comum. Não são. Um modelo analítico no S/4HANA é interpretado pelo Analytic Manager (a engine OLAP do ABAP) — ele lê as anotações, monta o plano, agrega, aplica exception aggregation e hierarquias, e devolve o resultado para o front-end (Analysis for Office, SAC, Query Browser). Rodar SELECT FROM zquery em ABAP simplesmente não invoca essa engine.
Por isso a modelagem analítica tem regras próprias: você encadeia views com papéis bem definidos — dimension, text, hierarchy, fact, cube e query — num esquema estrela. Errar a anotação que define o papel, ou esquecer a semântica de moeda, gera números errados silenciosamente.
Fluxo de chamada de uma CDS Analítica.
Este guia mostra como montar cada componente do jeito certo: o modelo multidimensional, as anotações que definem cada view, measures e agregação, a query moderna via projection view, autorização e os anti-patterns que mais aparecem.
O essencial em 4 linhas
Um modelo analítico é interpretado pelo Analytic Manager, não executado como SQL puro.
O papel de cada view vem das anotações:
@Analytics.dataCategory(#CUBE/#FACT/#DIMENSION) e@ObjectModel.dataCategory(#TEXT/#HIERARCHY).No cube: todo campo com
@DefaultAggregationvira measure; o resto vira dimensão. A autorização (DCL) mora aqui.A query moderna (ABAP Cloud) é uma projection view transient com
provider contract analytical_query.
Como funciona um modelo analítico CDS?
O modelo segue o schema estrela/floco de neve: fatos no centro, dimensões ao redor. Cada peça tem uma função específica, e elas se encadeiam até a query consumida pelo usuário. Diferente de um SELECT, a query analítica não é SQL — é um artefato que o Analytic Engine interpreta.
Componente | Papel |
|---|---|
Fact | Valores transacionais mensuráveis (quantidades, valores). |
Dimension | Atributos descritivos (cliente, material, centro de custo). |
Text / Hierarchy | Descrições por idioma e estruturas pai-filho ligadas à dimensão. |
Cube | Onde fatos + dimensões se conectam — o "InfoProvider". |
Query | Projeção do cubo para um cenário de consumo do usuário final. |
Quais anotações definem o papel de cada view?
Aqui mora um erro comum: misturar os dois namespaces de dataCategory. São duas anotações distintas, cada uma com seu conjunto de valores. Confundi-las gera erro de ativação ou um papel errado. O mapeamento correto, conforme a ABAP Keyword Documentation:
Categoria | Anotação → Valor |
|---|---|
Cube / Fact |
|
Dimension |
|
Aggregation level |
|
Text |
|
Hierarchy |
|
Value help |
|
Correção que vale ouro: #TEXT e #HIERARCHY pertencem a @ObjectModel.dataCategory, não a @Analytics.dataCategory. E não existe @ObjectModel.dataCategory: #MASTER_DATA — "master data" se expressa com @Analytics.dataCategory: #DIMENSION + @ObjectModel.representativeKey, não com um literal de categoria.
Como modelar uma Dimension View?
A dimension view representa master data — entidades descritivas (cliente, material, centro de custo) que classificam e filtram os fatos. É o ponto de entrada para textos, hierarquias e atributos navegáveis a partir do cubo. Use-a quando vários cubos precisam navegar para os mesmos atributos.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Dimensão - Cliente'
@Analytics.dataCategory: #DIMENSION
@VDM.viewType: #BASIC
@ObjectModel.representativeKey: 'Customer'
define view entity ZI_Customer
as select from kna1
association [0..*] to ZI_CustomerText as _Text
on $projection.Customer = _Text.Customer
{
@ObjectModel.text.association: '_Text'
key kunnr as Customer,
@ObjectModel.foreignKey.association: '_Country'
land1 as Country,
name1 as CustomerName,
_Text
}Três anotações garantem a integridade: @ObjectModel.representativeKey define a chave de negócio (obrigatória para os cubos fazerem foreign-key association); @ObjectModel.text.association liga a view de textos (sem ela, o front mostra só a chave técnica); e @ObjectModel.foreignKey.association em cada atributo garante navegação consistente.
A Text View (@ObjectModel.dataCategory: #TEXT) fornece descrições por idioma — o campo de idioma precisa de @Semantics.language: true e o descritivo de @Semantics.text: true. A Hierarchy View (@ObjectModel.dataCategory: #HIERARCHY) modela relações pai-filho via @Hierarchy.parentChild.recurse, sempre com associação de volta para a dimensão.
Como modelar o Cube View?
O cube é o coração do modelo: consolida fatos e associações para dimensões num star schema, e é o único tipo de view sobre o qual queries analíticas podem ser construídas. A classificação de cada campo é automática — todo campo com @DefaultAggregation vira measure; sem ela, vira dimensão.
@AccessControl.authorizationCheck: #MANDATORY
@EndUserText.label: 'Cubo - Vendas'
@Analytics.dataCategory: #CUBE
@VDM.viewType: #COMPOSITE
define view entity ZI_SalesCube
as select from ZI_SalesOrderItemFact as Fact
association [0..1] to ZI_Customer as _Customer
on $projection.Customer = _Customer.Customer
{
key Fact.SalesOrder,
key Fact.SalesOrderItem,
@ObjectModel.foreignKey.association: '_Customer'
Fact.Customer,
@DefaultAggregation: #SUM
@Semantics.amount.currencyCode: 'Currency'
Fact.NetAmount,
Fact.Currency,
_Customer
}Três regras inegociáveis no cube: (1) declare @Semantics.amount.currencyCode em todo valor monetário e @Semantics.quantity.unitOfMeasure em quantidades — sem isso o front soma BRL + USD como números puros. (2) Declare @DefaultAggregation em toda measure — esquecer faz o engine tratar o campo como dimensão e duplicar linhas. (3) Use @AccessControl.authorizationCheck: #MANDATORY — a proteção mora no cubo.
E não use parâmetros no cube (exceto conversão de moeda): parâmetros transformam o objeto numa HANA table function e degradam a performance. Mova-os para a query.
Como funcionam as measures e a exception aggregation?
O @DefaultAggregation define como o engine agrega o campo quando o usuário muda o drill-down. Os valores válidos são #SUM, #MIN, #MAX, #AVG, #COUNT, #COUNT_DISTINCT, #FORMULA e #NONE. Há também a anotação mais recente @Aggregation.default — relacionada, mas com conjunto de valores ligeiramente diferente (não são sinônimos exatos).
A exception aggregation permite agregar primeiro por um conjunto de campos diferente do drill-down do usuário — essencial para "contar pedidos distintos" sem inflar por linhas de item. Aqui o guia que originou este post tinha a anotação errada; a forma correta usa o namespace @AnalyticsDetails:
@DefaultAggregation: #SUM
@AnalyticsDetails: { exceptionAggregationSteps: [
{ exceptionAggregationBehavior: #COUNT_DISTINCT,
exceptionAggregationElements: [ 'SalesOrder' ] } ] }
1 as OrderCountCorreção de sintaxe: exception aggregation é @AnalyticsDetails.exceptionAggregationSteps — não @Aggregation.exception, como circula por aí. Os campos internos (exceptionAggregationBehavior, exceptionAggregationElements) estão corretos. Com isso, mesmo arrastando por Cliente/Material/Mês, o OrderCount conta pedidos distintos, não linhas.
Como expor a query analítica (o jeito moderno)?
No ABAP Cloud / S/4HANA Cloud, a forma atual e recomendada de criar uma query é uma projection view transient com provider contract analytical_query. O transient é obrigatório — significa que não existe view SQL no banco; o objeto só vive em runtime, interpretado pela engine. A fonte projetada deve ser um cube (ou dimension).
@EndUserText.label: 'Query - Análise de Vendas'
@AccessControl.authorizationCheck: #NOT_ALLOWED
@Metadata.allowExtensions: true
@Analytics.query: true
define transient view entity ZC_SalesAnalysisProj
provider contract analytical_query
as projection on ZI_SalesCube
{
@AnalyticsDetails.query.axis: #ROWS
@AnalyticsDetails.query.display: #KEY_TEXT
Customer,
@AnalyticsDetails.query.axis: #FREE
Material,
@AnalyticsDetails.query.axis: #COLUMNS
@AnalyticsDetails.query.decimals: 2
@Semantics.amount.currencyCode: 'Currency'
NetAmount,
Currency
}Legado vs. atual: a query antiga era uma view entity comum com apenas @Analytics.query: true (modelo V2). Para desenvolvimento novo, prefira a projection view transient (V3) acima. E note a autorização: a query usa @AccessControl.authorizationCheck: #NOT_ALLOWED — ela não é lida por ABAP SQL, então não recebe DCL; herda o filtro do cubo.
As anotações de layout posicionam os campos no grid: @AnalyticsDetails.query.axis aceita #ROWS, #COLUMNS e #FREE; .display aceita #KEY, #TEXT, #TEXT_KEY e #KEY_TEXT; e há .totals, .decimals e .scaling para subtotais e formatação.
Como criar restricted e calculated measures?
Uma restricted measure soma só as linhas que satisfazem uma condição — implementada com CASE na query. Já uma calculated measure com fórmula é o que garante razões e percentuais corretos, porque o cálculo acontece após a agregação:
@DefaultAggregation: #FORMULA
@AnalyticsDetails.query.formula: 'NDIV0( NetAmount / OrderQuantity )'
@AnalyticsDetails.query.decimals: 2
@EndUserText.label: 'Preço Médio Unitário'
1 as AvgUnitPricePor que #FORMULA importa: se você calcular NetAmount / OrderQuantity linha a linha e depois somar, o número fica errado. Com #FORMULA, o engine soma primeiro NetAmount e OrderQuantity e só então divide — matematicamente correto. A anotação da fórmula é @AnalyticsDetails.query.formula (com o segmento .query.).
Como adicionar variáveis e filtros de consumo?
A anotação @Consumption.filter cria uma variável de prompt para o usuário no front-end. O selectionType aceita #SINGLE, #INTERVAL, #RANGE e #HIERARCHY_NODE, e o @AnalyticsDetails.query.variableSequence define a ordem dos prompts.
@Consumption.filter: { selectionType: #SINGLE,
multipleSelections: false,
mandatory: true }
@AnalyticsDetails.query.variableSequence: 10
CompanyCode,
@Consumption.filter: { selectionType: #INTERVAL,
multipleSelections: false,
mandatory: true }
@AnalyticsDetails.query.variableSequence: 20
FiscalPeriod,Onde fica a autorização (DCL) num modelo analítico?
A regra é direta: a autorização mora no cube, não na query. O cube usa @AccessControl.authorizationCheck: #MANDATORY + uma DCL; a query usa #NOT_ALLOWED e herda o filtro automaticamente. Aplicar DCL na query e deixar o cubo aberto fura o controle quando o cubo for consumido em outro contexto.
// DCL do cubo: ZI_SalesCube.dcls
@MappingRole: true
define role ZI_SalesCube {
grant select on ZI_SalesCube
where Plant = aspect pfcg_auth( Z_SD_PLANT, WERKS, ACTVT = '03' );
}Quer dominar a camada de DCL? A autorização por linha tem regras próprias (condições PFCG, user, herança). Veja o guia completo de CDS Access Control (DCL). Em ABAP Cloud, prefira aspect user e business roles para Clean Core.
O que muda entre On-Premise e ABAP Cloud?
A diferença central é o Clean Core. On-premise, você acessa tabelas direto e usa a query clássica; em ABAP Cloud, só views released e a projection view com provider contract são o caminho.
Tópico | On-Premise | ABAP Cloud |
|---|---|---|
Acesso a tabelas | Direto (KNA1, VBAP…) | Só views released (C1/C2) |
Query |
| Projection view + |
DCL |
|
|
Ferramentas | RSRT, Query Browser, KPI Modeler | Custom Analytical Queries, SAC, Datasphere |
Quais são os anti-patterns mais comuns?
A maioria dos modelos analíticos quebrados sofre dos mesmos enganos. Conhecê-los antes economiza retrabalho — e evita relatórios que somam errado sem ninguém perceber.
Anti-pattern | Como corrigir |
|---|---|
Calcular | Use |
Esquecer | Sempre anote moeda/UM ao lado do valor/qtd |
Usar parâmetros no cube | Mova para a query (cube vira table function) |
DCL na query, não no cube | DCL sempre no cube; query |
Esquecer | Anote toda measure (senão vira dimensão) |
Consumir a query via Open SQL | Consuma via OData/Analytic Manager |
Perguntas frequentes
Posso consumir uma query analítica com SELECT em ABAP?
Não como esperado. A query é interpretada pelo Analytic Manager, não executada como SQL. Um SELECT FROM zquery não invoca a engine analítica (agregação, exception aggregation, variáveis). Consuma via OData/Analytic Manager — Analysis for Office, SAC, Query Browser. Na V3, a query é até transient, sem view SQL no banco.
Qual a diferença entre measure e dimensão num cube?
A classificação é automática pela anotação: todo campo com @DefaultAggregation é tratado como measure (agregável); sem ela, vira dimensão (atributo de agrupamento). Esquecer o @DefaultAggregation num campo numérico é o erro clássico — o engine o trata como dimensão e o resultado aparece com linhas duplicadas.
Onde devo colocar a autorização: no cube ou na query?
No cube, sempre. Ele usa @AccessControl.authorizationCheck: #MANDATORY + DCL; a query usa #NOT_ALLOWED e herda o filtro. A query não pode ser lida por ABAP SQL, então não recebe DCL própria. Proteger só a query deixa o cubo exposto quando consumido em outro contexto.
Por que minha razão (preço médio) está somando errado?
Porque você provavelmente calculou linha a linha e depois somou. Soma de razões não é razão de somas. Use uma calculated measure com @DefaultAggregation: #FORMULA e @AnalyticsDetails.query.formula: o engine soma os operandos primeiro e só então aplica a fórmula — o resultado fica correto.
Posso usar parâmetros num Cube View?
Evite. Parâmetros num cube o transformam numa HANA table function, com impacto sério de performance — uma prática desaconselhada amplamente. A exceção comum é conversão de moeda. Para filtros e prompts, use a query com @Consumption.filter e variáveis, que é onde eles pertencem.
Conclusão
Modelar CDS analíticas é menos sobre SQL e mais sobre anotações corretas. Acerte o papel de cada view (@Analytics.dataCategory vs. @ObjectModel.dataCategory), proteja a semântica de moeda e quantidade, declare as measures com @DefaultAggregation e deixe a autorização no cube. Para a query, prefira a projection view transient com provider contract — o modelo atual do ABAP Cloud.
E guarde as duas correções que este guia traz: exception aggregation é @AnalyticsDetails.exceptionAggregationSteps, e a query analítica usa #NOT_ALLOWED, não #NOT_REQUIRED. Com as anotações certas, a engine faz o trabalho pesado — e os números chegam certos ao usuário.
Referências oficiais
Comentários 0
Ainda sem comentários
Seja o primeiro a comentar este artigo.
Entre na conversa
Faça login para deixar seu comentário neste artigo.