Techniques avancées avec AlaSQL

Magazine
Marque
GNU/Linux Magazine
Numéro
253
Mois de parution
novembre 2021
Spécialité(s)


Résumé

Dans un précédent article [1], j’avais présenté le projet AlaSQL au travers de techniques SQL présentant un niveau de difficulté allant de facile à moyennement complexe. Parmi les techniques les plus complexes, nous avions vu comment manipuler des données liées à des dates d’effet. Dans ce nouvel épisode, j’ai voulu tester AlaSQL sur un panel de techniques un peu plus large, dans le but de déterminer jusqu’à quel point cet outil est en mesure de nous accompagner dans le développement de règles métier. On va voir que malgré quelques limites, AlaSQL peut couvrir de nombreux besoins avec efficacité, et même élégance (au moins, dans certains cas).


Body

Dans GLMF n° 250 [1], je vous avais présenté le projet AlaSQL. Pour les personnes qui n’auraient pas lu cet article, je précise que AlaSQL est un projet open-source implémentant un moteur SQL écrit en JavaScript. Ce projet fonctionne aussi bien dans les navigateurs que dans Node.js. Il est capable de traiter des requêtes SQL complexes, combinant plusieurs niveaux de sous-requêtes. Grâce à cela, on peut par exemple sélectionner des données soumises à dates d’effet (notamment des tarifs), c’est ce que j’avais montré dans mon précédent article. Je rappelle que AlaSQL supporte les CTE (Common Table Expression), ce qui permet d’écrire des sous-requêtes plus lisibles, et donc plus maintenables que le principe des sous-requêtes imbriquées sur plusieurs niveaux (j’évoquais ce problème dans l’article précédent, alors je n’y reviens pas ici).

Ce nouvel article se compose de trois parties :

  • une première partie dans laquelle je vais réexpliquer brièvement comment installer AlaSQL, mais aussi, et surtout, comment étendre ses capacités en créant des fonctions personnalisées, ce que l’on désigne dans certains SGBD sous l’acronyme UDF (User Defined Functions) ;
  • une seconde partie dans laquelle je vais présenter des techniques SQL relativement simples, mais appliquées à des situations pour lesquelles on ne penserait pas forcément à ce langage. J’en profiterai pour comparer certaines techniques SQL avec leurs équivalences en programmation impérative et fonctionnelle ;
  • une troisième partie dans laquelle je présenterai des techniques SQL de niveau plus avancé, qui nous permettront de pousser AlaSQL dans ses retranchements, et même de contourner certaines de ses limites.

1. Rappel sur les bases de AlaSQL

1.1 Installation du projet

Juste un rappel très bref, avant d’entrer dans le vif du sujet, sur l’installation du projet. Vous pouvez utiliser AlaSQL dans votre navigateur en le chargeant localement ou via un CDN :

<script src="https://cdn.jsdelivr.net/npm/alasql@1.7"></script>

Côté Node.js, vous pouvez installer AlaSQL sous forme de package, via l’une des techniques suivantes :

$ npm install alasql     # chargement du projet en local
$ npm install -g alasql # chargement global pour utilisation en ligne de commande

Vous pouvez aussi l’installer via yarn, mais c’est expliqué dans la doc officielle.

Et une fois que vous avez installé AlaSQL, vous pouvez l’utiliser dans vos scripts Node.js via un require, comme ceci :

let alasql = require('alasql');

Dans cet article, je vous présenterai des exemples écrits dans un environnement Node.js, mais retenez que tous ces exemples peuvent être réutilisés côté navigateur.

1.2 Ajout d’extensions « maison »

Dans mon précédent article, j’avais évoqué le cas de calculs présentant des erreurs d’arrondi. Par exemple, sur une requête d’augmentation de tarif, on obtenait parfois des valeurs assez moches, comme dans le cas du produit 80 ci-dessous :

[
  { produit_id: 80, date_effet: '2021-07-01', prix: 481.95000000000005},
  { produit_id: 82, date_effet: '2021-07-01', prix: 613.2 },
  { produit_id: 31, date_effet: '2021-07-01', prix: 1391.25 }
]

Le prix affiché ci-dessus était la résultante du prix antérieur multiplié par 1,05.

Ce problème d’arrondi n’est pas spécifique à JavaScript, car on le retrouve dans de nombreux langages de programmation, il est lié à la manière dont ces langages manipulent les nombres réels. Pour obtenir des prix plus présentables, je vous propose d’étendre les capacités de AlaSQL, car ce dernier nous offre la possibilité d’enrichir son vocabulaire par l’ajout de fonctions personnalisées.

Dans l’exemple qui suit, j’ajoute à AlaSQL une fonction xNumDec, qui va recevoir en entrée 2 paramètres, le premier étant la valeur que l’on souhaite modifier, le second paramètre (optionnel) étant la précision maximale souhaitée en termes de nombre de décimales :

alasql.fn.xNumDec = (value, prec=2) => {
    let mult = Math.pow(10, prec);
    return Math.round(parseFloat(value) * mult) / mult;
};

