L’évolution des bases de données#

La section précédente a montré comment les données sont représentées : encodées en bits, sérialisées en formats textuels ou binaires, structurées par des schémas. Mais représenter des données ne suffit pas. Dès qu’un système doit conserver des données au-delà de l’exécution d’un programme, les retrouver efficacement, et garantir leur cohérence même en cas de panne ou d’accès concurrent, on entre dans le domaine des bases de données.

L’histoire des bases de données est une des plus riches de l’informatique. Elle s’étend sur plus de soixante ans et reflète, à chaque étape, les tensions fondamentales du génie logiciel : simplicité contre expressivité, performance contre flexibilité, cohérence contre disponibilité. Martin Kleppmann, dans Designing Data-Intensive Applications, montre que comprendre cette histoire n’est pas un exercice académique : les compromis qui ont guidé la conception d’IMS en 1966 sont les mêmes qui orientent aujourd’hui le choix entre une base relationnelle et une base NoSQL.

Nous allons parcourir cette évolution d’une manière un peu particulière : au lieu de décrire chaque modèle de manière abstraite, nous allons les implémenter. Le même scénario universitaire (départements, professeurs, cours, étudiants) sera modélisé successivement dans le modèle hiérarchique, le modèle en réseau, puis le modèle relationnel, ce qui permettra de voir concrètement ce que chaque paradigme gagne et ce qu’il perd par rapport au précédent. On poursuivra ensuite avec les transactions, les ORMs, la révolution NoSQL et les bases spécialisées.

Le modèle hiérarchique (IBM, années 60)#

Le modèle hiérarchique est le premier modèle de base de données à avoir été formalisé. Son incarnation la plus célèbre, IMS (Information Management System) d’IBM, a été développée à partir de 1966 pour le programme Apollo de la NASA : il fallait gérer la nomenclature de millions de pièces qui composaient le vaisseau spatial, et cette nomenclature avait naturellement une structure d’arbre (un module contient des sous-systèmes, qui contiennent des composants, qui contiennent des pièces). Le modèle hiérarchique généralise cette intuition : toutes les données sont organisées en arbre, où chaque enregistrement a exactement un parent (sauf la racine). L’accès aux données est navigationnel : on descend dans l’arbre en suivant les branches.

Pour comprendre concrètement ce que cela implique, implémentons un modèle hiérarchique simplifié en Python.

La structure est simple : un noeud contient un type, des données et une liste d’enfants. C’est précisément cette simplicité qui a rendu le modèle hiérarchique attrayant dans les années 60. Créons maintenant une petite base de données universitaire pour voir comment les données s’organisent dans cet arbre :

Université
  └── Département
        ├── Professeur
        └── Cours
              └── Étudiant

Pour explorer cette base, il nous faut des outils de navigation. Dans IMS, l’opération fondamentale était le Get Next (GN) : on parcourait l’arbre en profondeur, segment par segment. Notre fonction afficher_arbre reproduit cette logique :

On peut aussi naviguer vers un type de segment particulier en spécifiant un chemin dans l’arbre, l’équivalent simplifié de la commande GN d’IMS :

Et si on veut chercher un enregistrement par sa valeur plutôt que par sa position dans l’arbre, il faut parcourir toute la structure :

Alice est inscrite à INF3080 et INF3105. Dans le modèle hiérarchique, chaque nœud a UN SEUL parent, donc Alice doit apparaître DEUX FOIS dans l’arbre :

    Cours INF3080
    └── Étudiant Alice  ← copie 1
    Cours INF3105
    └── Étudiant Alice  ← copie 2

Conséquences :

  • Gaspillage d’espace
  • Risque d’incohérence si on met à jour une copie mais pas l’autre
  • Pas de moyen simple de poser la question : « À quels cours Alice est-elle inscrite ? » (il faut parcourir TOUT l’arbre)

Le modèle en réseau (CODASYL, fin des années 60)#

