DuckDB vs SQLite di Server: Mengapa Biasanya Keduanya
SQLite menyimpan state transaksi, sedangkan DuckDB menganalisis Parquet dan CSV. Pelajari alasan satu VPS biasanya menjalankan keduanya, lengkap dengan contoh.
DuckDB vs SQLite di server: jawaban dalam satu kalimat
SQLite adalah engine OLTP (online transaction processing): SQLite menyimpan data sebagai baris dan dirancang untuk membaca serta menulis beberapa baris sekaligus dengan aman dan cepat. DuckDB adalah engine OLAP (online analytical processing): DuckDB menyimpan data sebagai kolom dan dirancang untuk memindai jutaan baris lalu mengembalikan satu hasil agregasi. Keduanya merupakan library tersemat, keduanya membuka file biasa, dan tidak satu pun menjalankan proses server yang harus Anda pantau.
Jadi, jawaban yang jujur untuk pertanyaan "yang mana" hampir selalu adalah "keduanya, pada VPS yang sama". Aplikasi Anda menyimpan state aktifnya di SQLite. Pelaporan Anda membaca file Parquet dan CSV dengan DuckDB. Keduanya tidak saling bersaing karena tidak menjalankan tugas yang sama.
Mengapa penyimpanan berbasis baris dan berbasis kolom mengubah jawabannya
SQLite menulis satu baris sebagai bagian kontinu dalam sebuah page. Mengambil satu order berdasarkan primary key menyentuh satu index page dan satu data page, sehingga memerlukan dua pembacaan. Inilah yang dilakukan aplikasi ribuan kali per detik: membaca user ini, memperbarui session ini, dan memasukkan order ini.
DuckDB menulis setiap kolom secara terpisah dan melakukan kompresi. Menjumlahkan amount_cents pada lima juta baris hanya membaca kolom amount_cents, melewati setiap byte lain dalam file, lalu menjalankan penjumlahan melalui kode tervectorisasi pada sejumlah batch nilai. Kolom lain tidak pernah dibaca dari disk. Di situlah kecepatan tersebut berasal.
Sekarang jalankan setiap engine dengan workload milik engine lain. SQLite yang menjumlahkan sebuah kolom harus menelusuri setiap baris dan mengambil seluruh baris dari page untuk mencapai satu field. Akibatnya, SQLite membaca jauh lebih banyak data dari disk daripada yang diperlukan. DuckDB yang memasukkan satu order harus menyentuh penyimpanan setiap kolom untuk satu nilai dan mengambil write lock pada seluruh file database untuk melakukannya. Tidak ada engine yang rusak. Masing-masing menjawab pertanyaan yang tidak menjadi tujuan desainnya.
Keunggulan SQLite: state aplikasi transaksional
Pilih SQLite jika operasi tulis berukuran kecil, sering dilakukan, dan tidak boleh hilang. Gunakan SQLite untuk sesi, pesanan, baris antrean, pengaturan, atau data apa pun yang dibuat oleh permintaan web.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appBuat tabel dan aktifkan write-ahead logging dalam langkah yang sama.
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);
SQLBaris pertama output adalah wal. Nilai tersebut adalah PRAGMA yang melaporkan mode yang diaktifkan, dan merupakan pengaturan yang paling berguna pada server. Dalam mode rollback journal default, proses penulis memblokir semua proses pembaca. Dalam mode WAL, proses pembaca tetap membaca state terakhir yang telah di-commit sementara satu proses penulis menambahkan data. Dengan demikian, laporan yang lambat tidak lagi menunda permintaan web yang menunggunya.
Periksa apakah baris tersebut muncul kembali:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"Output yang diperoleh adalah 1|ana|2026-07-30T09:14:00Z|4200. Dua file lain muncul di sebelah database, yaitu app.db-wal dan app.db-shm, dan keduanya merupakan bagian dari database tersebut. Menyalin hanya app.db saat aplikasi masih berjalan menghasilkan backup yang tidak konsisten. Hal ini dibahas lebih lanjut di bawah.
SQLite tetap hanya mengizinkan satu proses penulis pada satu waktu. Batas tersebut berupa lock, bukan antrean. Karena itu, proses penulis kedua yang menunggu terlalu lama gagal dengan database is locked, bukan menunggu tanpa batas. Tingkatkan waktu tunggu menggunakan PRAGMA busy_timeout = 5000; pada setiap koneksi yang dibuka aplikasi. Waktu tunggu 5 detik biasanya menghilangkan sebagian besar error ini pada beban kerja web normal.
Keunggulan DuckDB: analitik atas file yang sudah Anda miliki
Pilih DuckDB jika pertanyaannya dimulai dengan "berapa banyak", "berapa besar", atau "sepuluh teratas yang mana", dan inputnya berupa kumpulan file CSV atau Parquet. Instal klien baris perintah, versi 1.5.5 per Juli 2026:
curl https://install.duckdb.org | shSkrip tersebut menginstal binary di ~/.duckdb/cli/latest/duckdb dan mencetak baris untuk menambahkannya ke PATH. Pastikan binary dapat dijalankan:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Buat file realistis untuk dikueri. Perintah ini menulis lima juta baris pesanan ke Parquet yang dikompresi dengan 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);"Sekarang ajukan pertanyaan analitiknya. Buka shell, aktifkan timer, lalu kueri file secara langsung tanpa langkah impor:
.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;Baca sendiri hasilnya dari .timer, bukan hanya mempercayai hasil yang dipublikasikan, karena hasilnya bergantung pada disk dan jumlah core Anda. Yang penting adalah polanya. Tidak ada CREATE TABLE, tidak ada INSERT, dan tidak ada langkah pemuatan: DuckDB membaca footer Parquet, menentukan chunk kolom yang diperlukan kueri, lalu hanya membaca chunk tersebut. Seluruh direktori dapat diproses dengan cara yang sama menggunakan glob, FROM '/srv/data/orders-*.parquet'. Dengan cara ini, ekspor harian selama satu bulan dapat diproses dalam satu kueri.
Kecepatan disk menjadi batas dasar untuk semua proses ini, dan pemindaian kolom merupakan pembacaan sekuensial yang panjang. Karena itu, perbedaan antara penyimpanan NVMe dan SATA lama pada VPS lebih terlihat di sini daripada pada pembacaan acak kecil SQLite.
Membaca database SQLite dari DuckDB
Kedua engine terhubung melalui ekstensi sqlite milik DuckDB. Lampirkan database aplikasi dalam mode hanya-baca agar kueri analitik tidak pernah menulis ke state aktif:
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;Perintah ini membaca baris dari file SQLite saat kueri dijalankan tanpa menyalin data. Cara ini praktis, tetapi tidak cepat karena data pada disk masih menggunakan penyimpanan berbasis baris dan DuckDB harus menelusurinya. Gunakan cara ini untuk ekspor, bukan untuk dashboard yang dimuat ulang setiap tiga puluh detik:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);Satu pernyataan itu sudah mencakup seluruh pola ini. SQLite menyimpan baris aktif terbaru. Ekspor terjadwal mengubah periode yang sudah ditutup menjadi Parquet. DuckDB menjawab setiap pertanyaan yang mencakup beberapa bulan, sementara database aplikasi tetap kecil sehingga proses penulisannya tetap cepat.
Jalankan ekspor sesuai jadwal, bukan secara manual. pasangan service dan timer systemd memiliki ukuran konfigurasi yang tepat untuk kebutuhan ini: satu unit yang menjalankan COPY dan satu timer yang menjalankannya setiap malam.
Menjalankan keduanya pada satu VPS
Tidak ada komponen di sini yang memerlukan container atau port. Kedua engine tersebut berupa library, sehingga instalasinya terdiri atas package dan path file. Jika stack Anda yang lain sudah berjalan menggunakan Docker Compose pada VPS yang sama, mount direktori data ke container yang memerlukannya. Jangan menambahkan service database karena tidak ada service yang perlu ditambahkan.
Dua aturan berikut mencegah masalah pada susunan ini.
Berikan direktori terpisah untuk setiap engine: /srv/app untuk file SQLite yang ditulis aplikasi, dan /srv/data untuk file Parquet yang dibaca analytics. Jika keduanya menggunakan direktori yang sama, job backup yang melakukan snapshot salah satunya dapat berjalan bersamaan dan berbenturan dengan proses lainnya.
Jangan mengarahkan dua proses ke satu file database DuckDB dalam mode read-write. Hanya satu proses yang boleh membuka file DuckDB untuk penulisan. Proses kedua akan gagal membukanya. Banyak proses pembaca dapat berjalan jika masing-masing menetapkan access_mode = 'READ_ONLY'. Hal ini sering mengejutkan pengguna yang sebelumnya menggunakan SQLite, karena beberapa proses biasanya berbagi satu file SQLite. Jika analytics Anda hanya membaca file Parquet, masalah ini tidak muncul. Ini menjadi alasan tambahan untuk menyimpan state yang persisten di SQLite.
Backup berbeda, dan perbedaannya dapat menimbulkan masalah
Database SQLite yang sedang berjalan terdiri atas tiga file. Menyalin ketiganya dengan cp saat proses penulisan berlangsung menghasilkan file yang dapat dibuka, tetapi isinya tidak benar. Gunakan perintah backup bawaan engine. Perintah ini mengambil snapshot yang konsisten saat aplikasi tetap menulis data:
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 menampilkan ok pada salinan yang baik. Jika hasilnya berbeda, hapus snapshot tersebut dan buat snapshot baru.
File Parquet tidak pernah berubah setelah ditulis. Karena itu, file tersebut tidak memerlukan penanganan khusus. Cukup backup direktorinya. Kirim kedua path ke luar server menggunakan backup restic dari VPS, sehingga seluruh lapisan data tercakup dalam dua direktori pada satu tugas backup.
Mode kegagalan dan string persis yang akan Anda lihat
Error: database is locked dari SQLite berarti koneksi lain menahan write lock lebih lama daripada batas waktu yang diizinkan. Ini bukan kerusakan data. Atur PRAGMA busy_timeout pada setiap koneksi, lalu cari transaksi panjang yang seharusnya dibagi menjadi beberapa transaksi singkat.
Error: unable to open database file setelah perubahan permission biasanya berarti proses dapat menulis file, tetapi tidak dapat menulis direktorinya. SQLite membuat app.db-wal dan app.db-shm di sebelah database, sehingga direktori itu sendiri harus dapat ditulis, bukan hanya file .db.
IO Error: Could not set lock on file dari DuckDB berarti proses kedua sudah membuka database tersebut untuk penulisan. Tutup shell lain, atau buka database dalam mode hanya-baca.
Out of Memory Error dari DuckDB pada VPS kecil berarti query memerlukan memori kerja yang lebih besar daripada yang tersedia. DuckDB menggunakan disk sebagai spill jika memungkinkan. Karena itu, sediakan lokasi spill dengan membuka file database pada disk, bukan :memory:, lalu batasi penggunaan memorinya dengan SET memory_limit = '2GB';. Pada server yang menjalankan service lain, batas ini mencegah query ad hoc menghabiskan RAM aplikasi Anda.
Binder Error: Referenced column "amount" not found saat melakukan query ke Parquet hampir selalu berarti schema file tidak sesuai dengan yang Anda ingat. Jalankan DESCRIBE SELECT * FROM '/srv/data/orders.parquet';, lalu baca kembali nama kolom yang sebenarnya.
Cara memilih dalam praktik
Tanyakan seperti apa pola penulisannya. Banyak penulisan kecil yang harus tetap tersimpan setelah listrik terputus berarti SQLite. Tanyakan seperti apa pola pembacaannya. Pemindaian penuh dengan agregasi atas histori yang panjang berarti DuckDB. Sebagian besar sistem nyata menjawab ya untuk kedua pertanyaan tersebut. Respons yang tepat adalah memberikan kepada setiap engine bagian yang memang ditanganinya dengan baik, bukan memaksa salah satunya menangani fungsi yang lebih sesuai untuk engine lain.
Migrasi yang harus dihindari adalah memindahkan state aplikasi yang sedang berjalan ke DuckDB karena sebuah laporan berjalan lambat. Laporan tersebut lambat karena tata letak penyimpanan. Jadi, solusinya adalah melakukan ekspor, bukan menulis ulang jalur penulisan Anda.
FAQ
Bisakah DuckDB menggantikan SQLite sebagai database aplikasi saya?
Tidak untuk aplikasi yang sering melakukan penulisan. DuckDB mengunci seluruh file database untuk penulisan, hanya mengizinkan satu proses baca-tulis pada satu waktu, dan dioptimalkan untuk perubahan massal, bukan penyisipan satu baris. Simpan status transaksional di SQLite dan biarkan DuckDB membacanya dengan ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); saat laporan memerlukannya.
Apakah DuckDB benar-benar lebih cepat daripada SQLite untuk analitik?
Untuk pemindaian dan agregasi pada tabel besar, ya. Penyebabnya adalah tata letak penyimpanan, bukan trik penyetelan. DuckDB hanya membaca kolom yang disebutkan oleh kueri dan memproses nilai dalam batch, sedangkan SQLite harus menelusuri seluruh baris untuk mencapai satu field. Untuk mengambil satu baris berdasarkan primary key, hasilnya berbalik karena SQLite hanya mengakses dua halaman, sedangkan DuckDB mengakses penyimpanan setiap kolom.
Apakah saya memerlukan banyak RAM untuk menjalankan DuckDB pada VPS?
Tidak, tetapi tetapkan batas dan sediakan disk. Buka file database, bukan :memory:, agar DuckDB dapat menumpahkan hasil perantara ke disk. Kemudian, tetapkan SET memory_limit = '2GB'; ke nilai yang dapat disediakan oleh VPS Anda. Tanpa batas, satu GROUP BY berukuran besar dapat meningkatkan Out of Memory Error atau mengeluarkan service lain dari RAM.
Bagaimana cara memindahkan data SQLite ke Parquet?
Lampirkan file SQLite dari DuckDB, lalu salin hasil kueri secara langsung dengan COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Jalankan proses ini secara terjadwal untuk periode yang sudah ditutup, seperti baris dari bulan lalu. Biarkan baris terbaru tetap berada di SQLite karena aplikasi masih menulis data ke sana.
Mana yang harus saya cadangkan, dan bagaimana caranya?
Keduanya, dengan cara yang berbeda. Buat snapshot SQLite menggunakan sqlite3 app.db ".backup '/srv/backup/app.db'", bukan cp, karena database yang sedang berjalan juga merupakan -wal dan file -shm, sehingga salinan biasa dapat menjadi tidak konsisten. File Parquet tidak pernah berubah setelah ditulis, jadi cukup salin direktorinya.