Si j’ai choisi de préfixer ma fonction par un « x », c’est avant tout pour éviter tout risque de conflit avec les fonctions standard de AlaSQL. De plus, avec ce « x », on comprend tout de suite qu’il s’agit d’une fonction externe, cela peut aider lors de la relecture de code SQL complexe.

En fixant une valeur par défaut à 2 pour le paramètre prec (comme « précision »), et sachant que mon objectif est de manipuler essentiellement des prix, cela m’évite de préciser la valeur 2 à chaque appel de la fonction. Je n’utiliserai ce second paramètre explicitement que lorsque j’aurai besoin d’un nombre de décimales différent (par exemple 3 ou plus).

Grâce à cette technique, je peux écrire la fonction SQL suivante :

SELECT produit_id, '2021-07-01' as date_effet, xNumDec(prix * 1.05) as prix
FROM tarifs
WHERE ...

… et je vais obtenir en sortie de nouveaux tarifs « nickel-chrome » :

[
  { produit_id: 80, date_effet: '2021-07-01', prix: 481.95},
  { produit_id: 82, date_effet: '2021-07-01', prix: 613.2 },
  { produit_id: 31, date_effet: '2021-07-01', prix: 1391.25 }
]

Pour vous éviter de devoir redéclarer la fonction xNumDec dans tous vos scripts, vous pouvez écrire un script de chargement et de personnalisation de AlaSQL, comme dans l’exemple suivant :

const alasql = require('alasql');
 
alasql.fn.xNumDec = (value, prec=2) => {
    let mult = Math.pow(10, prec);
    return Math.round(parseFloat(value) * mult) / mult;
};
 
alasql.fn.xLN = (a) => {
    return Math.log(a);
};
 
module.exports = {
    alasql: alasql,
};
 

Dans vos scripts suivants, au lieu de charger directement AlaSQL (via la fonction require), vous chargerez le script ci-dessus, que vous étendrez en fonction de vos besoins.

Dans l’exemple ci-dessus, vous constaterez qu’en plus de la fonction xNumDec, j’ai ajouté une fonction de calcul de logarithme, la fonction xLN. Nous l’utiliserons dans un instant.

Je conclurai cette partie en disant que vous pouvez aussi enrichir AlaSQL en ajoutant des fonctions personnalisées issues de librairies tierces comme Numeral.js [2]. Cet autre projet est particulièrement intéressant si vous avez besoin de présenter certaines valeurs numériques selon des formats particuliers. Vous pouvez aussi étendre les capacités de AlaSQL au niveau des fonctions de manipulation de dates, en important des fonctions venant du projet Moment.js, tel qu’expliqué dans le wiki de AlaSQL[3].

À propos du wiki [4] justement, je recommande vivement sa lecture, mais il n’est pas évident de s’y retrouver au premier abord, surtout si vous débutez sur AlaSQL. Je vous conseille de regarder d’abord les parties qui s’intitulent Supported SQL Statements, ainsi que SQL-99 Keywords, cela vous donnera un bon aperçu des possibilités de l’outil. La page Functions est également intéressante, mais malheureusement elle est incomplète, car quand on parcourt les « Issues » du projet, on s’aperçoit que certaines fonctions ont été ajoutées dans l’outil sans être référencées dans la documentation officielle.

2. Du SQL là où on ne l’attendrait pas

Dans les mini études de cas qui suivent, on verra que le langage SQL peut répondre avec une certaine élégance à des problématiques pour lesquelles on ne penserait pas à lui au premier abord.

2.1 Du bon calcul de ses intérêts

Supposons que vous ayez placé de l’argent sur 4 ans, avec un rendement variable. Les taux d’intérêt de ces 4 années sont les suivants :

const interets = {
        "datas": [
            {"an": 2020, "taux": 0.05},
            {"an": 2021, "taux": 0.04},
            {"an": 2022, "taux": 0.05},
            {"an": 2023, "taux": 0.03}
        ]
    };

Pour connaître le rendement effectif de notre placement, il faut multiplier le capital de départ par la formule suivante : 1,05 * 1,04 * 1,05 * 1,03, soit un taux effectif de 1,180998.

Donc, si notre capital de départ est de 100 euros, en multipliant ces 100 euros par la formule qui précède, on obtient la somme approximative de 118 euros et des poussières (j’ai arrondi). Le rendement effectif du placement est donc de 118 – 100 = 18 euros.

Comment calculer cela en SQL, sachant que l’on doit calculer le produit de certaines valeurs (les taux), valeurs qui se trouvent sur des lignes différentes (à raison d’une ligne par année). Or, si la norme SQL fournit une fonction d’agrégation SUM (pour calculer les sommes), elle ne fournit pas d’équivalent pour calculer les produits.

Heureusement, il existe une formule mathématique qui résout notre problème, et qui est facilement transposable en SQL. Elle consiste à calculer la somme du logarithme naturel - aussi appelé logarithme népérien - de chacun des taux. Une fois cette somme obtenue, on calcule son exposant et on obtient le même taux que tout à l’heure, à savoir 1,180998.

Du coup, en SQL, ça donne ceci :

