ABAP SQL Window Functions : Calculs analytiques directement dans la base de données

Catégorie
ABAP
Publié
Auteur
Johannes

Les Window Functions (également appelées fonctions analytiques) sont l’une des extensions les plus puissantes d’ABAP SQL. Elles permettent des calculs complexes sur des groupes de lignes sans les agréger – chaque ligne conserve son identité.

Que sont les Window Functions ?

Les Window Functions calculent des valeurs sur une “fenêtre” (Window) de lignes, définie par la clause OVER(). Contrairement à GROUP BY, elles ne réduisent pas l’ensemble des résultats.

FonctionDescriptionDisponible depuis
ROW_NUMBER()Numéro de ligne séquentielABAP 7.50
RANK()Rang avec écarts en cas d’égalitéABAP 7.50
DENSE_RANK()Rang sans écartsABAP 7.50
SUM() OVER()Somme cumulative/partitionnéeABAP 7.50
AVG() OVER()Moyenne cumulative/partitionnéeABAP 7.50
COUNT() OVER()Comptage cumulatif/partitionnéABAP 7.50
LAG()Valeur de la ligne précédenteABAP 7.50
LEAD()Valeur de la ligne suivanteABAP 7.50
FIRST_VALUE()Première valeur dans la fenêtreABAP 7.50
LAST_VALUE()Dernière valeur dans la fenêtreABAP 7.50

Window Functions vs. GROUP BY

" GROUP BY: Une ligne par groupe
SELECT carrid, SUM( price ) AS total
FROM sflight
GROUP BY carrid
INTO TABLE @DATA(lt_grouped).
" Résultat: LH | 50000
" AA | 30000
" Window Function: Toutes les lignes sont conservées
SELECT carrid, connid, price,
SUM( price ) OVER( PARTITION BY carrid ) AS carrier_total
FROM sflight
INTO TABLE @DATA(lt_windowed).
" Résultat: LH | 400 | 500 | 50000
" LH | 401 | 600 | 50000
" AA | 100 | 800 | 30000

La clause OVER()

La clause OVER() définit la fenêtre pour le calcul. Elle se compose de composants optionnels :

fonction() OVER(
PARTITION BY colonne1, colonne2 -- Regroupement (optionnel)
ORDER BY colonne3 [ASC|DESC] -- Tri (optionnel/requis)
ROWS BETWEEN ... AND ... -- Cadre de fenêtre (optionnel)
)

PARTITION BY

PARTITION BY divise les données en groupes (partitions). La Window Function est calculée séparément pour chaque partition :

