MySQL vs SQLite : le cas de la « vitesse gratuite » — quand une base fichier bat un serveur

Cet article vous a aidé ?

Vous déployez un service, tout semble correct en staging, puis le trafic de production arrive avec un petit cadeau cruel : la latence.
Pas le genre « on a besoin de requêtes plus rapides ». Le genre « pourquoi paie-t-on un serveur de base de données pour ensuite attendre ? ».

Parfois, la base de données la plus rapide est celle avec laquelle vous n’avez pas à parler via un socket. Parfois, le fichier ennuyeux sur le disque,
à côté de votre application, surpasse discrètement un cluster MySQL tout à fait respectable — du moins pour la charge réelle que vous avez.

Le postulat de la « vitesse gratuite » : où SQLite gagne sans effort

SQLite est une bibliothèque. MySQL est un service. Cette seule phrase explique 70 % des captures « SQLite était plus rapide »
que les gens agitent comme un drapeau de victoire.

Si vous exécutez SQLite en processus intégré, votre application appelle une bibliothèque qui lit un fichier local (ou le cache de pages) et renvoie des lignes. Pas de TCP.
Pas de drame autour du pool de connexions. Pas d’ordonnancement de threads côté serveur. Pas d’échange d’authentification. Pas de saut via un proxy. Pas d’attente derrière d’autres clients.
C’est l’équivalent base de données d’aller au frigo à pied au lieu de commander une livraison et de discuter avec l’interphone.

C’est ce que j’entends par « vitesse gratuite » : la vitesse obtenue en supprimant des pièces mobiles, pas en faisant preuve d’ingéniosité.
Votre plan de requête peut avoir la même complexité ; vos données peuvent être de la même taille ; votre stockage peut même être le même disque.
Mais la longueur du parcours — instructions CPU, changements de contexte, appels système, réveils — devient plus courte.

Le piège est de penser que cela signifie que SQLite est « meilleur » en général. Ce n’est pas le cas. Il est meilleur dans des scénarios spécifiques et courants :
applications « local-first », charges en périphérie, services majoritairement en lecture avec peu de concurrence d’écriture, et systèmes où la simplicité opérationnelle
est une caractéristique, pas une réflexion après coup.

Première petite blague (parce que ce sujet le mérite) : SQLite a une histoire opérationnelle fantastique — surtout parce qu’il refuse d’avoir des opérations.

Deux modèles mentaux : serveur de base de données vs fichier de base

MySQL : domaine de panne séparé, enveloppe de performance séparée

MySQL est un service réseau avec son propre processus, mémoire, threads et ordonnanceur d’E/S. Cette séparation est une superpuissance :
elle isole le travail de la base de données des crashs applicatifs, permet à de nombreux clients de se connecter, et supporte des patterns avancés de réplication
et de clustering.

Mais la séparation a un coût. Chaque requête traverse une frontière : bibliothèque client → socket → réseau du noyau → serveur → moteur de stockage.
Chaque frontière introduit des frais et des points d’attente supplémentaires. Sous charge, l’attente domine.
C’est pourquoi votre « simple SELECT par clé primaire » peut passer de sous-millisecondes à des dizaines de millisecondes sans que la requête elle-même ne change.

SQLite : la base est un fichier, le serveur est votre processus

SQLite s’exécute dans l’espace de votre processus. Il lit et écrit un seul fichier de base de données (plus éventuellement des fichiers journal/WAL),
en utilisant des verrous OS pour coordonner la concurrence. Il s’appuie fortement sur le cache de pages du système et bénéficie de la localité.

Le détail clé : dans de nombreuses charges, les données sont déjà en mémoire (cache de pages). SQLite peut toucher ces pages sans aller faire un aller-retour.
Quand vous benchmarkez « SELECT 1 ligne », vous mesurez des appels de fonction et des hits de cache — pas le surcoût du protocole client-serveur.

Alors que choisissez-vous, en réalité ?

Vous choisissez combien vous voulez payer pour l’accès partagé, l’accès distant et la concurrence multi-écrivains.
MySQL est optimisé pour « plusieurs clients, plusieurs écrivains, autorité centrale ».
SQLite est optimisé pour « une application possède les données, les lectures sont fréquentes, les écritures sont coordonnées ».

Si vous construisez une application web avec des centaines de transactions d’écriture concurrentes provenant de nombreux serveurs d’application, SQLite n’est pas une bonne affaire.
Si vous construisez un service avec principalement des lectures, un taux d’écriture modeste et une volonté de supprimer de l’infra,
SQLite peut être étonnamment efficace.