const query2 = `SELECT EXP(SUM(xLN(1 + taux))) * 100 as capital FROM ?`;
const capital2A = alasql(query2, [interets.datas]);
console.log(capital2A); //=> [ { capital: 118.09980000000002 } ]

Je rappelle que la fonction xLN est une fonction personnalisée que nous avons créée dans la partie qui précède. Elle utilise en interne la fonction JavaScript Math.log, fonction qui renvoie le logarithme naturel d’un nombre.

Voilà, en seulement trois lignes, dont une de code SQL, on a obtenu le résultat souhaité.

Pour vous aider à y voir plus clair, je me suis amusé à recréer le même calcul en pur JavaScript, donc sans l’aide de SQL, et selon deux approches : l’approche impérative et l’approche fonctionnelle.

Voici ce que cela donne avec l’approche impérative, approche parfois qualifiée de « old school » :

let somme = 0;
interets.datas.forEach(data => {
    somme += Math.log(1 + data.taux);
});
let capital2B = Math.exp(somme) * 100;
console.log(capital2B); //=> 118.09980000000002

Et voici un équivalent selon l’approche fonctionnelle :

const reducer = (accumulator, currentItem) => accumulator + Math.log( 1 + currentItem.taux);
let capital2C = Math.exp(interets.datas.reduce(reducer, 0)) * 100;
console.log(capital2C); //=> 118.09980000000002

Je ne sais pas ce que vous en pensez, mais personnellement j’aime bien l’approche SQL. Je trouve qu’elle permet de prendre de la hauteur et de comprendre très rapidement la logique métier utilisée. Logique métier qui en l’occurrence tient en une seule ligne :

const query2 = `SELECT EXP(SUM(xLN(1 + taux))) * 100 as capital FROM ?`;

Avec les approches impératives et fonctionnelles, j’ai l’impression de devoir faire un effort supplémentaire pour dégager la logique métier, même si je reconnais que cet effort est moindre avec la logique fonctionnelle.

Je vous propose d’aller un peu plus loin dans notre exemple, avec le même tableau de taux d’intérêt en entrée. Plutôt que de faire la somme des logarithmes naturels, je vais dans un premier temps me contenter de les énumérer avec la requête ci-dessous :

const query1 = `SELECT an,
   1 + taux AS facteur,
   xLN(1 + taux) AS log
FROM ?`;
const datas1A = alasql(query1, [interets.datas]);

… ce qui nous donne le résultat suivant :

[
    { an: 2002, facteur: 1.05, log: 0.04879016416943205 },
    { an: 2003, facteur: 1.04, log: 0.03922071315328133 },
    { an: 2004, facteur: 1.05, log: 0.04879016416943205 },
    { an: 2005, facteur: 1.03, log: 0.02955880224154443 }
]

Maintenant, ce serait intéressant de connaître le taux d’intérêt effectif cumulé, d’année en année. Par exemple, en 2003, il engloberait les taux de 2002 et 2003… en 2004, il engloberait les taux de 2002, 2003 et 2004. Cela nous permettrait de connaître le rendement de notre placement sur différentes périodes, au cas où on envisagerait de le stopper avant terme.

Pour réussir ce tour de force, en SQL, nous devons faire appel à une sous-requête scalaire, c’est-à-dire une sous-requête qui renvoie une seule ligne et une seule colonne. C’est important qu’elle soit scalaire, car nous souhaitons placer cette sous-requête à l’intérieur du jeu de colonnes renvoyées par la clause SELECT de la requête. La sous-requête, vous la connaissez déjà en partie, car pour l’écrire je vais me baser sur la requête que nous avions vue précédemment, à savoir :

const query2 = `SELECT EXP(SUM(xLN(1 + taux))) * 100 as capital FROM ?`;

Voici le code de la nouvelle requête, incluant sous forme de sous-requête scalaire la requête précédente, légèrement remaniée :

const query3 = `SELECT a.an,
   1 + a.taux AS facteur,
   (SELECT EXP(SUM(xLN(1 + b.taux))) * 100 as capital FROM ? b WHERE b.an <= a.an) as logcum
FROM ? a
`;
const datas3A = alasql(query3, [interets.datas, interets.datas]);
console.log(datas3A);

Il y a plusieurs points importants à souligner ici :

  • la sous-requête est placée entre parenthèses et le nom de la colonne produite par cette sous-requête s’appelle logcum ;
  • la table principale est préfixée par a, et la table de la sous-requête est préfixée par b, ce qui va permettre d’identifier précisément l’origine de chaque colonne. C’est d’autant plus important que les deux tables a et b sont générées dynamiquement à partir du même jeu de données (elles ont donc les mêmes propriétés). Ce jeu de données est transmis deux fois dans le second paramètre de la fonction alasql. Je rappelle que ce second paramètre est obligatoirement un tableau, pouvant contenir lui-même d’autres tableaux, comme c’est le cas ici.
  • la clause WHERE de la sous-requête est stratégique, car elle va nous permettre de filtrer les lignes renvoyées par la sous-requête scalaire, de manière à ne retenir que celles dont l’année est inférieure à l’année courante de la table principale.

