Archives de catégorie : Base de données

Interroger un cluster MongoDB Atlas avec C++

J’ai décidé de me replonger dans le langage C++ le temps d’un exercice destiné aux étudiants suivant ma formation sur MongoDB en école d’ingénieur. Tout d’abord, nous avons effectué toutes les requêtes de l’exercice sur les salles (PDF) à partir de l’interpréteur JavaScript du shell MongoDB, en direction de nos clusters créés sur Atlas. Ensuite, nous avons traduit ces requêtes dans le langage de programmation préféré de chaque étudiant. N’ayant pas pratiqué le C++ depuis l’époque où la monnaie nationale était encore le franc, j’ai quant à moi pris le pari de revenir à mes premiers amours et d’utiliser le langage de Stroustrup comme base…ce fut un plaisir !

Tout d’abord, il m’a fallu installer le pilote (driver) dédié à ce langage : mongocxx.

Installation et compilation du pilote mongocxx

Rien de plus simple pour y parvenir :

    cd mongo-cxx-driver
    mkdir build && cd build
    cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local ..
    make -j$(nproc)
    sudo make install

Le programme principal

Une fois le driver installé, j’ai pu commencer à coder le petit programme pour accéder à mon cluster Atlas et y exécuter toutes les requêtes préparées dans mon header exercices.hpp . Le voici :

#include <iostream>
#include <bsoncxx/json.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <mongocxx/exception/exception.hpp>
#include "exercices.hpp" 

using bsoncxx::to_json;

