SSD Nodes Learn 8GB RAM — $66/taon
Mga Gabay Matt ConnorNi Matt Connor · Na-update 2026-08-02

DuckDB o SQLite sa Server: Bakit Pareho ang Kailangan

SQLite para sa transactional state; DuckDB para sa analytics sa Parquet at CSV. Tingnan kung bakit parehong tumatakbo sa isang VPS, kasama ang worked example.

DuckDB kumpara sa SQLite sa isang server: ang sagot sa isang pangungusap

Ang SQLite ay isang OLTP engine (online transaction processing): iniimbak nito ang data bilang mga row, at idinisenyo ito para ligtas at mabilis na magbasa at magsulat ng iilang row sa bawat pagkakataon. Ang DuckDB ay isang OLAP engine (online analytical processing): iniimbak nito ang data bilang mga column, at idinisenyo ito para mag-scan ng milyun-milyong row at magbalik ng isang aggregate. Pareho silang embedded library, parehong nagbubukas ng plain file, at wala sa kanilang dalawa ang nagpapatakbo ng server process na kailangan mong bantayan nang tuloy-tuloy.

Kaya ang tapat na sagot sa tanong na “alin” ay halos palaging “pareho, sa iisang VPS”. Panatilihin ng application mo ang kasalukuyang state nito sa SQLite. Gagamit ang reporting mo ng DuckDB para magbasa ng mga Parquet at CSV file. Hindi sila nagkukumpitensya dahil magkaiba ang kanilang gawain.

Bakit binabago ng row storage at column storage ang sagot

Isinusulat ng SQLite ang isang row bilang isang magkakadugtong na bahagi ng isang page. Kapag kumukuha ng isang order gamit ang primary key nito, isang index page at isang data page ang ina-access. Dalawang read iyon. Ito mismo ang ginagawa ng isang application nang libo-libong beses bawat segundo: basahin ang user na ito, i-update ang session na ito, at mag-insert ng order na ito.

Isinusulat ng DuckDB ang bawat column nang hiwalay at kino-compress ito. Kapag nagso-sum ng amount_cents sa limang milyong row, amount_cents column lamang ang binabasa, nilalaktawan ang bawat ibang byte sa file, at pinapadaan ang sum sa vectorised code na gumagana sa mga batch ng value. Hindi kailanman binabasa mula sa disk ang ibang mga column. Dito nagmumula ang bilis.

Ngayon, patakbuhin ang bawat engine gamit ang workload ng isa pa. Kapag nagso-sum ng isang column ang SQLite, kailangan nitong dumaan sa bawat row at kunin ang buong row mula sa page para maabot ang isang field. Kaya mas marami itong disk na binabasa kaysa sa kinakailangan. Kapag nag-iinsert ng isang order ang DuckDB, kailangan nitong i-access ang storage ng bawat column para sa isang value. Nagkakaroon din ito ng write lock sa buong database file habang ginagawa iyon. Walang engine na may sira. Ang bawat isa ay sumasagot sa tanong na hindi nito idinisenyong pangasiwaan.

Kung saan mahusay ang SQLite: transactional na state ng application

Piliin ang SQLite kapag maliit at madalas ang mga write, at hindi dapat mawala ang mga ito. Kasama rito ang sessions, orders, queue rows, settings, at anumang ginagawa ng isang web request.

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

Gawin ang table at i-enable ang write-ahead logging sa parehong hakbang.

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

Ang unang linya ng output ay wal. Iyan ang PRAGMA na nag-uulat ng mode na pinaglipatan nito, at ito ang pinakamahalagang setting sa isang server. Sa default na rollback journal mode, bina-block ng isang writer ang lahat ng reader. Sa WAL mode, patuloy na nagbabasa ang mga reader ng huling committed state habang nag-a-append ang isang writer. Kaya hindi na napipigil ng mabagal na report ang web request na nasa likod nito.

Tiyaking bumalik ang row:

sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"

Makukuha mo ang 1|ana|2026-07-30T09:14:00Z|4200. May dalawang karagdagang file na lumitaw sa tabi ng database, app.db-wal at app.db-shm, at parehong kabilang dito. Kapag kinopya mo lamang ang app.db habang tumatakbo ang application, magkakaroon ka ng hindi kumpletong backup. Tatalakayin ito sa ibaba.

Pinapayagan pa rin ng SQLite ang isang writer sa bawat pagkakataon. Ang limitasyong ito ay isang lock, hindi isang queue. Kaya kapag masyadong matagal maghintay ang ikalawang writer, mabibigo ito sa database is locked sa halip na mag-block nang tuluyan. Itaas ang wait gamit ang PRAGMA busy_timeout = 5000; sa bawat connection na binubuksan ng application mo. Sa karaniwang web workload, inaalis ng limang segundong paghihintay ang karamihan sa mga error na ito.

Saan mahusay ang DuckDB: analytics sa mga file na mayroon ka na

Piliin ang DuckDB kapag nagsisimula ang tanong sa "how many", "how much" o "which top ten", at ang input ay maraming CSV o Parquet file. I-install ang command line client, bersyon 1.5.5 noong July 2026:

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

Ini-install ng script ang binary sa ilalim ng ~/.duckdb/cli/latest/duckdb at ipinapakita ang linya para maidagdag ito sa iyong PATH. Tiyaking gumagana ito:

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

Gumawa ng makatotohanang file na ite-query. Isinusulat nito ang limang milyong row ng orders sa Parquet, na naka-compress gamit ang 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);"

Itanong ngayon ang analytical na tanong. Buksan ang shell, i-on ang timer, at direktang i-query ang file nang walang import step:

.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;

Basahin ang sarili mong resulta mula sa .timer sa halip na umasa sa inilathalang resulta, dahil nakadepende ang output sa iyong disk at bilang ng iyong cores. Ang mahalaga ay ang hugis ng proseso. Walang CREATE TABLE, walang INSERT at walang load step: binasa ng DuckDB ang Parquet footer, tinukoy kung aling mga column chunk ang kailangan ng query, at ang mga iyon lamang ang binasa nito. Gumagana rin ang isang buong directory gamit ang glob, FROM '/srv/data/orders-*.parquet', kaya nagiging isang query ang isang buwang mga daily export.

Ang bilis ng disk ang nagtatakda ng pinakamababang performance sa lahat ng ito, at ang column scan ay isang mahabang sequential read. Kaya mas malinaw dito ang agwat sa pagitan ng NVMe at mas lumang SATA storage sa isang VPS kaysa sa maliliit na random read ng SQLite.

Pagbasa sa iyong SQLite database mula sa DuckDB

Nagkakaugnay ang dalawang engine sa pamamagitan ng sqlite extension ng DuckDB. I-attach ang application database bilang read only, para hindi kailanman makapagsulat sa live state ang analytics query:

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;

Binabasa nito ang mga row mula sa SQLite file sa oras ng query nang walang ginagawang kopya. Maginhawa ito, pero hindi ito mabilis, dahil row storage pa rin ang data sa disk at kailangan itong lakarin ng DuckDB. Gamitin ito para sa export, hindi para sa dashboard na nagre-reload bawat tatlumpung segundo:

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

Ang isang statement na iyon ang buong pattern. SQLite ang namamahala sa mga kamakailang live row. Ginagawang Parquet ng nakaiskedyul na export ang mga saradong period. Sinasagot ng DuckDB ang bawat tanong na sumasaklaw sa maraming buwan, at nananatiling maliit ang application database, kaya mabilis ang mga write nito.

Patakbuhin ang export ayon sa schedule sa halip na manu-mano. Ang isang magkaparis na systemd service at timer ang tamang laki para rito: isang unit na nagpapatakbo ng COPY, at isang timer na nagpapaandar nito gabi-gabi.

Pagpapatakbo ng dalawa sa iisang VPS

Walang kailangan dito na container o port. Parehong library ang dalawang engine, kaya package at file path ang kailangan sa pag-install. Kung tumatakbo na ang iba pang bahagi ng stack mo gamit ang Docker Compose sa parehong VPS, i-mount ang data directory sa container na nangangailangan nito sa halip na magdagdag ng database service, dahil walang service na kailangang idagdag.

Dalawang panuntunan ang makatutulong upang maiwasan ang mga problema sa setup na ito.

Bigyan ang bawat engine ng sariling directory: /srv/app para sa SQLite file na sinusulatan ng application, at /srv/data para sa mga Parquet file na binabasa ng analytics. Kapag iisang directory ang ginagamit nila, maaaring magkasabay ang backup job na kumukuha ng snapshot ng isa at ang paggamit ng isa pa.

Huwag ituro ang dalawang process sa iisang DuckDB database file kapag read-write mode ang gamit. Isang process lang ang maaaring magsulat sa DuckDB file, at mabibigo nang tuluyan ang ikalawang process na buksan ito. Ayos lang ang maraming reader kapag itinatakda ng bawat isa ang access_mode = 'READ_ONLY'. Nakalilito ito para sa mga galing sa SQLite, kung saan karaniwang nagbabahagi ng isang file ang ilang process. Kung Parquet file lang ang binabasa ng analytics mo, hindi lilitaw ang problemang ito. Isa pa itong dahilan upang panatilihin sa SQLite ang persistent state.

Magkakaiba ang mga backup, at nagdudulot ng problema ang pagkakaibang ito

Ang tumatakbong SQLite database ay binubuo ng tatlong file. Kapag kinopya ang mga ito gamit ang cp habang may isinusulat, makakakuha ka ng file na nagbubukas ngunit mali ang laman. Gamitin ang sariling backup command ng engine. Gumagawa ito ng consistent snapshot habang patuloy na nagsusulat ang application:

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;"

Ipinapakita ng integrity_check ang ok kapag maayos ang kopya. Anumang ibang resulta ay nangangahulugang dapat itapon ang snapshot na iyon at gumawa ng panibago.

Hindi nagbabago ang mga Parquet file pagkatapos maisulat. Dahil dito, wala silang kinakailangang espesyal na paghawak. I-back up ang directory. Ipadala ang parehong path palabas ng server gamit ang mga restic backup mula sa iyong VPS. Ang buong data layer ay binubuo ng dalawang directory sa iisang backup job.

Mga failure mode at ang eksaktong string na makikita mo

Ang Error: database is locked mula sa SQLite ay nangangahulugang mas matagal na hinawakan ng ibang connection ang write lock kaysa sa itinakda ng iyong timeout. Hindi ito corruption. Itakda ang PRAGMA busy_timeout sa bawat connection, pagkatapos ay hanapin ang long transaction na dapat sana ay hinati sa ilang maiikling transaction.

Ang Error: unable to open database file pagkatapos magbago ng permission ay karaniwang nangangahulugang maaaring magsulat ang proseso sa file ngunit hindi sa directory nito. Lumilikha ang SQLite ng app.db-wal at app.db-shm sa tabi ng database, kaya dapat writable mismo ang directory, hindi lamang ang .db file.

Ang IO Error: Could not set lock on file mula sa DuckDB ay nangangahulugang may ibang process nang nakabukas sa database para sa pagsusulat. Isara ang ibang shell, o buksan ang iyo nang read only.

Ang Out of Memory Error mula sa DuckDB sa isang maliit na VPS ay nangangahulugang nangangailangan ang query ng mas maraming working memory kaysa sa mayroon ito. Nag-i-spill ang DuckDB sa disk kapag posible, kaya bigyan ito ng lugar para mag-spill sa pamamagitan ng pagbukas ng database file sa disk sa halip na :memory:, at limitahan ang paggamit nito gamit ang SET memory_limit = '2GB';. Sa isang box na nagpapatakbo ng iba pang service, ang limitasyong iyon ang pumipigil sa isang ad hoc query na maubusan ng RAM ang iyong application.

