ACID — les 4 garanties d'une transaction
Atomicité, Cohérence, Isolation, Durabilité — quatre promesses que la base de données fait quand tu écris BEGIN.
Une transaction, c'est un groupe d'opérations SQL traité comme une seule unité indivisible. Soit tout réussit, soit rien ne change. La base de données fait quatre promesses pour garantir ça — résumées par l'acronyme ACID.
Atomicité
Si une opération échoue au milieu de la transaction, toutes les opérations précédentes sont annulées. La base revient à son état initial.
Imagine un virement bancaire : débiter le compte A et créditer le compte B. Si le crédit échoue après le débit, l'atomicité garantit que le débit est annulé automatiquement.
-- Virement atomique BEGIN; UPDATE comptes SET solde = solde - 100 WHERE id = 1; UPDATE comptes SET solde = solde + 100 WHERE id = 2; COMMIT; -- ou ROLLBACK si erreur
Cohérence
Avant et après la transaction, la base de données est dans un état valide : clés étrangères intactes, contraintes
CHECK respectées, soldes non négatifs…
La cohérence est garantie par la combinaison de l'atomicité ET des contraintes déclarées dans le schéma. Si une opération violerait une contrainte, la transaction est annulée.
Isolation
Deux transactions s'exécutant en parallèle ne doivent pas se perturber mutuellement — comme si chacune s'exécutait seule. Dans quelle mesure elles se voient est contrôlé par le niveau d'isolation.
C'est la propriété la plus complexe à mettre en œuvre. On verra dans les sections suivantes que les bases de données offrent plusieurs degrés d'isolation, chacun avec ses compromis.
Durabilité
Une fois que la base confirme le COMMIT, les données sont sur disque — même si le serveur crashe une milliseconde après. C'est le rôle du WAL (Write-Ahead Log).
Le WAL est un journal séquentiel : toute modification est d'abord écrite dans ce journal, puis confirmée au disque via fsync(), avant que le COMMIT soit acquitté. En cas de crash, le journal permet de rejouer les transactions commitées.
🧪 Simulateur — Atomicité
Anomalies de concurrence
Dirty read, non-repeatable read, phantom read, write skew — ce qui peut mal tourner quand deux transactions s'exécutent en même temps.
Quand plusieurs transactions s'exécutent en même temps, des interactions inattendues peuvent survenir. Ces anomalies de concurrence sont ce que les niveaux d'isolation cherchent à prévenir.
Dirty Read
Transaction A lit les modifications de Transaction B avant que B ait fait son COMMIT. Si B fait ROLLBACK, A a travaillé avec des données qui n'ont jamais existé officiellement.
BEGIN; -- Lit solde = 1100 -- (donnée non commitée !) SELECT solde FROM comptes WHERE id = 1; -- Prend une décision -- basée sur 1100...
BEGIN; UPDATE comptes SET solde = 1100 WHERE id = 1; -- pas encore commitée ROLLBACK; -- annulée !
Non-Repeatable Read
Transaction A lit une ligne. Transaction B modifie cette ligne et commite. Transaction A relit la même ligne et obtient une valeur différente.
A a pourtant lu la même requête dans la même transaction. Avec un Non-Repeatable Read, le résultat change entre deux lectures.
Phantom Read
Transaction A exécute
SELECT ... WHERE montant > 100 et obtient 5 lignes. Transaction B insère une nouvelle ligne avec montant = 150 et commite. Transaction A réexécute la même requête et obtient 6 lignes.
C'est comme le Non-Repeatable Read, mais pour un ensemble de lignes plutôt qu'une seule. Les "fantômes" sont ces nouvelles lignes qui apparaissent (ou disparaissent) entre deux lectures.
Lost Update
A et B lisent la même valeur (ex:
stock = 10). A décrémente et écrit 9. B décrémente et écrit 9 aussi — en écrasant le travail de A. Le stock devrait être à 8.
-- T1 lit stock=10, décrémente UPDATE produits SET stock = 10 - 1 WHERE id = 5; -- T2 a aussi lu stock=10, décrémente UPDATE produits SET stock = 10 - 1 WHERE id = 5; -- Résultat : stock=9 au lieu de 8 → mise à jour perdue
Write Skew
Exemple classique : deux médecins de garde. La règle exige qu'au moins un médecin soit en service. Chacun lit "deux médecins actifs", conclut "je peux me désinscrire", et se désinscrit. Résultat : zéro médecin.
Ce que rend le Write Skew insidieux : chaque transaction individuelle respecte les contraintes telles qu'elle les voit. C'est leur combinaison qui les viole. Seul SERIALIZABLE le prévient.
| Anomalie | Ce qui se passe | Niveau minimum pour l'éviter |
|---|---|---|
| Dirty Read | Lit les données non-commitées d'une autre TX | READ COMMITTED |
| Non-Repeatable Read | Même ligne = valeurs différentes dans la TX | REPEATABLE READ |
| Phantom Read | Ensemble de lignes change entre deux lectures | SERIALIZABLE |
| Lost Update | Écriture écrase celle d'une autre TX | REPEATABLE READ (selon impl.) |
| Write Skew | Décisions mutuellement contradictoires | SERIALIZABLE |
Les 4 niveaux d'isolation
READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE — chaque niveau ajoute une protection, retire une anomalie.
La norme SQL:1992 définit quatre niveaux d'isolation. Chaque niveau ajoute une protection contre une anomalie, au prix d'un coût de synchronisation un peu plus élevé. Tu choisis le compromis selon le besoin de ton application.
-- Définir le niveau avant ou juste après BEGIN SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- ... tes opérations ... COMMIT;
READ UNCOMMITTED
Une transaction peut lire les modifications non-commitées des autres. Les Dirty Reads, Non-Repeatable Reads et Phantom Reads sont tous possibles.
Pratiquement jamais utilisé en production. PostgreSQL l'accepte syntaxiquement mais le traite comme READ COMMITTED (un Dirty Read n'a aucun sens avec MVCC).
READ COMMITTED
Chaque lecture voit uniquement les données commitées au moment de cette lecture. Pas de Dirty Reads. Mais deux lectures dans la même transaction peuvent renvoyer des résultats différents.
C'est le niveau par défaut de PostgreSQL. Pour la majorité des applications web (lecture-modification-écriture sans dépendances complexes), il est suffisant.
REPEATABLE READ
Toutes les lectures dans la transaction voient un snapshot figé au moment du premier
SELECT. Pas de Dirty Read, pas de Non-Repeatable Read. En PostgreSQL, les Phantom Reads sont aussi éliminés (grâce à MVCC).
Défaut MySQL InnoDB. PostgreSQL l'implémente via MVCC — chaque transaction travaille avec un snapshot cohérent du début à la fin.
SERIALIZABLE
Zéro anomalie. Le moteur garantit que l'exécution concurrente produit un résultat équivalent à une exécution en série. PostgreSQL utilise SSI (Serializable Snapshot Isolation) — détection des cycles de dépendances, pas de locks.
| Niveau | Dirty Read | Non-Repeatable | Phantom | Write Skew |
|---|---|---|---|---|
| READ UNCOMMITTED | possible | possible | possible | possible |
| READ COMMITTED | protégé | possible | possible | possible |
| REPEATABLE READ | protégé | protégé | partiel* | possible |
| SERIALIZABLE | protégé | protégé | protégé | protégé |
* PostgreSQL REPEATABLE READ élimine aussi les Phantom Reads via MVCC.
🧪 Simulateur — Dirty Read
MVCC — PostgreSQL et les snapshots
Au lieu de locker, PostgreSQL crée des versions — chaque transaction vit dans son propre snapshot du passé.
PostgreSQL n'utilise pas de locks de lecture pour implémenter l'isolation. À la place, il crée des versions multiples de chaque ligne — c'est le MVCC (Multi-Version Concurrency Control).
Quand tu fais
UPDATE sur une ligne, PostgreSQL ne modifie pas la ligne en place. Il crée une nouvelle version de la ligne et marque l'ancienne comme obsolète. Les transactions en cours voient l'ancienne version ; les nouvelles transactions voient la nouvelle.
Résultat : les lecteurs ne bloquent jamais les écrivains, et les écrivains ne bloquent jamais les lecteurs.
xmin et xmax
Chaque ligne en PostgreSQL porte deux colonnes cachées :
| Colonne | Signification |
|---|---|
xmin |
ID de la transaction qui a créé cette version |
xmax |
ID de la transaction qui a supprimé/remplacé cette version (0 si encore active) |
-- Voir les versions cachées SELECT xmin, xmax, id, solde FROM comptes WHERE id = 1; -- Résultat possible : -- xmin | xmax | id | solde -- 1043 | 0 | 1 | 900
Le snapshot de transaction
Au démarrage de la transaction (ou du premier SELECT selon le niveau d'isolation), PostgreSQL prend un "snapshot" : la liste de toutes les transactions en cours. La transaction ne verra jamais les modifications de ces transactions, même si elles commitent ensuite.
commence
tx en cours: {101}
(non visible pour 100)
VACUUM — nettoyage des versions mortes
Après un UPDATE, l'ancienne version de la ligne reste sur disque jusqu'à ce que VACUUM la nettoie. VACUUM identifie les versions dont le
xmax est commitée et qu'aucune transaction active ne peut plus voir, puis libère l'espace.
-- VACUUM manuel (normalement automatique) VACUUM comptes; -- Voir les tables avec le plus de tuples morts SELECT relname, n_dead_tup, n_live_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
n_dead_tup est important pour éviter le table bloat (tables qui gonflent sur disque).
READ COMMITTED vs REPEATABLE READ en MVCC
| Niveau | Snapshot pris quand ? | Conséquence |
|---|---|---|
| READ COMMITTED | À chaque instruction SQL | Chaque SELECT voit les données les plus récentes commitées |
| REPEATABLE READ | Au premier SELECT de la transaction | Toute la transaction voit le même snapshot figé |
ERROR: could not serialize access due to read/write dependencies. L'application doit catcher cette erreur et rejouer la transaction.
InnoDB — row locks, gap locks, next-key locks
MySQL prévient les phantoms autrement que PostgreSQL — en lockant l'espace entre les valeurs d'index.
MySQL InnoDB utilise une approche différente de PostgreSQL. En plus du MVCC (qui existe aussi dans InnoDB), il emploie des locks au niveau des lignes et des index pour prévenir les anomalies de concurrence.
Le niveau par défaut d'InnoDB est REPEATABLE READ. Contrairement à PostgreSQL, InnoDB utilise des gap locks pour prévenir les Phantom Reads dans ce niveau — pas besoin de passer à SERIALIZABLE.
Row Lock
Quand tu modifies une ligne, InnoDB pose un row lock sur cette ligne. Les autres transactions qui tentent de modifier la même ligne sont bloquées jusqu'au COMMIT.
-- Row lock implicite lors d'un UPDATE UPDATE commandes SET statut = 'expédiée' WHERE id = 42; -- La ligne 42 est lockée jusqu'au COMMIT
Gap Lock
InnoDB peut locker l'espace entre deux valeurs d'index, empêchant toute insertion dans cet intervalle. C'est ce qui prévient les Phantom Reads.
Exemple : si tu lis SELECT ... WHERE montant BETWEEN 100 AND 200, InnoDB locke le gap entre 100 et 200. Aucune autre transaction ne peut insérer une ligne avec montant = 150 pendant que ta transaction est active.
Next-Key Lock
Le mécanisme de lock par défaut d'InnoDB pour les range queries. Il combine un row lock sur la ligne existante et un gap lock sur l'intervalle qui la précède.
-- Cette requête pose des next-key locks SELECT * FROM commandes WHERE montant > 100 FOR UPDATE; -- Locke les lignes ET les gaps dans l'index montant
| Type de lock | Ce qui est locké | Prévient |
|---|---|---|
| Row lock | Une ligne précise | Modifications concurrentes de la même ligne |
| Gap lock | Espace entre deux valeurs d'index | Insertions fantômes dans un range |
| Next-key lock | Ligne + gap précédent | Non-Repeatable Reads + Phantom Reads |
SHOW ENGINE INNODB STATUS pour diagnostiquer les deadlocks InnoDB.
SELECT FOR UPDATE — le verrou explicite
Quand tu as besoin de lire ET écrire sans conflit, FOR UPDATE acquiert le lock dès la lecture — pas au COMMIT.
SELECT ... FOR UPDATE est le verrou explicite de SQL. Il dit à la base de données : "Je vais lire ces lignes ET les modifier — acquiers le lock dès maintenant, pas au moment du COMMIT."
Sans lock explicite, deux transactions peuvent lire la même ligne en parallèle, décider toutes les deux de la modifier, et la dernière à écrire écrase la première — c'est le Lost Update.
-- T1 : lit stock=10 SELECT stock FROM produits WHERE id = 5; -- T2 aussi lit stock=10 -- T2 écrit stock=9 -- T1 écrit stock=9 -- (devrait être 8 !)
-- T1 : lit ET locke SELECT stock FROM produits WHERE id = 5 FOR UPDATE; -- T2 bloquée ici -- T1 écrit stock=9 -- T1 COMMIT -- T2 lit stock=9, écrit 8 ✓
FOR UPDATE vs FOR SHARE
| Commande | Type de lock | Bloque qui ? | Usage |
|---|---|---|---|
FOR UPDATE |
Exclusif | Autres FOR UPDATE + FOR SHARE + écritures | Lecture avant modification |
FOR SHARE |
Partagé | Autres FOR UPDATE + écritures (pas les autres FOR SHARE) | Lecture avec garantie de non-modification |
-- Lock exclusif : personne d'autre ne peut lire/écrire ces lignes SELECT * FROM commandes WHERE statut = 'en_attente' FOR UPDATE; -- Lock partagé : d'autres peuvent lire, mais pas modifier SELECT * FROM produits WHERE id = 5 FOR SHARE; -- MySQL : équivalent LOCK IN SHARE MODE
SKIP LOCKED et NOWAIT
Par défaut, FOR UPDATE attend que les lignes lockées soient libérées. Deux options permettent de changer ce comportement :
-- NOWAIT : échoue immédiatement si locked SELECT * FROM jobs WHERE statut = 'pending' FOR UPDATE NOWAIT; -- ERROR: could not obtain lock on row in relation "jobs" -- SKIP LOCKED : ignore les lignes lockées (idéal pour les queues) SELECT * FROM jobs WHERE statut = 'pending' LIMIT 1 FOR UPDATE SKIP LOCKED; -- Retourne la première ligne NON lockée — parfait pour workers parallèles
En PostgreSQL,
FOR UPDATE en READ COMMITTED lit toujours la version la plus récente de la ligne (pas le snapshot), puis pose le lock. Si la ligne a été modifiée entre le snapshot et le lock, PostgreSQL relit la ligne fraîche.
Deadlocks — détecter et prévenir les cycles
Transaction A attend B, B attend A — un cycle que la base brise en sacrifiant une transaction.
Un deadlock survient quand deux transactions (ou plus) s'attendent mutuellement, chacune tenant un verrou que l'autre veut. Le cycle est indéfini — aucune ne peut progresser sans l'aide du moteur.
T1 locke la ligne A, puis veut la ligne B.
T2 locke la ligne B, puis veut la ligne A.
→ Ni T1 ni T2 ne peut continuer. La base détecte le cycle et annule la transaction "victime".
Détection et résolution
Les bases de données modernes détectent automatiquement les deadlocks :
| Moteur | Mécanisme | Victime choisie |
|---|---|---|
| PostgreSQL | Détection de cycle dans le graphe d'attente | Transaction "moins coûteuse à rollbacker" |
| MySQL InnoDB | Détection de cycle + timeout (innodb_lock_wait_timeout) |
Transaction avec le moins de modifications |
-- PostgreSQL : l'erreur reçue par la victime -- ERROR: deadlock detected -- DETAIL: Process 12345 waits for ShareLock on transaction 789; -- blocked by process 67890. -- Hint: See server log for query details. -- MySQL : voir les deadlocks récents SHOW ENGINE INNODB STATUS;
Prévention
Si toutes les transactions acquièrent les verrous dans le même ordre (ex: toujours A avant B), un cycle devient impossible. Simple et efficace.
Plus une transaction dure longtemps, plus elle occupe des verrous et multiplie les risques de conflits. Réduis le travail entre BEGIN et COMMIT au strict nécessaire.
Sans index, un UPDATE peut scanner et locker toute la table au lieu d'une seule ligne. Des index bien placés réduisent drastiquement la surface de lock.
🧪 Simulateur — Deadlock
Observe comment deux transactions se bloquent mutuellement, et comment le moteur résout le cycle.
Optimistic locking — écrire sans verrouiller
Pas de lock — juste une colonne version. Au COMMIT, si quelqu'un a modifié entre-temps, la mise à jour échoue proprement.
Le locking pessimiste (FOR UPDATE) part du principe que les conflits sont probables — il locke préventivement. Le locking optimiste fait le pari inverse : les conflits sont rares, donc on ne locke pas. On vérifie au moment d'écrire que rien n'a changé.
versionChaque ligne porte une colonne
version (entier incrémental ou timestamp). Quand tu modifies une ligne, tu inclus la version que tu as lue dans ta condition WHERE. Si quelqu'un d'autre a modifié la ligne entre-temps, sa version est différente — ton UPDATE ne touche aucune ligne, et tu détectes le conflit.
-- Lecture (pas de lock) SELECT id, solde, version FROM comptes WHERE id = 1; -- Retourne : id=1, solde=1000, version=7 -- Modification avec vérification de version UPDATE comptes SET solde = 900, version = 8 WHERE id = 1 AND version = 7; -- Si 0 ligne affectée → conflit → rejouer la transaction
version=7
nouvelle valeur
version=7
RETRY
Optimiste vs Pessimiste — quand choisir ?
| Critère | Optimiste | Pessimiste (FOR UPDATE) |
|---|---|---|
| Taux de conflits | Faible — peu de modifications concurrentes | Élevé — beaucoup de concurrence sur les mêmes lignes |
| Durée de la transaction | Longue possible (pas de lock tenu) | Courte recommandée (lock tenu pendant toute la TX) |
| Coût en cas de conflit | Élevé (recalcul complet + retry) | Faible (attente puis exécution) |
| Deadlocks | Impossibles | Possibles |
| Exemple d'usage | Wiki, formulaires de saisie | Réservations, stocks en temps réel |
Implémentation en Doctrine ORM
Doctrine supporte nativement les deux modes via le LockMode :
// Pessimiste : FOR UPDATE $em->find(Compte::class, $id, LockMode::PESSIMISTIC_WRITE); // Optimiste : vérification de version try { $em->lock($compte, LockMode::OPTIMISTIC, $versionLue); $em->flush(); } catch (OptimisticLockException $e) { // Conflit — rejouer la transaction }
#[ORM\Version] sur une propriété int ou DateTimeImmutable de ton entité. Doctrine gère l'incrément et la vérification automatiquement.
If-Match. Le serveur vérifie que la ressource n'a pas changé avant d'appliquer la modification. Même principe, même bénéfice — zéro lock de base de données.
Savepoints — le point de sauvegarde partiel
Annuler une partie d'une transaction sans perdre tout le travail — SAVEPOINT, ROLLBACK TO, RELEASE.
Un savepoint est un point de restauration partielle à l'intérieur d'une transaction. Il permet d'annuler une partie du travail sans tout rollbacker — comme un "Ctrl+Z" sélectif.
SAVEPOINT nom crée un marqueur dans la transaction courante. Tu peux ensuite revenir à ce marqueur avec ROLLBACK TO SAVEPOINT nom sans annuler tout ce qui précède.
Les trois commandes
BEGIN; INSERT INTO logs (msg) VALUES ('étape 1'); -- OK SAVEPOINT avant_etape2; INSERT INTO commandes (id) VALUES (999); -- Peut échouer -- Si l'INSERT a échoué, on revient au savepoint ROLLBACK TO SAVEPOINT avant_etape2; -- L'INSERT de logs est toujours là, l'INSERT commandes est annulé RELEASE SAVEPOINT avant_etape2; -- Libère le savepoint (optionnel) COMMIT; -- Commite ce qui reste (logs)
Cas d'usage — boucle avec gestion d'erreurs partielles
Les savepoints sont particulièrement utiles pour importer un lot d'enregistrements où certains peuvent échouer individuellement, sans sacrifier les succès.
BEGIN; DO $$ DECLARE rec RECORD; i INT := 0; BEGIN FOR rec IN SELECT * FROM import_staging LOOP i := i + 1; SAVEPOINT sp; BEGIN INSERT INTO commandes VALUES (rec.id, rec.montant); EXCEPTION WHEN OTHERS THEN ROLLBACK TO SAVEPOINT sp; -- annule juste cet INSERT INSERT INTO erreurs_import (ligne, msg) VALUES (i, SQLERRM); END; RELEASE SAVEPOINT sp; END LOOP; END; $$; COMMIT; -- Commite les succès, les erreurs sont loggées
Savepoints et Doctrine
Quand tu appelles
$em->beginTransaction() à l'intérieur d'une autre transaction (transactions imbriquées), Doctrine crée automatiquement un savepoint en interne — car SQL ne supporte pas les BEGIN imbriqués.
$conn->beginTransaction(); // BEGIN réel $conn->beginTransaction(); // SAVEPOINT doctrine_savepoint_1 try { // opérations partielles $conn->commit(); // RELEASE SAVEPOINT doctrine_savepoint_1 } catch (Exception $e) { $conn->rollBack(); // ROLLBACK TO SAVEPOINT doctrine_savepoint_1 } $conn->commit(); // COMMIT réel
Synthèse — choisir le bon niveau d'isolation
Quel niveau pour quelle application ? Optimistic ou pessimistic ? Le guide de décision.
Maintenant que tu connais tous les outils — niveaux d'isolation, MVCC, locks, optimistic locking — il s'agit de choisir le bon pour chaque situation. Voici le guide de décision.
Quel niveau d'isolation ?
| Situation | Niveau recommandé | Pourquoi |
|---|---|---|
| Lectures seules, analytics, reporting | READ COMMITTED | Performances maximales, Dirty Reads pas possibles avec snapshots |
| Transactions classiques CRUD | READ COMMITTED (PostgreSQL défaut) | Suffisant pour la majorité des applications web |
| Calculs financiers, comptabilité | REPEATABLE READ | Snapshot cohérent sur toute la transaction |
| Invariants complexes, write skew possible | SERIALIZABLE | Garantie totale, SSI en PostgreSQL sans locks inutiles |
Optimistic ou Pessimistic ?
| Choix | Quand l'utiliser | Exemple concret |
|---|---|---|
| Pessimistic (FOR UPDATE) | Conflits fréquents, stocks limités, réservations | Dernier siège d'avion, stock produit ≤ 5 |
| Optimistic (version column) | Conflits rares, formulaires longs, APIs REST | Édition de profil utilisateur, wiki |
| SKIP LOCKED | Workers parallèles sur une queue de jobs | Traitement d'emails, jobs async sans Redis |
Commandes SQL de référence
-- Définir le niveau d'isolation SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; -- Ou au niveau session (PostgreSQL) SET default_transaction_isolation = 'repeatable read'; -- Lock explicite SELECT * FROM t WHERE id = 1 FOR UPDATE; SELECT * FROM t WHERE id = 1 FOR UPDATE NOWAIT; SELECT * FROM t LIMIT 1 FOR UPDATE SKIP LOCKED; -- Savepoints SAVEPOINT mon_save; ROLLBACK TO SAVEPOINT mon_save; RELEASE SAVEPOINT mon_save; -- Diagnostique PostgreSQL SELECT * FROM pg_locks; SELECT * FROM pg_stat_activity; -- Diagnostique InnoDB SHOW ENGINE INNODB STATUS;
Checklist de bonnes pratiques
- Transactions courtes : moins de temps entre BEGIN et COMMIT = moins de locks tenus = moins de contentions.
- Index sur les colonnes de filtre : un UPDATE sans index peut locker une table entière au lieu d'une ligne.
- Ordre de lock cohérent : si plusieurs entités sont lockées, toujours dans le même ordre dans toutes les transactions.
- Retry automatique : deadlock = erreur transitoire. Implémenter un retry avec backoff. Idem pour les serialization failures.
- Surveiller pg_stat_activity : les transactions idle-in-transaction qui traînent tiennent des locks et bloquent VACUUM.
- SERIALIZABLE n'est pas gratuit : en charge élevée, les false positives SSI peuvent augmenter le taux de retry. Mesure avant de généraliser.
La plupart des problèmes de transactions viennent d'une incompréhension du niveau d'isolation en vigueur. Connais le défaut de ton moteur (READ COMMITTED pour PostgreSQL, REPEATABLE READ pour MySQL), teste avec de la concurrence réelle, et monte le niveau uniquement quand tu as un problème prouvé à résoudre.