SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor

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/app

Cré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);
SQL

La 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 dont vous disposez déjà

Choisissez DuckDB lorsque la question commence par « combien », « quelle quantité » ou « quels sont les dix premiers », et que les données se trouvent dans un ensemble de fichiers CSV ou Parquet. Installez le client en ligne de commande, en version 2.0.0 (une version LTS) au 27 octobre 2026 :

curl https://install.duckdb.org | sh

Le script installe la dernière version stable, sauf si vous en imposez une, par exemple curl https://install.duckdb.org | DUCKDB_VERSION=1.5.6 sh pour rester sur la version précédente. Il installe le binaire sous ~/.duckdb/cli/latest/duckdb et affiche la ligne qui l’ajoute à votre PATH. Vérifiez qu’il s’exécute :

~/.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, compressées avec 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;

Lisez votre propre résultat 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é les groupes de colonnes nécessaires à la requête, puis lu uniquement ceux-ci. Un répertoire entier fonctionne de la même manière avec un glob, FROM '/srv/data/orders-*.parquet' : un mois d’exports quotidiens devient ainsi une seule requête.

La vitesse du disque constitue la limite inférieure de l’ensemble, et un scan de colonnes correspond à une longue lecture séquentielle. L’écart entre le stockage NVMe et l’ancien stockage SATA sur un VPS apparaît donc ici plus clairement que dans 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

database is locked de SQLite, qu’il provienne du pilote de votre application ou du shell sqlite3, signifie qu’une autre connexion a conservé le verrou d’écriture plus longtemps que ne l’autorisait votre délai d’attente. 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.

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 de DuckDB signifie qu’un second 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 de DuckDB sur un petit VPS signifie qu’une requête avait besoin de plus de mémoire de travail qu’elle n’en disposait. DuckDB déporte par défaut les résultats intermédiaires sur le disque, y compris dans une session :memory:. Le déport ne dépend donc pas de l’ouverture d’un fichier de base de données. DuckDB écrit ces fichiers temporaires dans un répertoire .tmp à côté du fichier de base de données ou, pour une session en mémoire, dans un répertoire .tmp relatif à l’emplacement depuis lequel vous avez démarré la session. Démarrez donc DuckDB dans un emplacement accessible en écriture disposant d’un espace disque libre, ou indiquez un autre emplacement avec SET temp_directory = '/var/tmp/duckdb';. Le paramètre important est la limite : SET memory_limit = '2GB';. La valeur par défaut correspond à 80% de la RAM de la machine. Sur un système qui exécute d’autres services, c’est cette limite qui empêche une requête ponctuelle de faire sortir votre application de la RAM. Certains agrégats, comme string_agg(), ne peuvent pas être déportés, pas plus que certains agrégats fondés sur un tri. Une requête qui les utilise peut donc tout de même manquer de mémoire.

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 récupérez 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 bloc 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 est nécessaire.

DuckDB est-il vraiment plus rapide que SQLite pour l’analytique ?

Pour les parcours et les agrégations sur une grande table, oui. La raison tient à l’organisation du stockage, et non à une astuce de tuning. DuckDB ne lit que les colonnes nommé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.

Ai-je besoin de beaucoup de RAM pour exécuter DuckDB sur un VPS ?

Non, mais définissez-lui une limite et prévoyez de l’espace pour le spill. DuckDB écrit par défaut les résultats intermédiaires sur le disque, même dans une session :memory:, à condition que son répertoire temporaire soit accessible en écriture et dispose d’espace libre. Définissez ensuite SET memory_limit = '2GB'; sur une valeur que votre VPS peut fournir. Avec sa valeur par défaut de 80 % de la RAM, un grand GROUP BY peut provoquer Out of Memory Error ou pousser les autres services hors de la RAM.

Comment transférer mes données SQLite dans Parquet ?

Attachez le fichier SQLite depuis DuckDB et exportez directement le résultat d’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 clôturées, par exemple les lignes du mois dernier, et laissez les lignes récentes dans SQLite, là où l’application continue de les modifier.

Quelle base dois-je sauvegarder, et comment ?

Les deux, de manière différente. Créez des snapshots 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 également un -wal et un fichier -shm, et une copie simple peut être incohérente. Les fichiers Parquet ne changent plus une fois écrits. Il suffit donc de copier le répertoire.

Cet article vous a-t-il été utile ?