Le modèle hiérarchique avait un défaut structurel : la contrainte d’un seul parent par enregistrement rendait les relations plusieurs-à-plusieurs impossibles sans duplication. Le comité CODASYL (Conference on Data Systems Languages), le même organisme qui avait standardisé COBOL, s’est attaqué à ce problème à la fin des années 60. Son Data Base Task Group (DBTG) a proposé en 1969 le modèle en réseau, qui généralise l’arbre en graphe : un enregistrement peut désormais avoir plusieurs parents, grâce à des ensembles nommés (sets) qui relient un type « propriétaire » (owner) à un type « membre » (member). La duplication disparaît, mais l’accès aux données reste navigationnel.

Remarquons que les enregistrements existent désormais indépendamment, sans être imbriqués dans un arbre. Les liens entre eux sont explicites et nommés. Reconstruisons notre scénario universitaire avec ce modèle :

Le résultat ressemble à ce qu’on obtenait avec le modèle hiérarchique, mais la structure sous-jacente est fondamentalement différente : Alice et Bob n’existent qu’une seule fois dans la base :

La navigation avant fonctionne comme avant : trouver les étudiants inscrits à un cours donné :

Mais la vraie nouveauté, c’est la navigation inverse. La question « à quels cours Alice est-elle inscrite ? », qui exigeait un parcours complet de l’arbre dans le modèle hiérarchique, se résout maintenant directement :

Alice n’existe qu’UNE SEULE FOIS dans la base de données. Elle est membre de deux sets INSCRIPTION (un par cours).

  • Plus de duplication !
  • La question « quels cours suit Alice ? » se résout par navigation inverse (FIND OWNER WITHIN SET).

Mais il reste des problèmes :

  • L’accès est toujours NAVIGATIONNEL : le programmeur doit connaître le schéma des sets et écrire des boucles pour traverser les liens.
  • Ajouter un nouveau type de lien exige de modifier le schéma et le code de navigation.
  • Pas de langage déclaratif : on dit COMMENT chercher, pas CE QU’ON cherche.

Le modèle relationnel et SQL#

En 1970, Edgar F. Codd, un mathématicien britannique travaillant chez IBM, publie un article qui va transformer le domaine : A Relational Model of Data for Large Shared Data Banks. Sa proposition est radicale : abandonner complètement la navigation. Au lieu de dire au système comment trouver les données (en descendant dans un arbre ou en suivant des liens), on lui dit ce qu’on cherche, et c’est le système qui détermine la meilleure façon de l’obtenir. Les données sont organisées en tables (que Codd appelle « relations », d’où le nom), et les requêtes s’expriment dans un langage déclaratif : SQL. SQL est d’ailleurs souvent considéré comme l’exemple le plus abouti d’un langage de quatrième génération (4GL) : un langage où le programmeur décrit ce qu’il veut obtenir, pas comment y arriver.

Les générations de langages de programmation

La classification des langages en « générations » est une grille de lecture historique qui a été très influente dans les années 80 et 90 :

  • 1GL : le code machine, des séquences de 0 et de 1 directement exécutées par le processeur.
  • 2GL : l’assembleur, qui remplace les codes binaires par des mnémoniques lisibles (MOV, ADD, JMP), mais reste lié à une architecture matérielle spécifique.
  • 3GL : les langages procéduraux de haut niveau (FORTRAN, C, COBOL, Java, Python), où le programmeur écrit des algorithmes qui décrivent comment résoudre un problème, de manière indépendante du matériel.
  • 4GL : les langages déclaratifs spécialisés, où le programmeur décrit ce qu’il veut sans spécifier la procédure. SQL en est l’exemple canonique : on écrit SELECT ... WHERE ... et c’est l’optimiseur de requêtes qui choisit le plan d’exécution.

À l’époque, certains prédisaient l’avènement d’un 5GL qui permettrait de programmer en langage naturel. Cette vision ne s’est pas concrétisée sous la forme imaginée, mais on peut noter que les LLMs modernes réalisent en quelque sorte cette promesse, d’une manière que personne n’avait anticipée.