À l’exécution, on obtient le résultat suivant :

[
  { an: 2020, facteur: 1.05, logcum: 105 },
  { an: 2021, facteur: 1.04, logcum: 109.2 },
  { an: 2022, facteur: 1.05, logcum: 114.66000000000001 },
  { an: 2023, facteur: 1.03, logcum: 118.09980000000002 }
]

C’est franchement pas mal, mis à part les erreurs d’arrondi sur deux valeurs de la colonne logcum.

Pour corriger ce problème d’arrondi, nous pouvons appliquer notre fonction xNumDec sur le résultat de la sous-requête, comme ceci :

xNumDec( (SELECT EXP(SUM(xLN(1 + b.taux))) * 100 as capital FROM ? b WHERE b.an <= a.an) ) as logcum

Attention aux parenthèses : pour que AlaSQL fonctionne convenablement, il est nécessaire de les doubler, comme dans l’exemple ci-dessus. Essayez avec un seul jeu de parenthèses au lieu de deux, et vous obtiendrez un beau message d’erreur.

Nous retrouverons la technique de la sous-requête scalaire dans un autre exemple, plus complexe. C’est une technique qui offre beaucoup de souplesse, et d’ailleurs certaines opérations ne peuvent être réalisées que de cette façon. Cette technique est décriée par certains experts SQL, parce qu’elle entraînerait une dégradation des performances. Très honnêtement, je l’ai utilisée sur des SGBD aussi différents que DB2 et MySQL (dont MariaDB), ainsi que sur PostgreSQL, et je n’ai jamais constaté de problème avec cette technique (à condition que les bons index soient en place).

Par contre, j’ai rencontré un petit souci avec cette technique sur AlaSQL. En effet, dans le contexte de AlaSQL, l’utilisation de sous-requêtes scalaires à l’intérieur de CTE provoque des erreurs. J’ai l’impression que AlaSQL se prend les pieds dans le tapis, au niveau du référencement des tables utilisées dans des sous-requêtes scalaires. Malgré cette réserve, vous reconnaîtrez avec moi que cette technique est drôlement pratique, et que nous avons résolu notre problème avec une certaine élégance (de mon point de vue).

J’espère que ce petit échauffement vous a plu, et que vous êtes prêt à passer à la vitesse supérieure. Dans l’exemple qui suit, je vous propose d’appliquer au SQL un concept très prisé dans les startups, ainsi que dans les cours de danse, j’ai nommé : le pivot.

2.2 L’art de bien pivoter

Vous avez des données en lignes et vous voulez les redisposer en colonnes ? Faites un pivot SQL. Si vous ne connaissez pas, vous allez voir, c’est plutôt amusant à faire, et AlaSQL se prête très bien à l’exercice.

Supposons que vous vous occupiez d’un centre de formation, et que vous deviez produire des tableaux de synthèse avec les résultats de vos étudiants. Vous avez par exemple une liste d’étudiants, avec leurs noms, les matières et les notes obtenues, qui se présente ainsi :

[
  { etudiant: 'Christophe', cours: 'Java', note: 80 },
  { etudiant: 'Christophe', cours: 'SQL', note: 77 },
  { etudiant: 'Christophe', cours: 'Python', note: 50 },
  { etudiant: 'Wilfrid', cours: 'Java', note: 62 },
  { etudiant: 'Wilfrid', cours: 'SQL', note: 95 },
  { etudiant: 'Wilfrid', cours: 'Python', note: 63 }
]

Notez que dans la « vraie vie », on aurait sans doute mis des identifiants dans les colonnes etudiant et cours, histoire d’éliminer certaines redondances et d’avoir une base de données mieux normalisée, mais pour les besoins de l’exercice, ce jeu de données convient parfaitement.

Maintenant, vous aimeriez reformater ces données de manière à n’avoir plus qu’une ligne par étudiant, avec pour chacune de ces lignes les matières disposées en colonnes. En résumé, vous avez besoin de faire pivoter les colonnes cours et note pour obtenir le résultat suivant :

[
  { etudiant: 'Christophe', java: 80, bd: 77, python: 50 },
  { etudiant: 'Wilfrid',    java: 62, bd: 95, python: 63 }
]

Pour exécuter un pivot, il existe deux techniques que je vais vous présenter dans un instant. Mais avant toute chose, commençons par créer notre table des notes. Nous allons créer la table et insérer des données dedans, avec une technique « deux en un » que j’avais présentée dans mon précédent article sur AlaSQL :

alasql(function(){/*
CREATE TABLE notes (
    etudiant CHAR (20 ),
    cours CHAR (20 ),
    note INTEGER
    );
INSERT INTO notes (etudiant, cours, note) VALUES
    ('Christophe', 'Java', 80),
    ('Christophe', 'SQL', 77),
    ('Christophe', 'Python', 50),
    ('Wilfrid', 'Java', 62),
    ('Wilfrid', 'SQL', 95),
    ('Wilfrid', 'Python', 63);
*/});