int main() {

    const std::string USERNAME      = "mongosensei";
    const std::string PASSWORD      = "xxxxx";
    const std::string DATABASE      = "xxxxx";
    const std::string COLLECTION    = "salles";
    const std::string ATLAS_CLUSTER = "xxx.mongodb.net";

    mongocxx::instance inst{};

    try {
        std::string uriString = "mongodb+srv://" + USERNAME + ":" + PASSWORD + "@" + ATLAS_CLUSTER + "/" + DATABASE;
        mongocxx::uri uri(uriString);
        mongocxx::client conn(uri);

        std::vector<bsoncxx::document::value> exercices = getExercices();

        for (const auto& exercice : exercices) {
            auto numExercice = exercice["num"].get_int32();
            auto filterDocument = exercice["filtre"].get_document();
            auto projectionDocument = exercice["options"]["projection"].get_document();
            
            auto options = mongocxx::options::find{};
            options.projection(projectionDocument.view());

            auto collection = conn[DATABASE][COLLECTION];
            auto cursor = collection.find(filterDocument.view(), options);

            int nbDocuments = 0;

            for (auto&& doc : cursor) {
                nbDocuments++;
                std::cout << bsoncxx::to_json(doc) << std::endl;
            }

            std::cout << "Nb de documents pour l'exercice " << numExercice << " : " << nbDocuments << std::endl;
        }

    } catch (const mongocxx::exception& e) {
        std::cerr << "Erreur de connexion : " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Ce programme fait appel à un fichier qui contient la solution à chacun des exercices contenus dans le PDF dont le lien a été donné plus haut. Voici ce à quoi il ressemble :

#ifndef EXERCICES_HPP
#define EXERCICES_HPP

#include <vector>
#include <bsoncxx/builder/basic/document.hpp>

time_t parseDate(const std::string& dateStr) {
    std::tm tm = {};
    strptime(dateStr.c_str(), "%Y-%m-%d", &tm);
    return mktime(&tm);
}

std::vector<bsoncxx::document::value> getExercices() {
    using bsoncxx::builder::basic::kvp;
    using bsoncxx::builder::basic::make_document;
    using bsoncxx::builder::basic::make_array;

    std::string date_str = "2021-11-15";
    auto timestamp = std::chrono::milliseconds{parseDate(date_str) * 1000};

    std::vector<bsoncxx::document::value> exercices = {
        make_document(
            kvp("num", 1),
            kvp("filtre", make_document(
                kvp("smac", true)
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 2),
            kvp("filtre", make_document(
                kvp("capacite", make_document(
                    kvp("$gt", bsoncxx::types::b_int64{1000})
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 3),
            kvp("filtre", make_document(
                kvp("adresse.numero", make_document(
                    kvp("$exists", false)
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 4),
            kvp("filtre", make_document(
                kvp("avis", make_document(
                    kvp("$size", bsoncxx::types::b_int64{1})
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 5),
            kvp("filtre", make_document(
                kvp("styles", "blues")
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("styles", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 6),
            kvp("filtre", make_document(
                kvp("styles.0", "blues")
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("styles", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 7),
            kvp("filtre", make_document(
                kvp("$and", make_array(
                    make_document(
                        kvp("adresse.codePostal", bsoncxx::types::b_regex{"^84"}),
                        kvp("capacite", make_document(
                            kvp("$lt", 500)
                        ))
                    )
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("adresse.ville", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 8),
            kvp("filtre", make_document(
                kvp("$or", make_array(
                    make_document(
                        kvp("_id", make_document(kvp("$mod", make_array(2, 0))))
                    ),
                    make_document(
                        kvp("avis", make_document(
                            kvp("$exists", false)
                        ))
                    )
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 9),
            kvp("filtre", make_document(
                kvp("avis", make_document(
                    kvp("$elemMatch", make_document(
                        kvp("note", make_document(
                            kvp("$gte", 8),
                            kvp("$lte", 10)
                        ))
                    ))
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 10),
            kvp("filtre", make_document(
                kvp("avis.date", make_document(
                        kvp("$gt", bsoncxx::types::b_date{timestamp}
                    ))
                ))
            ),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
    };

    return exercices;
}

#endif // EXERCICES_HPP

La fonction parseDate est là pour nous faciliter la transformation des dates pour leur utilisation avec mes méthodes de la lib BSON, elle ne sert ici qu’une fois.

Pour construire les documents, j’ai utilisé la version basic du builder BSON, la version stream étant beaucoup moins lisible pour moi.

Compilation et exécution du code

J’ai essayé plusieurs solutions :

  • la compilation avec c++
  • la compilation avec g++
  • l’installation de l’outil pkg-config, MongoDB proposant un fichier .pc

D’abord, la version avec pkg-config :

sudo apt install pkg-config
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
c++ -o mongo_exercices --std=c++11 mongo_exercices.cpp $(pkg-config --cflags --libs libmongocxx)
./mongo_exercices

Ensuite la version avec g++, qui marche aussi avec c++ :

g++ -o mongo_exercices mongo_exercices.cpp -lmongocxx -lbsoncxx -std=c++11 \
    -I/usr/local/include/mongocxx/v_noabi \
    -I/usr/local/include/bsoncxx/v_noabi \
    -I/usr/local/include/bsoncxx/v_noabi/bsoncxx/third_party/mnmlstc \
    -L/usr/local/lib -Wl,-rpath=/usr/local/lib \
    && ./mongo_exercices

Voici la sortie produite par notre exécutable :

{ "_id" : 1, "nom" : "AJMI Jazz Club" }
{ "_id" : 2, "nom" : "Paloma" }
Nb de documents pour l'exercice 1 : 2
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 2 : 1
{ "_id" : 3 }
{ "_id" : { "$oid" : "65e08cc6fa1cb047c0a5e5ea" } }
Nb de documents pour l'exercice 3 : 2
{ "_id" : 2, "nom" : "Paloma" }
Nb de documents pour l'exercice 4 : 1
{ "styles" : [ "jazz", "soul", "funk", "blues" ] }
{ "styles" : [ "blues", "rock" ] }
Nb de documents pour l'exercice 5 : 2
{ "styles" : [ "blues", "rock" ] }
Nb de documents pour l'exercice 6 : 1
{ "adresse" : { "ville" : "Avignon" } }
{ "adresse" : { "ville" : "Le Thor" } }
Nb de documents pour l'exercice 7 : 2
{ "_id" : 2 }
{ "_id" : 3 }
{ "_id" : { "$oid" : "65e08cc6fa1cb047c0a5e5ea" } }
Nb de documents pour l'exercice 8 : 3
{ "nom" : "AJMI Jazz Club" }
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 9 : 2
{ "nom" : "AJMI Jazz Club" }
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 10 : 2

Pour pousser plus loin votre expérience avec C++, suivez ce lien !

MongoDB – lookup et l’agrégation

Cette étape de recherche (lookup) permet d’effectuer l’équivalent d’une jointure externe dans un système de gestion de bases de données relationnelles (SGBDR) et ne peut opérer que sur des collections non shardées situées dans la même base de données.
Look up

Syntaxe de lookup

{
    $lookup:
    {
        from: < collection à joindre >,
        localField: < champ dans les documents de la collection de départ >,
        foreignField: < champ dans les documents de la collection à joindre>,
        as: < nom du tableau qui sera ajouté aux documents du jeu de résultat >
    }
}

Dans chacun des documents reçus en entrée, l’étape $lookup va rajouter un tableau contenant des données en provenance de la collection sur laquelle la jointure a été requise avant de passer ces documents à la prochaine étape du pipeline le cas échéant. Le terme de jointure n’est pas toujours très approprié notamment parce qu’il est possible d’effectuer des requêtes décorrélées, c’est à dire sans clause d’égalité.

Afin de voir $lookup en situation, nous allons utiliser trois collections qui stockeront des élèves, leurs devoirs individuels et les projets sur lesquels ils travaillent à plusieurs.

Elèves, au travail !

Les travaux individuels

Commençons par les travaux individuels de nos élèves, voici les collections impliquées, aucun index n’a été mis pour garder les choses les plus simples possibles mais évidemment qu’il faudra y songer !

db.eleves.insertMany([
    {"nom": "Sébastien Ferrandez", "code": "NAT123"},
    {"nom": "Evelyne Durand", "code": "NAT125"},
    {"nom": "Christian Ton", "code": "NAT120"},
    {"nom": "Claire Annela", "code": "NAT127"}
])

db.devoirs.insertMany([
    {"code": "NAT123", "matiere": "SVT", "note": 12},
    {"code": "NAT123", "matiere": "Maths", "note": 10},
    {"code": "NAT125", "matiere": "Maths", "note": 11.75},
    {"code": "NAT120", "matiere": "Français", "note": 18},
    {"code": "NAT127", "matiere": "Latin", "note": 19}
])

Nous allons partir d’eleves pour aller vers devoirs; dans les deux cas notre nom de champ de « jointure » est code:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   }
])

Voici le résultat produit par ce pipeline, il est assez difficilement lisible mais nous avons l’idée générale : les notes sont « raccrochées » à l’élève !

{
	"_id": ObjectId("5d6e6115d9a18feb0291b605"),
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b60e"),
		"code": "NAT123",
		"matiere": "SVT",
		"note": 12
	}, {
		"_id": ObjectId("5d6e6174d9a18feb0291b60f"),
		"code": "NAT123",
		"matiere": "Maths",
		"note": 10
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b606"),
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b610"),
		"code": "NAT125",
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b607"),
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b611"),
		"code": "NAT120",
		"matiere": "Français",
		"note": 18
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b608"),
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b612"),
		"code": "NAT127",
		"matiere": "Latin",
		"note": 19
	}]
}

Ajoutons une étape project pour rendre cet affichage plus « digeste »…Nous éliminons les id et les champs redondants:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   },
   {
     $project: {
         "_id": 0,
         "detail_notes._id": 0,
         "detail_notes.code": 0
     }
   }
])

Voilà qui est nettement mieux:

{
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"matiere": "SVT",
		"note": 12
	}, {
		"matiere": "Maths",
		"note": 10
	}]
} {
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"matiere": "Français",
		"note": 18
	}]
} {
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"matiere": "Latin",
		"note": 19
	}]
}

Les projets

Voici notre collection projets, elle contient les codes des élèves impliqués ainsi que la note obtenue:

db.projets.insertMany([
    {"codes": ["NAT123", "NAT125"], "matiere": "Dessin", "note": 15}
])

Le champ codes est un tableau ici, pour que chaque élève puisse récupérer sa note, il va falloir éclater ce tableau à l’aide d’unwind avant d’utiliser lookup dessus, nous allons partir de projets cette fois:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Voilà le jeu de résultat lié:

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}]
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}]
}

Nous allons rajouter une étape addFields pour que le tableau contenant un seul document (eleve) devienne un document tout simple, voici le pipeline final:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $addFields: {
       "eleve": { $arrayElemAt: ["$eleve", 0] }
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Et voilà l’travail !

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}
}

Le prédicat SQL EXISTS

Le mot clé SQL EXISTS est ce que l’on appelle un prédicat, il évalue une (sous-)requête et dit si elle contient (true) ou non (false) des tuples. Pour travailler avec, nous allons utiliser deux tables au schéma simplissime, clients et commandes, dont voici les extensions, c’est à dire l’ensemble des tuples qu’elles contiennent.

clients

comm

Pour trouver les noms et prénoms des clients qui ont une commande:

SELECT nom, prenom FROM clients WHERE EXISTS (select * from commandes)

Voilà le résultat:
requete1

Heu…attendez voir, je fais quoi parmi ces résultats, je n’ai pas passé commande ! Que dit en réalité notre requête ? « Donne-moi le nom et prénom des clients tant qu’il EXISTE des commandes ». Il existe 5 commandes, j’ai donc 5 clients. Et oui, attention car à ce stade, nous n’avons absolument pas demandé de corrélation entre les résultats. Faisons-le et notez au passage que nous utilisons TRUE en lieu et place du joker « * » dans la sous-requête:

SELECT nom, prenom FROM clients c WHERE EXISTS (SELECT TRUE FROM commandes co WHERE c.id = co.client_id)

Voilà qui est nettement mieux, vous en conviendrez !

requete2

Alors, vous allez me dire, « Quel est l’intérêt d’utiliser EXISTS, nous sommes en train de faire ni plus ni moins qu’une jointure interne » et vous aurez raison. D’ailleurs, certaines personnes qui ont grand besoin d’une mise à jour en SQL s’en servent comme « jointure du pauvre ». La grosse différence, c’est que vous ne pouvez pas projeter des attributs potentiellement contenus dans la sous-requête, ce qui peut être fait avec un JOIN, interne ou externe. Ici nous ne pouvons projeter que nom et prenom, qui sont des attributs de la table clients. Pensez aussi à l’utilisation des index (utiliser EXPLAIN !)

To EXIST or not to EXIST?

Evidemment, EXISTS a aussi son contraire – comme NULL – c’est NOT EXISTS.
Reprenons la dernière requête (notez que TRUE est devenu 1)

SELECT nom, prenom FROM clients c WHERE NOT EXISTS (SELECT 1 FROM commandes co WHERE c.id = co.client_id)

Et hop ! Me voilà apparaissant dans les résultats !
requete3

Le prédicat EXISTS sur un UPDATE

Si je souhaite mettre le montant des commandes pour lesquelles il n’existe pas de client à 1, je ferai:

UPDATE commandes AS co
SET montant = 1
WHERE NOT EXISTS (SELECT * FROM clients c WHERE c.id = co.client_id)

C’est un peu moins délicat à écrire qu’un UPDATE avec une jointure.

Le prédicat EXISTS sur un DELETE

Ce n’est guère plus difficile, notez seulement que DELETE n’accepte pas nativement l’alias (AS), il faut donc mettre commandes en toutes lettres.

DELETE FROM commandes
WHERE NOT EXISTS (SELECT * FROM clients c WHERE c.id = commandes.client_id)

Autre exemple

Altérons notre relation commandes pour en faire celle qui suit:

requete4

Et rajoutons une relation produits elle aussi tout à fait basique:

requete5

Maintenant supposons que nous souhaitions trouver le nom des produits de toute commande contenant une écharpe et qui ne sont pas une écharpe justement:

SELECT p.nom FROM commandes AS co
INNER JOIN produits p ON (p.id = co.produit_id)
WHERE produit_id != 2
AND EXISTS (select * from commandes AS co2 WHERE co.client_id = co2.client_id AND co2.produit_id = 2)

Nous cherchons les commandes qui contiennent le produit 2 dans la sous-requête et nous projetons ceux dont l’id n’est pas 2.

Les formes normales

Relation, es-tu bien normale ?

Le but de la normalisation d’un schéma de base de données relationnelle ? Principalement l’élimination de la redondance et l’assurance d’avoir des données cohérentes (pas la même adresse orthographiée de N façons différentes, par exemple). Je dis « de la redondance inutile » car même si cela semble un pléonasme, il existe des cas où elle peut avoir une utilité et notamment quand la rapidité des accès aux données est placée au centre des préoccupations du concepteur.

normaliser

Normalisez avant qu’il ne faille tout jeter !

Les formes normales, des règles à suivre pour rester en forme !

Les formes normales sont des règles à suivre pour parvenir à éliminer cette redondance inutile. Nous n’irons pas en détail dans les aspects formels de ces formes normales, parce qu’ils ne sont pas indispensables à la compréhension de celles-ci et parce qu’ils rebutent inutilement le débutant. Sim-pli-ci-té !

1NF

Que nous dit cette première forme normale (1st Normal Form) ? Elle nous dit que tout attribut d’une relation (toute colonne d’une table, si vous préférez) ne doit posséder qu’une valeur pour chacun de ses enregistrements (ou tuples).

Plus concrètement, voici une table enseignant qui viole allégrement 1NF :

Table violant 1NF

La colonne prenom contient deux valeurs, cette colonne n’est donc pas mono-valuée comme nous l’impose 1NF mais multi-valuée, on dit que prenom ne contient pas de valeur atomique, que c’est un groupe de valeurs.

Comment se mettre en conformité avec 1NF ?

Dans notre cas ici, il ne faut pas faire l’économie d’une colonne dédiée au deuxième prénom (il pourrait même y en avoir un troisième, voire N car « le nombre de prénoms qui peuvent être attribués par les parents à un même enfant n’est pas fixé par la loi » nous dit le site Service Public). Créons donc cette nouvelle colonne et notons que la création de celle-ci nous permettra de faire des recherches de meilleure qualité sur les prénoms, par exemple en utilisant un index !

Table en conformité avec 1NF

Plutôt simple, n’est-ce pas ? Que dire de cette relation :

Table violant 1NF

Encore une fois, notre colonne adresse est multi-valuée ! Même si elle ne contient qu’une seule adresse, cette adresse elle-même contient plusieurs informations, de nature différente: un nom de rue, un numéro dans cette rue, un code postal, un nom de ville…Pourquoi ne pas créer une table adresse avec un champ numero, type_voie (rue, avenue, boulevard etc.), code_postal, ville etc. ? Cela peut servir si l’on veut normaliser nos adresses grâce à des outils tierce-partie et les rendre « propres ». Parfois il arrive que l’on laisse volontairement l’adresse dans cet état pour minimiser les jointures et donc accéder le plus rapidement possible à l’information…A vous d’étudier la pertinence de chacune de ces façons de faire : gardez simplement à l’esprit que l’absence de normalisation peut coûter cher plus tard…quand les données auront enflé !

2NF

Tout d’abord, pour être en 2NF, il faut être en 1NF : on ne peut pas être sans avoir été, lorsque l’on normalise ! Pour évoquer 2NF, il nous faut parler des dépendances fonctionnelles ! Qu’est-ce qu’une DF ? Rien de bien compliqué, regardons de plus près cette relation enseigne :

2nf

Nous avons ici une clé primaire composée du couple (identifiant_enseignant, identifiant_cours). Notre colonne ville nous dit dans quelle ville a lieu le cours.
Nous voyons que la colonne ville dépend de identifiant_cours : le cours 1 est toujours donné à Avignon et le 2, toujours à Aix.

On dit ici que identifiant_cours détermine ville ou bien, dans le sens inverse, que ville dépend fonctionnellement de identifiant_cours.

Si pour des valeurs identiques d’une colonne C on a les mêmes valeurs dans la colonne C2, alors C détermine C2 et nous le noterons C → C2.

Ici, pour la valeur 1 dans la colonne identifiant_cours nous avons toujours la valeur Avignon en face et pour la valeur 2 dans la colonne identifiant_cours nous avons toujours la valeur Aix. Ce n’est pas le cas de identifiant_enseignant, qui n’a pas les mêmes valeurs de ville en face (regardez l’enseignant 1, il enseigne dans la ville d’Aix et dans la ville d’Avignon).

Dans notre relation enseigne nous distinguons:

  • des attributs (des colonnes, c’est pareil) qui forment une clé primaire
  • un attribut non clé (ville)

Ici nous avons identifié une dépendance fonctionnelle entre une partie de la clé (la colonne identifiant_cours) et une colonne non clé (ville). Voilà pourquoi cette relation viole 2NF.

Pour qu’une relation soit conforme à 2NF, les attributs non clé de cette relation doivent dépendre fonctionnellement de toute la clé et non d’une partie seulement

Ici nous avions : identifiant_cours → ville. La colonne identifiant_cours est une partie (la moitié pour être parfaitement exact) de la clé. CQFD.

Comment se mettre en conformité avec 2NF ?

Nous allons isoler la DF identifiant_cours → ville qui nous pose problème dans sa propre table, que nous pourrions appeler cours par exemple :

2nf-bis

Vous noterez que nous avons mis fin à la redondance inutile jusqu’alors présente dans la table (la ville potentiellement répétée N fois); cela nous aide aussi à assurer comme je vous l’ai dit en introduction la cohérence des données (que se serait-il passé avant si nous avions écrit Avignon « Avinion », « Avignom » et « Avignon » ? Nous aurions eu trois villes différentes !). Dorénavant, Avignon figure une fois et une seule…et le nom est écrit comme il faut.

Que va devenir notre ancienne relation ? Eh bien oui, nous l’avons quelque peu rabotée !

2nf3

Nous avons sorti ville mais il nous faut bien garder le lien entre le cours et la ville, voilà pourquoi la source de la DF identifiant_cours → ville, c’est à dire identifiant_cours, RESTE dans la table d’origine. Grâce à identifiant_cours, qui existe maintenant dans les deux tables, nous pouvons effectuer des jointures et ainsi garantir qu’on a maintenu l’information initiale qui était « tel cours se donne dans telle ville ».

En pratique ici, identifiant_cours sera clé primaire dans la nouvelle table cours et une contrainte d’intégrité référentielle partira depuis identifiant_cours situé dans enseigne vers identifiant_cours situé dans cours.

3NF

Tout comme il nous faut être en 1NF pour être en 2NF, il nous faut être en 2NF pour être en 3NF : nous procédons de manière incrémentale !

Pour être en 3NF, il faut que toute colonne non clé ne dépende que de la clé de la relation

Revenons un instant à nos enseignants, dont la structure a quelque peu évolué :

3nf

Nous avons souhaité pour chaque enseignant dire à quel département il était rattaché et où ce dernier se situait. Dans le cas présent, localisation dépend de departement (noté departement → localisation). Notre attribut departement ne fait pas partie de la clé, pas plus que localisation, dont il dépend. Voilà pourquoi cette relation n’est pas conforme à 3NF !

Les problèmes posés par cet exemple sont les mêmes que pour la 2NF, à savoir :

  • la localisation du département est répétée dans chaque enregistrement (regardez les deux premiers) et elle peut aussi être orthographiée différemment suite à une erreur de saisie, compromettant ainsi la cohérence des données et donc leur qualité
  • si la localisation du département change, il faut mettre à jour tous les enseignants qui y travaillent
  • Si le département n’a plus d’enseignant, nous perdons l’information de sa localisation

Comment se mettre en conformité avec 3NF ?

Comme pour 2NF, il va nous falloir isoler la DF anormale departement → localisation dans sa propre table, comme ceci :

3nf2

Là encore vous noterez que nous avons mis fin à la redondance qui jusque là existait dans notre table. Notre relation enseignant va « maigrir » elle aussi mais on doit également y faire exister un lien vers notre nouvelle relation, ce lien ce sera la colonne identifiant_departement :

3nf3

Conclusion

Nous entretenons désormais des relations tout à fait normales !

En réalité, il existe d’autres formes normales après 3NF, mais quand on a normalisé en 3NF, on a déjà éliminé la grande partie de ces vilaines redondances que nous voulons éviter.

Pour retenir facilement 2NF et 3NF, les gens qui travaillent sur les bases de données connaissent par cœur cette phrase de feu William Kent :

« The key, the whole key, and nothing but the key »

qui reprend de façon assez humoristique le serment que font les témoins devant un tribunal : « Je jure de dire la vérité, toute la vérité, rien que la vérité » (des petits malins on rajouté « So help me Codd » – E.F Codd étant considéré comme le père des bases de données relationnelles – pour remplacer le « so help me God » (Dieu me vienne en aide) qui termine la phrase aux Etats-Unis).

En effet :

  • En 2NF, tout attribut (ou colonne) non clé doit dépendre de la clé toute entière (the whole key)
  • En 3NF, tout attribut (ou colonne) non clé ne doit dépendre QUE de la clé (nothing but the key)

Vous voilà maintenant armés pour partir à l’assaut de vos tables et les normaliser ! Bon courage !