SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-07

DuckDB أم SQLite على الخادم؟ لماذا تحتاجهما معاً

يخزّن SQLite حالة التطبيق والمعاملات، بينما يحلل DuckDB ملفات Parquet وCSV. تعرّف إلى سبب تشغيلهما معاً على VPS، مع مثال عملي لكل محرك.

DuckDB مقابل SQLite على خادم: الإجابة في جملة واحدة

SQLite هو محرك OLTP (معالجة المعاملات عبر الإنترنت): يخزّن البيانات في صفوف، وصُمّم لقراءة عدد قليل منها وكتابته بأمان وسرعة في كل مرة. DuckDB هو محرك OLAP (المعالجة التحليلية عبر الإنترنت): يخزّن البيانات في أعمدة، وصُمّم لفحص ملايين الصفوف وإرجاع قيمة تجميعية واحدة. كلاهما مكتبتان مضمّنتان، وكلاهما يفتح ملفاً عادياً، ولا يتطلب أيٌّ منهما تشغيل عملية خادم تحتاج إلى مراقبتها باستمرار.

لذلك، فإن الإجابة الصادقة عن سؤال «أيّهما؟» هي غالباً «كلاهما على VPS نفسه». يحتفظ تطبيقك بحالته الحية في SQLite. وتقرأ تقاريرك ملفات Parquet وCSV باستخدام DuckDB. لا يتنافس المحركان لأنّهما لا يؤديان المهمة نفسها.

لماذا تغيّر طريقة تخزين الصفوف وتخزين الأعمدة الإجابة

يكتب SQLite الصف في جزء متصل واحد من الصفحة. يؤدي جلب طلب واحد باستخدام مفتاحه الأساسي إلى الوصول إلى صفحة فهرس واحدة وصفحة بيانات واحدة، أي إجراء عمليتي قراءة. وهذا يطابق ما يفعله التطبيق آلاف المرات في الثانية: قراءة مستخدم معين، وتحديث جلسة، وإدراج طلب.

يكتب DuckDB كل عمود على حدة ويضغطه. يؤدي جمع amount_cents عبر خمسة ملايين صف إلى قراءة العمود amount_cents فقط، وتجاوز كل بايت آخر في الملف، وتنفيذ عملية الجمع باستخدام تعليمات متجهة على دفعات من القيم. لا تُقرأ الأعمدة الأخرى من القرص أبداً، ومن هنا تأتي السرعة.

شغّل الآن كل محرك على عبء العمل الخاص بالمحرك الآخر. عند جمع عمود في SQLite، يجب المرور على كل صف وجلب الصف بأكمله من الصفحة للوصول إلى حقل واحد، ولذلك يقرأ من القرص بيانات أكثر بكثير مما يحتاج إليه. وعند إدراج طلب واحد في DuckDB، يجب الوصول إلى مخزن كل عمود لكتابة قيمة واحدة، كما يفرض قفل كتابة على ملف قاعدة البيانات بأكمله لتنفيذ ذلك. لا يوجد خلل في أي من المحركين. لكن كل محرك يجيب عن سؤال لم يُصم لمعالجته.

متى تتفوق SQLite: حالة التطبيق المعاملاتية

استخدم SQLite عندما تكون عمليات الكتابة صغيرة ومتكررة، ويجب ألّا تُفقد. يشمل ذلك الجلسات والطلبات وصفوف قائمة الانتظار والإعدادات وكل ما ينشئه طلب ويب.

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

أنشئ الجدول وفعّل تسجيل الكتابة المسبقة في الخطوة نفسها.

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 التي تعرض الوضع الذي انتقلت إليه، وهي الإعداد الأكثر فائدة على الخادم. في وضع سجل التراجع الافتراضي، تمنع عملية كتابة واحدة جميع عمليات القراءة. في وضع 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; في كل اتصال يفتحه التطبيق. ويؤدي الانتظار مدة خمس ثوانٍ إلى إزالة معظم هذه الأخطاء في حمل ويب عادي.