Faits & histoire qui changent votre raisonnement

  • SQLite (2000) a été conçu par D. Richard Hipp pour être embarqué, sans serveur et autonome, visant la fiabilité et la portabilité plutôt que l’accumulation de fonctionnalités.
  • L’approche « domaine public » de SQLite (pratiquement sans friction de licence) lui a permis d’atterrir partout : navigateurs, téléphones, routeurs, imprimantes et applications desktop.
  • MySQL (milieu des années 1990) a émergé à une époque où « base de données » signifiait processus serveur séparé et où l’hébergement partagé demandait l’accès multi-tenant.
  • InnoDB est devenu le moteur MySQL par défaut parce qu’il apportait récupération après crash, transactions et verrous au niveau des lignes à un monde qui avait appris à craindre les verrous de table.
  • SQLite a ajouté le mode WAL pour améliorer drastiquement la concurrence en lecture en séparant lecteurs et écrivains, un tournant pour les charges réelles d’applications.
  • SQLite utilise un design B-tree compact optimisé pour le stockage local et des performances prévisibles, d’où son bon comportement sur les petits appareils.
  • La réplication MySQL a façonné les pratiques ops modernes (réplicas de lecture, basculement, binlogs), mais ces bénéfices viennent avec des responsabilités opérationnelles.
  • La culture de tests de SQLite est célèbre : une couverture de tests automatisés énorme et un fuzzing agressif ont fait de SQLite l’un des composants d’infrastructure les plus éprouvés dont vous dépendez déjà.

Charges de travail : formes exactes où SQLite bat MySQL (et où il ne le fait pas)

SQLite gagne quand la « base » est surtout un index local

Pensez : un orchestrateur de tâches suivant l’état des jobs, un service mettant en cache des réponses d’API, un outil CLI stockant des métadonnées, une application desktop,
un collecteur edge tamponnant des événements, ou un service web mono-nœud avec un schéma de lecture clair.

Dans ces configurations, les forces de MySQL — arbitrage multi-client, accès distant, forte concurrence — sont sous-utilisées.
Vous payez un taxi pour traverser la rue.

SQLite gagne quand les lectures dominent et que les écritures sont coordonnées

SQLite peut servir de nombreux lecteurs concurrents efficacement, surtout en mode WAL. Les écritures, cependant, sont sérialisées au niveau de la base.
Si votre taux d’écriture est faible ou si vous pouvez canaliser les écritures via un seul worker (ou un leader), vous pouvez obtenir un excellent débit avec une faible latence.

MySQL gagne quand vous avez besoin d’une concurrence soutenue multi-écrivains

MySQL avec InnoDB est conçu pour les écritures concurrentes. Verrous au niveau des lignes, MVCC, vidage en arrière-plan et pools de buffers séparés sont
tous pensés pour le monde des « nombreux écrivains ». Si votre système reçoit des écritures fréquentes depuis de nombreuses instances d’application et que vous ne pouvez pas les coordonner,
SQLite devient un générateur de contention.

MySQL gagne quand vous avez besoin d’accès distant et de propriété partagée

Si plusieurs services ou équipes doivent accéder au même jeu de données, centraliser la base est souvent la bonne décision organisationnelle,
pas seulement technique. Vous voulez contrôle d’accès, audit, sauvegardes et un comportement multi-client prévisible.

SQLite gagne quand vous voulez « livrer avec l’app » simplicite

Déployer SQLite, c’est livrer une bibliothèque. Déployer MySQL, c’est livrer un écosystème : config, upgrades, backups, monitoring,
gestion des utilisateurs, et l’occasionnelle post-mortem « pourquoi ça swappe ».

Seconde petite blague : la migration de base de données la plus facile est celle que vous ne faites jamais, c’est pourquoi les gens gardent SQLite comme un vieux hoodie fiable.

Le budget de latence : votre requête est innocente, le trajet est coupable

En production, la plupart des « lenteurs de base de données » ne sont pas le plan de requête. C’est tout le reste :
churn de connexion, ordonnancement des threads, attentes de verrous, attentes fsync, voisins bruyants et latence en queue réseau.

SQLite supprime les couches réseau et d’ordonnancement serveur. C’est la vitesse gratuite. Si votre charge tient en RAM
(ou est majoritairement dans le cache OS), et si les écritures sont modestes, la latence médiane et la queue p99 peuvent être bien meilleures.

MySQL peut être extrêmement rapide aussi — mais il nécessite un tuning compétent et des conditions stables. SQLite demande moins d’efforts pour être correct,
car il y a moins de choses à régler.

Ce que vous mesurez réellement dans les benchmarks

Quand quelqu’un publie un benchmark disant « SQLite est 3× plus rapide que MySQL », demandez :

  • Utilisent-ils des sockets localhost ou un véritable réseau ?
  • Réutilisent-ils les connexions ou se reconnectent-ils à chaque requête ?
  • MySQL fait-il un fsync à chaque transaction pendant que SQLite ne le fait pas (ou l’inverse) ?
  • Le jeu de données est-il en cache pour l’un et pas pour l’autre ?
  • Mesurent-ils un seul thread ou une vraie concurrence ?
  • Utilisent-ils le mode WAL et des réglages synchrones sensés ?

Si le benchmark ne répond pas à ces questions, c’est une histoire, pas une preuve.