Nous allons créer aussi une table des étudiants, table qui nous sera utile par la suite. Pour se faciliter la tâche, on va générer cette table des étudiants à partir des données se trouvant dans la table des notes :

alasql(function(){/*
CREATE TABLE etudiants (
    etudiant CHAR (20 )
    );
INSERT INTO etudiants (etudiant)
SELECT DISTINCT etudiant FROM notes;
*/});

Nos données étant en place, je ne vais pas vous faire lanterner plus longtemps, voici la première technique de pivot :

const query1 = `
SELECT a.etudiant, jav.note as java, bas.note as bd, pyt.note as python
FROM etudiants a
LEFT OUTER JOIN notes jav
   ON (a.etudiant = jav.etudiant AND jav.cours = 'Java')
LEFT OUTER JOIN notes bas
   ON (a.etudiant = bas.etudiant AND bas.cours = 'SQL')
LEFT OUTER JOIN notes pyt
   ON (a.etudiant = pyt.etudiant AND pyt.cours = 'Python')`;
 
const pivot1 = alasql(query1);
console.log(pivot1);

La table principale, c’est la table des étudiants, et comme nous avons trois matières à traiter, nous avons trois jointures quasi identiques entre la table des étudiants et la table des notes. Ce qui change à chaque fois, c’est le préfixe attribué à chaque occurrence de la table des notes (jav, bas et pyt). Et l’autre élément qui change à chaque fois, c’est la clé de jointure spécifique à chaque matière (Java, SQL, Python).

Dans la clause SELECT de la requête, on notera que nous avons donné un alias différent à chacune des notes, pour être sûrs de bien les distinguer.

Voilà, c’est tout. Si vous exécutez le code ci-dessus, vous obtiendrez le résultat suivant, qui correspond bien à nos attentes :

[
  { etudiant: 'Christophe', java: 80, bd: 77, python: 50 },
  { etudiant: 'Wilfrid',    java: 62, bd: 95, python: 63 }
]

Voici maintenant la seconde technique de pivot, tout aussi efficace, mais peut être plus déroutante :

const query2 = `
SELECT a.etudiant,
  MAX(CASE WHEN b.cours = 'Java' THEN b.note ELSE 0 END) as Java,
  MAX(CASE WHEN b.cours = 'SQL' THEN b.note ELSE 0 END) as BD,
  MAX(CASE WHEN b.cours = 'Python' THEN b.note ELSE 0 END) as Python
FROM etudiants a
LEFT OUTER JOIN notes b
ON (a.etudiant = b.etudiant)
GROUP BY a.etudiant `;
const pivot2 = alasql(query2);
console.log(pivot2);

La subtilité ici, c’est que l’on n’a plus qu’une seule jointure entre la table des étudiants et la table des notes. Donc pour faire pivoter nos notes, nous utilisons la clause CASE WHEN qui nous permet de « ventiler » les notes sur les bonnes colonnes du jeu de données résultant. L’élément qui peut sembler le plus déroutant ici, c’est l’usage qui est fait de la fonction MAX. À quoi sert-elle ? Pour le comprendre, je vous propose de supprimer la fonction MAX sur les trois lignes (tout en gardant les CASE WHEN) et de réexécuter le code, vous devriez obtenir ceci :

[
  { etudiant: 'Christophe', Java: 0, BD: 0, Python: 0 },
  { etudiant: 'Wilfrid', Java: 0, BD: 0, Python: 0 }
]

Eh oui, toutes les notes sont à zéro ! Les étudiants risquent de ne pas apprécier. En fait, le CASE WHEN renvoie deux données, qui sont soit zéro, soit la note correspondant à une matière donnée. Ce qui nous intéresse, c’est la valeur la plus élevée entre ces deux données, donc la note effective de chaque étudiant dans chaque matière. La fonction MAX est donc un subterfuge permettant de récupérer la bonne donnée.

Dans notre cas, on pourrait remplacer la fonction MAX par la fonction SUM, on obtiendrait strictement le même résultat du fait de la structure de notre jeu de données. Le résultat serait bien évidemment différent si certains étudiants avaient plusieurs notes pour une même matière.

J’en vois d’ici qui vont me dire : « dis donc, ta technique là... elle utilise des valeurs en dur (Java, Python, etc.). Ce n’est pas très souple, ni très évolutif. Si on ajoute de nouvelles matières, ça devient carrément le bazar... ».

Mmmh, ce n’est pas faux. Mais dans notre contexte, on dispose d’un atout considérable, c’est JavaScript. Grâce à lui, on peut générer dynamiquement le code SQL, sur la base de quelques critères à définir.

Supposons que nous disposions d’un tableau des matières comme celui-ci :

const matieres = [
    { "prefix": 'jav', "key": "Java", "colname": "java" },
    { "prefix": 'bas', "key": "SQL",   "colname": "bd"   },
    { "prefix": 'pyt', "key": "Python", "colname": "python" }
];

Grâce à ce tableau d’objets, nous pouvons utiliser un peu de code JavaScript pour générer le code SQL correspondant aux deux techniques que nous venons d’étudier.

Pour générer le code SQL conforme à la première technique, nous pouvons utiliser le code suivant :