أين يتفوق DuckDB: تحليل الملفات الموجودة لديك

اختر DuckDB عندما يبدأ السؤال بـ«كم عدد» أو «ما مقدار» أو «ما أفضل عشرة»، وتكون المدخلات مجموعة من ملفات CSV أو Parquet. ثبّت عميل سطر الأوامر، وإصداره 1.5.5 اعتباراً من July 2026:

curl https://install.duckdb.org | 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);"

اطرح الآن السؤال التحليلي. افتح الصدفة، وشغّل المؤقت، ونفّذ الاستعلام على الملف مباشرةً من دون خطوة استيراد:

.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. أرفق قاعدة بيانات التطبيق بوضع القراءة فقط، حتى لا يتمكن استعلام تحليلي من الكتابة إلى الحالة الحية مطلقاً:

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 وقت تنفيذ الاستعلام، من دون إنشاء نسخة. هذا مريح، لكنه ليس سريعاً، لأن البيانات على القرص ما تزال مخزنة في صفوف، ولأن DuckDB يجب أن يمر عليها. استخدمه للتصدير، وليس للوحة معلومات يُعاد تحميلها كل ثلاثين ثانية:

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

هذا البيان الواحد يوضّح النمط بأكمله. تملك SQLite الصفوف الحية الحديثة. ويحوّل تصدير مجدول الفترات المغلقة إلى Parquet. ويجيب DuckDB عن كل سؤال يمتد عبر أشهر، بينما تبقى قاعدة بيانات التطبيق صغيرة، مما يحافظ على سرعة عمليات الكتابة فيها.

شغّل التصدير وفق جدول زمني بدلاً من تشغيله يدوياً. إن زوجاً من خدمة ومؤقت systemd هو الحجم المناسب لذلك: وحدة واحدة تشغّل COPY، ومؤقت واحد يشغّلها كل ليلة.

التشغيل معاً على VPS واحد

لا يحتاج أيٌّ من هذين المحركين إلى حاوية أو منفذ. فكلاهما مكتبة، لذا يقتصر التثبيت على حزمة ومسار ملف. إذا كانت بقية مكوّنات الحزمة لديك تعمل بالفعل ضمن Docker Compose على VPS نفسه، فقم بتركيب دليل البيانات داخل الحاوية التي تحتاج إليه بدلاً من إضافة خدمة قاعدة بيانات، إذ لا توجد خدمة لإضافتها.

تمنع قاعدتان حدوث مشكلات في هذا الترتيب.

امنح كل محرك دليله الخاص: /srv/app لملف SQLite الذي يكتب فيه التطبيق، و/srv/data لملفات Parquet التي يقرأ منها نظام التحليلات. عند مشاركة دليل واحد، قد تتنافس مهمة النسخ الاحتياطي التي تنشئ لقطة لأحدهما مع الآخر.

لا توجّه عمليتين إلى ملف قاعدة بيانات DuckDB واحد في وضع القراءة والكتابة. يمكن لعملية واحدة فقط فتح ملف DuckDB للكتابة، وستفشل العملية الثانية في فتحه بالكامل. يمكن لعدد كبير من القرّاء العمل بشكل طبيعي عندما يضبط كل منهم access_mode = 'READ_ONLY'. يفاجئ هذا السلوك القادمين من SQLite، حيث تتشارك عدة عمليات ملفاً واحداً بصورة اعتيادية. إذا كان نظام التحليلات يقرأ ملفات Parquet فقط، فلن تظهر هذه المشكلة، وهذا سبب إضافي للاحتفاظ بالبيانات الدائمة في SQLite.

تختلف النسخ الاحتياطية، والفرق يسبب مشكلات

