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.
| Fonction | Description | Disponible depuis |
|---|---|---|
ROW_NUMBER() | Numéro de ligne séquentiel | ABAP 7.50 |
RANK() | Rang avec écarts en cas d’égalité | ABAP 7.50 |
DENSE_RANK() | Rang sans écarts | ABAP 7.50 |
SUM() OVER() | Somme cumulative/partitionnée | ABAP 7.50 |
AVG() OVER() | Moyenne cumulative/partitionnée | ABAP 7.50 |
COUNT() OVER() | Comptage cumulatif/partitionné | ABAP 7.50 |
LAG() | Valeur de la ligne précédente | ABAP 7.50 |
LEAD() | Valeur de la ligne suivante | ABAP 7.50 |
FIRST_VALUE() | Première valeur dans la fenêtre | ABAP 7.50 |
LAST_VALUE() | Dernière valeur dans la fenêtre | ABAP 7.50 |
Window Functions vs. GROUP BY
" GROUP BY: Une ligne par groupeSELECT 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éesSELECT 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 | 30000La 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 :
| Fonction | Comportement en cas d’égalité | Exemple (Valeurs: 10, 20, 20, 30) |
|---|---|---|
ROW_NUMBER() | Numéro unique | 1, 2, 3, 4 |
RANK() | Même rang, puis écart | 1, 2, 2, 4 |
DENSE_RANK() | Même rang, pas d’écart | 1, 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 connexionSELECT 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édenteSELECT 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
| Conseil | Description |
|---|---|
| Garder les partitions petites | Les grandes partitions augmentent la consommation mémoire |
| Utiliser des index | Les colonnes ORDER BY doivent être indexées |
| Calculer plusieurs à la fois | Plusieurs Window Functions dans un SELECT sont plus efficaces que plusieurs requêtes |
| Sous-requêtes pour les filtres | Les 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.