C’est un changement de paradigme au sens propre du terme. Le modèle hiérarchique et le modèle en réseau demandaient au programmeur de connaître la structure physique des données et d’écrire des boucles de navigation. Le modèle relationnel sépare la structure logique de l’implémentation physique, une application directe du principe d’information hiding de Parnas que nous avons vu dans la section sur l’architecture.

Remarquons la différence fondamentale : plus d’arbre, plus de sets. Chaque entité a sa propre table, et les relations plusieurs-à-plusieurs passent par une table de jointure. Le schéma est déclaré une fois, et le SGBD se charge de l’appliquer. Ajoutons nos données :

C’est dans les requêtes que la puissance du modèle relationnel se révèle. Chaque requête décrit ce qu’on veut, pas comment le trouver. Reprenons les mêmes questions que dans les modèles précédents, plus une nouvelle qui aurait été quasi impossible à formuler avant :

Les transactions#

Les exemples précédents montrent comment stocker et interroger des données, mais ils passent sous silence un problème fondamental : que se passe-t-il quand plusieurs opérations doivent réussir ou échouer ensemble ? Prenons un cas concret : inscrire Alice à un cours implique au minimum deux vérifications (le cours existe-t-il ? l’étudiante est-elle déjà inscrite ?) et une insertion. Si le système tombe entre la vérification et l’insertion, ou si deux processus tentent la même inscription simultanément, les données peuvent se retrouver dans un état incohérent. Dans un programme simple, on gère ça avec des conditions et des verrous. Dans un SGBD, on utilise une transaction.

Jim Gray, chercheur chez IBM puis chez Microsoft Research, a formalisé dans les années 70 et 80 les propriétés fondamentales des transactions, un travail qui lui a valu le prix Turing en 1998. Ces propriétés sont connues sous l’acronyme ACID :

  • Atomicité (Atomicity) : une transaction est tout ou rien. Soit toutes ses opérations réussissent (COMMIT), soit aucune n’a d’effet (ROLLBACK). Il n’y a pas d’état intermédiaire visible.
  • Cohérence (Consistency) : une transaction amène la base d’un état valide à un autre état valide. Toutes les contraintes du schéma (clés primaires, clés étrangères, unicité) sont respectées à la fin de la transaction.
  • Isolation (Isolation) : les transactions concurrentes ne se voient pas mutuellement. Tout se passe comme si elles s’exécutaient l’une après l’autre, même si en pratique le SGBD les entrelace pour la performance.
  • Durabilité (Durability) : une fois qu’une transaction est validée (COMMIT), ses effets survivent aux pannes, même un crash immédiat du serveur.

Illustrons ces propriétés avec notre base universitaire. On tente d’inscrire Alice à un cours auquel elle est déjà inscrite, ce qui viole la clé primaire de la table d’inscription :

Kleppmann consacre une partie importante de Designing Data-Intensive Applications aux niveaux d’isolation, montrant que la propriété I d’ACID est en réalité un spectre. L’isolation totale (serializable) est coûteuse en performance, et la plupart des SGBD offrent par défaut un niveau plus faible (read committed ou repeatable read) qui autorise certaines anomalies en échange de la concurrence. Comprendre ces compromis est essentiel dès qu’un système a plus d’un utilisateur simultané, ce qui est, en pratique, presque toujours le cas.

Les ORMs (object-relational mappers)#

Le modèle relationnel organise les données en tables, avec des lignes et des colonnes. Les langages de programmation, eux, manipulent des objets, des classes, des dictionnaires. Entre les deux, il y a un décalage structurel que la communauté a baptisé l’impedance mismatch, par analogie avec l’électronique : deux systèmes qui ne « parlent pas le même langage » perdent de l’énergie à l’interface. En pratique, cela se traduit par du code de conversion répétitif : transformer les lignes d’un résultat SQL en objets Python, et inversement, convertir des objets en requêtes INSERT ou UPDATE.