let colonnes = matieres.map (item => `${item.prefix}.note as ${item.colname}`).join(', ');
let jointures = matieres.map(item => `LEFT OUTER JOIN notes ${item.prefix}
  ON (a.etudiant = ${item.prefix}.etudiant and ${item.prefix}.cours = '${item.key}')`).join('\n');
const query1 = `SELECT a.etudiant, ${colonnes}
FROM etudiants a
${jointures}`;
console.log(query1);

Pour générer le code SQL correspondant à la seconde technique, nous pouvons écrire ceci :

let colonnes2 = matieres.map (item => ` max(case when b.cours = '${item.key}'
    then b.note else 0 end) as ${item.colname}`).join(',\n');
const query2 = `SELECT a.etudiant,
  ${colonnes2}
FROM etudiants a
LEFT OUTER JOIN notes b
ON (a.etudiant = b.etudiant)
GROUP BY a.etudiant`;
console.log(query2);

Je ne remets pas ci-dessous le code SQL généré, car il est identique au code SQL que je vous ai présenté plus haut.

C’était cool cette histoire de pivot, non ?

À noter que, dans cette seconde partie de l’article, mes exemples sur les taux d’intérêt et sur les pivots sont inspirés d’exemples que j’avais trouvés dans un excellent livre de Andrew Cumming et Gordon Russell, qui s’intitule « SQL Hacks » (édité chez O’Reilly). C’est grâce à l’étude de ce livre, et à quelques autres, que j’ai pu acquérir une solide maîtrise du SQL et que j’ai été capable de développer des techniques pointues, comme celles relatives aux dates d’effet (cf. l’article précédent sur AlaSQL) et comme celles que nous allons voir dans la troisième et dernière partie de cet article.

3. Techniques SQL avancées

Pour ne rien vous cacher, je voulais vous présenter plusieurs sujets dans cette troisième partie, mais l’article étant déjà relativement long, j’ai dû faire des choix. Et j’ai finalement retenu une étude de cas que je trouve intéressante, car elle permet de manipuler un panel de techniques assez large.

3.1 Trouver les périodes de vacance

Dans un article que j’avais publié en janvier 2018 [5], j’avais expliqué comment identifier des périodes de vacance (ou d’inactivité) au sein de données SQL. J’avais résolu le problème en écrivant une seule requête SQL combinant plusieurs CTE. Dans l’une de ces CTE, j’avais utilisé des fonctions OLAP [6]. J’ai eu l’opportunité de traiter cette problématique des périodes de vacance, aussi bien avec DB2 qu’avec MariaDB. C’est d’ailleurs la version pour MariaDB que j’avais présentée dans l’article de janvier 2018.   

Je vous préviens tout de suite, la plupart des fonctions OLAP dont j’ai besoin pour cet exercice font défaut à AlaSQL. À tel point que j’ai d’abord pensé que cela ne valait pas le coup d’implémenter cela sur AlaSQL. Mais en y réfléchissant, j’ai réalisé que c’était au contraire un excellent exercice, qui allait me permettre de pousser un peu plus loin mon exploration de l’outil. Une étude de cas dans l’étude de cas, en somme...

Vous n’avez peut-être pas lu mon article de 2018, ou alors vous l’avez oublié, alors il me semble nécessaire de réexpliquer la problématique, qui est la suivante :

Comment faire pour détecter les périodes de vacance ou d’inactivité au sein de données d’entreprise ? Par exemple, une société qui loue des biens immobiliers cherchera à déterminer combien d’argent elle a perdu par rapport aux périodes de vacance (d’inoccupation) des biens qu’elle gère. Dans un autre registre, une société de services qui place du personnel en régie aura besoin de déterminer les périodes d’inactivité (on parle généralement de périodes d’intercontrat) de ses employés.

Si on prend l’exemple de logements en location, nous aurons dans notre base de données une table des occupations de logements, avec pour chaque logement une série de lignes définissant les dates d’entrée et de sortie des occupants respectifs. Si on souhaite dans ce contexte déterminer les périodes de vacance, nous devons trier notre jeu de données par référence de logement et dates d’entrée, et identifier les périodes de vacance en analysant la date de sortie de l’occupant N par rapport à la date d’entrée de l’occupant N+1. Et c’est là où ça se corse, car l’occupant N est sur une ligne SQL, et l’occupant N+1 est sur une autre ligne de la même table. Comment faire pour lier ces deux lignes et répondre à notre problématique, c’est ce que nous allons voir dans la suite.

Commençons par créer un jeu de données avec une table des occupants :

alasql(function(){/*
create table occupants (
ref_logement char(10),
date_entree date,
date_sortie date
) ;
insert into occupants
( ref_logement, date_entree, date_sortie )
values
( '11492', '2016–09–01', null ),
( '11492', '2016–05–15', '2016–07–31' ),
( '11492', '2016–02–01', '2016–04–30' ),
( '11481', '2016–12–15', '2017–03–31' ),
( '11481', '2016–08–01', '2016–10–31' ),
( '11481', '2016–02–01', '2016–02–29' ),
( '11182', '2016–11–15', '2017–01–31' ),
( '11182', '2016–01–01', '2016–10–31' );
*/});