Durabilité et sémantiques de crash : ce que signifie « sûr »

La première question sérieuse que posent les SRE n’est pas « à quel point c’est rapide », c’est « que se passe-t-il si l’hôte tombe en panne en plein écrit ? ».
MySQL et SQLite peuvent être durables. Tous deux peuvent aussi être configurés en pièges à pieds.

Les réglages de durabilité de SQLite : mode journal et synchronous

La durabilité de SQLite est gouvernée principalement par le mode de journalisation (DELETE, TRUNCATE, PERSIST, MEMORY, WAL)
et PRAGMA synchronous (OFF, NORMAL, FULL, EXTRA). Le mode WAL améliore typiquement la concurrence et le débit d’écriture
en ajoutant dans un fichier WAL et en checkpointant plus tard.

La vérité inconfortable : beaucoup d’apps « benchmarkent » SQLite avec synchronous=OFF puis s’étonnent quand une panne de courant cause une corruption.
Ce n’est pas un problème de base de données, c’est un problème de décision.

Les réglages de durabilité de MySQL : innodb_flush_log_at_trx_commit et compagnons

La durabilité de MySQL vit dans InnoDB : redo logs, doublewrite buffer, vidage du buffer pool. Le fameux réglage est
innodb_flush_log_at_trx_commit. Le mettre à 1 déclenche un fsync au commit (durable, plus lent).
Le mettre à 2 ou 0 fait un compromis durabilité vs débit.

Les deux systèmes vous laissent choisir. L’important est de choisir consciemment, documenter la décision, et tester le comportement en cas d’échec.

Une idée paraphrasée sur la fiabilité à garder

Idée paraphrasée de John Allspaw : la fiabilité vient de la conception pour l’échec et de l’apprentissage, pas de faire comme si l’échec n’existait pas.

Concurrence : verrous, MVCC, et pourquoi « plusieurs écrivains » est un mode de vie

SQLite : un seul écrivain à la fois, par conception

SQLite permet plusieurs lecteurs et un seul écrivain. Le mode WAL améliore l’expérience des lecteurs car ceux-ci peuvent continuer à lire l’ancienne snapshot pendant qu’un écrivain ajoute au WAL.
Mais si vous avez plusieurs écrivains concurrentiels, ils se mettent en file d’attente.

Le résultat n’est pas « ça casse ». Le résultat est des pics de latence, des timeouts busy, et parfois le genre de foule qui se répète
où tout le monde réessaie en même temps et aggrave la situation.

MySQL/InnoDB : conçu pour les écrivains concurrents, mais pas magique

InnoDB offre des verrous au niveau des lignes et MVCC pour permettre des transactions concurrentes. Mais la contention existe :
lignes chaudes, index secondaires chauds, verrous d’auto-incrément (selon la configuration), verrous de métadonnées et pression sur le buffer pool.

MySQL peut gérer une forte concurrence — jusqu’à ce que votre schéma ou vos patterns de requête le transforment en simulateur d’attente de verrous.
Vous n’obtenez pas la concurrence gratuitement ; vous l’obtenez avec un indexage et une gestion des transactions soigneux.

Stratégies de coordination qui rendent SQLite viable

  • Architecture single-writer : canalisez les écritures via un seul processus ou thread. Les lecteurs peuvent être nombreux.
  • Écritures par lot : moins de transactions, commits plus gros (dans la raison).
  • Utiliser WAL + busy_timeout : réduire les échecs spuriaires sous faible contention.
  • Garder les transactions courtes : « faire le travail, puis écrire » est mieux que « écrire en réfléchissant ».

Surcharge opérationnelle : la taxe cachée que MySQL ajoute (et que SQLite n’ajoute pas)

MySQL n’est pas « difficile », mais c’est un système. Il nécessite des patchs, des sauvegardes, des privilèges, la gestion de la réplication,
le dimensionnement disque, le monitoring, des alertes et des personnes qui se souviennent de ce qu’elles ont fait il y a six mois.

Le modèle ops de SQLite est : sauvegarder un fichier ; surveiller le disque ; vérifier que vous ne le corrompez pas ; et ne pas laisser dix écrivains se battre dans un couloir.
Cette simplicité a une valeur.

L’envers de la médaille : SQLite pousse la responsabilité vers la frontière applicative. L’emplacement du fichier, les sémantiques du système de fichiers, le stockage en conteneur
et la cohérence des sauvegardes deviennent maintenant votre problème. Si vous le traitez comme un blob magique, il vous traitera comme un amateur.

Trois mini-récits d’entreprise venus des tranchées

Mini-récit 1 : l’incident causé par une mauvaise hypothèse

Une équipe produit a construit un petit service de tableau de bord interne. Il était fortement orienté lecture et servait principalement des résultats analytiques mis en cache.
Ils ont choisi SQLite pour éviter de monter MySQL. Raisonnable.