Les ORMs (Object-Relational Mappers) tentent de résoudre ce problème en créant une correspondance automatique entre les classes d’un langage et les tables d’une base de données. L’idée a émergé progressivement dans les années 90, mais c’est avec Hibernate (Java, 2001) puis ActiveRecord (Ruby on Rails, 2004) qu’elle s’est imposée dans la pratique courante. En Python, SQLAlchemy, créé par Mike Bayer en 2006, est devenu la référence. Il offre deux niveaux d’abstraction : un Core qui fournit une API Python pour construire des requêtes SQL sans écrire de SQL brut, et un ORM complet qui permet de définir des classes Python mappées directement sur des tables.

from sqlalchemy import create_engine, Column, String, Boolean, ForeignKey, Table
from sqlalchemy.orm import declarative_base, relationship, Session

Base = declarative_base()

# Table de jointure pour la relation M:N
inscription = Table('inscription', Base.metadata,
    Column('matricule', String, ForeignKey('etudiant.matricule'), primary_key=True),
    Column('sigle', String, ForeignKey('cours.sigle'), primary_key=True),
)

class Etudiant(Base):
    __tablename__ = 'etudiant'
    matricule = Column(String, primary_key=True)
    nom       = Column(String, nullable=False)
    cours     = relationship('Cours', secondary=inscription, back_populates='etudiants')

class Cours(Base):
    __tablename__ = 'cours'
    sigle     = Column(String, primary_key=True)
    titre     = Column(String, nullable=False)
    etudiants = relationship('Etudiant', secondary=inscription, back_populates='cours')

# Utilisation — on manipule des objets Python, pas du SQL
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)

with Session(engine) as session:
    alice = Etudiant(matricule='TRAA01', nom='Alice')
    bd    = Cours(sigle='INF3080', titre='Bases de données')
    algo  = Cours(sigle='INF3105', titre='Structures de données')

    alice.cours.append(bd)
    alice.cours.append(algo)

    session.add(alice)
    session.commit()

    # La question « à quels cours Alice est-elle inscrite ? »
    # s'écrit comme en Python natif :
    for cours in alice.cours:
        print(f"  {cours.sigle}{cours.titre}")

Le contraste avec le SQL brut est frappant : on ne voit plus aucune requête SQL, et les relations entre entités se manipulent comme des listes Python ordinaires. Mais cette abstraction a un coût. L’ORM génère du SQL en coulisse, et ce SQL n’est pas toujours celui qu’un développeur expérimenté écrirait. Le problème classique est le N+1 query : charger une liste d’étudiants puis accéder aux cours de chacun peut déclencher une requête par étudiant, au lieu d’une seule jointure. C’est un compromis récurrent en génie logiciel : l’abstraction simplifie le cas courant, mais elle peut masquer des inefficacités que seule la compréhension de la couche sous-jacente permet de diagnostiquer.

La révolution NoSQL#

Le modèle relationnel a dominé le paysage des bases de données pendant plus de trente ans, et il reste aujourd’hui le choix par défaut pour une majorité d’applications. Mais à partir du milieu des années 2000, un ensemble de pressions convergentes a commencé à remettre en question son hégémonie. Les géants du web (Google, Amazon, Facebook) faisaient face à des volumes de données et des niveaux de trafic que les bases relationnelles traditionnelles peinaient à absorber, même sur du matériel coûteux. Parallèlement, beaucoup de développeurs trouvaient que le modèle relationnel imposait une rigidité excessive pour des données dont la structure évoluait rapidement ou ne se prêtait pas naturellement aux tables.

Le terme « NoSQL » a été popularisé en 2009, lors d’un meetup organisé à San Francisco par Johan Oskarsson. Le nom est un peu trompeur : il ne signifie pas « pas de SQL » mais plutôt « Not Only SQL », l’idée étant que le modèle relationnel n’est pas la seule réponse à tous les problèmes de stockage. Kleppmann, dans Designing Data-Intensive Applications, montre que derrière ce label se cachent des motivations très différentes : le besoin de scalabilité horizontale (répartir les données sur plusieurs machines), le désir de schémas plus flexibles, ou la recherche de modèles de données mieux adaptés à certains cas d’usage.