CLASS zcl_window_partition DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_partition IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Calculer la somme par transporteur, conserver tous les vols
SELECT carrid, connid, fldate, price,
SUM( price ) OVER( PARTITION BY carrid ) AS carrier_total,
AVG( price ) OVER( PARTITION BY carrid ) AS carrier_avg,
COUNT( * ) OVER( PARTITION BY carrid ) AS carrier_count
FROM sflight
WHERE carrid IN ( 'LH', 'AA' )
ORDER BY carrid, connid
INTO TABLE @DATA(lt_flights).
LOOP AT lt_flights INTO DATA(ls_flight).
out->write( |{ ls_flight-carrid } { ls_flight-connid }: | &&
|Prix { ls_flight-price }, | &&
|Somme transporteur { ls_flight-carrier_total }, | &&
|Moyenne transporteur { ls_flight-carrier_avg DECIMALS = 2 }| ).
ENDLOOP.
" Calculer la part du chiffre d'affaires du transporteur
SELECT carrid, connid, price,
price / SUM( price ) OVER( PARTITION BY carrid ) * 100
AS percentage
FROM sflight
WHERE carrid = 'LH"
INTO TABLE @DATA(lt_shares).
out->write( '--- Part du chiffre d''affaires du transporteur ---' ).
LOOP AT lt_shares INTO DATA(ls_share).
out->write( |{ ls_share-connid }: { ls_share-percentage DECIMALS = 1 }%| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

ORDER BY dans les Window Functions

ORDER BY dans la clause OVER() détermine l’ordre au sein de chaque partition :

CLASS zcl_window_order DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_order IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Somme cumulative (Running Total) avec ORDER BY
SELECT carrid, fldate, price,
SUM( price ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS running_total
FROM sflight
WHERE carrid = 'LH"
ORDER BY carrid, fldate
INTO TABLE @DATA(lt_running).
out->write( '--- Somme cumulative par transporteur ---' ).
LOOP AT lt_running INTO DATA(ls_run).
out->write( |{ ls_run-fldate DATE = USER }: | &&
|Prix { ls_run-price }, | &&
|Cumulé { ls_run-running_total }| ).
ENDLOOP.
" Sans ORDER BY: Somme totale de la partition
" Avec ORDER BY: Somme cumulative jusqu'à la ligne actuelle
ENDMETHOD.
ENDCLASS.

Fonctions de classement : ROW_NUMBER, RANK, DENSE_RANK

Les trois fonctions de classement diffèrent dans leur gestion des égalités :

FonctionComportement en cas d’égalitéExemple (Valeurs: 10, 20, 20, 30)
ROW_NUMBER()Numéro unique1, 2, 3, 4
RANK()Même rang, puis écart1, 2, 2, 4
DENSE_RANK()Même rang, pas d’écart1, 2, 2, 3
CLASS zcl_window_ranking DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_ranking IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Comparaison des trois fonctions de classement
SELECT carrid, connid, price,
ROW_NUMBER( ) OVER( ORDER BY price DESC ) AS row_num,
RANK( ) OVER( ORDER BY price DESC ) AS rank,
DENSE_RANK( ) OVER( ORDER BY price DESC ) AS dense_rank
FROM sflight
WHERE carrid IN ( 'LH', 'AA' )
INTO TABLE @DATA(lt_ranked).
out->write( '--- Classement de tous les vols par prix ---' ).
out->write( |{ 'Transp.'(car) WIDTH = 8 }| &&
|{ 'Conn'(con) WIDTH = 6 }| &&
|{ 'Prix'(pri) WIDTH = 10 }| &&
|{ 'Row#'(row) WIDTH = 6 }| &&
|{ 'Rank'(rnk) WIDTH = 6 }| &&
|{ 'Dense'(dns) WIDTH = 6 }| ).
out->write( |{ '=' WIDTH = 42 PAD = '=' }| ).
LOOP AT lt_ranked INTO DATA(ls_rank).
out->write( |{ ls_rank-carrid WIDTH = 8 }| &&
|{ ls_rank-connid WIDTH = 6 }| &&
|{ ls_rank-price WIDTH = 10 }| &&
|{ ls_rank-row_num WIDTH = 6 }| &&
|{ ls_rank-rank WIDTH = 6 }| &&
|{ ls_rank-dense_rank WIDTH = 6 }| ).
ENDLOOP.
" Top 3 par transporteur
out->write( '' ).
out->write( '--- Top 3 des vols par transporteur ---' ).
SELECT carrid, connid, price, rank
FROM (
SELECT carrid, connid, price,
DENSE_RANK( ) OVER(
PARTITION BY carrid
ORDER BY price DESC
) AS rank
FROM sflight
)
WHERE rank <= 3
INTO TABLE @DATA(lt_top3).
LOOP AT lt_top3 INTO DATA(ls_top).
out->write( |{ ls_top-carrid } #{ ls_top-rank }: | &&
|{ ls_top-connid } - { ls_top-price }| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

ROW_NUMBER pour la déduplication

ROW_NUMBER est utile pour éliminer les doublons ou ne conserver que la première/dernière ligne d’un groupe :

" Conserver uniquement le dernier vol par connexion
SELECT carrid, connid, fldate, price
FROM (
SELECT carrid, connid, fldate, price,
ROW_NUMBER( ) OVER(
PARTITION BY carrid, connid
ORDER BY fldate DESC
) AS rn
FROM sflight
)
WHERE rn = 1
INTO TABLE @DATA(lt_latest_flights).

Fonctions d’agrégation comme Window Functions

Les fonctions d’agrégation standard peuvent devenir des Window Functions avec OVER() :

CLASS zcl_window_aggregates DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_aggregates IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Différents agrégats par partition
SELECT carrid, connid, price,
" Agrégats sur toute la partition
SUM( price ) OVER( PARTITION BY carrid ) AS sum_carrier,
AVG( price ) OVER( PARTITION BY carrid ) AS avg_carrier,
MIN( price ) OVER( PARTITION BY carrid ) AS min_carrier,
MAX( price ) OVER( PARTITION BY carrid ) AS max_carrier,
COUNT( * ) OVER( PARTITION BY carrid ) AS cnt_carrier,
" Écart par rapport à la moyenne
price - AVG( price ) OVER( PARTITION BY carrid ) AS diff_from_avg
FROM sflight
WHERE carrid IN ( 'LH', 'AA' )
ORDER BY carrid, connid
INTO TABLE @DATA(lt_stats).
out->write( '--- Statistiques par transporteur ---' ).
LOOP AT lt_stats INTO DATA(ls_stat).
out->write( |{ ls_stat-carrid } { ls_stat-connid }: | &&
|Prix { ls_stat-price }, | &&
|Moyenne { ls_stat-avg_carrier DECIMALS = 0 }, | &&
|Écart { ls_stat-diff_from_avg SIGN = LEFT }| ).
ENDLOOP.
" Part en pourcentage
SELECT carrid, connid, price,
CAST( price AS DECFLOAT34 ) /
SUM( price ) OVER( PARTITION BY carrid ) * 100
AS pct_of_carrier,
CAST( price AS DECFLOAT34 ) /
SUM( price ) OVER( ) * 100
AS pct_of_total
FROM sflight
INTO TABLE @DATA(lt_pct).
out->write( '' ).
out->write( '--- Parts en pourcentage ---' ).
LOOP AT lt_pct INTO DATA(ls_pct).
out->write( |{ ls_pct-carrid } { ls_pct-connid }: | &&
|{ ls_pct-pct_of_carrier DECIMALS = 1 }% du transporteur, | &&
|{ ls_pct-pct_of_total DECIMALS = 2 }% du total| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Sommes cumulées et moyennes mobiles

Avec ORDER BY et un cadre de fenêtre optionnel, les fonctions d’agrégation calculent des valeurs cumulatives :

CLASS zcl_window_running DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_running IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Somme et moyenne cumulatives
SELECT carrid, fldate, price,
SUM( price ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS running_sum,
AVG( price ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS running_avg,
COUNT( * ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS running_count
FROM sflight
WHERE carrid = 'LH"
ORDER BY fldate
INTO TABLE @DATA(lt_running).
out->write( '--- Valeurs cumulatives pour LH ---' ).
LOOP AT lt_running INTO DATA(ls_run).
out->write( |{ ls_run-fldate DATE = USER }: | &&
|Prix { ls_run-price }, | &&
|Somme { ls_run-running_sum }, | &&
|Moyenne { ls_run-running_avg DECIMALS = 0 }, | &&
|Nombre { ls_run-running_count }| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

LAG et LEAD pour prédécesseur/successeur

LAG() et LEAD() accèdent aux valeurs des lignes précédentes ou suivantes :

CLASS zcl_window_lag_lead DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_lag_lead IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" LAG: Ligne précédente, LEAD: Ligne suivante
SELECT carrid, fldate, price,
LAG( price, 1 ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS prev_price,
LEAD( price, 1 ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS next_price,
" Variation de prix par rapport à la période précédente
price - LAG( price, 1 ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS price_change
FROM sflight
WHERE carrid = 'LH"
ORDER BY fldate
INTO TABLE @DATA(lt_comparison).
out->write( '--- Comparaison de prix avec le prédécesseur ---' ).
LOOP AT lt_comparison INTO DATA(ls_comp).
DATA(lv_change) = COND string(
WHEN ls_comp-price_change IS NULL THEN 'n/d"
WHEN ls_comp-price_change > 0 THEN |+{ ls_comp-price_change }|
WHEN ls_comp-price_change < 0 THEN |{ ls_comp-price_change }|
ELSE '±0"
).
out->write( |{ ls_comp-fldate DATE = USER }: | &&
|Prix { ls_comp-price }, | &&
|Avant { ls_comp-prev_price }, | &&
|Variation { lv_change }| ).
ENDLOOP.
" LAG/LEAD avec décalage > 1 et valeur par défaut
SELECT carrid, fldate, price,
LAG( price, 2, 0 ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS price_2_periods_ago,
LEAD( price, 2, 0 ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS price_in_2_periods
FROM sflight
WHERE carrid = 'LH"
INTO TABLE @DATA(lt_offset).
out->write( '' ).
out->write( '--- Comparaison avec 2 périodes d''écart ---' ).
LOOP AT lt_offset INTO DATA(ls_off).
out->write( |{ ls_off-fldate DATE = USER }: | &&
|Actuel { ls_off-price }, | &&
|Il y a 2: { ls_off-price_2_periods_ago }, | &&
|Dans 2: { ls_off-price_in_2_periods }| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Calcul des variations en pourcentage

" Variation de prix en pourcentage par rapport à la période précédente
SELECT carrid, fldate, price,
CASE
WHEN LAG( price, 1 ) OVER(
PARTITION BY carrid ORDER BY fldate ) IS NULL THEN 0
WHEN LAG( price, 1 ) OVER(
PARTITION BY carrid ORDER BY fldate ) = 0 THEN 0
ELSE ( price - LAG( price, 1 ) OVER(
PARTITION BY carrid ORDER BY fldate ) ) /
LAG( price, 1 ) OVER(
PARTITION BY carrid ORDER BY fldate ) * 100
END AS pct_change
FROM sflight
WHERE carrid = 'LH"
INTO TABLE @DATA(lt_pct_change).

FIRST_VALUE et LAST_VALUE

Ces fonctions retournent la première ou la dernière valeur dans la fenêtre :

CLASS zcl_window_first_last DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS zcl_window_first_last IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Premier et dernier prix par transporteur
SELECT carrid, connid, fldate, price,
FIRST_VALUE( price ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS first_price,
" Différence par rapport au premier prix
price - FIRST_VALUE( price ) OVER(
PARTITION BY carrid
ORDER BY fldate
) AS diff_from_first
FROM sflight
WHERE carrid IN ( 'LH', 'AA' )
ORDER BY carrid, fldate
INTO TABLE @DATA(lt_first).
out->write( '--- Comparaison avec le premier prix ---' ).
LOOP AT lt_first INTO DATA(ls_first).
out->write( |{ ls_first-carrid } { ls_first-fldate DATE = USER }: | &&
|Prix { ls_first-price }, | &&
|Premier { ls_first-first_price }, | &&
|Diff { ls_first-diff_from_first SIGN = LEFT }| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Exemple pratique : Analyse des ventes

Voici un exemple complet combinant plusieurs Window Functions :

CLASS zcl_sales_analysis DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
TYPES: BEGIN OF ty_s_sale,
sale_id TYPE sysuuid_x16,
region TYPE c LENGTH 10,
product TYPE c LENGTH 20,
sale_date TYPE d,
amount TYPE p DECIMALS 2,
END OF ty_s_sale.
TYPES ty_t_sales TYPE STANDARD TABLE OF ty_s_sale WITH EMPTY KEY.
ENDCLASS.
CLASS zcl_sales_analysis IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
" Données d'exemple (normalement depuis une table DB)
DATA(lt_sales) = VALUE ty_t_sales(
( region = 'NORD' product = 'Laptop' sale_date = '20260101' amount = '1200.00' )
( region = 'NORD' product = 'Souris' sale_date = '20260102' amount = '30.00' )
( region = 'NORD' product = 'Laptop' sale_date = '20260115' amount = '1100.00' )
( region = 'SUD' product = 'Laptop' sale_date = '20260105' amount = '1250.00' )
( region = 'SUD' product = 'Écran' sale_date = '20260110' amount = '400.00' )
( region = 'SUD' product = 'Laptop' sale_date = '20260120' amount = '1300.00' )
( region = 'OUEST' product = 'Laptop' sale_date = '20260108' amount = '1150.00' )
).
" Analyse avec Window Functions
" Note: Pour les vraies tables DB, utiliser directement dans SELECT
SELECT FROM @lt_sales AS sales
FIELDS
region,
product,
sale_date,
amount,
" Classement par région
ROW_NUMBER( ) OVER(
PARTITION BY region
ORDER BY amount DESC
) AS rank_in_region,
" Part du chiffre d'affaires régional
CAST( amount AS DECFLOAT34 ) /
SUM( amount ) OVER( PARTITION BY region ) * 100
AS pct_of_region,
" Somme cumulative par région
SUM( amount ) OVER(
PARTITION BY region
ORDER BY sale_date
) AS running_total,
" Comparaison avec la vente précédente de la région
amount - LAG( amount, 1, amount ) OVER(
PARTITION BY region
ORDER BY sale_date
) AS diff_to_prev,
" Moyenne régionale
AVG( amount ) OVER( PARTITION BY region ) AS region_avg,
" Moyenne globale
AVG( amount ) OVER( ) AS total_avg
ORDER BY region, sale_date
INTO TABLE @DATA(lt_analysis).
" Affichage
out->write( '=== Analyse des ventes avec Window Functions ===' ).
out->write( '' ).
DATA(lv_current_region) = VALUE #( lt_analysis[ 1 ]-region OPTIONAL ).
LOOP AT lt_analysis INTO DATA(ls_a).
" En-tête de région
IF ls_a-region <> lv_current_region.
out->write( '' ).
lv_current_region = ls_a-region.
ENDIF.
IF sy-tabix = 1 OR ls_a-region <> VALUE #( lt_analysis[ sy-tabix - 1 ]-region OPTIONAL ).
out->write( |--- Région { ls_a-region } ---| ).
out->write( | Moyenne: { ls_a-region_avg DECIMALS = 2 }| ).
ENDIF.
out->write( | #{ ls_a-rank_in_region } { ls_a-product WIDTH = 10 }: | &&
|{ ls_a-amount WIDTH = 10 } | &&
|({ ls_a-pct_of_region DECIMALS = 1 }% de la région) | &&
|Cumulé: { ls_a-running_total }| ).
ENDLOOP.
out->write( '' ).
out->write( |Moyenne globale: { lt_analysis[ 1 ]-total_avg DECIMALS = 2 }| ).
" Extraire la meilleure vente par région
out->write( '' ).
out->write( '=== Meilleure vente par région ===' ).
LOOP AT lt_analysis INTO DATA(ls_top) WHERE rank_in_region = 1.
out->write( |{ ls_top-region }: { ls_top-product } - { ls_top-amount }| ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Conseils de performance

Utiliser efficacement les Window Functions

ConseilDescription
Garder les partitions petitesLes grandes partitions augmentent la consommation mémoire
Utiliser des indexLes colonnes ORDER BY doivent être indexées
Calculer plusieurs à la foisPlusieurs Window Functions dans un SELECT sont plus efficaces que plusieurs requêtes
Sous-requêtes pour les filtresLes filtres de classement (WHERE rank <= 3) nécessitent une sous-requête

Quand ne pas utiliser les Window Functions

  • Avec de très grandes partitions : vérifier les solutions alternatives
  • Quand seuls des agrégats sont nécessaires : GROUP BY est plus efficace
  • Avec des logiques conditionnelles complexes : le post-traitement ABAP peut être plus judicieux

Résumé

Les Window Functions en ABAP SQL permettent des calculs analytiques puissants directement dans la base de données :

  • OVER() définit la fenêtre de calcul avec PARTITION BY et ORDER BY
  • ROW_NUMBER, RANK, DENSE_RANK pour les classements et requêtes Top-N
  • SUM, AVG, COUNT OVER() pour les agrégats partitionnés et cumulatifs
  • LAG, LEAD pour les comparaisons avec les lignes prédécesseur/successeur
  • FIRST_VALUE, LAST_VALUE pour les valeurs limites dans la fenêtre

Le grand avantage : les calculs sont effectués dans la base de données et réduisent considérablement le transfert de données vers la couche applicative.

Articles connexes