SQLite est l’équivalent en base de données d’un couteau de poche bien conçu. Il est compact, tranchant et, d’une certaine façon, toujours là quand vous en avez besoin.
Puis un jour vous essayez de construire une maison avec, et le manche commence à faire mal.
C’est à ce moment-là que les équipes commencent à se disputer sur Slack : « SQLite suffit. » « Non, c’est le goulot d’étranglement. » « Il nous faut juste des index. »
Pendant ce temps, les utilisateurs regardent des chargeurs qui tournent, et votre téléphone d’astreinte chauffe.
La véritable différence : base fichier vs base serveur (et pourquoi ça compte)
SQLite et MySQL parlent tous deux SQL. Ce vocabulaire partagé pousse les gens à croire qu’ils sont interchangeables.
Ils ne le sont pas. La première différence n’est ni la syntaxe, ni les fonctionnalités, ni même la vitesse. C’est l’architecture.
SQLite : une bibliothèque avec un fichier
SQLite est un moteur de base de données embarqué. Il vit à l’intérieur de votre processus et stocke les données dans un seul fichier (plus des fichiers annexes comme
-wal et -shm en mode WAL). Il n’y a pas de serveur de base de données auquel se connecter. Votre application lit et écrit
des octets via la bibliothèque SQLite, directement, en utilisant la sémantique du système de fichiers comme frontière de durabilité.
C’est pourquoi SQLite est facile à expédier, facile à tester et étonnamment rapide sur une seule machine avec une concurrence simple.
C’est aussi pourquoi le système de fichiers et le sous-système de stockage deviennent partie prenante de l’histoire de la correction de la base de données — que vous le souhaitiez ou non.
MySQL : un service séparé avec concurrence au niveau processus
MySQL est un serveur. C’est un processus séparé avec sa propre gestion mémoire, comportement du pool de threads, verrous internes, journaux de redo, pool de tampons,
capacités de réplication et un protocole réseau. Votre application envoie des requêtes ; MySQL les planifie, coordonne la concurrence et persiste
les modifications via son moteur de stockage (généralement InnoDB).
Cette séparation vous donne une surface opérationnelle claire : vous pouvez la surveiller, l’ajuster, la répliquer, la sauvegarder sans arrêter le monde, et
isoler la charge de la base de données de la charge applicative. Elle introduit aussi une charge opérationnelle. Félicitations, vous possédez maintenant un service de base de données.
Ce que cela implique en production
Si votre application est un seul processus sur une seule machine avec une faible concurrence d’écriture, SQLite peut être un excellent choix.
Si vous avez plusieurs instances d’application, une file d’attente d’écritures, des attentes de durabilité strictes, ou une quelconque ambition de haute disponibilité, le « c’est juste un fichier » de SQLite
devient « c’est juste un fichier… partagé à travers un système distribué », et cette phrase finit par un rapport d’incident.
La décision n’est pas « SQLite est un jouet, MySQL est sérieux ». La décision est de savoir si les modes de panne de votre système sont mieux gérés par
le verrouillage au niveau fichier et la sémantique de l’OS (SQLite) ou par une couche de concurrence et de durabilité conçue pour cela avec des outils opérationnels (MySQL).
Idée paraphrasée de Werner Vogels (CTO d’Amazon) : vous devriez planifier la défaillance plutôt que prétendre qu’elle n’arrivera pas. Les bases de données sont l’endroit où prétendre revient cher.
Faits et historique qui expliquent les modes de panne actuels
- SQLite a été créé en 2000 par D. Richard Hipp, initialement pour soutenir des outils internes. Il a été conçu pour être embarqué et simple à déployer.
- La licence « domaine public » de SQLite est une raison majeure de sa ubiquité — les fournisseurs n’ont pas besoin de gymnastique juridique pour l’intégrer à leurs produits.
- SQLite est la base par défaut dans de nombreuses piles mobiles parce qu’il est petit, fiable sur un appareil unique et ne nécessite pas de serveur.
- Le mode WAL (write-ahead logging) dans SQLite a considérablement amélioré la concurrence, mais il ne vous donne toujours pas « plusieurs écrivains » comme le font les bases serveurs.
- MySQL a commencé au milieu des années 1990 et a grandi dans l’hébergement web, où de nombreux clients concurrents et des services de longue durée sont la norme.
- InnoDB est devenu le moteur de stockage par défaut de MySQL à partir de MySQL 5.5, principalement parce qu’il offrait transactions, verrouillage au niveau ligne et récupération après crash.
- La réplication a façonné l’identité opérationnelle de MySQL : réplicas asynchrones, montée en lecture et schémas de basculement sont devenus des pratiques standard en exploitation web.
- La correction de SQLite repose sur les garanties du système de fichiers. La plupart des systèmes de fichiers locaux vont bien ; les systèmes de fichiers réseau et certaines couches de stockage « créatives » peuvent devenir problématiques.
Signes de migration : quand exactement SQLite a dépassé son rôle
1) Vous voyez « database is locked » sous vraie charge
SQLite peut gérer plusieurs lecteurs, et en WAL il peut gérer un lecteur pendant qu’un écrivain est actif. Mais la concurrence d’écriture est la falaise.
Un seul écrivain à la fois. Si votre charge comporte des pics d’écritures — sessions, événements, compteurs, états de boîte de réception, files d’attente de tâches — la latence de queue augmentera.
Le signe : les requêtes ne tombent pas en erreur immédiatement ; elles se bloquent. Votre p95 devient votre p99. Puis les utilisateurs se plaignent. Puis vous ajoutez des retries.
Ensuite vous amplifiez le thundering herd. C’est le moment où il faut cesser de négocier avec la physique et commencer à planifier la migration.
2) Vous avez mis l’app à l’échelle horizontalement et SQLite est devenu un problème de fichier partagé
Exécuter SQLite avec plusieurs instances d’app fonctionne seulement quand chaque instance a son propre fichier de base (par locataire, par nœud, ou par appareil).
Au moment où vous placez un fichier SQLite sur un stockage partagé pour plusieurs instances, vous créez un problème de verrouillage et de cohérence distribué.
Même si cela « fonctionne en staging », cela peut dégénérer en tempêtes de verrous, lectures obsolètes ou risques de corruption selon votre couche de stockage.
MySQL existe précisément pour que vous n’ayez pas à parier votre disponibilité sur le comportement de votre montage NFS pendant une perte de paquets.
3) Vous avez besoin de haute disponibilité, pas seulement de sauvegardes
Les sauvegardes ne sont pas de la haute disponibilité. SQLite est excellent pour les sauvegardes parce que l’artéfact est un fichier, mais le basculement n’est pas un problème de copie de fichiers.
Si votre activité exige de continuer à accepter des écritures lors d’une défaillance de nœud, vous voulez de la réplication, des outils d’élection/basculement et un endroit pour appliquer
des politiques opérationnelles. C’est le territoire de MySQL.
4) Vous avez besoin d’une latence prévisible sous des charges mixtes
SQLite peut être fulgurant pour des lectures ponctuelles et de petites transactions. Mais quand une requête devient volumineuse — un scan complet accidentel, un index manquant,
une opération type vacuum selon vos patterns — tout votre processus peut en souffrir parce que le moteur de la base est embarqué.
Avec MySQL, la base a sa propre mémoire et son comportement d’ordonnancement. Vous pouvez isoler et régler. Vous pouvez tuer une requête. Vous pouvez définir des limites par utilisateur.
Vous pouvez éviter que votre application ne devienne des dommages collatéraux.
5) Vous luttez avec la durabilité et la sémantique des sauvegardes
La durabilité de SQLite dépend d’une utilisation correcte des transactions et du respect par le stockage sous-jacent des sémantiques fsync. Beaucoup d’équipes
définissent involontairement des pragmas qui échangent durabilité contre performance. Puis elles sont surprises quand un crash efface des écritures récentes.
Si vous en êtes au point de discuter des réglages synchronous et de « quel est le vrai risque », vous payez déjà le coût cognitif.
MySQL vous offre des contrôles de durabilité standard dans l’industrie et des schémas établis de sauvegarde/basculement.
6) Vous avez besoin d’observabilité opérationnelle exploitable par l’astreinte
SQLite n’est pas livré avec un performance schema intégré, des journaux de requêtes lentes, l’état de réplication, des métriques de buffer pool, ou des commandes admin standardisées.
Vous pouvez l’instrumenter, mais vous construisez votre propre couche d’opérations de base de données.
Si l’astreinte doit répondre à « que fait la base en ce moment ? » et que le mieux que vous puissiez offrir est « je peux ajouter des logs et redéployer »,
ce n’est pas une stratégie d’exploitation. C’est de l’espoir avec des étapes supplémentaires.
7) Votre modèle de données a dépassé l' »un fichier » opérationnellement
La nature monofichier de SQLite est pratique jusqu’à ce qu’elle devienne un artefact de déploiement. L’expédition, la migration, le verrouillage, la copie et la validation de ce fichier
se transforment en un rituel à haut risque. Avec MySQL, les migrations de schéma restent risquées, mais elles font partie d’un monde avec des outils matures,
des approches de migration en ligne et des playbooks établis.
8) Vous construisez des flux de données multi-tenant ou contrôlés par accès
SQLite n’a pas de comptes utilisateurs, pas d’authentification au niveau réseau, et pas de modèle de privilèges fin comme une base serveur.
Si votre posture de sécurité nécessite une séparation réelle des tâches ou un contrôle d’accès auditable, MySQL est un choix plus naturel.
Blague n°1 : SQLite est comme un videur très efficace pour un petit club — jusqu’à ce que votre application invite tout Internet et insiste pour dire que c’est toujours « une petite salle ».
Mode d’emploi de diagnostic rapide : trouver le goulot en 15 minutes
Le travail n’est pas de « décider que MySQL est meilleur ». Le travail est de prouver ce qui échoue. Voici l’ordre qui fait gagner du temps.
Première étape : confirmer la catégorie de symptôme (verrouillage, IO, CPU ou conception de requête)
- Vérifier la contention de verrous : erreurs comme
database is locked, timeouts, ou longues attentes autour des écritures. - Vérifier la latence de stockage : temps d’attente disque élevé, blocages fsync, ou IOPS saturés.
- Vérifier le CPU : cœur unique saturé dans le processus app (SQLite s’exécute en processus), ou temps d’exécution des requêtes augmentant avec l’utilisation CPU.
- Vérifier les plans de requête : scans complets et index manquants. SQLite fera volontiers la mauvaise chose rapidement jusqu’à ce qu’il ne le fasse plus.
Deuxième étape : reproduire avec un benchmark contrôlé
Utilisez un test orienté écriture et un test orienté lecture. Mesurez la latence p95/p99, pas seulement le débit. SQLite a souvent l’air correct jusqu’à ce que la latence de queue devienne moche.
Troisième étape : décider si la correction est tactique ou stratégique
- Correction tactique : ajouter un index, réduire la fréquence des écritures, grouper les écritures, activer WAL, définir un busy timeout, ou changer les frontières des transactions.
- Correction stratégique : migrer vers MySQL lorsque vous avez besoin d’écritures concurrentes, de HA, d’un meilleur contrôle opérationnel, ou d’une performance prévisible avec plusieurs clients.
Tâches pratiques : commandes, sorties et décisions (12+)
Ce sont les contrôles « montre-moi ». Chaque tâche inclut une commande, ce que signifie la sortie, et ce que vous faites ensuite.
Les commandes supposent un hôte Linux avec un fichier SQLite à /var/lib/app/app.db et un service MySQL si vous comparez.
Tâche 1 : Confirmer le mode WAL et les réglages synchronous (durabilité vs vitesse)
cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA journal_mode; PRAGMA synchronous;'
wal
2
Ce que cela signifie : wal améliore la concurrence lecteurs/écrivains. synchronous=2 est FULL (plus sûr).
Si vous voyez off ou 0, vous pourriez échanger la sécurité en cas de crash contre la performance.
Décision : Si vous avez besoin de durabilité et utilisez synchronous=OFF, corrigez cela en priorité. Si FULL pénalise les performances et que les écritures sont fréquentes, c’est un signal de migration.
Tâche 2 : Vérifier le busy timeout (comment vous vous comportez sous contention)
cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA busy_timeout;'
0
Ce que cela signifie : 0 signifie « échouer immédiatement » en cas de contention de verrou (ou remonter les erreurs rapidement).
Décision : Définissez un busy timeout raisonnable dans l’application (et éventuellement dans les pragmas DB) si les pics de verrous sont mineurs. Si vous avez besoin de longs busy timeouts pour survivre, vous camouflez une architecture un-écrivain.
Tâche 3 : Identifier rapidement les tables chaudes et la couverture par index
cr0x@server:~$ sqlite3 /var/lib/app/app.db ".schema" | sed -n '1,40p'
CREATE TABLE events(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
payload TEXT NOT NULL
);
CREATE INDEX idx_events_user_created ON events(user_id, created_at);
Ce que cela signifie : Vous vérifiez si les chemins d’accès évidents existent. L’absence d’un index composite est une raison fréquente pour laquelle SQLite « devient soudainement lent ».
Décision : Si les problèmes de performance sont dus à des index manquants, vous pouvez souvent retarder la migration. Si l’indexation aide les lectures mais que les écritures restent bloquées, vous migrez quand même.
Tâche 4 : Expliquer le plan de requête (attraper le scan complet)
cr0x@server:~$ sqlite3 /var/lib/app/app.db "EXPLAIN QUERY PLAN SELECT * FROM events WHERE user_id=42 ORDER BY created_at DESC LIMIT 50;"
QUERY PLAN
`--SEARCH events USING INDEX idx_events_user_created (user_id=?)
Ce que cela signifie : C’est bon : recherche par index, pas de scan complet.
Décision : Si vous voyez SCAN TABLE sur une grande table dans des chemins de production, corrigez le schéma/la requête d’abord. Ne migrez pas juste pour éviter d’ajouter un index.
Tâche 5 : Vérifier la taille de la base et les statistiques de pages (croissance et pression IO)
cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA page_size; PRAGMA page_count;'
4096
258000
Ce que cela signifie : La taille approximative est page_size * page_count (~1,0 GiB ici). Les bases plus volumineuses augmentent les pénalités de cache miss et la douleur des opérations de vacuum/maintenance.
Décision : Si la DB croît rapidement et que vous êtes sur un nœud unique, planifiez MySQL plus tôt — surtout si vous avez besoin de maintenance en ligne.
Tâche 6 : Vérifier le gonflement du fichier WAL (pression de checkpoint)
cr0x@server:~$ ls -lh /var/lib/app/app.db*
-rw-r----- 1 app app 1.0G Dec 30 10:12 /var/lib/app/app.db
-rw-r----- 1 app app 6.2G Dec 30 10:12 /var/lib/app/app.db-wal
-rw-r----- 1 app app 32K Dec 30 10:12 /var/lib/app/app.db-shm
Ce que cela signifie : Un fichier WAL énorme signifie généralement que les checkpoints ne suivent pas (lecteurs longs, checkpoints mal configurés, ou rafales d’écritures).
Décision : Enquêter sur les transactions de lecture de longue durée. Si vous ne pouvez pas les contrôler (plusieurs services, jobs en arrière-plan), migrez — MySQL gère plus élégamment cette classe de problèmes.
Tâche 7 : Mesurer la latence disque (l’IO est le tueur silencieux)
cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (server) 12/30/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.00 0.00 6.00 18.00 0.00 64.00
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 120.0 300.0 6400.0 22000.0 28.5 1.1 95.0
Ce que cela signifie : await ~28ms à 95 % d’utilisation est sévère. Les écritures SQLite peuvent être fsync-intensives selon les pragmas et les patterns de transaction.
Décision : Si le stockage est lent, corriger le disque peut résoudre à la fois les problèmes SQLite et MySQL. Si vous disposez déjà d’un stockage correct et voyez toujours de la contention de verrous, la migration reste probable.
Tâche 8 : Trouver les handles de fichiers ouverts et les processus touchant la DB (vérif réalité multi-écrivains)
cr0x@server:~$ lsof /var/lib/app/app.db | head
app 2314 app 12u REG 253,0 1073741824 12345 /var/lib/app/app.db
app 2314 app 13u REG 253,0 6657199308 12346 /var/lib/app/app.db-wal
worker 2551 app 10u REG 253,0 1073741824 12345 /var/lib/app/app.db
Ce que cela signifie : Plusieurs processus utilisent le même fichier DB. C’est acceptable sur une machine si c’est coordonné, mais cela augmente le risque de contention de verrous et de transactions longues.
Décision : Si votre architecture veut naturellement de nombreux écrivains, cessez d’essayer de faire se comporter SQLite comme une BD serveur.
Tâche 9 : Vérifier le type de système de fichiers et les options de montage (hypothèses de durabilité)
cr0x@server:~$ findmnt -no FSTYPE,OPTIONS /var/lib/app
ext4 rw,relatime,errors=remount-ro
Ce que cela signifie : Un ext4 local est généralement sain. Si vous voyez des montages NFS/CIFS/FUSE, les sémantiques de durabilité et de verrouillage peuvent devenir intéressantes de la mauvaise façon.
Décision : Si le fichier DB vit sur un stockage réseau et que la disponibilité compte, migrez. Ne transformez pas votre base en une expérience de système de fichiers distribué.
Tâche 10 : Observer les attentes de verrou et timeouts dans les logs de l’app (le symptôme visible par l’humain)
cr0x@server:~$ grep -E "database is locked|SQLITE_BUSY|timeout" /var/log/app/app.log | tail -n 5
2025-12-30T10:11:58Z ERROR db write failed: SQLITE_BUSY: database is locked
2025-12-30T10:11:59Z WARN retrying transaction after SQLITE_BUSY
2025-12-30T10:12:02Z ERROR request_id=9f2d api=/events POST latency_ms=4210 sqlite_busy_retries=5
Ce que cela signifie : Les retries accroissent la latence. Vous n’êtes pas juste lent ; vous êtes instable sous rafale.
Décision : Si les retries de verrous corrèlent avec les pics de trafic et que les écritures sont au cœur du produit, planifiez la migration. Si c’est une opération d’administration rare, vous pouvez l’isoler à la place.
Tâche 11 : Lancer un micro-test de concurrence d’écriture rapide (s’effondre-t-il sous deux écrivains ?)
cr0x@server:~$ bash -lc 'for i in {1..2}; do (time sqlite3 /var/lib/app/app.db "BEGIN; INSERT INTO events(user_id,created_at,payload) VALUES(42,datetime(\"now\"),\"x\"); COMMIT;" ) & done; wait'
real 0m0.012s
user 0m0.003s
sys 0m0.002s
real 0m1.104s
user 0m0.004s
sys 0m0.003s
Ce que cela signifie : Un écrivain termine rapidement ; l’autre attend autour d’une seconde (ou plus sous charge). Cette attente devient visible pour l’utilisateur.
Décision : Si votre profil de production attend des écritures concurrentes, migrez. Si les écritures sont rares et que vous pouvez les regrouper, vous pouvez rester sur SQLite.
Tâche 12 : Valider l’intégrité SQLite (êtes-vous déjà en difficulté ?)
cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA integrity_check;"
ok
Ce que cela signifie : Bon. Si la commande renvoie autre chose, vous avez une corruption et devez la traiter comme un incident de production.
Décision : Le risque de corruption est un facteur contraignant : prioriser la migration et corriger immédiatement le stockage/la gestion des transactions.
Tâche 13 : Comparer avec une base MySQL de référence (peut-elle absorber la concurrence ?)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_time';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Threads_running | 18 |
+-----------------+-------+
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| Innodb_row_lock_time | 1240 |
+------------------------+-------+
Ce que cela signifie : MySQL peut avoir de nombreux threads actifs. Le temps de verrouillage de ligne InnoDB vous donne une fenêtre sur la contention (pas parfait, mais utile).
Décision : Si MySQL montre un temps de verrouillage gérable tandis que SQLite expire, c’est votre justification de migration en une capture d’écran.
Tâche 14 : Vérifier la posture de durabilité MySQL (ne migrez pas vers un nouveau risque)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; SHOW VARIABLES LIKE 'sync_binlog';"
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+------------------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog | 1 |
+---------------+-------+
Ce que cela signifie : Ces réglages sont les valeurs par défaut « je tiens à la durabilité » pour de nombreux systèmes de production.
Décision : Si vous migrez vers MySQL mais définissez ces valeurs de manière relaxée sans comprendre les sémantiques de panne, vous n’avez pas amélioré — vous avez juste changé le style de perte de données.
Trois mini-récits d’entreprise (et la leçon à retenir)
Mini-récit 1 : L’incident causé par une fausse hypothèse
Une petite équipe plateforme a livré un service qui stockait l’état du flux client dans SQLite. C’était élégant : un binaire, un fichier DB, des sauvegardes faciles.
Ils utilisaient même le mode WAL et avaient un « busy timeout », donc ils se sentaient prêts.
Puis l’entreprise a ajouté une seconde instance derrière un load balancer pour la redondance. Le fichier DB a été déplacé sur un montage partagé pour que les deux instances « voient le même état ».
Ça a fonctionné pendant une semaine, ce qui est la durée la plus dangereuse en ingénierie parce qu’elle enseigne la mauvaise leçon.
Sous forte utilisation, les deux instances ont tenté d’écrire. Les attentes de verrou ont commencé à s’accumuler. La logique de retry a repris, ce qui a augmenté le taux d’écriture,
et donc la contention de verrous. Les utilisateurs ont vu des timeouts, et l’astreinte a vu le CPU essentiellement inactif. « Mais ce n’est pas le calcul », ont-ils dit. « Tout va bien. »
Le vrai coupable était l’hypothèse : « Si les deux processus peuvent lire le fichier, ils peuvent coordonner les écritures via un système de fichiers réseau. »
Cette hypothèse est un piège. SQLite attend certaines sémantiques de verrouillage et de fsync. Le stockage réseau peut les fournir parfois, jusqu’à ce qu’il ne le fasse plus.
La correction n’a pas été héroïque : ils ont déployé MySQL, pointé les deux instances dessus, et retiré complètement la couche de fichier partagé.
Le premier incident après cela a été ennuyeux. L’ennui est le bon résultat.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une autre équipe avait un service d’ingestion très écrit. Ils subissaient des pics de latence. Quelqu’un a trouvé un article de blog sur les pragmas de performance et a poussé un changement : définir PRAGMA synchronous=OFF et PRAGMA journal_mode=MEMORY.
Les graphiques étaient splendides. Moins d’IO disque, plus haut débit, moins de timeouts. L’équipe a célébré discrètement parce qu’ils avaient appris à ne pas célébrer bruyamment.
Deux mois plus tard, un hôte a redémarré pendant une écriture lors d’un patch noyau de routine. Rien de dramatique. Opérations normales.
Après le redémarrage, la DB a commencé à lancer des erreurs d’intégrité. Certaines données récentes manquaient, et certaines relations de clé étrangère étaient incohérentes.
L’équipe avait des sauvegardes, mais les restaurations impliquaient de perdre des écritures acceptées légitimes. Ils ont fini par reconstruire des données à partir des logs en amont et d’exports partiels.
Le retour de bâton n’était pas que les réglages de durabilité existent ; c’était que l’exigence réelle du système était « ne pas perdre les écritures acceptées ».
Si votre système exige cela, les optimisations qui affaiblissent la durabilité ne sont pas des optimisations — ce sont des prêts à intérêt prédateur.
Ils ont migré vers MySQL avec un journal de redo et des paramètres binlog synchronisés appropriés. Les performances sont redevenues acceptables, et les pannes sont devenues récupérables plutôt qu’existentiels.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une troisième organisation utilisait SQLite pour un agent de type desktop déployé chez les clients. Leur architecture avait du sens : un agent, une DB locale,
faible concurrence. Ils la traitaient néanmoins comme du stockage de production parce que les clients ne se soucient pas que ce soit « juste un agent ».
Ils ont mis en place trois pratiques ennuyeuses : écritures transactionnelles avec frontières claires, vérifications d’intégrité périodiques, et une routine de sauvegarde qui copiait la DB
en utilisant l’API de sauvegarde en ligne de SQLite plutôt que de copier le fichier brut pendant des écritures actives.
Un client avait un disque instable. L’agent a commencé à logger des erreurs IO. Parce que les vérifications d’intégrité étaient déjà en place, l’agent a détecté la corruption tôt,
mis la DB en quarantaine, restauré depuis la dernière sauvegarde saine, et rejoué un petit tampon d’événements récents depuis une file locale.
Le client n’a jamais déposé de ticket. En interne, l’équipe a vu l’alerte, ouvert un incident, et l’a fermé en haussant les épaules.
Ce haussement d’épaules est le son d’une bonne conception opérationnelle.
Leçon : vous ne migrez pas juste parce que vous le pouvez. Vous migrez parce que vos exigences opérationnelles ont changé. Jusqu’à ce moment-là, appliquez le travail ennuyeux de correction.
Blague n°2 : La façon la plus simple de rendre SQLite « hautement disponible » est d’imprimer le fichier de base et de le garder dans deux bureaux différents.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom : timeouts aléatoires pendant les pics de trafic
Cause racine : contention d’écriture (limitation un-écrivain), plus retries qui amplifient la charge.
Correction : réduire la fréquence des écritures (batching), raccourcir les transactions, utiliser WAL, ajouter un busy timeout avec backoff. Si plusieurs écrivains sont fondamentaux : migrer vers MySQL.
2) Symptom : des erreurs « database is locked » apparaissent après l’ajout d’un worker en arrière-plan
Cause racine : un nouveau processus a introduit une seconde source d’écriture ; les transactions se chevauchent ; les lecteurs longs empêchent le checkpoint WAL.
Correction : assurer la sérialisation des écrivains (queue d’un seul écrivain) ou déplacer la charge d’écriture vers MySQL. Auditer les jobs en arrière-plan pour les lectures de longue durée.
3) Symptom : le fichier WAL grossit sans limite
Cause racine : le checkpoint ne peut pas se terminer parce que des lecteurs longue durée maintiennent d’anciennes pages vivantes ; ou les réglages de checkpoint sont trop laxistes.
Correction : éliminer les transactions de lecture longues ; lancer des checkpoints explicites en période de faible trafic ; envisager une migration si de nombreux services lisent concurremment.
4) Symptom : après crash/reboot, des données récentes manquent
Cause racine : réglages de durabilité affaiblis (synchronous=OFF, système de fichiers inapproprié, options de montage non sûres) ou écritures non englobées dans des transactions.
Correction : restaurer les pragmas de durabilité, utiliser des transactions, déplacer la DB vers un stockage local fiable. Si vous avez besoin de garanties de durabilité fortes à l’échelle : MySQL avec réglages flush/binlog corrects.
5) Symptom : la requête est rapide en dev, lente en prod
Cause racine : jeu de données dev petit ; prod avec skew ; index manquant ; changement de plan de requête ; LIKE/ORDER BY lourds sans index.
Correction : exécuter EXPLAIN QUERY PLAN sur des données similaires à la prod ; ajouter des index composites ; réécrire les requêtes. Ne blâmez pas SQLite pour un tri non indexé.
6) Symptom : le CPU de l’app spike quand un rapport s’exécute
Cause racine : SQLite s’exécute dans le processus app ; les requêtes lourdes volent le CPU au traitement des requêtes.
Correction : isoler la charge de reporting, l’exécuter sur une réplique (MySQL), ou déplacer l’analytics vers un magasin séparé. Au minimum, exécuter les rapports dans un processus séparé avec limites de ressources.
7) Symptom : « ça marchait jusqu’à ce qu’on le conteneurise »
Cause racine : le fichier DB placé sur un système de fichiers overlay ou un volume réseau ; comportement fsync/verrouillage modifié ; IO ralentie.
Correction : mettre SQLite sur un volume persistant local avec des sémantiques connues, ou arrêter d’utiliser SQLite dans un scénario multi-conteneurs en écriture et migrer vers MySQL.
Listes de contrôle / plan pas à pas : migrer sans héroïsme
Checklist de décision : devez-vous migrer ce trimestre ?
- Avez-vous plus d’un écrivain en production (processus multiples, workers, cron, instances d’app) ?
- La latence de queue (p95/p99) est-elle due à des attentes de verrous ou des retries ?
- Avez-vous besoin de haute disponibilité (continuer les écritures lors d’une défaillance de nœud) plutôt que « nous avons des sauvegardes » ?
- Avez-vous besoin d’opérations en ligne : sauvegardes sans downtime, changements de schéma à impact minimal, kill de requêtes, observabilité ?
- Mettez-vous la base SQLite sur un stockage partagé ou réseau ?
Si vous avez répondu « oui » à deux éléments ou plus, planifiez la migration. Si vous avez répondu « oui » au stockage partagé ou aux exigences HA, arrêtez de débattre et planifiez-la.
Plan de migration : la séquence raisonnable
- Définir les exigences de correction : durabilité (que peut-on perdre ?), cohérence (read-your-writes ?), downtime acceptable.
- Inventorier les différences de schéma : types de données, comportement autoincrement, stockage date/heure, contraintes, valeurs par défaut.
- Choisir une stratégie de migration :
- Coupe brute : arrêter les écritures, exporter/importer, basculer. Simple, nécessite une fenêtre de downtime.
- Double écriture : écrire dans les deux, lire depuis SQLite, puis basculer les lectures, puis arrêter SQLite. Plus dur, moins de downtime.
- Type change-data-capture : généralement excessif pour SQLite sauf si vous avez déjà un journal d’événements.
- Déployer MySQL avec des paramètres production : sauvegardes, monitoring, journal des requêtes lentes, réglages de durabilité sensés.
- Backfiller les données de SQLite vers MySQL et valider les comptes et checksums.
- Faire des lectures canari : comparer les résultats entre SQLite et MySQL pour les chemins critiques.
- Basculer le trafic progressivement si possible ; sinon, faire une coupe nette avec un plan de rollback.
- Maintenir SQLite en lecture seule pendant une période définie comme filet de sécurité, puis archiver.
Checklist opérationnelle pour MySQL (pour ne pas « passer à l’enfer »)
- Sauvegardes testées par restauration (pas seulement « les jobs de backup sont verts »).
- Monitoring de la latence de réplication (si réplicas), de l’espace disque, du hit rate du buffer pool, des requêtes lentes, de la saturation des connexions.
- Pool de connexions dans l’app. N’ouvrez pas 2 000 connexions MySQL parce que vous avez découvert des threads.
- Plan de migration de schéma (en ligne si possible, ou fenêtres planifiées).
- Plan de capacité pour la croissance du stockage (InnoDB grossit et apprécie de l’espace libre pour la maintenance).
FAQ
1) SQLite n’est-il pas « pour la production » ?
Il l’est absolument — quand « production » signifie embarqué, mono-nœud et faible concurrence d’écriture. Il est en production sur des milliards d’appareils.
Le mauvais choix est de l’utiliser comme base multi-écrivants partagée pour un service mis à l’échelle horizontalement.
2) Le mode WAL permet-il à SQLite de gérer beaucoup d’écrivains ?
WAL aide les lecteurs à ne pas bloquer les écrivains et vice versa, mais il ne transforme pas SQLite en système multi-écrivains. Vous avez toujours un seul écrivain à la fois.
WAL est une amélioration de la concurrence, pas un coordinateur de transactions distribuées.
3) Quel est le signe pratique le plus évident que je devrais migrer ?
La contention d’écriture persistante sous charge normale — timeouts, erreurs de verrou, retries qui gonflent la latence. Si votre produit est axé écriture, c’est la ligne.
4) Puis-je mettre SQLite sur NFS si je suis prudent ?
Vous pouvez, certains le font, et certains se font réveiller plus tard. Le verrouillage du système de fichiers et les sémantiques de durabilité sur du stockage réseau sont une taxe de fiabilité.
Si vous avez besoin de plusieurs machines, utilisez un serveur de base de données.
5) MySQL est-il toujours plus rapide que SQLite ?
Non. SQLite peut être plus rapide pour des lectures locales simples et de petites écritures car il n’y a pas de passage réseau et peu de surcharge.
MySQL l’emporte lorsque la concurrence, l’isolation, la mise en mémoire tampon et les contrôles opérationnels comptent.
6) Que penser de l’utilisation de SQLite comme cache et MySQL comme source de vérité ?
Cela peut fonctionner si vous traitez SQLite comme jetable et reconstruisible. Dès que SQLite devient « le seul endroit » où quelque chose vit, il cesse d’être un cache et redevient votre base.
7) Comment éviter de migrer trop tôt ?
Prouvez le goulot. Si votre problème est des index manquants ou des frontières de transaction négligées, corrigez cela d’abord. La migration se justifie lorsque l’architecture vous limite, pas quand le schéma a besoin d’amour.
8) Quelle est la méthode de sauvegarde la plus simple et sûre pour SQLite ?
Utilisez l’API de sauvegarde en ligne de SQLite (via vos bindings langage ou les fonctionnalités de sauvegarde CLI) plutôt que de copier le fichier pendant des écritures en cours.
Validez les sauvegardes en restaurant et en exécutant PRAGMA integrity_check;.
9) Si je migre vers MySQL, quel nouveau mode de panne me mordra en premier ?
Les tempêtes de connexions et une mise en pool mal configurée. SQLite le cachait parce qu’il est in-process. MySQL acceptera volontiers votre test de charge jusqu’à ce qu’il manque de threads ou de mémoire.
10) Devrais-je utiliser MySQL ou autre chose (Postgres, etc.) ?
Si votre choix est spécifiquement SQLite vs MySQL, choisissez MySQL lorsque vous avez besoin de concurrence de niveau serveur, de réplication et d’outillage opérationnel.
Si vous évaluez plus largement, décidez en fonction des compétences de l’équipe et des contraintes opérationnelles. Le point central reste : base serveur quand vous avez besoin de propriétés serveur.
Conclusion : prochaines étapes pratiques
SQLite ne « casse » pas. Les équipes lui demandent un travail pour lequel il n’a jamais été embauché, puis le blâment pour avoir des limites.
MySQL n’est pas « meilleur » en abstraction. Il est meilleur quand vous avez besoin d’écritures concurrentes, de patterns HA, d’observabilité et de contrôles opérationnels qui n’exigent pas de réinventer une équipe de bases de données.
Si vous n’êtes pas sûr, n’argumentez pas. Mesurez. Exécutez le playbook de diagnostic rapide, réalisez les tâches ci-dessus, et cherchez la signature :
attentes de verrous, inflation de la latence de queue, et risque opérationnel autour de la durabilité et du stockage partagé.
- Si le problème est la conception des requêtes : ajoutez des index, corrigez les transactions, retestez.
- Si le problème est le stockage : corrigez l’IO d’abord ; de mauvais disques font passer toute base pour incompétente.
- Si le problème est la concurrence et les exigences HA : planifiez la migration vers MySQL et traitez-la comme un projet d’infrastructure, pas comme un refactor.
Le meilleur moment pour migrer est avant de déboguer une base verrouillée à 3h du matin en essayant de se rappeler si synchronous=OFF était « juste temporaire ».