DuckDB ou SQLite sur un serveur : lequel choisir ?
SQLite gère l’état transactionnel, DuckDB analyse Parquet et CSV. Découvrez pourquoi un même VPS utilise souvent les deux, avec un exemple concret de chaque.
DuckDB et SQLite sur un serveur : la réponse en une phrase
SQLite est un moteur OLTP (traitement transactionnel en ligne) : il stocke les données sous forme de lignes et est conçu pour en lire et en écrire quelques-unes à la fois, de manière sûre et rapide. DuckDB est un moteur OLAP (traitement analytique en ligne) : il stocke les données sous forme de colonnes et est conçu pour parcourir des millions de lignes et renvoyer un seul agrégat. Ce sont deux bibliothèques embarquées qui ouvrent un fichier standard, et aucune ne nécessite un processus serveur à surveiller.
La réponse honnête à la question « lequel choisir ? » est donc presque toujours : « les deux, sur le même VPS ». Votre application conserve son état courant dans SQLite. Vos rapports lisent les fichiers Parquet et CSV avec DuckDB. Ils ne se concurrencent pas, car ils n’effectuent pas le même travail.
Pourquoi le stockage par lignes et le stockage par colonnes changent la réponse
SQLite écrit une ligne comme un bloc contigu dans une page. Récupérer une commande par sa clé primaire touche une page d’index et une page de données, soit deux lectures. C’est exactement ce que fait une application des milliers de fois par seconde : lire cet utilisateur, mettre à jour cette session, insérer cette commande.
DuckDB écrit chaque colonne séparément et la compresse. Additionner amount_cents sur cinq millions de lignes ne lit que la colonne amount_cents, ignore tous les autres octets du fichier et exécute la somme avec du code vectorisé sur des lots de valeurs. Les autres colonnes ne sont jamais lues sur le disque. C’est de là que vient le gain de vitesse.
Exécutez maintenant chaque moteur sur la charge de travail de l’autre. Pour additionner une colonne, SQLite doit parcourir chaque ligne et extraire la ligne entière de la page pour accéder à un champ. Il lit donc beaucoup plus de données sur le disque que nécessaire. Pour insérer une commande, DuckDB doit modifier le stockage de chaque colonne pour une seule valeur et prendre un verrou d’écriture sur l’ensemble du fichier de base de données. Aucun des deux moteurs n’est défectueux. Chacun répond à une question pour laquelle il n’a pas été conçu.
Quand SQLite est le bon choix : état transactionnel de l’application
Choisissez SQLite lorsque les écritures sont petites, fréquentes et ne doivent pas être perdues. Sessions, commandes, lignes de file d’attente, paramètres : tout ce qu’une requête web crée.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appCréez la table et activez le write-ahead logging dans la même étape.
sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer TEXT NOT NULL,
placed_at TEXT NOT NULL,
amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQLLa première ligne de sortie est wal. Il s’agit de PRAGMA qui indique le mode activé, et c’est le réglage le plus utile sur un serveur. Dans le mode par défaut avec rollback journal, une écriture bloque toutes les lectures. En mode WAL, les lecteurs continuent de lire le dernier état validé pendant qu’un writer ajoute des données. Un rapport lent ne bloque donc plus la requête web qui le suit.
Vérifiez que la ligne a bien été renvoyée :
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"Vous obtenez 1|ana|2026-07-30T09:14:00Z|4200. Deux autres fichiers sont apparus à côté de la base de données : app.db-wal et app.db-shm. Ils en font tous les deux partie. Copier uniquement app.db pendant que l’application fonctionne produit une sauvegarde incohérente. Ce point est abordé plus loin.
SQLite autorise toujours un seul writer à la fois. Cette limite repose sur un verrou, pas sur une file d’attente. Un second writer qui attend trop longtemps échoue donc avec database is locked au lieu de bloquer indéfiniment. Augmentez le délai d’attente avec PRAGMA busy_timeout = 5000; sur chaque connexion ouverte par votre application. Cinq secondes d’attente suffisent à éliminer la plupart de ces erreurs dans une charge web normale.
Quand DuckDB est le meilleur choix : analyser les fichiers que vous avez déjà
Choisissez DuckDB lorsque la question commence par « combien », « quelle quantité » ou « quels sont les dix premiers », et que les données sont réparties dans plusieurs fichiers CSV ou Parquet. Installez le client en ligne de commande, en version 1.5.5 en juillet 2026 :
curl https://install.duckdb.org | shLe script installe le binaire dans ~/.duckdb/cli/latest/duckdb et affiche la ligne qui l’ajoute à votre PATH. Vérifiez qu’il fonctionne :
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Créez un fichier réaliste à interroger. Cette commande écrit cinq millions de lignes de commandes au format Parquet, avec une compression zstd :
mkdir -p /srv/data
duckdb :memory: "
COPY (
SELECT i AS id,
'cust_' || (i % 5000) AS customer,
TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
(i * 37) % 20000 AS amount_cents
FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"Posez maintenant la question analytique. Ouvrez le shell, activez le chronomètre et interrogez directement le fichier, sans étape d’importation :
.timer on
SELECT customer,
count(*) AS orders,
sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;Relevez votre propre valeur dans .timer au lieu de faire confiance à une valeur publiée, car le résultat dépend de votre disque et du nombre de cœurs. C’est sa structure qui compte. Il n’y a eu ni CREATE TABLE, ni INSERT, ni étape de chargement : DuckDB a lu le footer Parquet, déterminé quels blocs de colonnes étaient nécessaires à la requête, puis lu uniquement ceux-ci. Un répertoire entier fonctionne de la même manière avec une expansion de chemin, FROM '/srv/data/orders-*.parquet', ce qui permet de transformer un mois d’exports quotidiens en une seule requête.
La vitesse du disque constitue la limite inférieure de l’ensemble du traitement, et un parcours de colonnes correspond à une longue lecture séquentielle. L’écart entre le stockage NVMe et l’ancien stockage SATA sur un VPS est donc plus visible ici qu’avec les petites lectures aléatoires de SQLite.
Lecture de votre base de données SQLite depuis DuckDB
Les deux moteurs communiquent grâce à l’extension sqlite de DuckDB. Attachez la base de données de l’application en lecture seule afin qu’une requête analytique ne puisse jamais modifier l’état courant :
INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;Cette commande lit les lignes directement dans le fichier SQLite au moment de la requête, sans effectuer de copie. C’est pratique, mais ce n’est pas rapide, car les données sur disque sont toujours stockées par lignes et DuckDB doit les parcourir. Utilisez cette méthode pour l’export, pas pour un tableau de bord qui se recharge toutes les trente secondes :
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);Cette seule instruction constitue le modèle complet. SQLite conserve les lignes récentes et actives. Un export planifié convertit les périodes clôturées en Parquet. DuckDB répond aux requêtes couvrant plusieurs mois, tandis que la base de données de l’application reste peu volumineuse, ce qui accélère ses écritures.
Planifiez l’export au lieu de l’exécuter manuellement. Une paire service et timer systemd convient parfaitement : une unité exécute le COPY, et un timer la déclenche chaque nuit.
Exécuter les deux sur un même VPS
Rien de tout cela ne nécessite de conteneur ni de port. Les deux moteurs sont des bibliothèques. L’installation consiste donc à installer un package et à fournir un chemin de fichier. Si le reste de votre stack s’exécute déjà avec Docker Compose sur le même VPS, montez le répertoire de données dans le conteneur qui en a besoin au lieu d’ajouter un service de base de données, car aucun service n’est nécessaire.
Deux règles permettent d’éviter les problèmes.
Attribuez à chaque moteur son propre répertoire : /srv/app pour le fichier SQLite écrit par l’application, /srv/data pour les fichiers Parquet lus par le moteur d’analytics. S’ils partagent un répertoire, un job de sauvegarde qui crée un snapshot de l’un peut entrer en conflit avec l’autre.
Ne faites pas pointer deux processus vers un même fichier de base de données DuckDB en mode read-write. Un seul processus peut détenir un fichier DuckDB en écriture. Le second échoue dès l’ouverture du fichier. Plusieurs lecteurs sont possibles si chacun définit access_mode = 'READ_ONLY'. Ce comportement surprend les utilisateurs venant de SQLite, où plusieurs processus partagent couramment un fichier. Si votre moteur d’analytics lit uniquement des fichiers Parquet, la question ne se pose jamais. C’est une raison supplémentaire de conserver l’état persistant dans SQLite.
Les sauvegardes diffèrent, et cette différence pose problème
Une base de données SQLite en cours d’exécution se compose de trois fichiers. Les copier avec cp pendant une écriture produit un fichier qui s’ouvre, mais dont le contenu est incorrect. Utilisez la commande de sauvegarde du moteur. Elle crée un instantané cohérent pendant que l’application poursuit ses écritures :
sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"integrity_check affiche ok avec une copie valide. Toute autre valeur signifie que vous devez supprimer cet instantané et en créer un nouveau.
Les fichiers Parquet ne changent plus après leur écriture. Ils ne nécessitent donc aucun traitement particulier : sauvegardez le répertoire. Envoyez les deux chemins hors du serveur avec des sauvegardes restic depuis votre VPS. Toute la couche de données tient ainsi dans deux répertoires traités par une seule tâche de sauvegarde.
Modes d’échec et chaînes exactes affichées
Error: database is locked dans SQLite signifie qu’une autre connexion a conservé le verrou d’écriture plus longtemps que le délai d’attente autorisé. Il ne s’agit pas d’une corruption. Définissez PRAGMA busy_timeout sur chaque connexion, puis recherchez une transaction longue qui aurait dû être divisée en plusieurs transactions courtes.
Error: unable to open database file après une modification des permissions signifie généralement que le processus peut écrire dans le fichier, mais pas dans son répertoire. SQLite crée app.db-wal et app.db-shm à côté de la base de données. Le répertoire lui-même doit donc être accessible en écriture, et pas seulement le fichier .db.
IO Error: Could not set lock on file dans DuckDB signifie qu’un autre processus a déjà ouvert cette base de données en écriture. Fermez l’autre shell ou ouvrez le vôtre en lecture seule.
Out of Memory Error dans DuckDB sur un petit VPS signifie qu’une requête nécessitait plus de mémoire de travail qu’elle n’en avait. DuckDB utilise le disque comme espace temporaire lorsque c’est possible. Donnez-lui donc un emplacement où effectuer ces écritures temporaires en ouvrant un fichier de base de données sur le disque plutôt que :memory:, puis limitez sa consommation avec SET memory_limit = '2GB';. Sur une machine qui exécute d’autres services, cette limite empêche une requête ad hoc de faire sortir votre application de la RAM.
Binder Error: Referenced column "amount" not found lors de l’interrogation d’un fichier Parquet signifie presque toujours que le schéma du fichier n’est pas celui dont vous vous souvenez. Exécutez DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; et consultez les vrais noms de colonnes.
Comment choisir en pratique
Commencez par déterminer le mode d’écriture. De nombreuses petites écritures qui doivent survivre à une coupure de courant indiquent qu’il faut utiliser SQLite. Déterminez ensuite le mode de lecture. Des parcours complets avec des agrégations sur un historique long indiquent qu’il faut utiliser DuckDB. Dans la plupart des systèmes réels, les deux réponses sont positives. La bonne approche consiste alors à attribuer à chaque moteur la partie qu’il gère le mieux, plutôt que de demander à l’un de remplacer l’autre.
Il faut éviter de déplacer l’état applicatif en production vers DuckDB parce qu’un rapport était lent. Le rapport était lent à cause de l’organisation du stockage. La correction consiste donc à effectuer un export, pas à réécrire le chemin d’écriture.
FAQ
DuckDB peut-il remplacer SQLite pour la base de données de mon application ?
Pas pour une base qui reçoit souvent des écritures. DuckDB verrouille l’ensemble du fichier de base de données en écriture, autorise un seul processus en lecture-écriture à la fois et est optimisé pour les modifications en masse plutôt que pour les insertions ligne par ligne. Conservez l’état transactionnel dans SQLite et laissez DuckDB le lire avec ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); lorsqu’un rapport en a besoin.
DuckDB est-il vraiment plus rapide que SQLite pour l’analytique ?
Oui, pour les parcours et les agrégations sur une grande table. La raison tient à l’organisation du stockage, pas à une astuce d’optimisation. DuckDB ne lit que les colonnes mentionnées par la requête et traite les valeurs par lots, tandis que SQLite doit parcourir les lignes entières pour atteindre un champ. Pour récupérer une seule ligne par clé primaire, l’ordre s’inverse : SQLite accède à deux pages, tandis que DuckDB accède au stockage de chaque colonne.
Faut-il beaucoup de RAM pour exécuter DuckDB sur un VPS ?
Non, mais attribuez-lui une limite et un espace disque. Ouvrez un fichier de base de données plutôt que :memory: afin que DuckDB puisse déporter les résultats intermédiaires sur le disque, puis définissez SET memory_limit = '2GB'; sur une valeur que votre VPS peut lui allouer. Sans limite, un GROUP BY volumineux peut augmenter Out of Memory Error ou faire sortir d’autres services de la RAM.
Comment transférer mes données SQLite dans Parquet ?
Attachez le fichier SQLite depuis DuckDB et exportez directement une requête avec COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Exécutez cette opération selon une planification pour les périodes closes, par exemple les lignes du mois dernier, et laissez les lignes récentes dans SQLite, où l’application continue de les modifier.
Quelle base faut-il sauvegarder, et comment ?
Les deux, de manière différente. Créez des instantanés SQLite avec sqlite3 app.db ".backup '/srv/backup/app.db'" plutôt qu’avec cp, car une base de données en cours d’exécution est aussi un -wal et un fichier -shm, et une copie directe peut être incohérente. Les fichiers Parquet ne changent plus une fois écrits ; il suffit donc de copier le répertoire.