Dans cette table, nous avons défini pour chaque logement plusieurs occupants, avec des dates d’entrée et de sortie variables. À noter que les périodes d’occupation ne se chevauchent pas, ce ne serait pas logique fonctionnellement parlant, ni très agréable pour les occupants concernés.

Par contre, on a des trous dans les périodes d’occupation, ou plus exactement on a des périodes de vacance. C’est par exemple le cas du logement 11492, pour lequel on trouve une période de vacance allant du 1er au 14 mai 2016, et une autre allant du 1er au 31 août 2016. Je vous laisse le soin de chercher les périodes de vacance des autres logements.

On a des dates de sortie à Null, ce qui n’est pas franchement pratique à gérer. Pour éviter que notre algorithme d’identification des vacances ne devienne une usine à gaz, on peut forcer ces dates Null avec une date de sortie située très loin dans le temps. On va le faire avec une requête relativement simple :

let query1 = `
WITH cte_step1 AS (
   SELECT ref_logement, date_entree, ifnull(date_sortie, '2030-12-31') AS date_sortie
   FROM occupants
   ORDER BY ref_logement, date_entree
)`;

Pourquoi j’ai écrit une CTE alors que c’est un peu superflu ici ? En fait, c’est parce que je suis reparti du code de mon article de janvier 2018, dans lequel tout était géré au travers d’une seule requête SQL incluant de nombreuses CTE. J’ai conservé ce découpage ici.

Dans mon article antérieur, l’étape suivante était gérée par une seconde CTE, utilisant les fonctions OLAP qui font défaut dans AlaSQL. Cette CTE se présentait de la façon suivante :

cte_step2 as (
  select ref_logement, date_entree, date_sortie,
   row_number() over(partition by ref_logement order by ref_logement,
                     date_entree) as rupture
  from cte_step1 a
  order by ref_logement, date_entree
)
select * from cte_step2

L’objectif de cette seconde CTE était de générer un compteur d’occupation fictif pour chaque logement, compteur stocké dans une nouvelle colonne que j’avais intitulée rupture. C’est la fonction ROW_NUMBER qui nous permettait de définir cette notion de rupture (car elle était appliquée sur la référence du logement). Cette information était enrichie, dans une troisième CTE, par l’ajout d’un pointeur vers l’occupant suivant, désigné par l’intitulé next_rupture :

cte_step3 as (
   select a.*, a.rupture+1 as next_rupture from cte_step2 a
),

… ce qui nous permettait d’obtenir à l’arrivée le jeu de données suivant :

ref_logement

date_entree

date_sortie

rupture

next_rupture

11182

2016-01-01

2016-10-31

1

2

11182

2016-11-15

2017-01-31

2

3

11481

2016-02-01

2016-02-29

1

2

11481

2016-08-01

2016-10-31

2

3

11481

2016-12-15

2017-03-31

3

4

11492

2016-02-01

2016-04-30

1

2

11492

2016-05-15

2016-07-31

2

3

11492

2016-09-01

2030-12-31

3

4

Comme nous ne disposons pas des fonctions OLAP dont nous avons besoin dans AlaSQL, pour obtenir le résultat ci-dessus, nous allons recourir à un code équivalent écrit en pur JavaScript :

function addRuptures(datas) {
    let xdatas = [];
    let rupture = {
        key: 'XXXX',
        count: -1
    };
    for (let i=0, imax=datas.length; i<imax; i++) {
        let ligne = datas[i];
        if (ligne.ref_logement == rupture.key) {
            rupture.count++;
        } else {
            rupture.key = ligne.ref_logement;
            rupture.count = 1;
        }
        ligne.rupture = rupture.count;
        ligne.next_rupture = rupture.count + 1;
        xdatas.push(ligne);
    }
    return xdatas
}
let datas2 = addRuptures(datas1);

En résumé, nous exécutons la première CTE et nous récupérons le jeu de données produit, trié dans le bon ordre, pour le passer en paramètre à la fonction addRuptures. Cette fonction va produire un jeu de données enrichi, contenant les fameuses colonnes rupture et next_rupture. Et le tour est joué.

La suite est très proche de ce que j’avais présenté dans mon article de janvier 2018, à savoir un enchaînement de CTE qui vise à comparer, pour chaque logement, la date de sortie de l’occupant N avec la date d’entrée de l’occupant N+1. Grâce à ça, on peut calculer le nombre de jours de vacance de chaque logement.

let query4 = `WITH
cte_stepx as (
select * from ? order by ref_logement, date_entree
),
cte_step4 as (
   select a.ref_logement, xAddDays(a.date_sortie, +1) as dat_deb_vacance,
     xAddDays(b.date_entree, -1) as dat_fin_vacance
     from cte_stepx a
     inner join cte_stepx b
        on a.ref_logement = b.ref_logement and a.next_rupture = b.rupture
),
cte_step5 as (
    select a.*, xDateDiff(a.dat_deb_vacance, a.dat_fin_vacance) + 1 as vacance
    from cte_step4 a
)
select * from cte_step5 where vacance > 0 ;`
 
