تفاوت DuckDB و SQLite در سرور و نحوه استفاده همزمان
تفاوت کلیدی SQLite برای تراکنشهای OLTP و DuckDB برای تحلیلهای OLAP را درک کنید. یاد بگیرید چگونه هر دو را روی یک VPS برای مدیریت دادههای Parquet و CSV اجرا کنید.
DuckDB در مقابل SQLite روی سرور: پاسخ در یک جمله
SQLite یک موتور OLTP (پردازش تراکنش آنلاین) است: دادهها را به صورت ردیفی ذخیره میکند و برای خواندن و نوشتن ایمن و سریع تعداد کمی از آنها ساخته شده است. DuckDB یک موتور OLAP (پردازش تحلیلی آنلاین) است: دادهها را به صورت ستونی ذخیره میکند و برای اسکن میلیونها ردیف و ارائه یک نتیجه تجمیعی ساخته شده است. هر دو کتابخانههای تعبیهشده هستند، هر دو یک فایل ساده را باز میکنند و هیچکدام فرآیند سروری که نیاز به مراقبت مداوم داشته باشد، اجرا نمیکنند.
بنابراین پاسخ صادقانه به اینکه «کدام یک» را انتخاب کنیم، تقریباً همیشه «هر دو، روی همان VPS» است. برنامه شما وضعیت زنده خود را در SQLite نگه میدارد. بخش گزارشگیری شما فایلهای Parquet و CSV را با DuckDB میخواند. این دو با هم رقابت نمیکنند، زیرا وظیفه یکسانی را انجام نمیدهند.
چرا ذخیرهسازی سطری و ستونی پاسخ را تغییر میدهد
SQLite هر سطر را به صورت یک قطعه پیوسته در یک صفحه مینویسد. بازیابی یک سفارش از طریق کلید اصلی (primary key)، یک صفحه ایندکس و یک صفحه داده را درگیر میکند که در مجموع دو عملیات خواندن است. این دقیقاً همان کاری است که یک برنامه هزاران بار در ثانیه انجام میدهد: خواندن یک کاربر، بهروزرسانی یک نشست، یا درج یک سفارش.
DuckDB هر ستون را به صورت جداگانه مینویسد و فشردهسازی میکند. محاسبه مجموع amount_cents برای پنج میلیون سطر، تنها ستون amount_cents را میخواند، از تمام بایتهای دیگر در فایل صرفنظر میکند و مجموع را از طریق کد برداریشده (vectorised) روی دستههایی از مقادیر اجرا میکند. سایر ستونها هرگز از دیسک خوانده نمیشوند و سرعت عملیات از همینجا ناشی میشود.
حال هر موتور را با بار کاری موتور دیگر بسنجید. SQLite برای محاسبه مجموع یک ستون، مجبور است تمام سطرها را پیمایش کند و کل سطر را از صفحه بیرون بکشد تا به یک فیلد خاص برسد؛ بنابراین بسیار بیشتر از حد نیاز از دیسک میخواند. DuckDB برای درج یک سفارش، مجبور است برای یک مقدار واحد، فضای ذخیرهسازی تمام ستونها را درگیر کند و برای انجام این کار، یک قفل نوشتاری (write lock) روی کل فایل پایگاه داده میگیرد. هیچکدام از این موتورها معیوب نیستند. هر کدام در حال پاسخ به پرسشی هستند که برای آن طراحی نشدهاند.
در چه مواردی 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، یک نویسنده تمام خوانندگان را مسدود میکند. در حالت WAL، خوانندگان به خواندن آخرین وضعیت ثبتشده ادامه میدهند در حالی که یک نویسنده دادهها را اضافه میکند؛ بنابراین، یک گزارش کند دیگر باعث توقف درخواستهای وب نمیشود.
بررسی کنید که ردیف بازگردانده شده است:
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 در حین اجرای برنامه، منجر به یک نسخه پشتیبان ناقص میشود که در ادامه بیشتر به آن پرداخته شده است.
SQLite همچنان اجازه میدهد در هر لحظه تنها یک نویسنده وجود داشته باشد. این محدودیت یک قفل است، نه یک صف؛ بنابراین نویسنده دومی که بیش از حد منتظر بماند، به جای مسدود شدن برای همیشه، با خطای database is locked مواجه میشود. با استفاده از PRAGMA busy_timeout = 5000; در هر اتصالی که برنامه شما باز میکند، زمان انتظار را افزایش دهید. 5 ثانیه صبر، اکثر این خطاها را در بار کاری معمول وب برطرف میکند.
برتری DuckDB: تحلیل دادهها روی فایلهای موجود
زمانی که پرسش با «چند تا»، «چه مقدار» یا «ده مورد برتر کدامند» شروع میشود و ورودی مجموعهای از فایلهای CSV یا Parquet است، DuckDB را انتخاب کنید. کلاینت خط فرمان نسخه 1.5.5 (مربوط به ژوئیه 2026) را نصب کنید:
curl https://install.duckdb.org | shاین اسکریپت فایل باینری را در ~/.duckdb/cli/latest/duckdb نصب میکند و خطی را چاپ میکند که آن را به PATH شما اضافه میکند. اجرای صحیح آن را تأیید کنید:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"یک فایل واقعی برای کوئری گرفتن بسازید. این دستور پنج میلیون ردیف از سفارشات را با فشردهسازی zstd در قالب Parquet مینویسد:
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);"حالا پرسش تحلیلی خود را مطرح کنید. شل را باز کنید، زمانسنج را فعال کنید و بدون نیاز به مرحله وارد کردن (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 بخوانید و به اعداد منتشر شده اعتماد نکنید، زیرا نتیجه به دیسک و تعداد هستههای پردازنده شما بستگی دارد. ساختار عملکرد اهمیت دارد. هیچ CREATE TABLE، هیچ INSERT و هیچ مرحله بارگذاری وجود نداشت: DuckDB فوتر فایل Parquet را خواند، تشخیص داد که کوئری به کدام بخشهای ستونی نیاز دارد و فقط همانها را خواند. یک دایرکتوری کامل نیز با استفاده از glob به صورت FROM '/srv/data/orders-*.parquet' به همین شکل عمل میکند؛ این همان روشی است که خروجیهای روزانه یک ماه را به یک کوئری واحد تبدیل میکند.
سرعت دیسک، کف عملکرد در تمام این موارد است و اسکن ستونی یک خواندن ترتیبی طولانی محسوب میشود، بنابراین تفاوت بین NVMe و حافظههای قدیمی SATA روی یک VPS در اینجا بسیار واضحتر از خواندنهای تصادفی کوچک در SQLite نمایان میشود.
خواندن پایگاه داده SQLite از طریق DuckDB
این دو موتور از طریق افزونه sqlite در DuckDB با یکدیگر ارتباط برقرار میکنند. پایگاه داده برنامه را به صورت فقطخواندنی (read-only) متصل کنید تا یک پرسوجوی تحلیلی هرگز نتواند در وضعیت زنده (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;این دستور، سطرها را در زمان اجرای پرسوجو و بدون کپی کردن از فایل SQLite میخواند. این روش راحت است اما سریع نیست، زیرا دادههای روی دیسک همچنان به صورت ذخیرهسازی سطری (row storage) هستند و DuckDB باید آنها را پیمایش کند. از این روش برای خروجی گرفتن استفاده کنید، نه برای داشبوردی که هر 30 ثانیه بازخوانی میشود:
COPY (SELECT * FROM app.orders)
TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);همین یک دستور، کل الگو را تشکیل میدهد. SQLite مالک سطرهای زنده و اخیر است. یک خروجی زمانبندیشده، دورههای بسته شده را به Parquet تبدیل میکند. DuckDB به تمام پرسشهایی که بازههای چند ماهه را در بر میگیرند پاسخ میدهد و پایگاه داده برنامه کوچک باقی میماند که باعث میشود سرعت نوشتن در آن بالا بماند.
خروجی گرفتن را به جای انجام دستی، طبق یک زمانبندی اجرا کنید. یک جفت سرویس و تایمر systemd برای این کار مناسب است: یک واحد که COPY را اجرا میکند و یک تایمر که آن را هر شب فعال میسازد.
اجرا روی یک VPS واحد
در اینجا نیازی به کانتینر یا پورت نیست. هر دو موتور کتابخانه هستند، بنابراین نصب آنها صرفاً شامل یک بسته و یک مسیر فایل است. اگر بقیه پشته (stack) شما در حال حاضر تحت Docker Compose on the same VPS اجرا میشود، به جای افزودن یک سرویس پایگاه داده، دایرکتوری داده را به کانتینری که به آن نیاز دارد متصل (mount) کنید، زیرا سرویسی برای اضافه کردن وجود ندارد.
دو قانون از بروز مشکل در این پیکربندی جلوگیری میکنند.
به هر موتور دایرکتوری اختصاصی خود را بدهید: /srv/app برای فایل SQLite که برنامه در آن مینویسد، و /srv/data برای فایلهای Parquet که بخش تحلیلگر آنها را میخواند. وقتی آنها یک دایرکتوری مشترک دارند، یک عملیات پشتیبانگیری که از یکی اسنپشات میگیرد، با دیگری دچار تداخل میشود.
دو پردازش را در حالت خواندن-نوشتن به یک فایل پایگاه داده DuckDB متصل نکنید. تنها یک پردازش میتواند یک فایل DuckDB را برای نوشتن در اختیار داشته باشد و پردازش دوم در باز کردن آن با شکست مواجه میشود. زمانی که همه پردازشها access_mode = 'READ_ONLY' را تنظیم کنند، داشتن چندین خواننده مشکلی ایجاد نمیکند. این موضوع برای کسانی که از SQLite میآیند، جایی که چندین پردازش به طور معمول یک فایل را به اشتراک میگذارند، تعجبآور است. اگر بخش تحلیلگر شما فقط فایلهای Parquet را میخواند، این مسئله هرگز پیش نمیآید، که این خود دلیل دیگری برای نگهداری وضعیت پایدار (durable state) در SQLite است.
تفاوت در پشتیبانگیری و پیامدهای آن
یک پایگاه داده SQLite در حال اجرا شامل 3 فایل است و کپی کردن آنها با cp در حین عملیات نوشتن، فایلی به شما میدهد که باز میشود اما حاوی دادههای نادرست است. از دستور پشتیبانگیری خودِ موتور استفاده کنید که در حین ادامه عملیات نوشتن توسط برنامه، یک اسنپشات منسجم تهیه میکند:
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 در صورت کپی موفق، ok را چاپ میکند. هر خروجی دیگری به این معنی است که باید آن اسنپشات را دور بریزید و یکی دیگر تهیه کنید.
فایلهای Parquet پس از نوشته شدن هرگز تغییر نمیکنند، بنابراین به هیچ رسیدگی خاصی نیاز ندارند: از دایرکتوری مربوطه پشتیبان بگیرید. هر دو مسیر را با استفاده از پشتیبانگیری restic از VPS از سرور خارج کنید تا کل لایه داده در یک عملیات پشتیبانگیری، شامل دو دایرکتوری باشد.
حالتهای شکست و رشتههای دقیقی که مشاهده خواهید کرد
Error: database is locked از SQLite به این معنی است که یک اتصال دیگر، قفل نوشتن را بیش از حد مجازِ تعیینشده توسط timeout شما نگه داشته است. این مورد خرابی (corruption) نیست. مقدار PRAGMA busy_timeout را روی تمام اتصالات تنظیم کنید، سپس به دنبال تراکنش طولانیمدتی بگردید که باید به چندین تراکنش کوتاه تقسیم میشد.
Error: unable to open database file پس از تغییر مجوزها معمولاً به این معنی است که پردازش میتواند در فایل بنویسد اما در دایرکتوری آن خیر. SQLite فایلهای app.db-wal و app.db-shm را در کنار پایگاه داده ایجاد میکند، بنابراین دایرکتوری باید قابل نوشتن باشد، نه فقط خود فایل .db.
IO Error: Could not set lock on file از DuckDB به این معنی است که پردازش دومی در حال حاضر آن پایگاه داده را برای نوشتن باز نگه داشته است. شل (shell) دیگر را ببندید یا پایگاه داده خود را فقط در حالت خواندنی (read only) باز کنید.
Out of Memory Error از DuckDB در یک VPS کوچک به این معنی است که کوئری به حافظه کاری بیشتری نسبت به مقدار موجود نیاز داشته است. DuckDB در صورت امکان دادهها را روی دیسک میریزد (spill)، بنابراین با باز کردن فایل پایگاه داده روی دیسک به جای :memory:، فضایی برای این کار فراهم کنید و اشتهای آن را با SET memory_limit = '2GB'; محدود نمایید. در سیستمی که سرویسهای دیگری را اجرا میکند، این محدودیت همان چیزی است که مانع از خارج شدن اپلیکیشن شما از RAM توسط یک کوئری موردی (ad hoc) میشود.
Binder Error: Referenced column "amount" not found هنگام کوئری گرفتن از Parquet تقریباً همیشه به این معنی است که شمای فایل، آن چیزی نیست که شما به خاطر دارید. دستور DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; را اجرا کنید و نامهای واقعی ستونها را بررسی نمایید.
نحوه انتخاب در عمل
بپرسید الگوی نوشتن چگونه است. بسیاری از عملیات نوشتن کوچک که باید در برابر قطع برق مقاوم باشند، به معنای استفاده از SQLite است. بپرسید الگوی خواندن چگونه است. اسکنهای کامل با تجمیع دادهها در طول یک تاریخچه طولانی، به معنای استفاده از DuckDB است. اکثر سیستمهای واقعی به هر دو سوال پاسخ مثبت میدهند و پاسخ درست این است که به هر موتور بخشی را بسپارید که در آن عملکرد بهتری دارد، به جای اینکه یکی از آنها را مجبور کنید دیگری را پوشش دهد.
مهاجرتی که باید از آن اجتناب کرد، انتقال وضعیت زنده برنامه به DuckDB به دلیل کند بودن یک گزارش است. گزارش به دلیل ساختار ذخیرهسازی کند بوده است، بنابراین راهحل، خروجی گرفتن (export) است، نه بازنویسی مسیر نوشتن شما.
FAQ
آیا DuckDB میتواند جایگزین SQLite برای پایگاه داده برنامه من باشد؟
خیر، برای برنامهای که عملیات نوشتن مکرر دارد مناسب نیست. DuckDB یک قفل نوشتن روی کل فایل پایگاه داده اعمال میکند، تنها اجازه میدهد یک پردازش خواندن-نوشتن در لحظه فعال باشد و برای تغییرات انبوه بهینهسازی شده است تا درجهای تکردیفی. وضعیت تراکنشی را در SQLite نگه دارید و اجازه دهید DuckDB هنگام نیاز به گزارش، آن را با ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); بخواند.
آیا DuckDB واقعاً برای تحلیل دادهها سریعتر از SQLite است؟
برای اسکن و تجمیع روی جداول بزرگ، بله؛ دلیل این امر ساختار ذخیرهسازی است و نه یک ترفند تنظیماتی. DuckDB فقط ستونهایی را میخواند که در پرسوجو نام برده شدهاند و مقادیر را به صورت دستهای پردازش میکند، در حالی که SQLite برای رسیدن به یک فیلد باید کل ردیفها را پیمایش کند. برای بازیابی یک ردیف واحد از طریق کلید اصلی، وضعیت برعکس میشود، زیرا SQLite به دو صفحه دسترسی پیدا میکند اما DuckDB باید به ذخیرهسازی تمام ستونها دسترسی داشته باشد.
آیا برای اجرای DuckDB روی یک VPS به رم زیادی نیاز دارم؟
خیر، اما برای آن محدودیت تعیین کنید و از دیسک استفاده کنید. به جای :memory:، یک فایل پایگاه داده را باز کنید تا DuckDB بتواند نتایج میانی را به دیسک منتقل کند، سپس SET memory_limit = '2GB'; را روی مقداری تنظیم کنید که VPS شما توانایی اختصاص آن را دارد. بدون محدودیت، یک GROUP BY بزرگ میتواند باعث بروز Out of Memory Error شود یا سایر سرویسها را از رم خارج کند.
چگونه دادههای SQLite خود را به Parquet منتقل کنم؟
فایل SQLite را از طریق DuckDB متصل کنید و یک پرسوجو را مستقیماً با COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd); کپی کنید. این کار را طبق یک زمانبندی برای دورههای بسته، مانند ردیفهای ماه گذشته، انجام دهید و ردیفهای اخیر را در SQLite باقی بگذارید، جایی که برنامه همچنان در حال نوشتن آنهاست.
از کدام یک باید نسخه پشتیبان تهیه کنم و چگونه؟
از هر دو، اما به روشهای متفاوت. از اسنپشاتهای SQLite با sqlite3 app.db ".backup '/srv/backup/app.db'" به جای cp استفاده کنید، زیرا یک پایگاه داده در حال اجرا، همزمان یک فایل -wal و یک فایل -shm است و کپی ساده ممکن است ناقص انجام شود. فایلهای Parquet پس از نوشته شدن هرگز تغییر نمیکنند، بنابراین کپی کردن دایرکتوری کافی است.