En pratique, le mouvement NoSQL a donné naissance à plusieurs familles de bases de données, chacune optimisée pour un type de problème particulier :

  • Les bases clé-valeur (Redis, Memcached) : le modèle le plus simple, un dictionnaire distribué. Idéal pour le caching et les sessions.
  • Les bases orientées documents (MongoDB, CouchDB) : chaque enregistrement est un document (typiquement JSON) avec une structure libre. Naturel pour des données hétérogènes ou des schémas qui évoluent vite.
  • Les bases orientées colonnes (Cassandra, HBase) : optimisées pour l’écriture massive et les requêtes analytiques sur de grands volumes. Inspirées de Bigtable de Google (2006).
  • Les bases orientées graphes (Neo4j, Amazon Neptune) : conçues pour les données fortement interconnectées, où les relations entre entités sont aussi importantes que les entités elles-mêmes.

Ce qui est frappant, comme le note Kleppmann, c’est que certains de ces modèles rappellent des paradigmes qu’on croyait dépassés. Les bases orientées documents stockent des données hiérarchiques (des arbres JSON), comme le faisait IMS en 1966. Les bases orientées graphes modélisent des réseaux de liens, comme le faisait CODASYL en 1969. L’histoire ne se répète pas, mais elle rime.

Les bases clé-valeur#

La base clé-valeur est le modèle NoSQL le plus simple : elle associe une clé unique à une valeur opaque, exactement comme un dictionnaire Python ou une table de hachage. Le système ne connaît pas la structure de la valeur ; il sait seulement la stocker, la retrouver et la supprimer par sa clé. Cette simplicité radicale est aussi sa force : en renonçant aux jointures, aux schémas et aux requêtes complexes, une base clé-valeur peut offrir des performances et une scalabilité que le modèle relationnel atteint difficilement.

L’exemple le plus emblématique est Redis, créé en 2009 par Salvatore Sanfilippo. Redis stocke toutes ses données en mémoire vive, ce qui lui permet des temps de réponse de l’ordre de la microseconde. Mais il va au-delà du simple dictionnaire : il supporte des structures de données riches (listes, ensembles, hachages, compteurs), ce qui en fait un outil polyvalent utilisé aussi bien comme cache que comme file de messages ou comme base de sessions utilisateur.

Le cas d’usage le plus courant des bases clé-valeur est le caching : stocker temporairement des résultats coûteux à calculer pour éviter de les recalculer à chaque requête. Le principe est simple, mais le défi principal est l’invalidation : s’assurer que le cache reste cohérent avec la source de vérité. Les stratégies courantes incluent le TTL (time-to-live, expiration après un délai fixe), le write-through (mise à jour simultanée du cache et de la source) et le cache-aside (le code vérifie d’abord le cache, puis interroge la source en cas d’absence). Python offre functools.lru_cache, un décorateur qui implémente un cache LRU (Least Recently Used) directement sur les appels de fonction :

Les bases orientées documents#

Les bases orientées documents poussent l’idée un cran plus loin que les bases clé-valeur : la valeur n’est plus opaque, c’est un document structuré (généralement en JSON) que la base sait interroger. On peut chercher par n’importe quel champ à l’intérieur du document, sans avoir besoin de connaître sa structure à l’avance. C’est un modèle naturel pour des données hétérogènes : un catalogue de produits où chaque catégorie a des attributs différents, des profils utilisateur dont les champs varient, des événements avec des charges utiles variables.

MongoDB, créé en 2009 par Dwight Merriman et Eliot Horowitz, est devenu le représentant le plus connu de cette famille. Son modèle est celui de « collections » de documents JSON (techniquement BSON, une variante binaire). Là où une base relationnelle aurait besoin de plusieurs tables liées par des clés étrangères, MongoDB permet d’imbriquer directement les données dans un seul document :