Puis le service a été « amélioré » pour supporter des annotations utilisateurs. Les écritures étaient petites, mais elles arrivaient en rafales :
chaque matin, des dizaines de personnes ouvraient le tableau de bord en même temps, créaient des notes et mettaient à jour des tags.
L’équipe a supposé que « petites écritures » signifiait « pas de problème ».

Le premier lundi après le lancement, le service a commencé à renvoyer des 500 intermittents. Les logs montraient « database is locked ».
L’astreignant a fait ce que font les astreints : augmenter les retries. Le taux d’erreur a empiré parce que maintenant chaque client réessayait en même temps,
transformant effectivement « un écrivain » en « une file avec un mégaphone ».

La correction n’a pas été d’abandonner SQLite. La correction a été de le traiter comme ce qu’il est : une base mono-écrivain. Ils ont introduit une file d’écriture
(un unique worker en background effectuant les transactions), raccourci la portée des transactions, activé WAL et fixé un busy timeout raisonnable.
Le taux d’erreur est tombé à zéro et la latence s’est normalisée.

La mauvaise hypothèse n’était pas « SQLite est rapide ». La mauvaise hypothèse était « la concurrence d’écriture n’a pas d’importance si les écritures sont petites ».
La concurrence se moque de vos sentiments.

Mini-récit 2 : l’optimisation qui a mal tourné

Une autre équipe utilisait MySQL pour un store de session avec des taux de lecture/écriture modérés. Ils poursuivaient la p99 et ont remarqué des attentes fsync.
Quelqu’un a proposé de réduire la durabilité parce que « les sessions sont éphémères ». Ils ont changé le comportement de flush d’InnoDB pour réduire la pression sur les fsync.
La latence s’est améliorée immédiatement. Le changement a été célébré.

Deux semaines plus tard, un redémarrage d’hôte pendant une maintenance a fait disparaître un lot d’écritures de session récentes. Les utilisateurs se sont déconnectés,
des paniers ont été perdus et le support client a eu un entraînement imprévu.

Le postmortem n’a pas été dramatique. Il a été ennuyeux, ce qui est pire. L’équipe avait redéfini silencieusement la signification de « commis ».
Ils avaient optimisé pour des benchmarks et oublié d’optimiser l’expérience utilisateur.

Ils ont annulé le changement de durabilité et ont corrigé la latence correctement : journaux redo plus grands, meilleur dimensionnement du buffer pool,
et lotissement des transactions au niveau applicatif. MySQL est redevenu stable.

La leçon : la durabilité fait partie de votre produit, pas seulement de la configuration de la base.

Mini-récit 3 : la pratique ennuyeuse mais correcte qui a sauvé la mise

Une petite flotte de collecteurs edge tamponnait la télémétrie localement et l’envoyait par lots. Ils utilisaient SQLite sur chaque appareil.
Les écritures étaient fréquentes mais coordonnées : un processus d’ingestion écrivait ; des uploaders lisaient.
L’équipe a fait quelque chose de douloureusement peu sexy : ils ont testé la perte d’alimentation et les conditions de disque plein.

Lors d’un déploiement, un bug a fait exploser les retries d’upload. Les appareils ont commencé à remplir les disques.
Le processus d’ingestion a commencé à échouer des écritures avec « disk I/O error ». Cela aurait pu devenir une perte de données silencieuse,
car les flottes edge excellent à échouer discrètement.

Mais ils avaient deux garde-fous : (1) Surveillance de l’espace disque avec un seuil dur qui mettait l’ingestion en pause avant l’épuisement total,
et (2) une vérification d’intégrité périodique de la DB SQLite qui s’exécutait pendant les fenêtres de faible trafic.

Quand l’incident est survenu, les appareils ont arrêté d’ingérer avant de corrompre la base, ont envoyé un signal de santé clair,
et ont récupéré automatiquement une fois le bug de retry corrigé. Pas d’éditions manuelles héroïques des fichiers de base.
Pas de corruption mystérieuse.

Pratique ennuyeuse, a sauvé la journée : tests proactifs d’échec plus simple et explicite backpressure.

Tâches pratiques : commandes, sorties et décisions (12+)

Ci-dessous des tâches réelles que j’exécuterais en production ou en staging ressemblant réellement à la production.
Chacune inclut : commande, ce que la sortie signifie, et la décision qu’elle entraîne.

Task 1: Confirm SQLite journal mode and synchronous level

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode; PRAGMA synchronous;"
wal
2

Signification : Le mode WAL est activé. synchronous=2 signifie FULL (orienté durabilité).
Selon la build, les valeurs numériques correspondent à OFF/NORMAL/FULL/EXTRA.

Décision : Si vous observez des pics de latence à l’écriture, envisagez NORMAL pour une durabilité acceptable dans de nombreux cas,
mais seulement si votre produit peut tolérer la perte de la dernière transaction en cas de panne de courant. Documentez ce compromis.

Task 2: Check SQLite for lock contention via busy_timeout and quick write test

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA busy_timeout=5000; BEGIN IMMEDIATE; SELECT 'got write lock'; COMMIT;"
got write lock

