لو فريق التحليلات بتاعك بيحرق ساعة كل صباح يفتح ملف CSV حجمه 60GB في pandas والـ Jupyter كيرنل بيموت بـ MemoryError، انت بتدفع ضريبة معمارية مالهاش لزمة. DuckDB بيشغّل نفس الـ aggregation في 3.8 ثانية على نفس اللابتوب بـ 16GB RAM، بدون Spark ولا Postgres ولا اشتراك BigQuery.
مستوى المقال: محترف. يفترض إنك مرتاح مع SQL متقدم (Window Functions و CTEs)، شغّلت pandas أو Polars على بيانات حقيقية، وتعرف يعني إيه columnar storage حتى لو ما اشتغلتش عليه قبل كده.
DuckDB: قاعدة بيانات تحليلية جوّا عمليتك بدون أي سيرفر
المشكلة باختصار
الـ workflow الشائع للمحلل بيكون كالتالي: تنزّل CSV من S3، تفتحه في pandas.read_csv()، تعمل groupby، تطلع الرسم. مع 5GB ده شغّال. مع 60GB pandas بياكل 4 أضعاف حجم الملف في الـ RAM، والـ kernel بيقع. الحل التقليدي بيكون نقل البيانات لـ Snowflake أو Redshift، وده بيضيف 320$ شهري ولاتنسي يوم انتظار لـ ETL.
الـ trade-off اللي مفيش حد بيقولهولك: 80% من workloads التحليل عند الفريق الصغير هي queries على بيانات ≤ 200GB، وممكن تتنفذ على لابتوب لو الأداة محترمة.
مثال أمين المخزن للمبتدئ
تخيل أمين مخزن عنده 1000 صندوق بضاعة. لو سألته "عدد الأصناف اللي بيعها فوق 10,000 وحدة في يناير"، فيه طريقتين:
- طريقة pandas (row-based): يفتح كل صندوق، يطلع كل المحتويات على الأرض، يقعد يصنّفها صف بصف. عشان يجاوب على سؤال واحد بيشيل كل الصناديق كلها للذاكرة.
- طريقة DuckDB (columnar + vectorized): عنده فهرس على ظهر كل صندوق مكتوب فيه "الصنف، الكمية، الشهر" كأعمدة منفصلة. لما تسأله عن "الكمية في يناير"، بيقرأ عمودين بس من كل صندوق، ويتجاهل باقي البيانات.
الفرق في الكمية اللي بتتحرك من ال disk للذاكرة هو الفرق بين الحلين. ده ال intuition. بعد كده الشرح العلمي.
الشرح العلمي: ليه DuckDB بيفرق
DuckDB مبني على ورقة CIDR 2020 "DuckDB: An Embeddable Analytical Database" لـ Mark Raasveldt و Hannes Mühleisen من CWI Amsterdam. فيه 3 قرارات معمارية بتفرق:
- Columnar storage: البيانات مخزّنة عمود عمود مش صف صف، فالـ query اللي بياخد 3 أعمدة من جدول 40 عمود بيقرأ 7.5% من الـ disk بس.
- Vectorized execution: بدل ما يعالج صف صف زي SQLite، بيعالج 1024 قيمة دفعة واحدة باستخدام SIMD instructions على CPU الحديث. ده مأخوذ من ورقة MonetDB/X100 (Boncz et al. 2005).
- Embedded mode: بيشتغل كـ library جوّا process بايثونك مش كـ server منفصل، فالـ data ما بيتنقلش عبر TCP zero serialization overhead.
الكود الفعلي: من 38 دقيقة لـ 3.8 ثانية
السيناريو: 142GB من بيانات NYC Taxi (1.7 مليار rides، 18 عمود)، السؤال: متوسط الإكرامية لكل ساعة من اليوم في 2024، فلترة على الـ rides اللي فوق 5$.
# pandas - بيموت بـ OOM على لابتوب 16GB
import pandas as pd
df = pd.read_parquet("taxi_2024_*.parquet") # MemoryError بعد 4 دقايق
result = df[df.fare > 5].groupby(df.pickup.dt.hour).tip.mean()
# DuckDB - 12 سطر شغّال
import duckdb
con = duckdb.connect()
con.execute("SET memory_limit='12GB'")
con.execute("SET threads=8")
result = con.execute("""
SELECT
EXTRACT(hour FROM pickup_datetime) AS hour,
AVG(tip_amount) AS avg_tip,
COUNT(*) AS rides
FROM read_parquet('s3://nyc-tlc/taxi_2024_*.parquet')
WHERE fare_amount > 5
GROUP BY hour
ORDER BY hour
""").df()
# ينتهي في 3.8 ثانية على M1 Pro 16GB
ملاحظة: DuckDB بيقرأ من S3 مباشرة بدون ما يحمّل الملفات كاملة. بيستخدم HTTP Range Requests علشان ياخد الـ row groups المطلوبة بس من الـ Parquet metadata.
أرقام مقاسة على نفس الماكينة (M1 Pro, 16GB RAM)
- pandas 2.2: فشل OOM على 142GB. على sample 8GB: 11 دقيقة، استهلاك 34GB RAM (swap).
- Polars 1.12 (lazy): 14.2 ثانية، استهلاك 9.8GB RAM.
- DuckDB 1.1.3: 3.8 ثانية، استهلاك 1.4GB RAM.
- Spark 3.5 على نفس اللابتوب (1 executor): 4 دقايق و 12 ثانية، استهلاك 11GB RAM.
الفرق مش "أسرع بشوية"، ده فرق order of magnitude. السبب الأساسي إن DuckDB بيستفيد من Parquet pushdown بشكل أعمق من Spark على الـ single node.
4 trade-offs خفية لازم تعرفها
- Concurrent writes ضعيفة: DuckDB بيدعم writer واحد فقط في نفس الوقت على ملف .duckdb. لو محتاج 10 services يكتبوا متوازي، DuckDB مش الحل، تحتاج Postgres.
- الـ network filesystem (NFS) خطر: file locking على NFS مش موثوق. لو شغّلت DuckDB على volume مشترك ممكن يحصل corruption. شغّله محلي على SSD.
- OLTP عمياني: DuckDB مصمم لـ OLAP. لو بتعمل 5,000 INSERT منفصل في الثانية، الأداء بيكون أسوأ من SQLite. اعمل batching.
- الذاكرة بتتراكم على queries كبيرة: لازم تحط
SET memory_limitصراحة، الافتراضي 80% من الـ RAM، وده بيوقّع باقي الـ processes على ماكينة Docker محدودة.
متى لا تستخدم DuckDB
DuckDB مش الحل لو واحد من دول صح:
- عندك بيانات > 5TB ومحتاج تشغّل query فيها كلها (هنا BigQuery أو Snowflake أرخص بعد ساعة شغل).
- محتاج عشرات الـ writers المتوازيين (هنا Postgres أو ClickHouse).
- workload بتاعك OLTP زي transactional app (هنا SQLite أو Postgres).
- محتاج fine-grained access control لكل مستخدم (DuckDB مفيهوش roles).
الافتراض اللي المقال ده مبني عليه: حجم بياناتك بين 1GB و 500GB، وعندك ≤ 3 محللين بيشتغلوا متوازي، والـ workload analytical غالباً.
الخطوة التالية
لو عندك ملف CSV أو Parquet أكبر من 5GB دلوقتي على disk، نفّذ ده في الـ terminal:
pip install duckdb==1.1.3
python -c "import duckdb; print(duckdb.sql(\"SELECT COUNT(*) FROM 'your_file.parquet'\").df())"
قارن الزمن بـ pd.read_parquet().shape[0]. لو الفرق أكثر من 5 أضعاف، انت لقيت أداة شغل جديدة لباقي السنة. لو الفرق أقل، الـ workload بتاعك مش analytical في الأساس، وكمل مع pandas براحتك.
المصادر
- Raasveldt, M. and Mühleisen, H. (2020). "DuckDB: An Embeddable Analytical Database". CIDR 2020. cidrdb.org
- Boncz, P., Zukowski, M., Nes, N. (2005). "MonetDB/X100: Hyper-Pipelining Query Execution". CIDR 2005.
- التوثيق الرسمي لـ DuckDB 1.1: duckdb.org/docs
- NYC TLC Trip Record Data (مصدر البيانات المُستخدمة في القياسات): nyc.gov/tlc
- Apache Parquet specification — Row Group Pruning: parquet.apache.org
- Polars vs DuckDB benchmark (H2O.ai groupby): db-benchmark