Ce n’est pas parce que la perspective de me débarrasser bientôt de SAP se profile nettement à l’horizon que mes critiques vont se tarir.[1]. La semaine dernière j’ai découvert une nouvelle manière de l’ABAP de fusiller le travail de la base Oracle en-dessous de lui.
Un exemple concret en ventes : je recherche quelques articles (premier SELECT
sur la table MARA
) et je veux afficher la liste des commandes portant sur ces articles. Le résultat ressemble à ça :
Pour obtenir ceci, je peux :
- soit faire un
SELECT
sur la tableVBAP
pour chaque article précédemment récupéré (méthode bovinement simple, fiable, très efficace si le but est de faire s’effondrer la base de données par des centaines ou des milliers de requêtes en rafale) ; - soit tenter de récupérer toutes les commandes concernées en bloc : la base recherchera d’un coup, optimisera son parcours de diverses manières (ne pas analyser 10 000 fois la même requête[2], récupérer des blocs contigus, choisir un index plutôt qu’un autre...).
La deuxième manière est un chouilla plus complexe (car on récupère en bloc un tableau des commandes voulues, qu’il faudra manipuler ensuite) mais monstrueusement plus efficace.
Si on utilise Oracle directement via la SQL ou le PL/SQL, on tente au maximum de tout regrouper en un seul ordre SQL, ou on utilise les ordres BULK.
Si on est contraint de passer par l’ABAP, on utilise FOR ALL ENTRIES
[3].
Concrètement, cela donne ce qui suit. Noter qu’en ABAP un SELECT
/ENDSELECT
est une boucle sur les lignes ramenées.
*
* Report Z_FAE_TEST
*
*
* But : démonstration d utilisation basique de FOR ALL ENTRIES
* Recherche de quelques articles et affichage des commandes liées à
* ces articles. On aurait pu faire une simple jointure mais on passera
* par une table interne (un tableau) qui ontient ces commandes.
*
REPORT Z_FAE_TEST .
* vbap est la table des commandes, et la déclaration
* suivante permet de ne pas déclarer des variables temporaires
* pour la parcourir
TABLES: vbap.
* Structure de table simple avec juste un champ article
TYPES: BEGIN OF article,
matnr TYPE matnr, " type prédéfini : un article
END OF article.
* Table des articles
DATA: tab_articles TYPE TABLE OF article.
* Variable tampon car impossible de faire cette operation
* directement au sein du SQL ci-dessous.
DATA: date_min TYPE datum.
* Récupérer les articles créés dans les 200 derniers jours
date_min = sy-datum - 200.
SELECT matnr FROM mara
INTO TABLE tab_articles
WHERE ersda > date_min.
.
* Pour tous ces articles, afficher
* toutes les commandes existantes
SELECT * FROM vbap
* Prendre articles dans table interne précédente
FOR ALL ENTRIES IN tab_articles
* Jointure sur code article
WHERE vbap~matnr = tab_articles-matnr
.
* Affiche article/commande/poste
WRITE :/ vbap-matnr, vbap-vbeln, vbap-posnr .
ENDSELECT.
Quand on lit ceci, et qu’on connaît un peu Oracle, on se dit que l’ABAP sur ce point n’est pas trop mal foutu, et qu’il a même eu un temps d’avance sur l’Oracle « pur » d’avant l’implémentation de BULK COLLECT
, qui forçait à tout fusionner en un seul ordre SQL si l’on voulait éviter les boucles.
On se dit que l’ABAP doit lui-même écrire l’ordre SQL qui va bien. Ou qu’il rédige une clause WHERE
de centaines de lignes avec les flopées de valeurs possibles.
Mais non.
Le SQL que SAP demande à Oracle d’exécuter servilement se présente ainsi (transaction ST05
sous SAP) :
Il s’agit bien d’un bête SELECT
avec une condition sur... CINQ valeurs possibles. Ceci répété x fois avec des valeurs différentes.
SAP fait donc exécuter à Oracle la boucle même que l’on cherchait à éviter à la main. Le seul avantage est que cette recherche se fait par paquet de cinq lignes. C’est déjà mieux que rien mais on est loin du Saint Graal de l’instruction SQL unique.
Il existe un moyen de rentabiliser un peu plus chaque requête en passant à des bloc de vingt valeurs (rajouter le hint %_HINTS ORACLE '&max_blocking_factor 20&'
), sans que j’en connaisse l’efficacité réelle.
Pourquoi faire simple (un bête curseur Oracle qui parcourt les lignes ramenées par paquets de quelques dizaines, avec un plan d’exécution mitonné pour l’intégrale du recordset à récupérer) quand on peut faire compliqué ?
Notes
[1] Sisi, un jour, je tenterai de faire un billet sur les gadgets sympa de SAP et de l’ABAP. Il y en a. Ajout de 2008 : C’est fait !
[2] Le plus terrible est de générer des requêtes en dynamique, réellement différentes, sans variables bind.
[3] Et c’est un des rares et précieux exemples de sucre syntaxique de ce langage fossile.
9 réactions
1 De Balise - 16/11/2006, 22:35
J'aime. Et après, en entretien : « Y a-t-il des choses que vous ne souhaitez pas faire ? - Du SAP. - Ah, pouquoi ça ? ».
2 De Le webmestre - 17/11/2006, 09:41
Balise>Effectivement, mon entretien annuel, ça a donné à peu près ça :o)
Et j’en ai tiré les conséquences puisque je pars et du client et de ma SSII trop spécialisée sur SAP dans la région.
3 De bob - 19/07/2007, 15:17
le prob, c'est que tu fais autant d'accès en base que de write ...
essaye plutôt ça avant de critiquer un langage que tu ne connais pas !
DATA : matable TYPE TABLE OF vbap WITH HEADER LINE.
SELECT * FROM vbap
INTO TABLE matable
FOR ALL ENTRIES IN tab_articles
WHERE vbap~matnr = tab_articles-matnr.
LOOP AT matable.
WRITE :/ matable-matnr, matable-vbeln, matable-posnr .
ENDLOOP.
4 De Le webmestre - 22/07/2007, 19:42
@bob : Et non, il fait 5 fois moins d'accès en base que de WRITE (de lignes ramenées), mais c'est encore beaucoup trop, et c'est bien ce que je lui reproche.
Au lieu d’une requête dont chaque ligne est utilisée (bête curseur), il crée une requête différente pour cinq lignes de paramètres !
Qu’une table interne permette de contourner/éviter le problème, pourquoi pas (et ce serait après tout la moindre des choses), mais ce n’est pas forcément possible ou souhaitable, et de mon point de vue cela en rajoute encore dans la lourdeur de programmation (alors que le SELECT...ENDSELECT a le bon goût d’exister).
5 De ECIR - 13/09/2007, 22:03
Bonsoir,
remarque pertinent du webmestre: le FOR ALL ENTRIES découpe les requêtes par plage de 5 valeur contenues dans la table interne. si la table interne contient 2000 enregistrements, il y a aura donc 400 requêtes SQL exécutées.
Il y a plusieurs solutions possibles:
- utiliser le select ... endselect, tel que c'est recommandé dans la transaction SE30
- utiliser les possibilités de jointures, de sous-requêtes
(:))
6 De Le webmestre - 14/09/2007, 07:39
@ECIR : Le SELECT...ENDSELECT, c'est ce que j'utilisais justement... Quant aux jointures, évidemment, comme en SQL il vaut mieux tenter de tout faire en une seule requête. Par contre c'est loin d'être toujours possible, et l'ABAP n'a pas la souplesse du SQL quand il s'agit de faire des « requêtes de feu » (écrire à la main des requêtes SQL de 2 pages avec 5 niveaux de sous-select ne me fait pas peur, donc je suis assez exigeant sur le sujet, mais quand même).
7 De taryck - 12/03/2009, 18:04
Je pense que le point qui a été “zappé” est simplement que le code SQL en ABAP est de l’OPEN SQL transcrit en SQL-Natif Oracle, mais pas seulement. Il est donc possible que certaines limites présentes sur les autres BdD supportées par SAP amène à cette limite constatée de 5 clés.
Même si je n’ai aucune idée de la raison…
8 De Le webmestre - 12/03/2009, 21:46
@taryck : Oui, en gros c’est le problème : le SQL généré par l’ABAP est « basique » et ne tient pas compte de la puissance de la base sous-jacente et la sous-utilise…
9 De taryck - 16/11/2011, 15:51
Le paramètre par défaut de 5 est dans le paramètre base de donnée : rsdb/max_in_blocking_factor & rsdb/max_blocking_factor
modifiable via RZ11, voir note SAP 881083.
On peut ainsi faire monter pour tout les FOR ALL ENTRIES la taille des paquets de 5 à xx. Chez nous 50.
Pour certaines requettes (celle ou on a la clé primaire par exemple) on peux juger que 50 serait alors trop faible, mais que pour ces requêtes là. On utilise alors le %_HINTS ORACLE avec ;
- '&max_blocking_factor 20&'
- ou '&max_in_blocking_factor 20&'
SAP ne peut être plus puissant que la base de donnée qu'il utilise pour des extractions seules.
Donc dans ton cas :
devrait avoir l'efficacité souhaitée.