BLOGS
La petite monnaie
Depuis la parution de L'effet papillon de nombreux lecteurs me réclament une suite : et bien la voici !
Dans ce billet, je vous propose à nouveau de commenter certains exemples de codes que je rencontre fréquemment dans les applications 4D que j'audite et qui peuvent être améliorés avec très peu d'effort et beaucoup d'efficacité. J'ai aussi révisé mon Benabar... (mais je précise que mes citations de l'artiste ne sont que stylistiques... Que personne ne se sente visé ou ne se vexe si quelques lignes de son code ressemblent à mes "contre-exemples"... Le code parfait n'existe pas, of course...). Allez, c'est parti !
Rien que le début ça fait un petit peu flipper ...
Effectivement, regardons les lignes suivantes :
CHERCHER DANS SELECTION([Personne]:[Personne]Ville="Nantes";*)
CHERCHER DANS SELECTION([Personne]; | ;[Personne]Ville="La Roche-sur-Yon")
Nous avons ici une première recherche qui retourne la sélection des personnes de plus de 30 ans, puis une seconde recherche pour extraire de cette sélection les personnes résidant à Nantes ou à La Roche-sur-Yon. Il y a donc deux échanges entre le serveur et le client, la constitution de deux sélections, le chargement de deux enregistrements courants... Cependant, la moitié des informations échangées entre le client et le serveur ne le sont que parce que le code est ainsi écrit, mais ne présente pas d'intérêt particulier. Il serait donc judicieux de rassembler les deux recherches en une seule. Vous allez me dire : "mais c'est un OU donc on ne peut pas faire autrement ...". Certes, certes, mais on peut quand même écrire :
CHERCHER([Personne]; | ;[Personne]Ville="La Roche-sur-Yon";*)
CHERCHER([Personne]; & ;[Personne]Age>30)
Vous pouvez vérifier que le résultat est strictement identique, à ceci près que le travail effectué est beaucoup plus rapide. Le gain est-il substantiel ? Cela va dépendre de nombreux facteurs, mais l'on peut tabler sur un facteur 2 sans excès d'optimisme. Si en plus le réseau est chargé ou de type WAN, le gain sera plus important.
Pourquoi cette syntaxe fonctionne-t-elle comme souhaité ? Lors de la lecture de la recherche, 4D prend la première ligne, puis effectue l'opération avec la deuxième ligne (le OU) puis fait l'opération des deux lignes précédentes avec la troisième (le ET).
En général, à ce moment-là on me demande : "C'est quand même dommage que 4D ne gère pas les parenthèses". Mais si ! il les gère :
Dans ce cas, il y a une seule requête entre le client et le serveur, et la vitesse est similaire à la recherche précédente.
Il est vrai que la commande CHERCHER PAR FORMULE est rayée du vocabulaire de nombreux développeurs car elle était peu performante. Mais ce temps est révolu et vous pouvez tirer parti de sa pleine puissance, désormais. Elle utilise aussi les index, ce qui augmente encore les performances pour les syntaxes que je propose.
Supposons que le champ [Personne]Age soit indexé. Dans la syntaxe courante nous commençons par une recherche indexée, puis nous faisons une recherche séquentielle dans la sélection. Dans la syntaxe que je recommande, le champ indexé est situé en dernier. Hérésie, me direz-vous ! On pourrait craindre un impact néfaste sur les performances... Pour en avoir le cœur net utilisons la nouvelle commande apparue en v11 : DECRIRE EXECUTION RECHERCHE. Voici le code mis en œuvre :
DECRIRE EXECUTION RECHERCHE(Vrai)
CHERCHER([Personne]:[Personne]Ville="Nantes";*)
CHERCHER([Personne]; | ;[Personne]Ville="La Roche sur Yon";*)
CHERCHER([Personne]; & ;[Personne]Age>30)
$plan:=Lire dernier plan recherche(Description format Texte)
DECRIRE EXECUTION RECHERCHE(Faux)
Après exécution du code voici ce que donne la variable $plan :
And
Personne.Ville = Nantes
Or
Personne.Ville = La Roche sur Yon
A travers cette information nous constatons que l'index est bien utilisé et son appel est prioritaire par rapport aux champs non indexés. Ainsi 4D optimise la requête en utilisant l'index afin de réduire au maximum la recherche séquentielle à suivre.
Qu'en sera-t-il si c'est l'autre champ qui est indexé ou les deux ? Je vous laisse faire le test dans le moteur de 4D... Emmenées de main de maître par la reine de la danse ...
Un murmure devient vacarme ...
Voici un code fréquemment rencontré sur le terrain :
APPELER 4D
Fin tant que
AJOUTER A TABLEAU(<>_objet_id;dernier_id)
EFFACER SEMAPHORE("ListeObjets")
Ce code permet, par exemple, de conserver dans un tableau interprocess les derniers ID (identifiants) des objets créés sur le poste client afin de faciliter des actions à suivre : historique de navigation, synchronisation, archivage, statistiques, calculs de performance...
A priori, rien à redire (cf Espèce de sémaphore !). En y réfléchissant bien, vous avez devant vous un grand classique de l'effet papillon : avertir le serveur d'une action qui ne concerne que le client. En effet le sémaphore "ListeObjets" est un sémaphore global donc géré par le serveur. Cela nécessite au minimum deux échanges entre le client et le serveur : un pour poser le sémaphore, et un autre pour le libérer. Et ceci pour rien du tout car le tableau interprocess <>_objet_id est entièrement géré sur le client. Un simple sémaphore local aurait donc largement suffit :
APPELER 4D
Fin tant que
AJOUTER A TABLEAU(<>_objet_id;dernier_id)
EFFACER SEMAPHORE("$ListeObjets")
Le signe $ au début d'un nom de sémaphore limite la portée de ce dernier au poste client ; il n'y a donc plus de communication entre le client et le serveur.
Si, de plus, ce code est exécuté très régulièrement vous avez là une source de ralentissement de la base totalement inutile. En effet, si le serveur ne voit pas le tableau interprocess d'un client, un autre client le voit encore moins. Dans ce cas pourquoi demander à un autre client d'attendre la libération d'un sémaphore global posé par un client tiers, si les tableaux inter-process de ces deux clients sont totalement étanches ?
Voici donc une source très fréquente de bruit inutile sur le réseau. A cause de toi on fait un détour d'une centaine de bornes, si peu ...
Cachons-nous sous les draps ...
Oui cachons sous les draps du client ce qui ne concerne que le client. Comme dans le cas précédent, inutile de porter à la connaissance du serveur ce qui ne concerne que le client, comme par exemple l'interface. Citons le classique process créé pour gérer l'affichage d'un thermomètre de progression d'une action. Ce thermomètre est géré par un process dédié auquel on passe des messages pour qu'il affiche un suivi de l'action très utile pour faire patienter l'utilisateur. En aucun cas ce process ne réalise d'action (autre que mettre à jour les informations du thermomètre), et encore moins ne manipule de données. Pourquoi alors lui donner un attribut global qui oblige le serveur à en garder une copie (au cas où des données soient demandées, ce qui n'arrivera pas). La création d'un process est une opération coûteuse, mais son maintien sur le serveur peut s'avérer réellement pénalisant même s'il ne fait absolument rien. En effet pour chaque process sur le client, deux process jumeaux sont déclarés sur le serveur, des piles pour recevoir les variables sont initialisées...
Là aussi un simple $ pour débuter le nom du process permettra de limiter sa portée (et surtout sa gestion) au client.
Si ça se trouve je me trompe ...
La lecture de la documentation est parfois piégeuse. En effet, il y a souvent plus d'informations qu'on ne le pense. On consulte la doc lorsqu'on a besoin d'une précision sur une commande (souvent la syntaxe), en ignorant totalement ce qui ne nous préoccupe pas à l'instant présent. Après coup, on peut se rendre compte de la présence d'une information capitale, et on s'en mord parfois les doigts...
Trouverez-vous l'anomalie du code suivant, très répandu dans la vraie vie, pour cause de doc trop vite lue ?
TRIER TABLEAU ($_region)
Solution : la commande VALEURS DISTINCTES retourne naturellement un tableau trié. Trier le tableau une nouvelle fois ne peut que ralentir inutilement l'exécution ... (Au fait cette propriété de la commande VALEURS DISTINCTES me sert de support à la détection des éventuels index corrompus. J'explique tout cela dans une notre technique déjà assez ancienne : Détecter des erreurs dans les index en cours d'utilisation).
À quatre sur la banquette arrière
Il y a toujours plusieurs façons de faire la même chose, mais pas toutes avec la même efficacité !
Supposons que vous désiriez projeter une sélection d'une table vers une autre. Par exemple vous avez une table de clients devant être rappelés. Dans chaque enregistrement client vous avez l'identifiant du commercial dédié au client. Vous désirez obtenir la liste des commerciaux concernés pour les prévenir qu'ils ont des actions à mener. Face à cette situation la réponse la plus fréquemment rencontrée est :
SELECTION VERS TABLEAU([Client]ID_Commercial;$_id_commercial)
CHERCHER PAR TABLEAU([Commercial]ID;$_id_commercial)
À quatre sur la banquette arrière
À six dans une petite voiture
On tourne maintenant depuis deux heures ...
Effectivement, l'image est appropriée ! Le tableau $_id_commercial risque fort de contenir de multiples instances de chaque identifiant ? Sauf dans le cas ou chaque commercial ne s'occupe que d'un client (ce qui est rare comme situation), ie tableau $_id_commercial sera bien plus gros que nécessaire.
"On tourne maintenant depuis deux heures", car le traitement de la commande SELECTION VERS TABLEAU peut prendre beaucoup de temps pour construire le tableau avec les multiples valeurs répétées.
Regardons à présent ce code corrigé :
VALEURS DISTINCTES([Client]ID_Commercial;$_id_commercial)
CHERCHER PAR TABLEAU([Commercial]ID;$_id_commercial)
Le résultat sera strictement le même. Par contre le tableau $_id_commercial généré sera bien plus petit. N'oubliez pas que ce tableau est généré sur le serveur et transmis au client suite aux commandes SELECTION VERS TABLEAU ou VALEURS DISTINCTES. Un fois sur le client le tableau est renvoyé au serveur pour l'utilisation de la commande CHERCHER PAR TABLEAU. Donc plus le tableau est petit, moins cela demandera de ressources réseau ! Nous sommes en plein dans l'effet papillon ...
De plus la commande VALEURS DISTINCTES est bien plus rapide car non seulement elle doit faire bien moins de choses, mais en plus, contrairement à SELECTION VERS TABLEAU, elle n'est pas polymorphe et ne sait faire qu'une seule chose.
Bref, l'optimisation de code 4D, c'est un vrai bonheur
et comme j'sais plus qui disait...
le bonheur ça s'trouve pas en lingot
mais en p'tite monnaie.















Bonsoir Pierre-Yves,
Désolé, mais cela prends du temps d'écrire ce genre de billet, et j'étais en vacances en juillet :-)
Il serait utile de voir le plan de recherche pour s'assurer que le résultat est le même, ou non dans ce cas.
@ Pierre-Yves
VALEURS DISTINCTES sera toujours plus rapide que SELECTION VERS TABLEAU, mais par contre le résultat sera toujours trié. L'attribut unique ne changera rien.
On peux combiner un CHERCHER et un CHERCHER PAR FORMULE mais seulement en tant que deux recherches distinctes. On ne peut pas finir un CHERCHER par un CHERCHER PAR FORMULE.
Bonjour Anne,
Effectivement, si la sélection ne comporte que des valeurs uniques, alors les deux commandes créeront le même tableau résultant. Du moins la même taille et les mêmes éléments. Par contre l'ordre ne sera pas le même. En effet avec la commande VALEURS DISTINCTES les valeurs seront triées dans le tableau, alors que la commande SELECTION VERS TABLEAU pourra proposer un ordre totalement différent (ce la sera en fait l'ordre de la sélection courante).
Personnellement je préconise de favoriser l'emploi de la commande VALEURS DISTINCTES dès que possible. Le seul cas ou ce n'est pas possible c'est quand l'ordre de la sélection courante doit être préservé.
@Pierre-Yves : Malheureusement, CHERCHER PAR TABLEAU n'est pas cumulable par des étoiles. Par contre il y a la commande CHERCHER PAR TABLEAU DANS SELECTION.
Poster un nouveau commentaire