SSD Nodes Learn 8GB RAM — $66/jaar
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-02

DuckDB versus SQLite op een server: waarom beide

SQLite bewaart transactiestatus, DuckDB analyseert Parquet en CSV. Ontdek waarom u op één VPS meestal beide gebruikt, met een uitgewerkt voorbeeld per engine.

DuckDB versus SQLite op een server: het antwoord in één zin

SQLite is een OLTP-engine (online transaction processing): deze slaat gegevens op als rijen en is ontworpen om enkele rijen tegelijk veilig en snel te lezen en te schrijven. DuckDB is een OLAP-engine (online analytical processing): deze slaat gegevens op als kolommen en is ontworpen om miljoenen rijen te scannen en één aggregaatresultaat terug te geven. Beide zijn embedded bibliotheken, beide openen een gewoon bestand en geen van beide voert een serverproces uit dat u moet beheren.

Het eerlijke antwoord op de vraag "welke" is daarom vrijwel altijd "beide, op dezelfde VPS". Uw applicatie bewaart de actuele status in SQLite. Uw rapportages lezen Parquet- en CSV-bestanden met DuckDB. Ze concurreren niet, omdat ze niet dezelfde taak uitvoeren.

Waarom rijopslag en kolomopslag het antwoord veranderen

SQLite schrijft een rij als één aaneengesloten geheel van een pagina. Als u één order op basis van de primaire sleutel ophaalt, raakt u één indexpagina en één gegevenspagina. Dat zijn twee leesbewerkingen. 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. Als u amount_cents over vijf miljoen rijen optelt, leest u alleen de kolom amount_cents. Elke andere byte in het bestand wordt overgeslagen. De optelling wordt uitgevoerd met gevectoriseerde code over batches met waarden. De andere kolommen worden nooit van schijf gelezen. Daar komt de snelheid vandaan.

Voer nu elke engine uit op de werklast van de andere. Als SQLite een kolom optelt, moet het elke rij doorlopen en de volledige rij van de pagina ophalen om één veld te bereiken. Daardoor leest het veel meer gegevens van schijf dan nodig is. Als DuckDB één order invoegt, moet het voor één waarde de opslag van elke kolom aanpassen. Hiervoor neemt het een schrijfvergrendeling op het volledige databasebestand. Geen van beide engines werkt verkeerd. Elke engine beantwoordt een vraag waarvoor deze niet is ontworpen.

Waar SQLite uitblinkt: transactionele applicatiestatus

Kies SQLite wanneer schrijfbewerkingen klein en frequent zijn en niet verloren mogen gaan. Sessies, bestellingen, wachtrijrijen, instellingen en alles wat een webaanvraag aanmaakt.

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

Maak de tabel aan en schakel write-ahead logging in dezelfde 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);
SQL

De eerste uitvoerregel is wal. Dat is PRAGMA, die de modus meldt waarnaar is overgeschakeld. Dit is de nuttigste instelling op een server. In de standaardmodus met rollback-journal blokkeert een schrijver elke lezer. In de WAL-modus blijven lezers de laatst vastgelegde status lezen terwijl één schrijver gegevens toevoegt. Een traag rapport blokkeert daardoor de webaanvraag die erop wacht niet langer.

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. Naast de database zijn twee bestanden verschenen: app.db-wal en app.db-shm. Beide horen bij de database. Als u alleen app.db kopieert terwijl de toepassing actief is, krijgt u een inconsistente back-up. Dit wordt verderop behandeld.

SQLite staat nog steeds slechts één schrijver tegelijk toe. Deze beperking is een vergrendeling, geen wachtrij. Daarom mislukt een tweede schrijver die te lang wacht met database is locked, in plaats van voor onbepaalde tijd te blokkeren. Verhoog de wachttijd met PRAGMA busy_timeout = 5000; op elke verbinding die uw toepassing opent. Vijf seconden wachttijd voorkomt de meeste van deze fouten bij een normale webbelasting.

Waar DuckDB uitblinkt: analyses op bestanden die u al hebt

Kies DuckDB wanneer de vraag begint met "hoeveel", "welk volume" of "welke top tien", en de invoer bestaat uit een verzameling CSV- of Parquet-bestanden. Installeer de opdrachtregelclient, versie 1.5.5 per juli 2026:

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

Het script installeert het binaire bestand onder ~/.duckdb/cli/latest/duckdb en toont de regel waarmee u het aan uw PATH toevoegt. Controleer of het werkt:

~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"

Maak een realistisch bestand om op te zoeken. Hiermee schrijft u vijf miljoen orderregels 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 rechtstreeks, 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 waarde af van .timer in plaats van op een gepubliceerde waarde te vertrouwen, omdat het resultaat afhankelijk is van uw schijf en het aantal cores. De vorm van het resultaat 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 blokken. Een volledige directory werkt op dezelfde manier met een glob, FROM '/srv/data/orders-*.parquet'. Zo wordt een maand met dagelijkse exports één query.

De schijfsnelheid vormt hiervoor de ondergrens. Een kolomscan is een lange sequentiële leesbewerking. Daarom is het verschil tussen NVMe- en oudere SATA-opslag op een VPS hier duidelijker zichtbaar dan bij de kleine willekeurige leesbewerkingen van SQLite.

Uw SQLite-database lezen vanuit DuckDB

De twee engines communiceren via de sqlite-extensie van DuckDB. Koppel de applicatiedatabase alleen-lezen, zodat een query voor analyses 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;

Hiermee worden rijen tijdens het uitvoeren van de query rechtstreeks uit het SQLite-bestand gelezen, zonder kopie. Dit is handig, maar niet snel, omdat de gegevens op schijf nog steeds als rijen zijn opgeslagen en DuckDB deze moet doorlopen. Gebruik dit voor de export, niet voor een dashboard dat elke dertig seconden opnieuw wordt geladen:

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

Deze ene instructie bevat het volledige patroon. SQLite beheert de recente actieve rijen. Een geplande export zet afgesloten perioden om naar Parquet. DuckDB beantwoordt alle vragen die meerdere maanden omvatten, terwijl de applicatiedatabase klein blijft. Daardoor blijven schrijfbewerkingen snel.

Voer de export volgens een schema uit in plaats van handmatig. Een combinatie van een systemd-service en timer heeft hiervoor de juiste omvang: één unit voert de COPY uit en één timer start deze elke nacht.

Beide op één VPS uitvoeren

Hiervoor is geen container en geen poort nodig. Beide engines zijn bibliotheken. De installatie bestaat daarom uit een pakket en een bestandspad. Als de rest van uw stack al onder Docker Compose op dezelfde VPS draait, koppelt u de gegevensdirectory aan de container die deze nodig heeft in plaats van een databaseservice toe te voegen. Er is namelijk geen service om toe te voegen.

Met twee regels voorkomt u problemen in deze opstelling.

Geef elke engine een eigen directory: /srv/app voor het SQLite-bestand waarin de applicatie schrijft en /srv/data voor de Parquet-bestanden die analytics leest. Als ze een directory delen, gaat een back-uptaak die een van beide momentopnamen maakt de andere tegelijkertijd gebruiken.

Laat twee processen niet in één DuckDB-databasebestand openen in lees-schrijfmodus. Slechts één proces mag een DuckDB-bestand voor schrijven openen. Het tweede proces kan het bestand dan helemaal niet openen. Meerdere lezers zijn toegestaan als ze allemaal access_mode = 'READ_ONLY' instellen. Dit is verrassend voor mensen die van SQLite komen, waar meerdere processen regelmatig één bestand delen. Als uw analytics alleen Parquet-bestanden leest, doet deze vraag zich niet voor. Dat is nog een reden om duurzame status in SQLite te bewaren.

Back-ups verschillen, en dat verschil veroorzaakt problemen

Een actieve SQLite-database bestaat uit drie bestanden. Als u deze tijdens het schrijven met cp kopieert, krijgt u een bestand dat wel wordt geopend maar onjuist is. Gebruik de eigen back-upopdracht van de engine. Deze maakt een consistente snapshot terwijl de toepassing blijft 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 bij een goede kopie ok weer. Elke andere uitvoer betekent dat u die snapshot moet verwijderen en een nieuwe moet maken.

Parquet-bestanden veranderen niet meer nadat ze zijn geschreven. Daarvoor is daarom geen speciale verwerking nodig: maak een back-up van de directory. Stuur beide paden van de server naar een andere locatie met restic-back-ups vanaf uw VPS. Zo bestaat de volledige datalaag uit twee directories in één back-uptaak.

Foutmodi en de exacte tekenreeksen die u ziet

