DuckDB vs SQLite pada pelayan: Perlu guna yang mana?
SQLite menyimpan keadaan transaksi, manakala DuckDB menganalisis Parquet dan CSV. Lihat sebab satu VPS biasanya menjalankan kedua-duanya, dengan contoh setiap satu.
DuckDB berbanding SQLite pada pelayan: jawapan satu ayat
SQLite ialah enjin OLTP (pemprosesan transaksi dalam talian): SQLite menyimpan data sebagai baris dan dibina untuk membaca serta menulis beberapa baris pada satu-satu masa dengan selamat dan pantas. DuckDB ialah enjin OLAP (pemprosesan analitik dalam talian): DuckDB menyimpan data sebagai lajur dan dibina untuk mengimbas jutaan baris serta mengembalikan satu hasil agregat. Kedua-duanya ialah pustaka terbenam, kedua-duanya membuka fail biasa, dan tiada satu pun menjalankan proses pelayan yang perlu anda pantau secara berterusan.
Jadi, jawapan yang jujur kepada soalan "yang mana satu" hampir selalu ialah "kedua-duanya, pada VPS yang sama". Aplikasi anda menyimpan keadaan aktifnya dalam SQLite. Pelaporan anda membaca fail Parquet dan CSV menggunakan DuckDB. Kedua-duanya tidak bersaing kerana tugas yang dilakukan adalah berbeza.
Mengapa penyimpanan berasaskan baris dan penyimpanan berasaskan lajur menghasilkan jawapan yang berbeza
SQLite menulis satu baris sebagai satu bahagian halaman yang bersebelahan. Mendapatkan satu pesanan berdasarkan kunci utamanya menyentuh satu halaman indeks dan satu halaman data, iaitu dua operasi baca. Inilah yang dilakukan oleh aplikasi beribu-ribu kali sesaat: membaca pengguna ini, mengemas kini sesi ini, dan memasukkan pesanan ini.
DuckDB menulis setiap lajur secara berasingan dan memampatkannya. Menjumlahkan amount_cents merentasi lima juta baris hanya membaca lajur amount_cents, melangkau setiap bait lain dalam fail, dan menjalankan penjumlahan menggunakan kod bervektor pada kelompok nilai. Lajur lain tidak pernah dibaca daripada cakera. Di situlah puncanya kelajuan tersebut.
Sekarang jalankan setiap enjin untuk beban kerja enjin yang satu lagi. SQLite yang menjumlahkan satu lajur perlu merentasi setiap baris dan mengambil keseluruhan baris daripada halaman untuk mencapai satu medan. Oleh itu, SQLite membaca jauh lebih banyak data daripada cakera berbanding keperluannya. DuckDB yang memasukkan satu pesanan perlu menyentuh storan setiap lajur untuk satu nilai, dan mengambil kunci tulis pada keseluruhan fail pangkalan data untuk melakukannya. Tiada satu pun enjin tersebut rosak. Setiap enjin menjawab soalan yang tidak direka bentuk untuk dikendalikannya.
Keadaan aplikasi transaksi: apabila SQLite unggul
Pilih SQLite apabila penulisan bersaiz kecil, kerap berlaku dan tidak boleh hilang. Sesi, pesanan, baris giliran, tetapan dan apa-apa sahaja yang dicipta oleh permintaan web.
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appCipta jadual dan aktifkan pengelogan write-ahead 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 output pertama ialah wal. Itu ialah PRAGMA yang melaporkan mod yang telah diaktifkannya, dan merupakan tetapan paling berguna pada pelayan. Dalam mod jurnal rollback lalai, penulis menyekat setiap pembaca. Dalam mod WAL, pembaca terus membaca keadaan terakhir yang telah dilakukan sementara seorang penulis menambah data. Oleh itu, laporan yang perlahan tidak lagi melengahkan permintaan web yang menunggunya.
Semak bahawa baris tersebut telah dipulangkan:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"Anda akan mendapat 1|ana|2026-07-30T09:14:00Z|4200. Dua fail lagi muncul di sebelah pangkalan data, iaitu app.db-wal dan app.db-shm, dan kedua-duanya merupakan sebahagian daripadanya. Menyalin app.db sahaja semasa aplikasi sedang berjalan menghasilkan sandaran yang tidak konsisten. Perkara ini diterangkan lebih lanjut di bawah.
SQLite masih hanya membenarkan seorang penulis pada satu-satu masa. Had itu ialah kunci, bukan baris gilir. Oleh itu, penulis kedua yang menunggu terlalu lama gagal dengan database is locked dan bukannya tersekat selama-lamanya. Tingkatkan tempoh menunggu dengan PRAGMA busy_timeout = 5000; pada setiap sambungan yang dibuka oleh aplikasi anda. Tempoh menunggu selama lima saat menghapuskan kebanyakan ralat ini dalam beban kerja web biasa.
Kelebihan DuckDB: analisis terhadap fail yang sudah ada
Pilih DuckDB apabila soalan bermula dengan "berapa banyak", "berapa" atau "sepuluh teratas yang mana", dan inputnya ialah himpunan fail CSV atau Parquet. Pasang klien baris perintah, versi 1.5.5 pada Julai 2026:
curl https://install.duckdb.org | shSkrip ini memasang perduaan di bawah ~/.duckdb/cli/latest/duckdb dan mencetak baris untuk menambahkannya pada PATH anda. Sahkan bahawa ia boleh dijalankan:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Bina fail yang realistik untuk ditanya. Perintah ini menulis lima juta baris pesanan ke Parquet dan memampatkannya 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 soalan analitik itu. Buka shell, aktifkan pemasa dan buat pertanyaan terus terhadap fail tanpa langkah import:
.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 nombor anda sendiri daripada .timer dan bukannya mempercayai nombor yang diterbitkan, kerana hasilnya bergantung pada cakera dan bilangan teras anda. Bentuk keseluruhan hasil itu yang penting. Tiada CREATE TABLE, tiada INSERT dan tiada langkah pemuatan: DuckDB membaca footer Parquet, menentukan ketulan lajur yang diperlukan oleh pertanyaan dan membaca ketulan itu sahaja. Seluruh direktori berfungsi dengan cara yang sama menggunakan glob, FROM '/srv/data/orders-*.parquet', dan inilah cara eksport harian selama sebulan menjadi satu pertanyaan.
Kelajuan cakera ialah had asas bagi semua ini, dan imbasan lajur ialah bacaan berjujukan yang panjang. Oleh itu, perbezaan antara NVMe dan storan SATA lama pada VPS lebih jelas di sini berbanding ketika SQLite melakukan bacaan rawak yang kecil.
Membaca pangkalan data SQLite anda daripada DuckDB
Kedua-dua enjin berhubung melalui sambungan sqlite DuckDB. Lampirkan pangkalan data aplikasi dalam mod baca sahaja supaya pertanyaan analitik tidak boleh menulis ke keadaan langsung:
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;Ini membaca baris daripada fail SQLite pada masa pertanyaan tanpa membuat salinan. Kaedah ini mudah, tetapi tidak pantas kerana data pada cakera masih dalam bentuk storan baris dan DuckDB perlu melaluinya. Gunakannya untuk eksport, bukan untuk papan pemuka yang dimuat semula setiap tiga puluh saat:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);Pernyataan itu merangkumi keseluruhan corak ini. SQLite mengurus baris langsung yang terkini. Eksport berjadual menukarkan tempoh yang telah ditutup kepada Parquet. DuckDB menjawab setiap pertanyaan yang merangkumi beberapa bulan, manakala pangkalan data aplikasi kekal kecil supaya operasi tulisnya pantas.
Jalankan eksport mengikut jadual, bukan secara manual. Pasangan service dan timer systemd sesuai untuk tujuan ini: satu unit menjalankan COPY, dan satu timer mencetuskannya setiap malam.
Menjalankan kedua-duanya pada satu VPS
Tiada apa-apa di sini yang memerlukan container atau port. Kedua-dua enjin ialah pustaka, jadi pemasangannya melibatkan pakej dan laluan fail. Jika komponen lain dalam susunan anda sudah berjalan di bawah Docker Compose pada VPS yang sama, lekapkan direktori data ke dalam container yang memerlukannya dan bukannya menambah perkhidmatan pangkalan data, kerana tiada perkhidmatan untuk ditambah.
Dua peraturan membantu memastikan susunan ini tidak bermasalah.
Berikan setiap enjin direktori sendiri: /srv/app untuk fail SQLite yang ditulis oleh aplikasi, dan /srv/data untuk fail Parquet yang dibaca oleh analitik. Jika kedua-duanya berkongsi satu direktori, tugas sandaran yang membuat snapshot salah satu daripadanya akan bersaing dengan proses yang satu lagi.
Jangan halakan dua proses kepada satu fail pangkalan data DuckDB dalam mod baca-tulis. Hanya satu proses boleh membuka fail DuckDB untuk menulis, dan proses kedua akan gagal membukanya sama sekali. Ramai pembaca boleh berjalan serentak jika setiap satunya menetapkan access_mode = 'READ_ONLY'. Perkara ini mengejutkan pengguna yang biasa dengan SQLite, kerana beberapa proses lazimnya berkongsi satu fail SQLite. Jika analitik anda hanya membaca fail Parquet, persoalan ini tidak timbul. Ini ialah satu lagi sebab untuk menyimpan keadaan tahan lama dalam SQLite.
Sandaran berbeza, dan perbezaan itu menimbulkan masalah
Pangkalan data SQLite yang sedang berjalan terdiri daripada tiga fail. Menyalin fail tersebut dengan cp semasa operasi penulisan sedang berlangsung menghasilkan fail yang boleh dibuka tetapi mengandungi data yang salah. Gunakan arahan sandaran terbina dalam enjin itu. Arahan ini mengambil petikan yang konsisten semasa aplikasi terus menulis:
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 memaparkan ok untuk salinan yang baik. Sebarang hasil lain bermaksud petikan itu perlu dibuang dan sandaran perlu diambil semula.
Fail Parquet tidak pernah berubah selepas ditulis. Oleh itu, tiada pengendalian khas diperlukan. Sandarkan direktori tersebut. Hantar kedua-dua laluan keluar dari pelayan dengan sandaran restic daripada VPS anda. Keseluruhan lapisan data itu terdiri daripada dua direktori dalam satu tugas sandaran.
Mod kegagalan dan rentetan tepat yang akan anda lihat
Error: database is locked daripada SQLite bermaksud sambungan lain memegang kunci tulis lebih lama daripada tempoh tamat masa yang dibenarkan. Ini bukan kerosakan data. Tetapkan PRAGMA busy_timeout pada setiap sambungan, kemudian cari transaksi panjang yang sepatutnya dipecahkan kepada beberapa transaksi pendek.
Error: unable to open database file selepas perubahan keizinan biasanya bermaksud proses boleh menulis fail tetapi tidak boleh menulis direktorinya. SQLite mencipta app.db-wal dan app.db-shm di sebelah pangkalan data, jadi direktori itu sendiri mesti boleh ditulis, bukan hanya fail .db.
IO Error: Could not set lock on file daripada DuckDB bermaksud proses kedua sudah membuka pangkalan data itu untuk penulisan. Tutup shell yang satu lagi atau buka shell anda dalam mod baca sahaja.
Out of Memory Error daripada DuckDB pada VPS kecil bermaksud pertanyaan memerlukan lebih banyak memori kerja daripada yang tersedia. DuckDB menggunakan cakera apabila boleh, jadi sediakan lokasi untuknya menulis data sementara dengan membuka fail pangkalan data pada cakera dan bukannya :memory:, kemudian hadkan penggunaannya dengan SET memory_limit = '2GB';. Pada pelayan yang menjalankan perkhidmatan lain, had ini menghalang pertanyaan ad hoc daripada menyebabkan aplikasi anda kehabisan RAM.
Binder Error: Referenced column "amount" not found semasa membuat pertanyaan terhadap Parquet hampir selalu bermaksud skema fail itu bukan seperti yang anda ingat. Jalankan DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; dan semak nama lajur sebenar yang dipaparkan.
Cara memilih dalam amalan
Tentukan corak penulisan. Banyak operasi tulis kecil yang mesti kekal selepas bekalan kuasa terputus bermaksud SQLite. Tentukan corak pembacaan. Imbasan penuh dengan agregat merentas sejarah yang panjang bermaksud DuckDB. Kebanyakan sistem sebenar menjawab ya kepada kedua-dua soalan. Tindakan yang betul ialah memberikan setiap enjin bahagian yang dikuasainya, bukan memaksa salah satu enjin mengendalikan tugas yang sepatutnya dikendalikan oleh enjin yang lain.
Migrasi yang perlu dielakkan ialah memindahkan keadaan aplikasi yang sedang aktif ke DuckDB kerana laporan berjalan perlahan. Laporan itu perlahan kerana susun atur storan. Oleh itu, penyelesaiannya ialah eksport, bukan menulis semula laluan penulisan anda.
FAQ
Bolehkah DuckDB menggantikan SQLite untuk pangkalan data aplikasi saya?
Tidak untuk aplikasi yang kerap melakukan penulisan. DuckDB mengunci keseluruhan fail pangkalan data untuk penulisan, membenarkan hanya satu proses baca-tulis pada satu-satu masa, dan dioptimumkan untuk perubahan pukal, bukan sisipan baris tunggal. Simpan keadaan transaksi dalam SQLite dan biarkan DuckDB membacanya dengan ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); apabila laporan memerlukannya.
Adakah DuckDB benar-benar lebih pantas daripada SQLite untuk analitik?
Ya, untuk imbasan dan agregat pada jadual besar. Sebabnya ialah susun atur storan, bukan helah penalaan. DuckDB hanya membaca lajur yang dinyatakan oleh pertanyaan dan memproses nilai dalam kelompok, manakala SQLite perlu menelusuri keseluruhan baris untuk mencapai satu medan. Untuk mendapatkan satu baris berdasarkan kunci primer, keadaannya berbalik kerana SQLite mengakses dua halaman, manakala DuckDB mengakses storan bagi setiap lajur.
Adakah saya memerlukan banyak RAM untuk menjalankan DuckDB pada VPS?
Tidak, tetapi tetapkan had dan sediakan ruang cakera. Buka fail pangkalan data dan bukannya :memory: supaya DuckDB boleh menulis hasil perantaraan ke cakera apabila perlu, kemudian tetapkan SET memory_limit = '2GB'; kepada nilai yang boleh diperuntukkan oleh VPS anda. Tanpa had, satu GROUP BY besar boleh menyebabkan Out of Memory Error atau menyebabkan perkhidmatan lain kehabisan RAM.
Bagaimanakah saya memindahkan data SQLite ke Parquet?
Sambungkan fail SQLite daripada DuckDB dan salin hasil pertanyaan terus dengan COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);. Jalankan proses ini mengikut jadual untuk tempoh yang telah ditutup, seperti baris bulan lepas, dan biarkan baris terkini dalam SQLite kerana aplikasi masih menulis padanya.
Yang manakah perlu saya sandarkan, dan bagaimana?
Kedua-duanya, dengan cara yang berbeza. Ambil petikan SQLite dengan sqlite3 app.db ".backup '/srv/backup/app.db'" dan bukannya cp kerana pangkalan data yang sedang berjalan juga merupakan -wal serta fail -shm, dan salinan biasa boleh menjadi tidak lengkap. Fail Parquet tidak berubah selepas ditulis, jadi menyalin direktori sudah memadai.