تفاوت 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 را انتخاب کنید. کلاینت خط فرمان را نصب کنید؛ نسخه 2.0.0، که تا تاریخ 27 October 2026 یک انتشار LTS است:
curl https://install.duckdb.org | shاین اسکریپت، مگر آنکه نسخهای را مشخص کنید، جدیدترین انتشار پایدار را نصب میکند؛ برای نمونه، curl https://install.duckdb.org | DUCKDB_VERSION=1.5.6 sh برای ماندن روی انتشار قبلی. باینری را در ~/.duckdb/cli/latest/duckdb نصب میکند و خطی را چاپ میکند که آن را به PATH شما اضافه میکند. بررسی کنید که اجرا میشود:
~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"یک فایل واقعگرایانه برای جستوجو بسازید. این دستور پنج میلیون ردیف سفارش را در قالب Parquet و با فشردهسازی 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);"اکنون پرسش تحلیلی را مطرح کنید. shell را باز کنید، زمانسنج را فعال کنید و فایل را مستقیماً، بدون مرحله 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 و هیچ مرحله load وجود نداشت: DuckDB footer مربوط به Parquet را خواند، مشخص کرد query به کدام column chunkها نیاز دارد و فقط همانها را خواند. یک directory کامل نیز با glob، یعنی FROM '/srv/data/orders-*.parquet'، به همین روش کار میکند؛ به این ترتیب، exportهای روزانه یک ماه به یک query تبدیل میشوند.
سرعت دیسک کف عملکرد همه این مراحل است و column scan یک خواندن ترتیبی طولانی محسوب میشود؛ بنابراین تفاوت بین NVMe و storage قدیمیتر 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 از سرور خارج کنید تا کل لایه داده در یک عملیات پشتیبانگیری، شامل دو دایرکتوری باشد.
حالتهای خطا و رشتههای دقیق خطایی که خواهید دید
database is locked از SQLite، چه از طریق driver برنامه شما و چه از طریق shell یعنی sqlite3، یعنی connection دیگری قفل نوشتن را بیشتر از زمانی که timeout شما اجازه میداد در اختیار داشته است. این وضعیت corruption نیست. PRAGMA busy_timeout را روی هر connection تنظیم کنید و سپس transaction طولانیای را پیدا کنید که باید به چند transaction کوتاه تقسیم میشد.
unable to open database file پس از تغییر permission معمولاً یعنی process میتواند فایل را بنویسد، اما نمیتواند directory آن را بنویسد. SQLite فایلهای app.db-wal و app.db-shm را کنار database ایجاد میکند؛ بنابراین خود directory باید قابل نوشتن باشد، نه فقط فایل .db.
IO Error: Could not set lock on file از DuckDB یعنی process دیگری آن database را برای نوشتن باز کرده است. shell دیگر را ببندید یا shell خودتان را بهصورت read only باز کنید.
Out of Memory Error از DuckDB روی یک VPS کوچک یعنی query به working memory بیشتری از مقدار موجود نیاز داشته است. DuckDB بهطور پیشفرض نتیجههای میانی را روی دیسک spill میکند و این شامل یک session از نوع :memory: نیز میشود؛ بنابراین باز کردن یک database file چیزی نیست که spill شدن را فعال کند. این فایلهای موقت را در یک directory از نوع .tmp در کنار database file مینویسد یا، برای یک session درونحافظهای، در یک directory از نوع .tmp نسبت به محلی که از آنجا اجرا شده است. بنابراین آن را در محلی قابل نوشتن با فضای خالی کافی روی دیسک اجرا کنید یا با SET temp_directory = '/var/tmp/duckdb'; مسیر دیگری برای آن مشخص کنید. تنظیم مهم، سقف مصرف است: SET memory_limit = '2GB';. مقدار پیشفرض 80% از RAM ماشین است؛ بنابراین روی سیستمی که سرویسهای دیگری اجرا میکند، همین محدودیت مانع میشود یک query موقت application شما را از RAM خارج کند. برخی aggregateها، مانند string_agg()، قابلیت spill شدن ندارند. برخی aggregateهای مبتنی بر sorting نیز همین وضعیت را دارند؛ بنابراین queryای که بر پایه آنها ساخته شده است همچنان ممکن است با کمبود memory مواجه شود.
Binder Error: Referenced column "amount" not found هنگام query روی Parquet تقریباً همیشه یعنی schema فایل همان چیزی نیست که به یاد دارید. DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; را اجرا کنید و نام واقعی columnها را بخوانید.
نحوه انتخاب در عمل
بپرسید الگوی نوشتن چگونه است. بسیاری از عملیات نوشتن کوچک که باید در برابر قطع برق مقاوم باشند، به معنای استفاده از 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 به رم زیادی نیاز دارم؟
خیر، اما برای آن محدودیت تعیین کنید و فضایی برای سرریز (spill) در نظر بگیرید. DuckDB بهطور پیشفرض نتایج میانی را روی دیسک میریزد، حتی در یک نشست :memory:، به شرطی که دایرکتوری موقت آن قابل نوشتن باشد و فضای خالی داشته باشد. سپس SET memory_limit = '2GB'; را روی مقداری تنظیم کنید که VPS شما توانایی اختصاص آن را دارد. اگر آن را روی مقدار پیشفرض یعنی 80 درصد رم رها کنید، یک GROUP BY بزرگ میتواند باعث Out of Memory Error شود یا سایر سرویسها را از رم خارج کند.
چگونه دادههای SQLite خود را به Parquet منتقل کنم؟
فایل SQLite را از طریق DuckDB ضمیمه (attach) کنید و یک کوئری را مستقیماً با COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd); کپی کنید. این کار را طبق یک زمانبندی برای دورههای بسته، مانند ردیفهای ماه گذشته، اجرا کنید و ردیفهای اخیر را در SQLite باقی بگذارید، جایی که برنامه همچنان در حال نوشتن آنهاست.
از کدامیک باید نسخه پشتیبان تهیه کنم و چگونه؟
از هر دو، اما به روشهای متفاوت. از SQLite به جای cp، با استفاده از sqlite3 app.db ".backup '/srv/backup/app.db'" اسنپشات بگیرید، زیرا یک پایگاهداده در حال اجرا همزمان یک -wal و یک فایل -shm است و کپی ساده ممکن است ناقص (torn) باشد. فایلهای Parquet پس از نوشته شدن هرگز تغییر نمیکنند، بنابراین کپی کردن دایرکتوری کافی است.