Signification : Le processus a acquis rapidement un verrou d’écriture. S’il bloque ou plante, un écrivain est déjà actif.

Décision : Si cela attend fréquemment, votre architecture a besoin d’une file d’écriture unique ou d’une portée de transaction réduite.
Ne « corrigez » pas cela en ajoutant des retries partout.

Task 3: Observe active SQLite access patterns (file locks and writers)

cr0x@server:~$ sudo lsof /var/lib/myapp/app.db | head
myapp     1187 appuser   12u  REG  259,0  52428800  1048577 /var/lib/myapp/app.db
myapp     1187 appuser   13u  REG  259,0   8388608  1048578 /var/lib/myapp/app.db-wal
myapp     1187 appuser   14u  REG  259,0     32768  1048579 /var/lib/myapp/app.db-shm

Signification : Les fichiers WAL et SHM existent et sont ouverts. C’est attendu en mode WAL.
De nombreux processus gardant le fichier ouvert peuvent indiquer un risque multi-écrivain.

Décision : Si vous voyez beaucoup de PIDs différents ouvrant la DB pour écrire, redesigniez pour qu’un seul composant écrive.

Task 4: Measure SQLite database and WAL growth (checkpoint pressure)

cr0x@server:~$ ls -lh /var/lib/myapp/app.db /var/lib/myapp/app.db-wal
-rw------- 1 appuser appuser  48M Dec 30 09:41 /var/lib/myapp/app.db
-rw------- 1 appuser appuser 512M Dec 30 09:43 /var/lib/myapp/app.db-wal

Signification : Le WAL est beaucoup plus grand que la DB principale. Le checkpoint peut ne pas avoir lieu (ou être bloqué par des lecteurs longs).

Décision : Investiguer les lectures longue durée ; envisager un checkpoint manuel ou réglé ; s’assurer que les lecteurs ne conservent pas de snapshots indéfiniment.
L’explosion du WAL peut générer une pression disque et ralentir les checkpoints.

Task 5: Force and inspect a SQLite checkpoint result

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0

Signification : Les trois nombres sont (busy, log, checkpointed) pages. Tous les zéros signifie souvent rien à faire ou déjà checkpointé.
Si « busy » est non nul, le checkpoint n’a pas pu procéder à cause de lecteurs actifs.

Décision : Si des pages busy persistent, trouvez et corrigez les lecteurs longue durée (connexions qui fuient, requêtes en streaming).

Task 6: Run a quick SQLite integrity check (catch corruption early)

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA quick_check;"
ok

Signification : L’intégrité structurelle est OK. Si vous voyez autre chose, traitez-le en urgence.

Décision : Si une corruption apparaît : arrêter les écritures, snapshotter le fichier, et restaurer depuis une sauvegarde connue bonne.
Puis investiguer le stockage, le comportement en cas de perte de puissance, et les PRAGMA dangereux.

Task 7: Check filesystem mount options (fsync semantics matter)

cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/myapp
/dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro

Signification : Vous êtes sur ext4 avec des options typiques. Si vous voyez des options exotiques (comme la désactivation des barriers),
les hypothèses de durabilité peuvent casser.

Décision : Si vous avez besoin d’une durabilité stricte, conservez des options de montage conservatrices et évitez les flags « performance » que vous ne pourriez pas expliquer dans un postmortem.

Task 8: See if disk is your real bottleneck (iostat)

cr0x@server:~$ iostat -x 1 3
Linux 6.5.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   rMB/s   wMB/s avgrq-sz avgqu-sz await r_await w_await  svctm  %util
nvme0n1         120.0   400.0    3.2    18.5    86.0     5.2  11.8    2.1   14.7   0.6   31.0

Signification : Non-trivial iowait et des write await plus élevées suggèrent une pression fsync/flush.

Décision : Si la latence disque est élevée, aucune optimisation de requête ne vous sauvera. Réduisez la fréquence des syncs prudemment (si autorisé),
batcher les écritures, ou migrez vers un stockage plus rapide.

Task 9: Check MySQL server health and immediate contention hints

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Questions';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 64    |
+-----------------+-------+
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| Questions     | 12893412 |
+---------------+----------+

Signification : De nombreux threads en cours peuvent indiquer saturation CPU, attentes de verrou, ou une foule massive.

Décision : Si Threads_running est élevé et la latence aussi, vérifiez les lock waits et les requêtes lentes avant d’ajouter des workers applicatifs.

Task 10: Identify MySQL lock waits in InnoDB

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
2025-12-30 09:48:12 0x7f2c1c1fe700 INNODB MONITOR OUTPUT
=====================================
...
LATEST DETECTED DEADLOCK
------------------------
...
TRANSACTIONS
------------
Trx id counter 1829341
Purge done for trx's n:o < 1829200 undo n:o < 0 state: running
History list length 1234
...