تتكوّن قاعدة بيانات SQLite قيد التشغيل من ثلاثة ملفات. يؤدي نسخها باستخدام 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 يعني أن اتصالاً آخر احتفظ بقفل الكتابة مدة أطول من المهلة المسموح بها. هذا لا يعني تلف البيانات. اضبط 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 يعني أن عملية ثانية تفتح قاعدة البيانات للكتابة بالفعل. أغلق الصدفة الأخرى، أو افتح قاعدة البيانات للقراءة فقط.

ظهور Out of Memory Error من DuckDB على VPS صغير يعني أن استعلاماً احتاج إلى ذاكرة عمل أكبر من المتاح. يفرّغ DuckDB البيانات إلى القرص عند الإمكان، لذلك وفّر له مساحة للتفريغ بفتح ملف قاعدة بيانات على القرص بدلاً من :memory:، وحدد استهلاكه باستخدام SET memory_limit = '2GB';. على خادم يشغّل خدمات أخرى، تمنع هذه الحدود استعلاماً مؤقتاً من دفع تطبيقك خارج ذاكرة RAM.

ظهور Binder Error: Referenced column "amount" not found عند الاستعلام من Parquet يعني غالباً أن مخطط الملف ليس المخطط الذي تتذكره. شغّل DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; واقرأ أسماء الأعمدة الفعلية.

كيف تختار عملياً

اسأل عن نمط الكتابة. إذا كانت هناك عمليات كتابة صغيرة كثيرة يجب أن تبقى بعد انقطاع التيار الكهربائي، فاستخدم SQLite. اسأل عن نمط القراءة. إذا كانت هناك عمليات مسح كاملة مع تجميعات على سجل تاريخي طويل، فاستخدم DuckDB. تجيب معظم الأنظمة الفعلية بنعم عن السؤالين. والاستجابة الصحيحة هي منح كل محرك الجزء الذي يجيده، بدلاً من إجبار أحدهما على أداء دور الآخر.

يجب تجنّب نقل حالة التطبيق الحية إلى DuckDB لأن أحد التقارير كان بطيئاً. كان التقرير بطيئاً بسبب طريقة ترتيب التخزين، ولذلك يكون الحل هو التصدير، وليس إعادة كتابة مسار الكتابة لديك.

FAQ

هل يمكن لـ DuckDB أن يحل محل SQLite لقاعدة بيانات تطبيقي؟

ليس لقاعدة تُجري عمليات كتابة متكررة. يفرض DuckDB قفل كتابة على ملف قاعدة البيانات بأكمله، ويسمح بعملية واحدة للقراءة والكتابة في كل مرة، وهو مُهيّأ للتغييرات المجمّعة لا لإدراج صفوف مفردة. احتفظ بالحالة المعاملاتية في SQLite، ودَع DuckDB يقرأها باستخدام ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); عند الحاجة إلى تقرير.

هل DuckDB أسرع فعلاً من SQLite في التحليلات؟

نعم، عند إجراء عمليات المسح والتجميع على جدول كبير. والسبب هو تخطيط التخزين، لا حيلة ضبط. يقرأ DuckDB الأعمدة التي يطلبها الاستعلام فقط، ويعالج القيم على دفعات، بينما يجب على SQLite اجتياز الصفوف بأكملها للوصول إلى حقل واحد. أما عند جلب صف واحد باستخدام المفتاح الأساسي، فتنقلب الأفضلية، لأن SQLite يصل إلى صفحتين، بينما يصل DuckDB إلى مساحة تخزين كل عمود.

هل أحتاج إلى قدر كبير من RAM لتشغيل DuckDB على VPS؟

لا، لكن خصص له حداً ومساحة على القرص. افتح ملف قاعدة بيانات بدلاً من :memory: حتى يتمكن DuckDB من تفريغ النتائج الوسيطة إلى القرص، ثم اضبط SET memory_limit = '2GB'; على قيمة يستطيع VPS توفيرها. من دون حد، يمكن لعملية GROUP BY كبيرة أن ترفع Out of Memory Error أو تدفع الخدمات الأخرى خارج RAM.

كيف أنقل بيانات 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 بعد كتابتها، لذلك يكفي نسخ الدليل.