DuckDB vs SQLite: wanneer gebruikt u welke database?
Kies niet tussen DuckDB en SQLite. SQLite beheert uw transactionele applicatiedata, terwijl DuckDB razendsnel analyses uitvoert op Parquet en CSV. Leer hoe u beide combineert.
DuckDB versus SQLite op een server: het antwoord in één zin
SQLite is een OLTP-engine (online transaction processing): het slaat gegevens op als rijen en is gebouwd om er enkele tegelijk te lezen en te schrijven, veilig en snel. DuckDB is een OLAP-engine (online analytical processing): het slaat gegevens op als kolommen en is gebouwd om miljoenen rijen te scannen en één aggregaat terug te geven. Beide zijn embedded libraries, beide openen een standaardbestand en bij geen van beide hoeft u een serverproces te beheren.
Het eerlijke antwoord op de vraag "welke moet ik kiezen" is daarom bijna altijd "beide, op dezelfde VPS". Uw applicatie houdt de actieve status bij in SQLite. Uw rapportage leest Parquet- en CSV-bestanden met DuckDB. Ze concurreren niet met elkaar omdat ze niet dezelfde taak uitvoeren.
Waarom rij-opslag en kolom-opslag het antwoord veranderen
SQLite schrijft een rij als één aaneengesloten stuk van een pagina. Het ophalen van één order via de primaire sleutel raakt één indexpagina en één datapagina, wat neerkomt op twee leesacties. Dat is precies wat een applicatie duizenden keren per seconde doet: deze gebruiker lezen, deze sessie bijwerken, deze order invoegen.
DuckDB schrijft elke kolom afzonderlijk en comprimeert deze. Het sommeren van amount_cents over vijf miljoen rijen leest alleen de amount_cents-kolom, slaat elke andere byte in het bestand over en voert de som uit via gevectoriseerde code over batches van waarden. De andere kolommen worden nooit van de schijf gelezen, en daar komt de snelheid vandaan.
Voer nu elke engine uit met de werklast van de andere. SQLite moet bij het sommeren van een kolom elke rij doorlopen en de volledige rij van de pagina halen om bij één veld te komen, waardoor het veel meer van de schijf leest dan nodig is. DuckDB moet bij het invoegen van één order de opslag van elke kolom voor een enkele waarde aanraken, en het neemt een schrijfvergrendeling op het volledige databasebestand om dit te doen. Geen van beide engines is defect. Elk beantwoordt een vraag waarvoor het niet is ontworpen.
Waar SQLite uitblinkt: transactionele applicatiestatus
Kies voor SQLite wanneer schrijfacties klein en frequent zijn en niet verloren mogen gaan. Denk aan sessies, bestellingen, wachtrijen, instellingen en alles wat een webverzoek genereert.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appMaak de tabel aan en schakel write-ahead logging in één stap in.
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);
SQLDe eerste regel van de output is wal. Dit is de PRAGMA die de modus rapporteert waarnaar is overgeschakeld; dit is de meest nuttige instelling op een server. In de standaard rollback journal-modus blokkeert een schrijfactie elke lezer. In WAL-modus blijven lezers de laatst doorgevoerde status lezen terwijl één schrijver gegevens toevoegt, waardoor een traag rapport het webverzoek erachter niet langer vertraagt.
Controleer of de rij is teruggekomen:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"U krijgt 1|ana|2026-07-30T09:14:00Z|4200. Er zijn twee extra bestanden verschenen naast de database, app.db-wal en app.db-shm, en beide horen bij de database. Het kopiëren van alleen app.db terwijl de applicatie draait, resulteert in een inconsistente back-up; dit wordt verderop behandeld.
SQLite staat nog steeds slechts één schrijver tegelijk toe. Die limiet is een lock, geen wachtrij, dus een tweede schrijver die te lang wacht, faalt met database is locked in plaats van voor eeuwig te blokkeren. Verhoog de wachttijd met PRAGMA busy_timeout = 5000; op elke verbinding die uw applicatie opent. Vijf seconden geduld elimineert de meeste van deze fouten bij een normale web-workload.
Waar DuckDB uitblinkt: analyses over bestanden die u al heeft
Kies voor DuckDB wanneer de vraag begint met "hoeveel" of "wat zijn de top tien", en de invoer een verzameling CSV- of Parquet-bestanden is. Installeer de command-line client, versie 1.5.5 per juli 2026:
curl https://install.duckdb.org | shHet script installeert het binaire bestand onder ~/.duckdb/cli/latest/duckdb en toont de regel die het toevoegt aan uw PATH. Controleer of het werkt:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Maak een realistisch bestand om te bevragen. Dit schrijft vijf miljoen rijen aan orders naar Parquet, gecomprimeerd met 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);"Stel nu de analytische vraag. Open de shell, schakel de timer in en bevraag het bestand direct zonder importstap:
.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;Lees uw eigen getal af van .timer in plaats van te vertrouwen op een gepubliceerd getal, omdat het resultaat afhangt van uw schijf en het aantal cores. De vorm van de query is wat telt. Er was geen CREATE TABLE, geen INSERT en geen laadstap: DuckDB las de Parquet-footer, bepaalde welke kolomblokken de query nodig had en las alleen die. Een hele map werkt op dezelfde manier met een glob, FROM '/srv/data/orders-*.parquet', waardoor een maand aan dagelijkse exports één query wordt.
Schijfsnelheid vormt de ondergrens van dit alles, en een kolomscan is een lange sequentiële leesactie. Het verschil tussen NVMe en oudere SATA-opslag op een VPS is hier duidelijker zichtbaar dan bij de kleine willekeurige leesacties van SQLite.
Uw SQLite-database uitlezen met DuckDB
De twee engines komen samen via de sqlite-extensie van DuckDB. Koppel de applicatiedatabase alleen-lezen, zodat een analyse-query nooit naar de actieve status kan schrijven:
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;Dit leest rijen uit het SQLite-bestand op het moment van de query zonder kopieeractie. Dit is handig, maar niet snel, omdat de data op schijf nog steeds in rij-opslag staat en DuckDB deze moet doorlopen. Gebruik dit voor de export, niet voor een dashboard dat elke dertig seconden ververst:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);Dat ene statement vormt het volledige patroon. SQLite beheert de recente actieve rijen. Een geplande export zet afgesloten periodes om naar Parquet. DuckDB beantwoordt elke vraag die maanden beslaat, en de applicatiedatabase blijft klein, waardoor de schrijfbewerkingen snel blijven.
Voer de export uit volgens een schema in plaats van handmatig. Een systemd service en timer-paar is hiervoor de juiste oplossing: één unit die de COPY uitvoert, één timer die deze elke nacht activeert.
Beide draaien op één VPS
Niets hiervan vereist een container en niets vereist een poort. Beide engines zijn bibliotheken, dus de installatie bestaat uit een pakket en een bestandspad. Als de rest van uw stack al draait onder Docker Compose op dezelfde VPS, mount dan de datamap in de container die deze nodig heeft in plaats van een databaseservice toe te voegen, aangezien er geen service is om toe te voegen.
Twee regels voorkomen problemen met deze opstelling.
Geef elke engine zijn eigen map: /srv/app voor het SQLite-bestand waar de applicatie naar schrijft, /srv/data voor de Parquet-bestanden die de analytics-tool leest. Wanneer ze een map delen, ontstaat er een raceconditie bij een back-upjob die een snapshot van één van beide maakt.
Wijs niet twee processen naar één DuckDB-databasebestand in read-write modus. Slechts één proces mag een DuckDB-bestand openen voor schrijven; het tweede proces zal het bestand helemaal niet kunnen openen. Meerdere lezers zijn geen probleem wanneer elk proces access_mode = 'READ_ONLY' instelt. Dit verrast gebruikers die afkomstig zijn van SQLite, waar meerdere processen routinematig een bestand delen. Als uw analytics-tool alleen Parquet-bestanden leest, is dit nooit aan de orde, wat een extra reden is om de duurzame status in SQLite te bewaren.
Backups verschillen van elkaar, en dat verschil kan voor problemen zorgen
Een actieve SQLite-database bestaat uit drie bestanden. Het kopiëren hiervan met cp terwijl er naar geschreven wordt, resulteert in een bestand dat wel opent, maar incorrecte gegevens bevat. Gebruik het ingebouwde backup-commando van de engine; dit maakt een consistente snapshot terwijl de applicatie doorgaat met schrijven:
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 geeft ok weer bij een geslaagde kopie. Elke andere melding betekent dat u de snapshot moet verwijderen en een nieuwe moet maken.
Parquet-bestanden veranderen nooit nadat ze zijn geschreven, dus deze vereisen geen speciale afhandeling: maak een backup van de map. Verstuur beide paden vanaf de server met restic backups vanaf uw VPS, en de volledige datalaag bestaat uit twee mappen in één backup-taak.
Foutmodi en de exacte meldingen die u zult zien
Error: database is locked vanuit SQLite betekent dat een andere verbinding de schrijfvergrendeling langer vasthield dan uw time-out toestaat. Dit is geen corruptie. Stel PRAGMA busy_timeout in op elke verbinding en zoek vervolgens naar een lange transactie die uit meerdere korte transacties had moeten bestaan.
Error: unable to open database file na een wijziging van de rechten betekent meestal dat het proces het bestand wel kan schrijven, maar de bijbehorende map niet. SQLite maakt app.db-wal en app.db-shm aan naast de database, dus de map zelf moet schrijfbaar zijn, niet alleen het .db bestand.
IO Error: Could not set lock on file vanuit DuckDB betekent dat een tweede proces de database al open heeft staan voor schrijven. Sluit de andere shell of open uw sessie in alleen-lezen modus.
Out of Memory Error vanuit DuckDB op een kleine VPS betekent dat een query meer werkgeheugen nodig had dan beschikbaar was. DuckDB schrijft gegevens naar de schijf wanneer dat mogelijk is; geef het dus een locatie om naar uit te wijken door een databasebestand op schijf te openen in plaats van :memory:, en beperk het verbruik met SET memory_limit = '2GB';. Op een server die andere services draait, voorkomt deze limiet dat een ad-hoc query uw applicatie uit het RAM verdringt.
Binder Error: Referenced column "amount" not found bij het bevragen van Parquet betekent bijna altijd dat het schema van het bestand niet overeenkomt met wat u verwacht. Voer DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; uit en lees de werkelijke kolomnamen uit.
Hoe u in de praktijk kiest
Vraag uzelf af wat het schrijfpatroon is. Veel kleine schrijfacties die een stroomstoring moeten overleven, betekenen dat u SQLite nodig heeft. Vraag uzelf af wat het leespatroon is. Volledige scans met aggregaties over een lange historie betekenen dat u DuckDB nodig heeft. De meeste echte systemen antwoorden 'ja' op beide vragen. Het juiste antwoord is om elke engine het deel te geven waar deze goed in is, in plaats van één van beide te dwingen de andere rol te vervullen.
De migratie die u moet vermijden, is het verplaatsen van actieve applicatiestatus naar DuckDB omdat een rapport traag was. Het rapport was traag vanwege de opslagindeling; de oplossing is daarom een export, niet een herschrijving van uw schrijftraject.
FAQ
Kan DuckDB SQLite vervangen voor mijn applicatiedatabase?
Niet voor een database die vaak schrijft. DuckDB plaatst een write lock op het volledige databasebestand, staat slechts één read-write proces tegelijk toe en is geoptimaliseerd voor bulkmutaties in plaats van inserts per rij. Houd transactionele data in SQLite en laat DuckDB deze inlezen met ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); wanneer een rapport dit vereist.
Is DuckDB voor analytics echt sneller dan SQLite?
Voor scans en aggregaties over een grote tabel wel, en de reden hiervoor is de opslagstructuur in plaats van een tuning-truc. DuckDB leest alleen de kolommen die in een query worden genoemd en verwerkt waarden in batches, terwijl SQLite volledige rijen moet doorlopen om één veld te bereiken. Voor het ophalen van een enkele rij op basis van een primary key is de verhouding omgekeerd, omdat SQLite twee pagina's raadpleegt en DuckDB de opslag van elke kolom moet aanroepen.
Heb ik veel RAM nodig om DuckDB op een VPS te draaien?
Nee, maar stel een limiet in en gebruik schijfruimte. Open een databasebestand in plaats van :memory: zodat DuckDB tussenresultaten naar de schijf kan wegschrijven, en stel vervolgens SET memory_limit = '2GB'; in op een waarde die uw VPS kan missen. Zonder limiet kan één grote GROUP BY leiden tot Out of Memory Error of andere services uit het RAM verdringen.
Hoe krijg ik mijn SQLite-data in Parquet?
Koppel het SQLite-bestand vanuit DuckDB en kopieer een query direct met COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Voer dit volgens een schema uit voor afgesloten periodes, zoals de rijen van de vorige maand, en laat recente rijen in SQLite staan waar de applicatie ze nog steeds wegschrijft.
Welke moet ik back-uppen, en hoe?
Beide, op verschillende manieren. Maak SQLite-snapshots met sqlite3 app.db ".backup '/srv/backup/app.db'" in plaats van cp, omdat een actieve database ook een -wal en een -shm bestand is; een eenvoudige kopie kan corrupt raken. Parquet-bestanden veranderen nooit nadat ze zijn geschreven, dus het kopiëren van de map is voldoende.