Signification : Cette sortie vous indique si vous avez des deadlocks, une accumulation de history list (undo),
ou des blocages sur des verrous.

Décision : Si vous voyez des deadlocks ou une énorme history list length, raccourcissez les transactions et ajoutez des index pour réduire la portée des verrous.
Si le « LATEST DETECTED DEADLOCK » se répète, corrigez le pattern applicatif, pas la base.

Task 11: Check MySQL durability setting that affects fsync behavior

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';"
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1   |
+------------------------------+-------+

Signification : La valeur 1 signifie que le redo log est flushé sur disque à chaque commit (durable).

Décision : Si la latence est dominée par les fsync et que vous pouvez tolérer une perte de données minimale, vous pourriez choisir 2.
Si vous ne pouvez pas la tolérer, gardez 1 et corrigez la performance ailleurs (batching, stockage, schéma).

Task 12: Inspect connection churn (a silent MySQL latency killer)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Connections'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';"
+---------------+--------+
| Variable_name | Value  |
+---------------+--------+
| Connections   | 904221 |
+---------------+--------+
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_connects | 1203  |
+------------------+-------+

Signification : Des Connections très élevées par rapport au QPS constant signifient souvent que vous vous connectez trop fréquemment.
Aborted_connects indique des problèmes d’auth/network ou des limites de ressources.

Décision : Si les connexions churnent : corrigez le pooling, augmentez les timeouts et arrêtez le « connect par requête ».
Si vous ne pouvez pas corriger rapidement, le modèle en-process de SQLite peut réellement vous surpasser pour la même charge.

Task 13: Verify whether MySQL is reading from disk or cache (buffer pool pressure)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 498221   |
+-------------------------+----------+
+-----------------------------------+-----------+
| Variable_name                     | Value     |
+-----------------------------------+-----------+
| Innodb_buffer_pool_read_requests  | 289223112 |
+-----------------------------------+-----------+

Signification : Les buffer pool reads sont des lectures physiques ; read requests sont des lectures logiques. Un faible ratio est bon.
Une augmentation d’Innodb_buffer_pool_reads signifie des misses de cache et des accès disque.

Décision : Si vous manquez le cache : augmentez le buffer pool, réduisez le working set, ajoutez des index, ou déplacez les données chaudes ailleurs.
Si votre working set est minuscule, SQLite avec le cache OS peut être plus simple et plus rapide.

Task 14: Check where your SQLite file lives (containers and network storage gotchas)

cr0x@server:~$ df -T /var/lib/myapp/app.db
Filesystem     Type  1K-blocks     Used Available Use% Mounted on
/dev/nvme0n1p2 ext4  205113320 80422344 114123456  42% /

Signification : La DB est sur un ext4 local. Bien. Si elle est sur NFS ou un overlay étrange, vos verrous et garanties fsync peuvent devenir intéressants.

Décision : Gardez SQLite sur un stockage local sauf si vous comprenez profondément les sémantiques de votre système de fichiers réseau et les avez testées sous panne.

Mode opératoire de diagnostic rapide : quoi vérifier en premier/deuxième/troisième

Quand « la base de données est lente », votre travail est de trouver quelle file se remplit. Ne commencez pas par réécrire le SQL.
Commencez par localiser la salle d’attente.

First: is it latency from crossing boundaries?

  • MySQL : vérifiez le churn de connexions, le dimensionnement des pools, DNS, overhead TLS, sauts de proxy.
  • SQLite : vérifiez si la DB est sur disque local et si vous ne l’avez pas accidentellement mise sur un stockage réseau ou un volume sujet à contention.

Si vous faites un connect-per-request vers MySQL, c’est votre goulot jusqu’à preuve du contraire.
Si SQLite est sur un montage réseau lent, c’est votre goulot jusqu’à preuve du contraire.

Second: is it lock contention?

  • MySQL : inspectez InnoDB status pour les lock waits/deadlocks ; cherchez les lignes chaudes et les transactions longues.
  • SQLite : cherchez « database is locked », lecteurs longue durée bloquant les checkpoints, et multiples écrivains.

La contention se manifeste par des pics de latence et des timeouts alors que le CPU peut sembler « correct ». C’est du queueing classique.

Third: is it disk flush / fsync pressure?

  • Les deux : vérifiez iowait, disk await, et si vous forcez un sync à chaque transaction.
  • SQLite : inspectez la croissance du WAL ; le comportement des checkpoints ; les réglages synchronous.
  • MySQL : surveillez le flush des redo logs et les misses du buffer pool.

Si le disque est lent, la base est lente. Point final.

Finally: only now, examine query plans

Les plans de requête comptent, mais dans de nombreux incidents réels, la « mauvaise requête » est celle qui tient un verrou trop longtemps ou provoque le churn du cache.
Corrigez d’abord l’attente. Puis optimisez le SQL.

Erreurs courantes : symptôme → cause racine → correction

1) SQLite returns “database is locked” under load