Error: database is locked van SQLite betekent dat een andere verbinding de schrijflok langer heeft vastgehouden dan uw time-out toestond. Dit betekent niet dat de gegevens beschadigd zijn. Stel PRAGMA busy_timeout in op elke verbinding en zoek vervolgens naar een lange transactie die meerdere korte transacties had moeten zijn.

Error: unable to open database file na een wijziging van de machtigingen betekent meestal dat het proces het bestand wel kan schrijven, maar niet de bijbehorende map. SQLite maakt app.db-wal en app.db-shm naast de database aan. Daarom moet de map zelf schrijfbaar zijn, en niet alleen het bestand .db.

IO Error: Could not set lock on file van DuckDB betekent dat een tweede proces die database al geopend heeft voor schrijven. Sluit de andere shell of open uw shell alleen-lezen.

Out of Memory Error van DuckDB op een kleine VPS betekent dat een query meer werkgeheugen nodig had dan beschikbaar was. DuckDB gebruikt indien mogelijk schijfruimte als tijdelijke opslag. Geef het daarvoor ruimte door een databasebestand op schijf te openen in plaats van :memory: en beperk het geheugengebruik met SET memory_limit = '2GB';. Op een systeem waarop andere services actief zijn, voorkomt deze limiet dat een ad-hocquery uw applicatie uit het RAM-geheugen verdringt.

Binder Error: Referenced column "amount" not found bij het opvragen van Parquet betekent vrijwel altijd dat het schema van het bestand niet overeenkomt met wat u zich herinnert. Voer DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; uit en lees de werkelijke kolomnamen uit.

Hoe u in de praktijk kiest

Vraag wat het schrijfpatroon is. Veel kleine schrijfbewerkingen die een stroomuitval moeten overleven, wijzen op SQLite. Vraag wat het leespatroon is. Volledige scans met aggregaties over een lange historie wijzen op DuckDB. De meeste echte systemen beantwoorden beide vragen met ja. De juiste aanpak is dan om elke engine het deel te laten uitvoeren waarin deze goed is, in plaats van een engine te dwingen de andere te vervangen.

Een migratie die u moet vermijden, is het verplaatsen van de actieve applicatiestatus naar DuckDB omdat een rapport traag was. Het rapport was traag door de opslagindeling. De oplossing is daarom een export en geen herschrijving van uw schrijfpad.

FAQ

Kan DuckDB SQLite vervangen als applicatiedatabase?

Niet als de database vaak schrijfbewerkingen uitvoert. DuckDB vergrendelt het volledige databasebestand voor schrijven, staat slechts één lees-schrijfproces tegelijk toe en is afgestemd op bulkbewerkingen in plaats van invoegingen van afzonderlijke rijen. Bewaar transactionele status in SQLite en laat DuckDB deze met ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); lezen wanneer een rapport dit nodig heeft.

Is DuckDB echt sneller dan SQLite voor analyses?

Voor scans en aggregaties over een grote tabel wel. De reden is de opslagindeling, niet een optimalisatietruc. DuckDB leest alleen de kolommen die een query benoemt en verwerkt waarden in batches, terwijl SQLite volledige rijen moet doorlopen om één veld te bereiken. Bij het ophalen van één rij op basis van de primaire sleutel is de verhouding omgekeerd, omdat SQLite twee pagina's aanraakt en DuckDB de opslag van elke kolom aanraakt.

Heb ik veel RAM nodig om DuckDB op een VPS uit te voeren?

Nee, maar geef het een limiet en schijfruimte. Open een databasebestand in plaats van :memory:, zodat DuckDB tussentijdse resultaten naar schijf kan wegschrijven. Stel vervolgens SET memory_limit = '2GB'; in op een waarde die uw VPS kan vrijmaken. Zonder limiet kan één grote GROUP BY Out of Memory Error verhogen of andere services uit het RAM-geheugen verdringen.

Hoe krijg ik mijn SQLite-gegevens in Parquet?

Koppel het SQLite-bestand vanuit DuckDB en kopieer een query rechtstreeks met COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Voer dit volgens een schema uit voor afgesloten perioden, zoals de rijen van vorige maand, en laat recente rijen in SQLite staan zolang de applicatie deze nog wijzigt.

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 en een gewone kopie beschadigd kan raken. Parquet-bestanden veranderen niet nadat ze zijn geschreven, dus het kopiëren van de directory volstaat.

#duckdb#sqlite#database#analytics#parquet#self-hosting