Redis, Mongo,
Cassandra, Neo4j
Quand et pourquoi sortir du relationnel — les quatre grandes familles NoSQL avec leurs commandes essentielles.
SQL vs NoSQL
NoSQL ne remplace pas SQL — il répond à des besoins différents. La bonne question n'est pas "lequel est meilleur ?" mais "lequel correspond à mon modèle de données et mes contraintes de charge ?"
En production, les architectures modernes combinent souvent les deux : PostgreSQL pour les données transactionnelles, Redis pour le cache, MongoDB pour les logs ou les profils utilisateurs. Ce n'est pas un choix exclusif.
Les quatre familles NoSQL
Comment choisir ?
| Besoin | Recommandation | Pourquoi |
|---|---|---|
| Données financières, commandes, stocks | SQL (MySQL/PostgreSQL) | Transactions ACID critiques, intégrité référentielle |
| Cache, sessions, rate limiting, files de messages | Redis | Microseconde de latence, structures en mémoire |
| Catalogues produits, CMS, profils utilisateurs | MongoDB | Schéma variable, imbrication naturelle, requêtes riches |
| IoT, logs, analytics, time-series à fort volume | Cassandra | Écritures massivement distribuées, haute disponibilité |
| Réseau social, recommandations, fraude | Neo4j | Les relations sont des données, traversal efficace |
| Recherche full-text, facettes | Elasticsearch / OpenSearch | Indexation inversée, scoring, agrégations |
Redis — Clé-valeur
Redis (Remote Dictionary Server) stocke toutes les données en RAM. C'est la base de données la plus rapide qui soit — au prix d'une capacité limitée à la mémoire disponible.
── Chaînes (String) ─────────────────────────
SET user:42:nom "Alice"
GET user:42:nom → "Alice"
SET compteur 0
INCR compteur → 1
INCRBY compteur 5 → 6
── Expiration ───────────────────────────────
SET session:abc "token123" EX 3600
TTL session:abc → 3598
EXPIRE session:abc 7200 ← redéfinir
PERSIST session:abc ← rendre permanent
── Suppression & existence ──────────────────
DEL user:42:nom
EXISTS user:42:nom → 0 ou 1
KEYS user:42:* ← pattern glob
Convention de nommage des clés : utiliser des : comme séparateurs hiérarchiques. Ex : user:42:session, product:99:views. Cela permet de grouper et de scanner par préfixe.
── GETSET : lire et remplacer atomiquement
GETSET lock:resource "owned"
── SETNX : SET seulement si inexistant
── (implémentation de verrou distribué)
SET lock:job "worker-1" NX EX 30
── MGET / MSET : lot de clés
MSET a 1 b 2 c 3
MGET a b c → ["1","2","3"]
Types de données Redis
HSET user:42 nom "Alice" age 30
HGET user:42 nom → "Alice"
HGETALL user:42 → tous les champs
HINCRBY user:42 age 1 → 31
RPUSH queue:emails "msg1" "msg2"
LPOP queue:emails → "msg1"
LLEN queue:emails → 1
LRANGE queue:emails 0 -1 → tout
SADD tags:article:5 "python" "web"
SMEMBERS tags:article:5
SISMEMBER tags:article:5 "python" → 1
SINTER tags:art:5 tags:art:6 ← intersection
ZADD leaderboard 9850 "Alice"
ZADD leaderboard 7200 "Bob"
ZADD leaderboard 11000 "Carol"
── Top 3 (scores décroissants)
ZREVRANGE leaderboard 0 2 WITHSCORES
→ Carol 11000, Alice 9850, Bob 7200
ZRANK leaderboard "Alice" → 1 (0-indexé)
ZSCORE leaderboard "Alice" → 9850
| Type | Usage typique |
|---|---|
String | Cache, compteurs, tokens, flags |
Hash | Objet utilisateur, configuration |
List | File de tâches, historique, flux |
Set | Tags, membres uniques, intersections |
Sorted Set | Classements, priorités, time-series |
Cas d'usage Redis
Cache applicatif
SET query:users:active
"[{...}]" EX 300Gestion de sessions
HSET session:<token>
user_id 42 role "admin"
EXPIRE session:<token> 1800Rate limiting
INCR rate:192.168.1.1
EXPIRE rate:192.168.1.1 60
→ si > 100 : bloquerPub/Sub messagerie
-- Publisher
PUBLISH notifications "msg"
-- Subscriber
SUBSCRIBE notificationsMongoDB — Documents
MongoDB stocke des documents BSON (JSON binaire) dans des collections. Pas de schéma imposé — chaque document peut avoir sa propre structure.
Vocabulaire SQL → MongoDB
| SQL | MongoDB |
|---|---|
| Base de données | Base de données |
| Table | Collection |
| Ligne / enregistrement | Document |
| Colonne | Champ (field) |
| PRIMARY KEY | _id (ObjectId auto) |
| JOIN | $lookup (aggregation) |
| INDEX | Index (même concept) |
Structure d'un document
{
"_id": ObjectId("64a1f2..."),
"nom": "Dupont",
"prenom": "Alice",
"email": "alice@example.com",
"actif": true,
"age": 30,
"adresses": [
{
"type": "domicile",
"ville": "Bruxelles",
"cp": "1000"
}
],
"preferences": {
"langue": "fr",
"newsletter": false
}
}
L'imbrication (embedding) est préférée aux jointures en MongoDB. Si vous affichez toujours adresses avec client, les stocker dans le même document évite un aller-retour réseau.
CRUD MongoDB
── CREATE ────────────────────────────────────────────
db.clients.insertOne({
nom: "Martin", email: "bob@example.com", age: 25
})
db.clients.insertMany([
{ nom: "A", age: 20 },
{ nom: "B", age: 30 }
])
── READ ──────────────────────────────────────────────
-- Tous les documents
db.clients.find()
-- Avec filtre
db.clients.find({ actif: true, age: { $gte: 18 } })
-- Projection (champs voulus seulement)
db.clients.find({}, { nom: 1, email: 1, _id: 0 })
-- Tri, limite, skip
db.clients.find().sort({age: -1}).limit(10).skip(20)
── UPDATE ────────────────────────────────────────────
-- Modifier un champ ($set) sans écraser le document
db.clients.updateOne(
{ email: "bob@example.com" },
{ $set: { actif: false }, $inc: { connexions: 1 } }
)
── DELETE ────────────────────────────────────────────
db.clients.deleteOne({ _id: ObjectId("64a1f2...") })
db.clients.deleteMany({ actif: false })
Opérateurs de requête
| Opérateur | Signification |
|---|---|
$eq / $ne | Égal / différent |
$gt / $gte | Supérieur (ou égal) |
$lt / $lte | Inférieur (ou égal) |
$in / $nin | Dans / hors d'une liste |
$and / $or | Combinaison logique |
$exists | Champ existe ou non |
$regex | Expression régulière |
Opérateurs de mise à jour
| Opérateur | Action |
|---|---|
$set | Modifier ou créer un champ |
$unset | Supprimer un champ |
$inc | Incrémenter un nombre |
$push | Ajouter à un tableau |
$pull | Retirer d'un tableau |
$rename | Renommer un champ |
Aggregation pipeline
Le pipeline d'agrégation enchaîne des étapes de transformation — l'équivalent MongoDB d'un SELECT avec GROUP BY et jointures.
db.commandes.aggregate([
-- Étape 1 : filtrer
{ $match: { statut: "livree" } },
-- Étape 2 : déplier le tableau de lignes
{ $unwind: "$lignes" },
-- Étape 3 : grouper par catégorie
{ $group: {
_id: "$lignes.categorie",
total_ventes: { $sum: "$lignes.montant" },
nb_articles: { $sum: "$lignes.qte" }
} },
-- Étape 4 : trier par total décroissant
{ $sort: { total_ventes: -1 } },
-- Étape 5 : top 5
{ $limit: 5 }
])
Étapes pipeline essentielles
| Étape | Rôle |
|---|---|
$match | Filtrer (équiv. WHERE) |
$project | Sélectionner / calculer des champs |
$group | Agréger (équiv. GROUP BY) |
$sort | Trier (équiv. ORDER BY) |
$limit / $skip | Paginer |
$unwind | Déplier un tableau en lignes |
$lookup | Jointure entre collections |
$addFields | Ajouter des champs calculés |
Cassandra — Colonnes larges
Cassandra est conçu pour écrire des millions de lignes par seconde, distribué sur des dizaines de nœuds. Il ne fait pas de jointures — chaque table est conçue pour répondre à une requête spécifique.
Philosophie Cassandra : modéliser autour des requêtes, pas autour des entités. Dénormaliser est normal et souhaitable. Une même donnée peut exister dans plusieurs tables.
-- Keyspace = base de données
CREATE KEYSPACE boutique
WITH replication = {
'class': 'SimpleStrategy',
'replication_factor': 3
};
-- Table optimisée pour requête :
-- "commandes d'un client, triées par date"
CREATE TABLE boutique.commandes_par_client (
id_client UUID,
date_cmd TIMESTAMP,
id_commande UUID,
statut TEXT,
total_ht DECIMAL,
-- Partition key : répartit sur les nœuds
PRIMARY KEY ((id_client), date_cmd, id_commande)
) WITH CLUSTERING ORDER BY
(date_cmd DESC);
Clés Cassandra — concept fondamental
| Concept | Rôle |
|---|---|
| Partition Key | Détermine sur quel nœud est stockée la donnée. Toutes les requêtes DOIVENT filtrer par partition key. |
| Clustering Key | Trie les données à l'intérieur d'une partition. Permet le filtre sur des plages (BETWEEN, >=). |
Sans partition key dans le WHERE, Cassandra doit scanner tous les nœuds (full cluster scan) — requête extrêmement lente. Toujours concevoir les tables query-first.
CQL — Cassandra Query Language
── INSERT (pas d'UPDATE si inexistant : toujours INSERT) ──
INSERT INTO boutique.commandes_par_client
(id_client, date_cmd, id_commande, statut, total_ht)
VALUES
(uuid(), toTimestamp(now()), uuid(), 'en_attente', 149.90)
USING TTL 2592000; -- expiration en 30 jours
── SELECT (partition key obligatoire) ──────────────────
SELECT * FROM boutique.commandes_par_client
WHERE id_client = 550e8400-e29b-41d4-a716-446655440000
AND date_cmd >= '2024-01-01'
LIMIT 50;
── UPDATE ──────────────────────────────────────────────
UPDATE boutique.commandes_par_client
SET statut = 'livree'
WHERE id_client = 550e8400-e29b-41d4-a716-446655440000
AND date_cmd = '2024-03-15 10:30:00'
AND id_commande = 123e4567-e89b-12d3-a456-426614174000;
── DELETE ──────────────────────────────────────────────
DELETE FROM boutique.commandes_par_client
WHERE id_client = 550e8400-e29b-41d4-a716-446655440000
AND date_cmd = '2024-03-15 10:30:00'
AND id_commande = 123e4567-e89b-12d3-a456-426614174000;
Neo4j — Base de graphes
Neo4j stocke des nœuds (entités) et des relations (liens nommés et directionnels) comme objets de première classe. Les relations sont aussi importantes que les données elles-mêmes.
Modèle de données
Quand utiliser Neo4j ?
En SQL, traverser 6 niveaux de relations (amis d'amis d'amis...) nécessite 6 jointures récursives. Neo4j traverse nativement n'importe quelle profondeur en quelques millisecondes.
Cypher — Requêtes Neo4j
Cypher est le langage déclaratif de Neo4j. Sa syntaxe utilise des patterns visuels pour décrire le graphe à traverser.
── CREATE — créer des nœuds et relations ────────────────
CREATE (alice:Personne {nom: "Alice", age: 30})
CREATE (bob:Personne {nom: "Bob", age: 25})
CREATE (alice)-[:CONNAIT {depuis: 2019}]->(bob)
── MATCH — lire des patterns ────────────────────────────
-- Tous les amis d'Alice
MATCH (p:Personne {nom: "Alice"})-[:CONNAIT]->(ami)
RETURN ami.nom, ami.age
-- Amis d'amis (profondeur 2)
MATCH (p {nom: "Alice"})-[:CONNAIT*2]->(ami2)
RETURN ami2.nom
-- Chemin le plus court entre Alice et Dave
MATCH p = shortestPath(
(a {nom:"Alice"})-[:CONNAIT*]-(d {nom:"Dave"})
)
RETURN p
── Recommandation : films vus par amis mais pas par moi ─
MATCH (moi:Personne {nom:"Alice"})
-[:CONNAIT]->(ami)-[:A_VU]->(film:Film)
WHERE NOT (moi)-[:A_VU]->(film)
RETURN film.titre, count(ami) AS popularite
ORDER BY popularite DESC
── MERGE — créer si n'existe pas (upsert) ───────────────
MERGE (p:Personne {email: "alice@ex.com"})
ON CREATE SET p.nom = "Alice", p.cree_le = datetime()
ON MATCH SET p.vu_le = datetime()
── SET / DELETE ─────────────────────────────────────────
MATCH (p {nom:"Bob"}) SET p.age = 26
MATCH (p {nom:"Bob"}) DETACH DELETE p ← supprime nœud + ses relations
Cheat sheet NoSQL
Redis
| SET / GET | Chaîne simple |
| HSET / HGET | Hash (objet) |
| RPUSH / LPOP | Liste (file FIFO) |
| SADD / SMEMBERS | Ensemble unique |
| ZADD / ZREVRANGE | Sorted set (score) |
| EXPIRE / TTL | Expiration de clé |
MongoDB
| insertOne / Many | Insérer |
| find({filtre}) | Lire |
| updateOne / $set | Modifier un champ |
| deleteOne / Many | Supprimer |
| aggregate([...]) | Pipeline d'agrégation |
| $match / $group | Filtrer / grouper |
Cassandra
| CREATE KEYSPACE | Base de données |
| PRIMARY KEY ((pk), ck) | Partition + clustering |
| INSERT INTO | Insérer (upsert) |
| SELECT (avec PK) | Requête obligatoire |
| USING TTL | Expiration automatique |
| Query-first design | Table par requête |
Neo4j — Cypher
| CREATE (n:Label) | Créer un nœud |
| -[:RELATION]-> | Créer une relation |
| MATCH … RETURN | Lire un pattern |
| MERGE | Upsert nœud/relation |
| [:REL*n] | Traverser n niveaux |
| shortestPath() | Chemin le plus court |