{
  "matricule": "TRAA01",
  "nom": "Alice",
  "cours": [
    {"sigle": "INF3080", "titre": "Bases de données", "session": "H2026"},
    {"sigle": "INF3105", "titre": "Structures de données", "session": "H2026"}
  ]
}

On retrouve ici une structure d’arbre, exactement comme dans le modèle hiérarchique d’IMS. L’avantage est la localité des données : tout ce qui concerne Alice est au même endroit, ce qui rend les lectures rapides. Le désavantage est le même qu’en 1966 : si Bob et Alice partagent le même cours, l’information du cours est dupliquée. Le modèle orienté documents fait le pari que cette duplication est acceptable pour la plupart des cas d’usage, en échange de la simplicité et de la performance en lecture.

Kleppmann souligne que le choix entre un modèle relationnel et un modèle orienté documents dépend fondamentalement de la nature des relations dans les données. Si les données sont principalement des agrégats autonomes (un utilisateur avec ses préférences, une commande avec ses lignes), le modèle orienté documents est naturel. Si les données sont fortement interconnectées (des relations plusieurs-à-plusieurs omniprésentes), le modèle relationnel reste plus adapté.

Les bases orientées colonnes#

Les bases de données que l’on a vues jusqu’ici (relationnelles, clé-valeur, orientées documents) sont optimisées pour le traitement transactionnel : insérer une commande, mettre à jour un profil, lire un enregistrement par sa clé. On parle de charges de travail OLTP (Online Transaction Processing). Mais il existe une autre catégorie de besoins, fondamentalement différente : l’analyse. « Quel est le chiffre d’affaires par région et par trimestre ? », « Quels produits ont vu leurs ventes baisser de plus de 10 % ce mois-ci ? ». Ce sont des requêtes OLAP (Online Analytical Processing), qui balaient des millions de lignes mais ne consultent que quelques colonnes.

Dans une base relationnelle classique, les données sont stockées ligne par ligne : toutes les colonnes d’un enregistrement sont physiquement côte à côte sur le disque. C’est idéal pour lire ou écrire un enregistrement complet, mais inefficace pour une requête analytique qui ne s’intéresse qu’à deux colonnes sur vingt. L’idée du stockage orienté colonnes est d’inverser l’organisation : on stocke ensemble toutes les valeurs d’une même colonne. Pour une requête « somme des montants par région », le moteur ne lit que les colonnes region et montant, en ignorant complètement toutes les autres. En plus de réduire les lectures disque, ce regroupement permet une compression spectaculaire, car les valeurs d’une même colonne sont souvent similaires (beaucoup de répétitions dans une colonne region ou pays).

L’article fondateur est le papier de Google sur Bigtable (2006), suivi de Dremel (2010) qui a inspiré BigQuery. Dans le monde open source, Apache Cassandra (2008, initialement développé chez Facebook) et HBase (2007, une implémentation open source de Bigtable) ont été les premiers systèmes orientés colonnes à grande échelle. Aujourd’hui, des moteurs comme ClickHouse, Apache Parquet (un format de fichier orienté colonnes) et DuckDB rendent cette approche accessible même pour des analyses locales.

Cette distinction OLTP / OLAP a donné naissance à une architecture classique : le data warehouse (entrepôt de données). Les données transactionnelles vivent dans une base OLTP (PostgreSQL, MySQL), puis sont périodiquement copiées et transformées vers un entrepôt orienté colonnes via un processus appelé ETL (Extract, Transform, Load). Les analystes interrogent l’entrepôt sans risquer de ralentir le système transactionnel. C’est une application directe du principe de séparation des préoccupations : les deux charges de travail ont des besoins si différents qu’il vaut mieux les servir avec des systèmes distincts.

En Python, on peut illustrer la différence entre les deux approches de stockage :

Les bases orientées graphes#