Symptôme : échecs sporadiques ou longues attentes sur les écritures, souvent pendant des pics de trafic.

Cause racine : multiples écrivains concurrents ; transactions longues ; busy_timeout absent ; mauvaise configuration WAL ; checkpoints bloqués par des lecteurs longs.

Correction : canalisez les écritures via un écrivain unique ; activez WAL ; définissez busy_timeout ; gardez les transactions courtes ; assurez-vous que les lecteurs ferment rapidement ; ajustez le checkpointing.

2) SQLite benchmarks look great, production loses data after crash

Symptôme : corruption ou écritures récentes perdues après une coupure de courant/reboot.

Cause racine : réglages de durabilité dangereux (par ex. synchronous=OFF), ou la couche de stockage qui ment sur les flushs.

Correction : utilisez WAL avec un niveau synchronous sensé ; gardez SQLite sur stockage local ; testez crash/power-loss ; implémentez sauvegardes et contrôles d’intégrité.

3) MySQL is “slow” but CPU is low and disks are fine

Symptôme : latence élevée des requêtes avec faible utilisation des ressources.

Cause racine : lock waits, tempêtes de connexion, ou queueing côté serveur dû à trop de threads.

Correction : réduisez le churn de connexions ; corrigez le pooling ; inspectez InnoDB pour lock waits et deadlocks ; raccourcissez les transactions ; ajoutez les index nécessaires.

4) MySQL gets fast in benchmarks, then painful in production

Symptôme : excellent p50, p99 terrible ; arrêts périodiques.

Cause racine : misses du buffer pool, rafales de fsync, flushs en arrière-plan, ou lag de réplication causant un backpressure applicatif.

Correction : dimensionnez le buffer pool ; évitez les index chauds ; batcher les écritures ; monitorer redo et flush ; assurez-vous que les réplicas ne sont pas surchargés si vous dépendez d’eux.

5) SQLite on Kubernetes behaves unpredictably

Symptôme : latence étrange, erreurs de verrou, ou données qui disparaissent après un rescheduling.

Cause racine : fichier DB stocké sur FS éphémère de conteneur, couches overlay, ou volume avec sémantiques de verrou inattendues.

Correction : utilisez un volume persistant aux sémantiques testées ; gardez la DB locale au nœud si possible ; traitez le rescheduling de pod comme un scénario de défaillance et prévoyez-le.

6) “We can just put SQLite on NFS so all pods share it”

Symptôme : risques de corruption, comportement de verrou bizarre, effondrement des performances.

Cause racine : sémantiques du système de fichiers réseau, comportement du gestionnaire de verrous, et garanties fsync qui ne correspondent pas aux hypothèses de SQLite.

Correction : ne le faites pas. Si vous avez besoin d’un accès partagé entre nœuds, utilisez un serveur de base de données (MySQL/Postgres) ou une architecture répliquée local-first avec synchronisation explicite.

Listes de contrôle / plan pas à pas

Decision checklist: should this workload be SQLite?

  1. Le jeu de données est-il possédé par une seule application ? Si plusieurs services indépendants doivent écrire, préférez MySQL.
  2. La concurrence d’écriture est-elle faible ou coordonnable ? Si oui, SQLite reste envisageable.
  3. Pouvez-vous garder la DB sur un stockage local ? Si non, réfléchissez-y sérieusement ; SQLite déteste les stockages « surprenants ».
  4. Le working set est-il petit et chaud ? SQLite plus le cache OS peuvent être extrêmement rapides.
  5. Avez-vous besoin de réplication, basculement et accès distant ? Si oui, MySQL l’emporte sauf si vous développez ces couches vous-même.
  6. La simplicité opérationnelle est-elle une exigence majeure ? SQLite vous donne moins de réglages et moins d’alertes.

SQLite production setup plan (boring, correct, repeatable)

  1. Placez la DB sur un système de fichiers persistant local aux sémantiques connues (ext4/xfs sur disques réels).
  2. Activez WAL et définissez un busy timeout au démarrage de l’application.
  3. Décidez explicitement de la durabilité (synchronous) et écrivez-le dans votre runbook.
  4. Gardez les transactions courtes ; ne maintenez pas une transaction pendant des appels réseau.
  5. Implémentez périodiquement PRAGMA quick_check pendant les fenêtres de faible trafic.
  6. Sauvegardez avec une méthode cohérente (par ex. API de backup en ligne SQLite ou snapshots contrôlés) et testez les restaurations.
  7. Surveillez la croissance du WAL et l’espace disque libre ; implémentez du backpressure avant l’épuisement du disque.
  8. Concevez pour des patterns many-reader / single-writer ; ajoutez une file d’écriture si nécessaire.