let datas4 = alasql(query4, [datas2]);
console.log(datas4);

L’étape capitale se situe dans cte_step4, avec une jointure particulière, liant la colonne rupture de la table b avec la colonne next_rupture de la table a :

     from cte_stepx a
     inner join cte_stepx b
        on a.ref_logement = b.ref_logement and a.next_rupture = b.rupture

On voit donc que l’étape consistant à enrichir le jeu de données avec des compteurs d’occupation fictifs était réellement stratégique pour pouvoir accomplir notre mission. Sans cela, je ne vois vraiment pas comment on aurait pu s’en sortir en SQL.

Pour les besoins de l’exercice, j’ai ajouté deux nouvelles fonctions personnalisées à AlaSQL. L’une permet d’ajouter un nombre de jours à une date, et l’autre permet de calculer le nombre de jours entre deux dates :

alasql.fn.xDateDiff = (pdate1, pdate2) => {
    let wdate1 = new Date(pdate1);
    let wdate2 = new Date(pdate2);
    let diffInTime = wdate2.getTime() - wdate1.getTime();
    return diffInTime / (1000 * 3600 * 24);
};
alasql.fn.xAddDays = (pdate, inc) => {
    let split_wdate = String(pdate).split(/\W+/);
    let wdate = new Date(split_wdate[0], split_wdate[1]-1, split_wdate[2], 0, 0, 0);
    wdate.setDate(wdate.getDate() + inc + 1);
    return wdate.toISOString().substr(0, 10);
};

Et voilà le résultat final, qui est strictement identique au résultat que j’obtenais avec MariaDB, dans mon article de janvier 2018 :

ref_logement

dat_deb_vacance

dat_fin_vacance

vacance

11182

2016-11-01

2016-11-14

14

11481

2016-03-01

2016-07-31

153

11481

2016-11-01

2016-12-14

44

11492

2016-05-01

2016-05-14

14

11492

2016-08-01

2016-08-31

31

Conclusion

J’espère que les exemples que j’ai utilisés dans cet article vont ont plu, et qu’ils vous ont permis de mieux appréhender les capacités de AlaSQL, et aussi ses limites.

Je ne pense pas que l’on puisse envisager d’utiliser AlaSQL avec de très gros volumes de données, comme des centaines de milliers de lignes, par exemple. Ou alors, il ne faut pas s’attendre à des performances extraordinaires. Néanmoins, si vous êtes amené à le faire, je vous recommande de le faire dans un Worker (Web Worker côté navigateur, ou Worker Thread côté Node.js). Cela permettra de soulager le thread principal qui pourra s’atteler à d’autres tâches.

On a vu que AlaSQL ne gère pas les CTE récursives et les fonctions OLAP, mais puisqu’on est dans l’écosystème JavaScript, on peut assez facilement pallier ces manques. On l’a d’ailleurs vu dans la dernière partie, où j’ai remplacé une CTE par une fonction écrite en pur JavaScript (la fonction addRuptures).

Je vous ai indiqué aussi que les fonctions scalaires ne fonctionnaient pas correctement dans les CTE, mais je vous ai montré que l’on pouvait assez facilement contourner ce problème.

Puisqu’il faut conclure, je dirais que AlaSQL a certes quelques limites, mais qu’il apporte un large éventail de possibilités, et peut considérablement améliorer le quotidien des développeurs JavaScript, sous réserve qu’ils veuillent faire l’effort de s’approprier l’outil.

Références

[1] G. JARRIGE, « AlaSQL, un puissant moteur SQL pour dev JS », GNU/Linux Magazine n°250, juin 2021 : https://boutique.ed-diamond.com/en-kiosque/1590-gnulinux-magazine-250.html

[2] Projet Numeral.js : http://numeraljs.com/

[3] Projet Moment.js : https://github.com/agershun/alasql/wiki/Moment.js

[4] Le wiki du projet : https://github.com/agershun/alasql/wiki/

[5] G. JARRIGE, « CTE et Window Functions avec MariaDB », GNU/Linux Magazine n°211, janvier 2018 : https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-211/CTE-et-Window-Functions-avec-MariaDB

[6] OLAP : acronyme de « OnLine Analytical Processing ».



Article rédigé par

Par le(s) même(s) auteur(s)

Et si nous retrouvions l’agilité, la vraie ?

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
115
Mois de parution
juillet 2021
Spécialité(s)
Résumé

Depuis quelques années, je croise de plus en plus de jeunes développeurs et développeuses, déboussolé-e-s, qui n’en peuvent plus, et veulent désespérément sortir du monde de l’IT. Pourtant, certaines de ces personnes sont particulièrement brillantes. Pourquoi sont-elles essorées, cramées, au bout de quelques années ? J’ai remarqué que toutes travaillent en mode agile. Alors… y aurait-il quelque chose de pourri au royaume de l’agilité ?

Les derniers articles Premiums

Les derniers articles Premium

PostgreSQL au centre de votre SI avec PostgREST

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 65 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous