সার্ভারে DuckDB বনাম SQLite: কোনটি কখন ব্যবহার করবেন?
SQLite এবং DuckDB-এর মূল পার্থক্য জানুন। SQLite ট্রানজ্যাকশনাল ডেটার জন্য সেরা, আর DuckDB বিশাল Parquet বা CSV ফাইল বিশ্লেষণের জন্য কার্যকর। কেন একটি VPS-এ দুটিই প্রয়োজন।
সার্ভারে DuckDB বনাম SQLite: এক বাক্যের উত্তর
SQLite একটি OLTP ইঞ্জিন (online transaction processing): এটি ডেটা সারি হিসেবে সংরক্ষণ করে এবং একবারে অল্প কয়েকটি সারি নিরাপদে ও দ্রুত পড়া এবং লেখার জন্য তৈরি। DuckDB একটি OLAP ইঞ্জিন (online analytical processing): এটি ডেটা কলাম হিসেবে সংরক্ষণ করে এবং লক্ষ লক্ষ সারি স্ক্যান করে একটি aggregate ফল ফেরত দেওয়ার জন্য তৈরি। দুটিই embedded library, দুটিই একটি সাধারণ ফাইল খোলে, এবং কোনোটিই এমন server process চালায় না যেটির নিয়মিত তদারকি করতে হবে।
তাই “কোনটি” প্রশ্নের সৎ উত্তর প্রায় সব সময়ই “একই VPS-এ দুটিই”। আপনার application-এর live state SQLite-এ থাকে। আপনার reporting DuckDB দিয়ে Parquet এবং CSV ফাইল পড়ে। তারা প্রতিদ্বন্দ্বী নয়, কারণ তারা একই কাজ করে না।
সারি-ভিত্তিক স্টোরেজ এবং কলাম-ভিত্তিক স্টোরেজ কেন ফলাফল বদলে দেয়
SQLite একটি পৃষ্ঠায় একটি সারিকে একটানা অংশ হিসেবে লেখে। প্রাথমিক কী ব্যবহার করে একটি অর্ডার আনতে একটি ইনডেক্স পৃষ্ঠা এবং একটি ডেটা পৃষ্ঠা অ্যাক্সেস করতে হয়, অর্থাৎ 2টি রিড। একটি অ্যাপ্লিকেশন প্রতি সেকেন্ডে হাজার হাজার বার ঠিক এই কাজই করে: এই ব্যবহারকারীকে পড়া, এই সেশন আপডেট করা, এই অর্ডার সন্নিবেশ করা।
DuckDB প্রতিটি কলাম আলাদাভাবে লেখে এবং সেটি কমপ্রেস করে। পাঁচ মিলিয়ন সারি জুড়ে amount_cents-এর যোগফল বের করার সময় এটি শুধু amount_cents কলাম পড়ে, ফাইলের অন্য প্রতিটি বাইট এড়িয়ে যায় এবং মানগুলোর ব্যাচের ওপর ভেক্টরাইজড কোড ব্যবহার করে যোগফল গণনা করে। অন্য কলামগুলো কখনোই ডিস্ক থেকে পড়া হয় না। গতি মূলত এখান থেকেই আসে।
এখন প্রতিটি ইঞ্জিনকে অন্য ইঞ্জিনের কাজের ধরন দিয়ে চালান। SQLite-তে একটি কলামের যোগফল বের করতে হলে প্রতিটি সারি অতিক্রম করতে হয় এবং একটি ক্ষেত্র পর্যন্ত পৌঁছানোর জন্য পৃষ্ঠা থেকে পুরো সারিটি আনতে হয়। ফলে প্রয়োজনের তুলনায় অনেক বেশি ডিস্ক পড়তে হয়। DuckDB-তে একটি অর্ডার সন্নিবেশ করতে একটি মানের জন্য প্রতিটি কলামের স্টোরেজে অ্যাক্সেস করতে হয় এবং কাজটি সম্পন্ন করতে পুরো ডেটাবেস ফাইলে write lock নিতে হয়। কোনো ইঞ্জিনই ত্রুটিপূর্ণ নয়। প্রতিটি ইঞ্জিন এমন একটি প্রশ্নের উত্তর দিচ্ছে, যার জন্য সেটিকে তৈরি করা হয়নি।
যেখানে SQLite কার্যকর: লেনদেনভিত্তিক অ্যাপ্লিকেশন স্টেট
যখন write অপারেশন ছোট, ঘন ঘন হয় এবং হারানো যাবে না, তখন SQLite নির্বাচন করুন। Sessions, orders, queue rows, settings—ওয়েব request যা কিছু তৈরি করে।
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appএকই ধাপে table তৈরি করুন এবং write-ahead logging চালু করুন।
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আউটপুটের প্রথম লাইন হলো wal। এটি PRAGMA, যা mode পরিবর্তনের পরের অবস্থা জানায়, এবং server-এ এটিই সবচেয়ে কার্যকর setting। Default rollback journal mode-এ একজন writer সব reader-কে block করে। WAL mode-এ একজন writer append করার সময় reader-রা সর্বশেষ committed state পড়তে থাকে। তাই ধীর report-এর কারণে তার পেছনে থাকা web request আর আটকে থাকে না।
Row-টি ফেরত এসেছে কি না যাচাই করুন:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"আপনি 1|ana|2026-07-30T09:14:00Z|4200 পাবেন। Database-এর পাশে আরও দুটি file তৈরি হয়েছে: app.db-wal এবং app.db-shm। দুটিই database-এর অংশ। Application চলার সময় শুধু app.db copy করলে backup অসম্পূর্ণ হবে। এ বিষয়ে নিচে আরও বলা হয়েছে।
SQLite-তে এক সময়ে এখনও মাত্র একজন writer কাজ করতে পারে। এই সীমাটি একটি lock, queue নয়। তাই দ্বিতীয় writer খুব বেশি সময় অপেক্ষা করলে অনন্তকাল block না হয়ে database is locked দিয়ে ব্যর্থ হয়। Application যে প্রতিটি connection খোলে, তাতে PRAGMA busy_timeout = 5000; দিয়ে অপেক্ষার সময় বাড়ান। স্বাভাবিক web workload-এ পাঁচ সেকেন্ড অপেক্ষা করলে এই error-গুলোর বেশিরভাগ দূর হয়।
DuckDB যেখানে কার্যকর: আপনার কাছে থাকা ফাইলের ওপর analytics
প্রশ্নটি যদি “কতগুলো”, “কত” বা “শীর্ষ দশটি কোনগুলো”—এভাবে শুরু হয় এবং input হিসেবে একগুচ্ছ CSV বা Parquet file থাকে, তাহলে DuckDB বেছে নিন। 27 October 2026 অনুযায়ী command line client, version 2.0.0 (একটি LTS release), install করুন:
curl https://install.duckdb.org | shআপনি কোনো version নির্দিষ্ট করে না দিলে script-টি latest stable release install করে। যেমন, আগের release-এ থাকতে curl https://install.duckdb.org | DUCKDB_VERSION=1.5.6 sh pin করতে পারেন। এটি binary-টি ~/.duckdb/cli/latest/duckdb-এর অধীনে install করে এবং আপনার PATH-এ সেটি যোগ করার line-টি print করে। এটি চলছে কি না নিশ্চিত করুন:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"Query করার জন্য বাস্তবসম্মত একটি file তৈরি করুন। এটি zstd compression ব্যবহার করে Parquet-এ orders-এর পাঁচ million row লেখে:
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);"এবার analytical প্রশ্নটি করুন। Shell খুলুন, timer চালু করুন এবং কোনো import step ছাড়াই সরাসরি file-টি query করুন:
.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;প্রকাশিত কোনো সংখ্যার ওপর নির্ভর না করে .timer থেকে নিজের ফলাফল পড়ুন, কারণ ফলাফল আপনার disk এবং core count-এর ওপর নির্ভর করে। ফলাফলের ধরনটিই গুরুত্বপূর্ণ। কোনো CREATE TABLE, কোনো INSERT এবং কোনো load step ছিল না: DuckDB Parquet footer পড়েছে, query-টির কোন column chunk প্রয়োজন তা নির্ধারণ করেছে এবং শুধু সেগুলোই পড়েছে। Glob, FROM '/srv/data/orders-*.parquet' ব্যবহার করলে পুরো directory-ও একইভাবে কাজ করে। এভাবেই এক মাসের daily export একটি query-তে পরিণত হয়।
এগুলোর সবকিছুর ভিত্তি হলো disk speed। Column scan হলো দীর্ঘ sequential read। তাই VPS-এ NVMe এবং পুরোনো SATA storage-এর পার্থক্য এখানে SQLite-এর ছোট random read-এর তুলনায় বেশি স্পষ্ট দেখা যায়।
DuckDB থেকে আপনার SQLite ডেটাবেস পড়া
দুটি ইঞ্জিন DuckDB-এর sqlite extension-এর মাধ্যমে একসঙ্গে কাজ করে। Application database-টি read only হিসেবে attach করুন, যাতে কোনো analytics query কখনও live state-এ লিখতে না পারে:
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;এটি কোনো copy তৈরি না করে query চলার সময় SQLite file থেকে rows পড়ে। এটি সুবিধাজনক, তবে দ্রুত নয়, কারণ disk-এ থাকা data এখনও row storage হিসেবে সংরক্ষিত এবং DuckDB-কে তা একে একে পড়তে হয়। এটি export-এর জন্য ব্যবহার করুন, প্রতি ত্রিশ সেকেন্ডে reload হওয়া dashboard-এর জন্য নয়:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);এই একটি statement-ই সম্পূর্ণ pattern। SQLite সাম্প্রতিক live rows-এর মালিক থাকে। একটি scheduled export বন্ধ হওয়া period-গুলোকে Parquet-এ রূপান্তর করে। DuckDB মাসজুড়ে থাকা data নিয়ে প্রতিটি প্রশ্নের উত্তর দেয়, আর application database ছোট থাকে, ফলে এর writes দ্রুত হয়।
Export-টি হাতে না চালিয়ে schedule অনুযায়ী চালান। একটি systemd service এবং timer জোড়া এই কাজের জন্য উপযুক্ত: একটি unit COPY চালাবে, আর একটি timer প্রতি রাতে সেটি চালাবে।
একই VPS-এ উভয়টি চালানো
এখানে কোনো container বা port প্রয়োজন নেই। উভয় engine library, তাই install বলতে package এবং file path বোঝায়। আপনার stack-এর বাকি অংশ যদি ইতিমধ্যে একই VPS-এ Docker Compose-এর অধীনে চলে, database service যোগ না করে যে container-এর এটি প্রয়োজন, তার মধ্যে data directory mount করুন। কারণ যোগ করার মতো কোনো service নেই।
এই বিন্যাসে সমস্যা এড়াতে দুটি নিয়ম অনুসরণ করুন।
প্রতিটি engine-এর জন্য আলাদা directory দিন: application যে SQLite file-এ লেখে তার জন্য /srv/app, analytics যে Parquet files পড়ে তার জন্য /srv/data। তারা একই directory ব্যবহার করলে, একটি snapshot নেওয়ার সময় backup job অন্যটির সঙ্গে প্রতিযোগিতায় জড়িয়ে পড়ে।
দুটি process-কে একই DuckDB database file read-write mode-এ নির্দেশ করবেন না। DuckDB file-এ লেখার জন্য এক সময়ে কেবল একটি process থাকতে পারে। দ্বিতীয়টি file-টি একেবারেই open করতে ব্যর্থ হয়। প্রতিটি process access_mode = 'READ_ONLY' সেট করলে অনেক reader ব্যবহার করা যায়। SQLite থেকে আসা ব্যবহারকারীদের কাছে এটি আশ্চর্যজনক হতে পারে, কারণ SQLite-এ একাধিক process নিয়মিতভাবে একই file ব্যবহার করে। আপনার analytics যদি শুধু Parquet files পড়ে, তাহলে এই সমস্যা ওঠে না। Durable state SQLite-এ রাখার এটি আরও একটি কারণ।
ব্যাকআপের মধ্যে পার্থক্য থাকে, এবং সেই পার্থক্য সমস্যা তৈরি করে
চলমান SQLite database তিনটি file নিয়ে গঠিত। লেখার মাঝখানে cp দিয়ে সেগুলো copy করলে এমন একটি file পাওয়া যায়, যা খোলে কিন্তু সঠিক নয়। Engine-এর নিজস্ব backup command ব্যবহার করুন। এটি application লিখতে থাকা অবস্থায় একটি সামঞ্জস্যপূর্ণ snapshot নেয়:
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;"সঠিক copy হলে integrity_check, ok প্রিন্ট করে। অন্য যেকোনো ফলাফলের অর্থ হলো সেই snapshot বাতিল করে আরেকটি নিতে হবে।
Parquet file লেখার পর আর পরিবর্তিত হয় না। তাই এগুলোর জন্য বিশেষ ব্যবস্থাপনার প্রয়োজন নেই। Directory-টি backup করুন। আপনার VPS থেকে restic backup ব্যবহার করে দুটি path-ই server-এর বাইরে পাঠান। এতে পুরো data layer একটি backup job-এ দুটি directory হিসেবে সংরক্ষিত হবে।
ব্যর্থতার ধরন এবং আপনি যে নির্দিষ্ট string দেখতে পাবেন
SQLite থেকে আসা database is locked—তা আপনার application-এর driver-এর মাধ্যমে আসুক বা sqlite3 shell-এর মাধ্যমে—এর অর্থ হলো অন্য একটি connection আপনার timeout-এর অনুমতির চেয়ে বেশি সময় write lock ধরে রেখেছিল। এটি corruption নয়। প্রতিটি connection-এ PRAGMA busy_timeout সেট করুন। এরপর এমন কোনো দীর্ঘ transaction খুঁজুন, যেটি কয়েকটি ছোট transaction হওয়া উচিত ছিল।
Permission পরিবর্তনের পরে unable to open database file দেখা গেলে সাধারণত এর অর্থ process-টি file-এ লিখতে পারে, কিন্তু এর directory-তে লিখতে পারে না। SQLite database-এর পাশে app.db-wal এবং app.db-shm তৈরি করে। তাই শুধু .db file নয়, directory-টিও writable হতে হবে।
DuckDB থেকে IO Error: Could not set lock on file দেখা গেলে এর অর্থ হলো দ্বিতীয় একটি process ইতিমধ্যে database-টি writing-এর জন্য open করে রেখেছে। অন্য shell-টি বন্ধ করুন, অথবা নিজেরটি read only হিসেবে open করুন।
ছোট VPS-এ DuckDB থেকে Out of Memory Error দেখা গেলে এর অর্থ হলো query-টির প্রয়োজনীয় working memory উপলব্ধ memory-এর চেয়ে বেশি ছিল। DuckDB defaultভাবে intermediate result disk-এ spill করে। এর মধ্যে :memory: session-ও অন্তর্ভুক্ত। তাই database file open করলেই spilling কাজ করবে—এমন নয়। এটি database file-এর পাশে .tmp directory-তে temporary file লেখে। In-memory session-এর ক্ষেত্রে আপনি যেখান থেকে এটি চালু করেছেন, তার relative .tmp directory ব্যবহার করে। তাই free disk-সহ writable কোনো জায়গা থেকে এটি চালু করুন, অথবা SET temp_directory = '/var/tmp/duckdb'; দিয়ে অন্য জায়গা নির্দিষ্ট করুন। গুরুত্বপূর্ণ setting হলো cap: SET memory_limit = '2GB';। Default হলো machine-এর RAM-এর 80%। তাই একই machine-এ অন্য service চললে এই limit-ই একটি ad hoc query-কে আপনার application-কে RAM-এর বাইরে ঠেলে দেওয়া থেকে আটকায়। string_agg()-এর মতো কিছু aggregate spill করতে পারে না। কিছু sorting-based aggregate-ও spill করতে পারে না। তাই এগুলোর ওপর তৈরি query-তে এখনও out of memory হতে পারে।
Parquet query করার সময় Binder Error: Referenced column "amount" not found দেখা গেলে প্রায় সব ক্ষেত্রেই এর অর্থ file-এর schema আপনার মনে থাকা schema-র সঙ্গে মিলছে না। DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; চালিয়ে প্রকৃত column name-গুলো পড়ে নিন।
বাস্তবে কীভাবে নির্বাচন করবেন
রাইট প্যাটার্ন কী, তা নির্ধারণ করুন। বিদ্যুৎ বিচ্ছিন্ন হলেও টিকে থাকতে হবে—এমন অনেক ছোট রাইট থাকলে SQLite ব্যবহার করুন। রিড প্যাটার্ন কী, তা নির্ধারণ করুন। দীর্ঘ সময়ের ইতিহাসের ওপর aggregate-সহ full scan প্রয়োজন হলে DuckDB ব্যবহার করুন। অধিকাংশ বাস্তব সিস্টেমে উভয় প্রশ্নের উত্তরই হ্যাঁ হয়। সেক্ষেত্রে একটি engine-কে অন্যটির কাজ করাতে বাধ্য না করে, প্রতিটি engine-কে তার উপযোগী অর্ধেক কাজ দিন।
যে migration এড়ানো উচিত, তা হলো কোনো report ধীর হওয়ায় live application state DuckDB-তে স্থানান্তর করা। Report ধীর ছিল storage layout-এর কারণে। তাই সমাধান হলো export, আপনার write path নতুন করে লেখার কাজ নয়।
FAQ
আমার অ্যাপ্লিকেশনের ডাটাবেসের জন্য SQLite-এর বদলে কি DuckDB ব্যবহার করা যাবে?
যদি ঘনঘন ডাটা রাইট করতে হয়, তবে না। DuckDB পুরো ডাটাবেস ফাইলের ওপর একটি রাইট লক (write lock) ধরে রাখে, যা একবারে কেবল একটি রিড-রাইট প্রসেসকে অনুমতি দেয়। এটি সিঙ্গেল-রো ইনসার্টের চেয়ে বাল্ক বা বড় আকারের পরিবর্তনের জন্য বেশি উপযোগী। ট্রানজ্যাকশনাল স্টেট SQLite-এ রাখুন এবং যখন কোনো রিপোর্টের প্রয়োজন হয়, তখন ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); ব্যবহার করে DuckDB দিয়ে তা রিড করুন।
অ্যানালিটিক্সের জন্য DuckDB কি সত্যিই SQLite-এর চেয়ে দ্রুত?
বড় টেবিলের ওপর স্ক্যান এবং অ্যাগ্রিগেট করার ক্ষেত্রে হ্যাঁ, এবং এর কারণ কোনো টিউনিং ট্রিক নয়, বরং এর স্টোরেজ লেআউট। DuckDB শুধুমাত্র কুয়েরিতে উল্লিখিত কলামগুলো রিড করে এবং ব্যাচ আকারে ভ্যালু প্রসেস করে, যেখানে SQLite-কে একটি ফিল্ডে পৌঁছাতে পুরো রো বা সারি ঘুরে আসতে হয়। প্রাইমারি কি (primary key) ব্যবহার করে একটি সিঙ্গেল রো খোঁজার ক্ষেত্রে বিষয়টি উল্টো হয়, কারণ SQLite মাত্র দুটি পেজ স্পর্শ করে আর DuckDB প্রতিটি কলামের স্টোরেজ স্পর্শ করে।
VPS-এ DuckDB চালানোর জন্য কি অনেক RAM প্রয়োজন?
না, তবে এটিকে একটি সীমা নির্ধারণ করে দিন এবং ডিস্কে স্পিল (spill) করার সুযোগ রাখুন। DuckDB ডিফল্টভাবেই ইন্টারমিডিয়েট রেজাল্ট ডিস্কে স্পিল করে, এমনকি :memory: সেশনেও, যদি এর টেম্পোরারি ডিরেক্টরি রাইটেবল হয় এবং সেখানে পর্যাপ্ত জায়গা থাকে। এরপর SET memory_limit = '2GB';-কে এমন একটি মানে সেট করুন যা আপনার VPS-এর জন্য সাশ্রয়ী। ডিফল্ট হিসেবে RAM-এর 80% রেখে দিলে, একটি বড় GROUP BY কুয়েরি Out of Memory Error বাড়িয়ে দিতে পারে অথবা অন্যান্য সার্ভিসকে RAM থেকে বের করে দিতে পারে।
আমি কীভাবে আমার SQLite ডাটা Parquet ফরম্যাটে পাব?
DuckDB থেকে SQLite ফাইলটি অ্যাটাচ করুন এবং COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd); ব্যবহার করে সরাসরি কুয়েরি কপি করুন। নির্দিষ্ট সময়ের ব্যবধানে এটি চালান (যেমন গত মাসের রো-গুলোর জন্য) এবং সাম্প্রতিক রো-গুলো SQLite-এই রেখে দিন, যেখানে অ্যাপ্লিকেশন নিয়মিত ডাটা রাইট করছে।
কোনটির ব্যাকআপ নেওয়া উচিত এবং কীভাবে?
উভয়টিরই ব্যাকআপ নেওয়া উচিত, তবে ভিন্ন ভিন্ন উপায়ে। cp-এর পরিবর্তে sqlite3 app.db ".backup '/srv/backup/app.db'" ব্যবহার করে SQLite-এর স্ন্যাপশট নিন, কারণ একটি চলমান ডাটাবেস একই সাথে একটি -wal এবং একটি -shm ফাইল, তাই সাধারণ কপি করলে ফাইলটি ক্ষতিগ্রস্ত হতে পারে। Parquet ফাইল একবার রাইট করার পর আর পরিবর্তিত হয় না, তাই ডিরেক্টরি কপি করাই যথেষ্ট।