MySQL “stop the bleeding” plan when you suspect it’s slower than it should be

  1. Vérifiez le pooling de connexions et réduisez le churn ; regardez le taux de croissance des Connections.
  2. Vérifiez les lock waits/deadlocks ; identifiez les lignes chaudes et les transactions longues.
  3. Vérifiez la latence disque et l’iowait ; les stalls fsync peuvent dominer le p99.
  4. Inspectez les misses du buffer pool ; si vous lisez constamment depuis le disque, vous êtes déjà en retard.
  5. Ce n’est qu’ensuite : optimisez les index et les plans des requêtes pour les requêtes réellement dominantes.

FAQ

1) Is SQLite “faster” than MySQL?

Parfois, oui — surtout pour des charges majoritairement en lecture où le surcoût de la communication client-serveur domine.
Mais MySQL peut surpasser SQLite sous de fortes écritures concurrentes et des charges multi-clients complexes.

2) When does SQLite beat MySQL in real production?

Quand la DB est locale, le working set est chaud, les lectures dominent, et les écritures sont coordonnées (single-writer ou faible contention).
Aussi quand vous voulez supprimer de l’infra et simplifier les opérations.

3) Can I use SQLite for a web app?

Oui, si vous exécutez une instance unique ou si vous pouvez router les écritures vers un écrivain unique et servir principalement des lectures.
Si vous avez plusieurs serveurs applicatifs stateless qui écrivent tous de manière concurrente, SQLite finira par vous punir avec de la contention.

4) Is WAL mode always the right answer for SQLite?

En général pour les lectures concurrentes, oui. WAL améliore le comportement lecteur/écrivain. Mais il introduit des considérations de checkpointing et des fichiers supplémentaires.
Vous devez surveiller la croissance du WAL et vous assurer que les lecteurs ne conservent pas des snapshots indéfiniment.

5) Is SQLite safe on network filesystems?

Traitez « sûr » comme « prouvé sûr sous votre système de fichiers, options de montage et modes de défaillance exacts ». En pratique, les systèmes de fichiers réseau partagés sont une source fréquente de problèmes.
Si vous avez besoin d’un accès partagé entre nœuds, utilisez MySQL (ou une autre base serveur) plutôt que d’essayer de faire agir SQLite comme tel.

6) What about backups for SQLite?

Sauvegarder un fichier est facile. Le sauvegarder de façon cohérente pendant que l’app écrit est la vraie exigence.
Utilisez l’approche de backup en ligne de SQLite ou arrêtez brièvement les écritures. Puis testez les restaurations ; une sauvegarde non restaurée n’est qu’une rumeur.

7) What about migrations: start with SQLite, move to MySQL later?

C’est une stratégie valide si vous la concevez : gardez le SQL portable quand c’est possible, évitez les particularités SQLite, et construisez tôt une pipeline de migration.
N’attendez pas d’être en feu pour inventer l’export des données.

8) Why does MySQL sometimes have worse tail latency than SQLite?

Parce qu’il a plus de points de mise en file : réseau, ordonnancement de threads, lock waits, misses du buffer pool, rafales fsync, effets de réplication.
Le chemin plus simple de SQLite peut produire un meilleur p99 — jusqu’à apparition de la contention d’écriture.

9) Can I scale SQLite with replicas?

Pas au sens MySQL. Vous pouvez répliquer le fichier ou streamer des changements, mais alors vous construisez un système distribué.
Si vous avez besoin d’une réplication et d’un basculement simples, MySQL est le choix mature.

10) If SQLite is so good, why doesn’t everyone use it for everything?

Parce que « tout » inclut l’accès multi-tenant, de nombreux écrivains concurrents, des clients distants, et des primitives opérationnelles fortes comme la réplication.
SQLite est un scalpel, pas un couteau suisse.

Prochaines étapes réalisables cette semaine

Si vous hésitez entre MySQL et SQLite — ou si vous essayez de sauver un système qui a mal choisi — faites ce qui suit dans l’ordre :

  1. Documentez la forme de votre charge : ratio lecture/écriture, pics d’écrivains concurrents, taille du dataset, exigences de durabilité, topologie de déploiement.
  2. Mesurez les coûts de frontière : churn de connexions et latence réseau pour MySQL ; emplacement du stockage et contention de verrous pour SQLite.
  3. Lancez un benchmark réaliste : même jeu de données, cache chaud et cache froid, vraie concurrence, et réglages de durabilité proches de la production.
  4. Choisissez l’architecture la plus simple qui répond aux besoins : si SQLite les respecte, profitez de la vitesse gratuite et de moins de pièces mobiles.
  5. Si vous avez besoin de MySQL, engagez-vous à bien l’exploiter : pooling, monitoring, sauvegardes et un schéma qui respecte la concurrence.

L’objectif n’est pas de gagner une querelle de base de données. L’objectif est de livrer un système rapide parce qu’il est sensé — et fiable parce qu’il est honnête sur l’échec.

← Précédent
Les sockets comme stratégie : pourquoi les plateformes comptent plus que les CPU aujourd’hui
Suivant →
Ubuntu 24.04 : mises à jour ont cassé les modules — reconstruire initramfs correctement (Cas n°88)

Laisser un commentaire