Certaines données sont fondamentalement des réseaux de relations. Un réseau social, une carte routière, une ontologie, les dépendances entre les composants d’un système logiciel. Dans ces cas, ce qui importe n’est pas tant les entités elles-mêmes que les connexions entre elles. Les requêtes typiques sont des traversées : « Qui sont les amis des amis d’Alice ? », « Quel est le plus court chemin entre Montréal et Vancouver ? », « Quels services dépendent transitoirement de ce composant ? ». En SQL, ces requêtes se traduisent par des cascades de JOIN qui deviennent rapidement illisibles dès que la profondeur de traversée augmente. Les bases orientées graphes sont conçues précisément pour ce type de navigation.

Le modèle réseau CODASYL des années 1970 permettait exactement ce genre de navigation, mais avec une rigidité qui l’a condamné : il fallait déclarer à l’avance tous les types de relations et naviguer de manière procédurale, pointeur par pointeur. Les bases orientées graphes modernes reprennent l’intuition de CODASYL (les données forment un réseau navigable) mais avec la flexibilité du monde NoSQL : on peut ajouter de nouveaux types de nœuds et de relations sans modifier un schéma global, et les requêtes sont déclaratives plutôt que procédurales. Neo4j (2007, Emil Eifrem et Johan Svensson) est la plus connue. Son langage de requête, Cypher, est au graphe ce que SQL est aux tables.

En Python, un graphe peut se représenter simplement comme un dictionnaire d’adjacence. Voici un exemple de traversée en largeur (breadth-first search, BFS) qui trouve tous les nœuds accessibles depuis un point de départ :

Cypher, le langage de requête de Neo4j, rend la structure du graphe visible directement dans la syntaxe. Les nœuds sont représentés par des parenthèses et les relations par des flèches, ce qui donne des requêtes dont la forme épouse celle des données. Reprenons notre exemple universitaire pour comparer avec les requêtes SQL vues plus haut :

// Créer des nœuds et des relations
CREATE (alice:Etudiant {matricule: "TRAA01", nom: "Alice"})
CREATE (bob:Etudiant {matricule: "MORB02", nom: "Bob"})
CREATE (bd:Cours {sigle: "INF3080", titre: "Bases de données"})
CREATE (algo:Cours {sigle: "INF3105", titre: "Structures de données"})

CREATE (alice)-[:INSCRIT_A]->(bd)
CREATE (alice)-[:INSCRIT_A]->(algo)
CREATE (bob)-[:INSCRIT_A]->(bd)

// À quels cours Alice est-elle inscrite ?
MATCH (e:Etudiant {nom: "Alice"})-[:INSCRIT_A]->(c:Cours)
RETURN c.sigle, c.titre

// Quels étudiants partagent au moins un cours avec Alice ?
MATCH (alice:Etudiant {nom: "Alice"})-[:INSCRIT_A]->(c)<-[:INSCRIT_A]-(autre)
RETURN DISTINCT autre.nom

La dernière requête est particulièrement révélatrice. En SQL, trouver les étudiants qui partagent un cours avec Alice nécessitait deux jointures sur la table inscription et une condition d’exclusion. En Cypher, le motif (alice)-[:INSCRIT_A]->(c)<-[:INSCRIT_A]-(autre) exprime la même idée en une ligne, et la structure de la requête dessine littéralement le chemin qu’on cherche dans le graphe. C’est cette correspondance entre la forme de la requête et la forme des données qui fait la force des langages de graphe.

Les bases de données de séries temporelles#

Les séries temporelles (time series) sont des séquences de points de données indexés par le temps : température toutes les minutes, cours boursier toutes les secondes, nombre de requêtes par heure sur un serveur, fréquence cardiaque d’un patient. Ce type de données est omniprésent dans la surveillance d’infrastructure (monitoring), l’Internet des objets (IoT), la finance et les sciences. Le concept de série temporelle est ancien, il remonte aux travaux fondateurs de l’économétrie et des statistiques dans les années 1920 (Yule, Slutsky). Mais les bases de données spécialisées pour ce type de données sont récentes : Prometheus (2012, développé chez SoundCloud par Matt Proud et Julius Volz) a montré que le monitoring d’infrastructure nécessitait un modèle de données dédié, suivi d’InfluxDB (2013) et de TimescaleDB (2017, une extension de PostgreSQL).