Ang Binder Error: Referenced column "amount" not found kapag nagku-query ng Parquet ay halos palaging nangangahulugang hindi tumutugma ang schema ng file sa iyong natatandaan. Patakbuhin ang DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; at basahin ang aktuwal na mga pangalan ng column.

Paano pumili sa aktuwal na paggamit

Alamin kung ano ang write pattern. Kung maraming maliliit na write ang kailangang manatili kahit mawalan ng kuryente, SQLite ang gamitin. Alamin din kung ano ang read pattern. Kung kailangan ng full scan at aggregate sa mahabang history, DuckDB ang gamitin. Sa karamihan ng totoong system, parehong oo ang sagot sa dalawang tanong. Ang tamang paraan ay gamitin ang bawat engine para sa bahaging mahusay nitong pangasiwaan, sa halip na pilitin ang isa na gawin ang trabaho ng isa pa.

Iwasang ilipat ang live application state sa DuckDB dahil mabagal ang isang report. Mabagal ang report dahil sa storage layout. Kaya export ang solusyon, hindi rewrite ng write path.

FAQ

Maaari bang palitan ng DuckDB ang SQLite para sa database ng aking application?

Hindi para sa database na madalas sinusulatan. Naglalagay ang DuckDB ng write lock sa buong database file, nagpapahintulot lamang ng isang read-write process sa bawat pagkakataon, at nakatutok sa maramihang pagbabago sa halip na single-row inserts. Panatilihin ang transactional state sa SQLite at ipabasa ito sa DuckDB gamit ang ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); kapag kailangan ito ng isang report.

Mas mabilis ba talaga ang DuckDB kaysa SQLite para sa analytics?

Oo, para sa mga scan at aggregate sa malaking table. Ang dahilan ay ang storage layout, hindi isang tuning trick. Binabasa lamang ng DuckDB ang mga column na tinutukoy ng query at pinoproseso ang mga value nang batch-batch, samantalang kailangang daanan ng SQLite ang buong row para maabot ang isang field. Para sa pagkuha ng isang row gamit ang primary key, nababaligtad ang resulta dahil dalawang page ang ina-access ng SQLite, samantalang ina-access ng DuckDB ang storage ng bawat column.

Kailangan ko ba ng maraming RAM para patakbuhin ang DuckDB sa isang VPS?

Hindi, pero bigyan ito ng limit at disk. Magbukas ng database file sa halip na :memory: upang mai-spill ng DuckDB sa disk ang mga intermediate result, pagkatapos ay itakda ang SET memory_limit = '2GB'; sa halagang kaya ng iyong VPS na ilaan. Kapag walang limit, maaaring tumaas ng isang malaking GROUP BY ang Out of Memory Error o mapaalis sa RAM ang ibang service.

Paano ko ililipat ang aking SQLite data sa Parquet?

I-attach ang SQLite file mula sa DuckDB at direktang i-copy palabas ang isang query gamit ang COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Patakbuhin ito ayon sa schedule para sa mga saradong period, gaya ng mga row noong nakaraang buwan, at iwan ang mga bagong row sa SQLite kung saan patuloy na nagsusulat ang application.

Alin ang dapat kong i-back up, at paano?

Pareho, pero magkaiba ang paraan. Gumawa ng SQLite snapshot gamit ang sqlite3 app.db ".backup '/srv/backup/app.db'" sa halip na cp, dahil ang tumatakbong database ay isa ring -wal at -shm file, at maaaring maputol ang isang simpleng kopya. Hindi nagbabago ang mga Parquet file kapag naisulat na, kaya sapat nang kopyahin ang directory.

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