سرور پر DuckDB بمقابلہ SQLite: کیا دونوں چلائیں؟
SQLite فعال transactional state سنبھالتا ہے، جبکہ DuckDB Parquet اور CSV پر analytics چلاتا ہے۔ ایک VPS پر دونوں کیوں اور عملی مثالیں دیکھیں۔
سرور پر DuckDB بمقابلہ SQLite: ایک جملے میں جواب
SQLite ایک OLTP انجن (online transaction processing) ہے: یہ ڈیٹا کو قطاروں کی صورت میں ذخیرہ کرتا ہے اور ایک وقت میں چند قطاریں محفوظ طریقے سے اور تیزی سے پڑھنے اور لکھنے کے لیے بنایا گیا ہے۔ DuckDB ایک OLAP انجن (online analytical processing) ہے: یہ ڈیٹا کو کالموں کی صورت میں ذخیرہ کرتا ہے اور لاکھوں قطاروں کو اسکین کرکے ایک مجموعی نتیجہ واپس کرنے کے لیے بنایا گیا ہے۔ دونوں embedded libraries ہیں، دونوں ایک سادہ فائل کھولتی ہیں، اور کسی بھی پروسیس کو سرور کے طور پر نہیں چلاتیں جس کی مسلسل نگرانی کرنا ضروری ہو۔
اس لیے "کون سا" سوال کا دیانت دار جواب تقریباً ہمیشہ "دونوں، ایک ہی VPS پر" ہوتا ہے۔ آپ کی application اپنی فعال حالت SQLite میں برقرار رکھتی ہے۔ آپ کی reporting Parquet اور CSV فائلیں DuckDB کے ذریعے پڑھتی ہے۔ دونوں ایک دوسرے کا متبادل نہیں ہیں کیونکہ دونوں ایک ہی کام نہیں کرتیں۔
قطاروں کے ذخیرے اور کالموں کے ذخیرے جواب کیوں بدل دیتے ہیں
SQLite ایک قطار کو صفحے کے ایک مسلسل حصے کے طور پر لکھتا ہے۔ بنیادی کلید کے ذریعے ایک order حاصل کرنے کے لیے ایک index page اور ایک data page تک رسائی درکار ہوتی ہے، یعنی دو reads۔ یہی کام application ہر سیکنڈ میں ہزاروں بار کرتی ہے: اس user کو پڑھنا، اس session کو update کرنا، اور یہ order insert کرنا۔
DuckDB ہر column کو الگ لکھتا اور اسے compress کرتا ہے۔ پانچ ملین rows پر amount_cents کا مجموعہ نکالتے وقت صرف amount_cents column پڑھا جاتا ہے، file کے ہر دوسرے byte کو چھوڑ دیا جاتا ہے، اور values کے batches پر vectorised code کے ذریعے مجموعہ نکالا جاتا ہے۔ دوسرے columns کو disk سے کبھی نہیں پڑھا جاتا۔ رفتار کی وجہ یہی ہے۔
اب ہر engine کو دوسرے engine کے workload پر چلائیں۔ SQLite کے لیے کسی column کا مجموعہ نکالتے وقت ہر row کو دیکھنا پڑتا ہے، اور ایک field تک پہنچنے کے لیے پوری row کو page سے پڑھنا پڑتا ہے۔ اس لیے جتنی disk درکار ہے، اس سے کہیں زیادہ disk پڑھی جاتی ہے۔ DuckDB میں ایک order insert کرنے کے لیے ایک value کے لیے ہر column کے storage تک رسائی درکار ہوتی ہے، اور اس عمل کے لیے پوری database file پر write lock لیا جاتا ہے۔ کوئی بھی engine خراب نہیں ہے۔ ہر engine ایسے سوال کا جواب دے رہا ہے جس کے لیے اسے ڈیزائن نہیں کیا گیا۔
جہاں SQLite مؤثر ہے: لین دین پر مبنی ایپلیکیشن اسٹیٹ
SQLite اس وقت منتخب کریں جب لکھنے کے عمل مختصر اور بار بار ہوں، اور ان کا ضائع نہ ہونا ضروری ہو۔ سیشنز، آرڈرز، قطار کی قطاریں، ترتیبات، اور ویب درخواست سے بننے والی ہر چیز۔
sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/appٹیبل بنائیں اور اسی مرحلے میں 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 ہے، جو اس موڈ کی اطلاع دیتا ہے جس میں تبدیلی کی گئی ہے، اور سرور پر یہ سب سے مفید ترتیب ہے۔ پہلے سے طے شدہ rollback journal موڈ میں ایک writer ہر reader کو روک دیتا ہے۔ WAL موڈ میں readers آخری committed state پڑھتے رہتے ہیں، جبکہ ایک writer نئی معلومات شامل کرتا ہے۔ اس لیے ایک سست رپورٹ اس کے پیچھے موجود ویب درخواست کو مزید نہیں روکتی۔
تصدیق کریں کہ row واپس آئی ہے:
sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"آپ کو 1|ana|2026-07-30T09:14:00Z|4200 ملے گا۔ ڈیٹابیس کے ساتھ مزید دو فائلیں، app.db-wal اور app.db-shm، ظاہر ہوں گی، اور دونوں اسی ڈیٹابیس سے متعلق ہیں۔ ایپلیکیشن کے چلتے ہوئے صرف app.db کو copy کرنے سے backup نامکمل ہو جاتا ہے۔ اس کی وضاحت آگے کی گئی ہے۔
SQLite اب بھی ایک وقت میں صرف ایک writer کی اجازت دیتا ہے۔ یہ حد lock ہے، queue نہیں۔ اس لیے دوسرا writer بہت دیر انتظار کرنے کے بعد database is locked کے ساتھ ناکام ہو جاتا ہے، ہمیشہ کے لیے بلاک نہیں رہتا۔ ایپلیکیشن کے کھولے گئے ہر connection پر PRAGMA busy_timeout = 5000; کے ذریعے انتظار کا وقت بڑھائیں۔ عام ویب workload میں پانچ سیکنڈ کا انتظار ان میں سے زیادہ تر errors ختم کر دیتا ہے۔
جہاں DuckDB کامیاب ہے: پہلے سے موجود فائلوں پر تجزیات
جب سوال "کتنے"، "کتنی مقدار" یا "سب سے اوپر کے دس کون سے ہیں" سے شروع ہو، اور ان پٹ CSV یا Parquet فائلوں کا مجموعہ ہو، تو DuckDB منتخب کریں۔ کمانڈ لائن کلائنٹ انسٹال کریں؛ July 2026 تک ورژن 1.5.5 ہے:
curl https://install.duckdb.org | shیہ اسکرپٹ بائنری کو ~/.duckdb/cli/latest/duckdb کے تحت انسٹال کرتا ہے اور وہ سطر دکھاتا ہے جو اسے آپ کے PATH میں شامل کرتی ہے۔ تصدیق کریں کہ یہ چل رہا ہے:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"استفسار کے لیے ایک حقیقت پسندانہ فائل بنائیں۔ یہ zstd سے کمپریس شدہ Parquet فائل میں آرڈرز کی 5 million قطاریں لکھتا ہے:
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);"اب تجزیاتی سوال پوچھیں۔ shell کھولیں، timer فعال کریں، اور کسی 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;اپنی قدر .timer سے حاصل کریں، شائع شدہ قدر پر انحصار نہ کریں، کیونکہ نتیجہ آپ کی disk اور core count پر منحصر ہوتا ہے۔ اہم بات نتیجے کی شکل ہے۔ یہاں کوئی CREATE TABLE، کوئی INSERT اور کوئی load step نہیں تھا: DuckDB نے Parquet footer پڑھا، معلوم کیا کہ استفسار کو کون سے column chunks درکار ہیں، اور صرف وہی chunks پڑھے۔ glob، FROM '/srv/data/orders-*.parquet'، کے ذریعے پوری directory بھی اسی طرح کام کرتی ہے۔ اسی طرح ایک ماہ کے روزانہ exports ایک ہی استفسار بن جاتے ہیں۔
Disk speed اس پورے عمل کی بنیادی حد ہے، اور column scan ایک طویل sequential read ہوتا ہے۔ اسی لیے VPS پر NVMe اور پرانے SATA storage کے درمیان فرق یہاں SQLite کی چھوٹی random reads کے مقابلے میں زیادہ واضح دکھائی دیتا ہے۔
DuckDB سے اپنی SQLite database پڑھنا
دونوں engines، DuckDB کی sqlite extension کے ذریعے ایک دوسرے سے جڑتے ہیں۔ Application database کو صرف پڑھنے کے لیے 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;یہ query کے وقت SQLite file سے rows پڑھتا ہے اور کوئی copy نہیں بناتا۔ یہ آسان ہے، لیکن تیز نہیں، کیونکہ disk پر موجود data اب بھی row storage میں ہے اور DuckDB کو اسے مکمل طور پر پڑھنا پڑتا ہے۔ اسے export کے لیے استعمال کریں، ایسے dashboard کے لیے نہیں جو ہر تیس سیکنڈ بعد reload ہوتا ہو:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);یہی مکمل pattern ہے۔ SQLite حالیہ live rows کا انتظام کرتا ہے۔ Scheduled export بند ادوار کو Parquet میں تبدیل کرتا ہے۔ DuckDB ان تمام سوالات کے جواب دیتا ہے جو کئی ماہ کے data پر محیط ہوں، جبکہ application database چھوٹی رہتی ہے، جس سے اس کی writes تیز رہتی ہیں۔
Export کو ہاتھ سے چلانے کے بجائے schedule پر چلائیں۔ systemd service اور timer کا جوڑا اس کام کے لیے مناسب ہے: ایک unit جو COPY چلاتی ہے، اور ایک timer جو اسے ہر رات چلاتا ہے۔
ایک VPS پر دونوں چلانا
یہاں کسی container یا port کی ضرورت نہیں۔ دونوں engines libraries ہیں، اس لیے installation کے لیے package اور file path درکار ہوتے ہیں۔ اگر آپ کے stack کا باقی حصہ پہلے ہی اسی VPS پر Docker Compose کے تحت چل رہا ہے، تو database service شامل کرنے کے بجائے data directory اس container میں mount کریں جسے اس کی ضرورت ہے، کیونکہ شامل کرنے کے لیے کوئی service موجود نہیں۔
دو اصول اس arrangement کو مسائل سے محفوظ رکھتے ہیں۔
ہر engine کے لیے الگ directory رکھیں: /srv/app اس SQLite file کے لیے ہے جس میں application لکھتی ہے، اور /srv/data ان Parquet files کے لیے ہے جنہیں analytics پڑھتی ہے۔ جب دونوں ایک directory استعمال کرتے ہیں، تو ایک file کا snapshot لینے والا backup job دوسری process کے ساتھ race condition پیدا کرتا ہے۔
دو processes کو ایک DuckDB database file کی read-write mode میں طرف متوجہ نہ کریں۔ صرف ایک process DuckDB file کو writing کے لیے hold کر سکتی ہے، اور دوسری اسے بالکل open نہیں کر پاتی۔ جب ہر process access_mode = 'READ_ONLY' set کرے تو متعدد readers ٹھیک کام کرتے ہیں۔ SQLite سے آنے والے صارفین کے لیے یہ بات غیر متوقع ہو سکتی ہے، کیونکہ وہاں کئی processes معمول کے مطابق ایک file share کرتی ہیں۔ اگر آپ کی analytics صرف Parquet files پڑھتی ہے، تو یہ سوال پیدا ہی نہیں ہوتا۔ یہی durable state کو SQLite میں رکھنے کی ایک اور وجہ ہے۔
بیک اپ مختلف ہوتے ہیں، اور یہ فرق نقصان دہ ہوتا ہے
چلتا ہوا SQLite database تین files پر مشتمل ہوتا ہے۔ cp کے ذریعے write کے دوران ان files کو copy کرنے سے ایسی file بنتی ہے جو کھل تو جاتی ہے، لیکن درست نہیں ہوتی۔ Engine کی اپنی backup command استعمال کریں۔ یہ application کے write جاری رکھنے کے دوران consistent 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 files لکھے جانے کے بعد کبھی تبدیل نہیں ہوتیں، اس لیے ان کے لیے کسی خاص طریقہ کار کی ضرورت نہیں ہے: directory کا backup لیں۔ دونوں paths کو server سے باہر اپنے VPS سے restic backups کے ذریعے بھیجیں۔ یوں پوری data layer ایک backup job میں دو directories پر مشتمل ہوگی۔
ناکامی کی صورتیں اور آپ کو نظر آنے والے عین strings
Error: database is locked از SQLite کا مطلب ہے کہ کسی دوسرے connection نے write lock آپ کے مقررہ timeout سے زیادہ دیر تک برقرار رکھا۔ یہ corruption نہیں ہے۔ ہر connection پر PRAGMA busy_timeout سیٹ کریں، پھر ایسی طویل transaction تلاش کریں جو کئی مختصر transactions پر مشتمل ہونی چاہیے تھی۔
اجازت تبدیل کرنے کے بعد Error: unable to open database file عموماً اس بات کی نشاندہی کرتا ہے کہ process فائل میں لکھ سکتا ہے، لیکن اس کی directory میں نہیں۔ SQLite database کے ساتھ app.db-wal اور app.db-shm بناتا ہے، اس لیے خود directory writable ہونی چاہیے، صرف .db فائل نہیں۔
IO Error: Could not set lock on file از DuckDB کا مطلب ہے کہ کوئی دوسرا process پہلے ہی اس database کو writing کے لیے کھول چکا ہے۔ دوسرا shell بند کریں، یا اپنا database read only mode میں کھولیں۔
چھوٹے VPS پر DuckDB سے Out of Memory Error کا مطلب ہے کہ query کو دستیاب working memory سے زیادہ memory درکار تھی۔ DuckDB ممکن ہونے پر data کو disk پر spill کرتا ہے، اس لیے :memory: کے بجائے disk پر database file کھول کر اسے spill کرنے کی جگہ دیں، اور SET memory_limit = '2GB'; کے ذریعے اس کی memory حد مقرر کریں۔ دوسرے services چلانے والے server پر یہی حد کسی ad hoc query کو آپ کی application کو RAM سے محروم کرنے سے روکتی ہے۔
Parquet سے query کرتے وقت Binder Error: Referenced column "amount" not found تقریباً ہمیشہ اس بات کی نشاندہی کرتا ہے کہ فائل کا schema وہ نہیں ہے جو آپ کو یاد ہے۔ DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; چلائیں اور اصل column names حاصل کریں۔
عملی انتخاب کیسے کریں
پوچھیں کہ تحریری پیٹرن کیا ہے۔ بجلی منقطع ہونے کے بعد بھی محفوظ رہنے والی بہت سی چھوٹی تحریری کارروائیاں SQLite کا تقاضا کرتی ہیں۔ پوچھیں کہ مطالعے کا پیٹرن کیا ہے۔ طویل مدتی تاریخ پر مکمل اسکینز اور مجموعی حساب DuckDB کے لیے موزوں ہیں۔ زیادہ تر حقیقی نظام دونوں سوالات کا جواب ہاں میں دیتے ہیں۔ درست طریقہ یہ ہے کہ ہر انجن کو وہ نصف کام دیا جائے جس کے لیے وہ موزوں ہے، نہ کہ ایک انجن کو دوسرے کے کام کے لیے مجبور کیا جائے۔
جس منتقلی سے گریز کرنا چاہیے وہ یہ ہے کہ رپورٹ سست ہونے کی وجہ سے فعال application state کو DuckDB میں منتقل کر دیا جائے۔ رپورٹ storage layout کی وجہ سے سست تھی، اس لیے درست حل export ہے، آپ کے write path کی ازسرنو تحریر نہیں۔
FAQ
کیا DuckDB میرے application database کے لیے SQLite کی جگہ لے سکتا ہے؟
ایسے database کے لیے نہیں جس میں بار بار write کیا جاتا ہو۔ DuckDB پوری database file پر write lock لگاتا ہے، ایک وقت میں صرف ایک read-write process کی اجازت دیتا ہے، اور اسے single-row inserts کے بجائے bulk changes کے لیے بہتر بنایا گیا ہے۔ Transactional state کو SQLite میں رکھیں، اور جب report درکار ہو تو DuckDB کو ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); کے ذریعے اسے پڑھنے دیں۔
کیا analytics کے لیے DuckDB واقعی SQLite سے تیز ہے؟
بڑے table پر scans اور aggregates کے لیے ہاں، اور اس کی وجہ tuning trick کے بجائے storage layout ہے۔ DuckDB صرف وہ columns پڑھتا ہے جنہیں query میں نامزد کیا گیا ہو، اور values کو batches میں process کرتا ہے، جبکہ SQLite کو ایک field تک پہنچنے کے لیے پوری rows دیکھنا پڑتی ہیں۔ Primary key کے ذریعے single row حاصل کرتے وقت نتیجہ الٹ جاتا ہے، کیونکہ SQLite دو pages تک رسائی کرتا ہے جبکہ DuckDB ہر column کی storage تک رسائی کرتا ہے۔
کیا VPS پر DuckDB چلانے کے لیے مجھے بہت زیادہ RAM درکار ہے؟
نہیں، لیکن اسے ایک limit اور disk فراہم کریں۔ :memory: کے بجائے database file کھولیں تاکہ DuckDB intermediate results کو disk پر منتقل کر سکے، پھر SET memory_limit = '2GB'; کو ایسی value پر set کریں جو آپ کا VPS فراہم کر سکے۔ Limit کے بغیر ایک بڑی GROUP BY Out of Memory Error پیدا کر سکتی ہے یا دیگر services کو RAM سے باہر نکال سکتی ہے۔
میں اپنے SQLite data کو Parquet میں کیسے منتقل کروں؟
DuckDB سے SQLite file کو attach کریں اور COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd); کے ذریعے query کا output براہِ راست copy کریں۔ بند periods، مثلاً گزشتہ ماہ کی rows، کے لیے اسے schedule کے مطابق چلائیں، اور حالیہ rows SQLite میں رکھیں جہاں application اب بھی انہیں write کرتی ہے۔
مجھے کس کا backup لینا چاہیے، اور کیسے؟
دونوں کا، مگر مختلف طریقوں سے۔ SQLite snapshots cp کے بجائے sqlite3 app.db ".backup '/srv/backup/app.db'" کے ذریعے لیں، کیونکہ چلتا ہوا database ایک -wal بھی ہوتا ہے، نیز -shm file بھی، اور عام copy نامکمل ہو سکتی ہے۔ Parquet files لکھے جانے کے بعد کبھی تبدیل نہیں ہوتیں، اس لیے directory کو copy کرنا کافی ہے۔