Les bases relationnelles classiques peuvent stocker des séries temporelles, mais elles ne sont pas optimisées pour leurs particularités. Les données arrivent principalement en mode append (on ajoute sans modifier), les requêtes portent presque toujours sur des intervalles de temps, et le volume peut être considérable (des millions de points par capteur par jour). Les bases spécialisées exploitent ces propriétés pour offrir une compression agressive (les horodatages successifs se compressent très bien car ils sont régulièrement espacés), des fonctions d’agrégation temporelle natives (downsample, rollup) et des politiques de rétention automatiques (supprimer les données de plus de 90 jours, par exemple).

En Python, on peut illustrer les opérations fondamentales sur les séries temporelles (rééchantillonnage, moyenne mobile, détection d’anomalies) avec les structures de données de base :

On retrouvera les séries temporelles dans le module 5, lorsqu’on abordera l’observabilité et le monitoring. Les métriques collectées par des outils comme Prometheus et Grafana sont précisément des séries temporelles, et les concepts de rétention, d’agrégation et d’alerting sur seuil sont au cœur de la surveillance d’un système en production.

Les bases de données vectorielles#

Les bases de données vectorielles sont un paradigme de stockage et de recherche conçu pour manipuler des vecteurs d’embedding, des représentations numériques de haute dimension (typiquement 256 à 4096 dimensions) produites par des modèles d’apprentissage automatique pour encoder le « sens » d’un texte, d’une image, d’un son ou de toute autre donnée. Le concept de word embedding remonte à Word2Vec (Mikolov et al., Google, 2013), qui a montré qu’on pouvait représenter des mots comme des vecteurs dans un espace où les relations sémantiques deviennent des opérations géométriques (le fameux exemple : « roi » - « homme » + « femme » ≈ « reine »). Mais c’est l’explosion des grands modèles de langage en 2022-2023 qui a créé un besoin massif de bases vectorielles dédiées, car le pattern RAG (Retrieval-Augmented Generation), qui consiste à chercher des documents pertinents pour enrichir le contexte d’un LLM, repose entièrement sur la recherche vectorielle.

L’opération fondamentale est la recherche par similarité : plutôt que de chercher une correspondance exacte (comme en SQL avec WHERE nom = 'Alice'), on cherche les vecteurs les plus proches d’un vecteur requête selon une mesure de distance (cosinus, euclidienne, produit scalaire). C’est cette capacité qui rend possible la recherche sémantique (trouver des documents qui parlent de la même chose, même avec des mots différents), les systèmes de recommandation, et le RAG qui alimente les assistants IA modernes. Pinecone (2019), Weaviate, Milvus et Qdrant sont des bases vectorielles spécialisées, tandis que pgvector ajoute cette capacité à PostgreSQL. Le défi technique principal est la recherche approximative des plus proches voisins (ANN, Approximate Nearest Neighbors), car une recherche exacte en haute dimension est prohibitivement lente. Des algorithmes comme HNSW (Hierarchical Navigable Small World) permettent des recherches quasi instantanées même sur des millions de vecteurs, au prix d’une approximation contrôlée.

En Python, on peut illustrer les concepts fondamentaux (embedding, distance cosinus, recherche par similarité) avec des vecteurs simplifiés :

On retrouvera les bases vectorielles et le RAG dans le module 6, lorsqu’on abordera le développement assisté par l’IA et l’écosystème des grands modèles de langage. Les embeddings et la recherche par similarité sont devenus des briques fondamentales de l’infrastructure logicielle moderne, au même titre que les bases relationnelles l’étaient une génération plus tôt.