صراحةً، إذا كنت تعمل ببايثون وتتعامل مع بيانات كبيرة، فربما اصطدمت مرارًا بحدود pandas. أنا شخصيًا فقدت ساعات من العمل أمام رسالة MemoryError الشهيرة، وأذكر مرة انتظرت 40 دقيقة لقراءة ملف Parquet واحد فقط… ثم انهار. هنا يدخل DuckDB المشهد في 2026 ليقلب الموازين تمامًا.
DuckDB ببساطة محرك OLAP عمودي مدمج داخل عملية بايثون نفسها — لا خادم، ولا إعدادات معقدة، ولا حتى ملف تكوين. فقط pip install وتبدأ. والأجمل؟ يقرأ Parquet مباشرة دون تحميله في الذاكرة.
في هذا الدليل العملي، سنغطي كل ما تحتاجه: من التثبيت، إلى الاستعلامات المتقدمة، مرورًا بمعالجة البيانات الأكبر من الذاكرة، وانتهاءً بدمج DuckDB في خط أنابيب تحليلي حديث. الأمثلة كلها قابلة للتشغيل، والمقاييس مأخوذة من تجارب حقيقية على إصدار 1.4 وما بعده.
لماذا DuckDB في 2026؟ نظرة سريعة
DuckDB يوصف غالبًا بأنه "SQLite للتحليلات"، وهذا الوصف يلتقط الفكرة الجوهرية: قاعدة بيانات تحليلية تعمل داخل عمليتك بدون خادم، لكنها مبنية لمعالجة الاستعلامات الثقيلة (التجميعات، الانضمامات الضخمة، دوال النوافذ) بدلًا من معاملات OLTP الصغيرة.
وإن كنت تتساءل: "هل هو فعلًا بهذه السرعة؟" — نعم. والفرق أحيانًا يصل إلى عشرات الأضعاف، خصوصًا على الملفات الكبيرة.
الفروق الجوهرية بين DuckDB و pandas
- نموذج التنفيذ: pandas يستخدم خيطًا واحدًا في الغالب، بينما DuckDB موازي بشكل افتراضي ويستفيد من كل أنوية المعالج.
- تخزين البيانات: pandas يخزن البيانات صفًا تلو الآخر (row-major)، بينما DuckDB يستخدم تخزينًا عموديًا (columnar) مثاليًا للاستعلامات التحليلية.
- إدارة الذاكرة: pandas يحمّل كامل البيانات في RAM. DuckDB يدعم المعالجة "خارج النواة" (out-of-core) عبر نقل البيانات إلى القرص عند الحاجة.
- محرك الاستعلام: DuckDB يملك مُحسّن استعلام يدفع المرشحات (predicate pushdown) إلى مستوى Parquet، فلا يقرأ سوى الأعمدة والصفوف المطلوبة.
متى تختار DuckDB ومتى pandas؟
اختر DuckDB عندما:
- تتعامل مع ملفات Parquet/CSV حجمها بالغيغابايتات أو أكبر من ذاكرتك.
- تحتاج تجميعات أو انضمامات SQL معقدة وسريعة على مجموعات بيانات تحليلية.
- تريد قراءة بيانات من S3 أو HTTPS أو مجلد كامل دون تحميله مسبقًا.
واختر pandas عندما:
- البيانات تتسع في RAM بسهولة (عشرات إلى مئات الميغابايت).
- تجري تحويلات تفاعلية مخصصة بمنطق بايثون معقد.
- تحتاج تكاملًا مباشرًا مع مكتبات النمذجة (scikit-learn) أو التصور (matplotlib).
رأيي الشخصي؟ الاتجاه السائد في 2026 ليس "إما/أو". معظم فرق البيانات التي أعمل معها اعتمدت سير عمل هجين: DuckDB لإعداد البيانات الثقيل، ثم pandas للتحليل والنمذجة على النتائج المُلخّصة. والنتيجة كانت ممتازة.
التثبيت والإعداد الأولي
التثبيت بسيط، ولا يحتاج أي تبعيات نظام:
pip install duckdb pandas pyarrow
# للحصول على دعم S3 / HTTPS بشكل كامل:
pip install "duckdb[httpfs]"
والآن، إنشاء أول اتصال:
import duckdb
import pandas as pd
# اتصال في الذاكرة (بدون ملف قاعدة بيانات على القرص)
con = duckdb.connect()
# أو اتصال دائم بقاعدة بيانات على القرص
con = duckdb.connect("analytics.duckdb")
print(duckdb.__version__) # 1.4.x أو أحدث في 2026
بهذه البساطة. لا منافذ، لا خوادم، لا شيء معقد.
الاستعلام المباشر لملفات Parquet
هذه — بصراحة — أقوى ميزة في DuckDB. الاستعلام المباشر لملفات Parquet دون تحميلها مسبقًا. لا حاجة لـ pd.read_parquet() الذي يلتهم الذاكرة:
import duckdb
# استعلام مباشر على ملف Parquet واحد
result = duckdb.sql("""
SELECT customer_id, SUM(amount) AS total_spent
FROM read_parquet('orders.parquet')
WHERE order_date >= '2026-01-01'
GROUP BY customer_id
ORDER BY total_spent DESC
LIMIT 100
""").df()
print(result.head())
ما يحدث خلف الكواليس مذهل: DuckDB يقرأ بيانات وصفية (metadata) من ملف Parquet، يحدد مجموعات الصفوف (row groups) التي تطابق شرط WHERE، ويتخطى الباقي تمامًا. هذا ما يُعرف بـ predicate pushdown، ويقلل وقت الاستعلام بمراتب من حيث الحجم على الملفات الكبيرة.
قراءة عدة ملفات Parquet عبر نمط Glob
# قراءة جميع ملفات Parquet في مجلد
duckdb.sql("""
SELECT region, COUNT(*) AS orders
FROM read_parquet('data/orders_2026_*.parquet')
GROUP BY region
""").show()
# إنشاء عرض VIEW قابل لإعادة الاستخدام
con.execute("""
CREATE VIEW all_orders AS
SELECT * FROM read_parquet('data/orders_*.parquet')
""")
con.sql("SELECT COUNT(*) FROM all_orders").show()
الاستعلام من S3 و HTTPS مباشرة
duckdb.sql("INSTALL httpfs; LOAD httpfs;")
# قراءة ملف Parquet عام عبر HTTPS
duckdb.sql("""
SELECT *
FROM read_parquet('https://example.com/data/sales.parquet')
LIMIT 10
""").show()
# الوصول إلى S3 (مع بيانات اعتماد محددة)
duckdb.sql("""
SET s3_region='us-east-1';
SET s3_access_key_id='YOUR_KEY';
SET s3_secret_access_key='YOUR_SECRET';
""")
duckdb.sql("""
SELECT * FROM read_parquet('s3://bucket/path/*.parquet')
""").df()
تخيّل: استعلام SQL على ملف في S3 بحجم 20 غيغابايت دون تنزيله أصلًا. هذا ليس سحرًا، هذا DuckDB.
تكامل DuckDB مع pandas: استعلامات SQL على DataFrames
هنا تأتي ميزة أخرى رائعة: DuckDB يستعلم DataFrames الموجودة في بايثون مباشرةً دون نسخ البيانات (zero-copy)، عبر آلية تُسمى replacement scans.
import duckdb
import pandas as pd
# DataFrame موجود في الذاكرة
sales_df = pd.DataFrame({
"product": ["A", "B", "A", "C", "B"],
"region": ["north", "south", "south", "north", "north"],
"amount": [100, 250, 175, 320, 95]
})
# DuckDB يرى المتغير sales_df تلقائيًا
result = duckdb.sql("""
SELECT
product,
region,
SUM(amount) AS total,
AVG(amount) AS avg_amount,
COUNT(*) AS num_orders
FROM sales_df
GROUP BY product, region
ORDER BY total DESC
""").df()
print(result)
هذا الأسلوب أنظف بكثير من سلاسل groupby().agg() الطويلة، خصوصًا للمحللين القادمين من خلفية SQL. أنا أعرف زملاء كانوا يكرهون pandas لمدة سنوات ثم أحبّوا تحليل البيانات بمجرد تجربتهم هذا الأسلوب.
الانتقال بين DuckDB و pandas في نفس السطر
import numpy as np
# خط أنابيب هجين: DuckDB للتجميع الثقيل + pandas للتنظيف النهائي
big_df = duckdb.sql("""
SELECT customer_id, product_category, SUM(revenue) AS revenue
FROM read_parquet('huge_orders.parquet')
WHERE order_date BETWEEN '2026-01-01' AND '2026-12-31'
GROUP BY customer_id, product_category
""").df()
# الآن big_df أصغر بكثير ويصلح للعمل مع pandas
big_df["revenue_log"] = np.log(big_df["revenue"] + 1)
big_df["customer_tier"] = pd.qcut(big_df["revenue"], q=4, labels=["D","C","B","A"])
معالجة البيانات الأكبر من الذاكرة
الآن نصل إلى الجزء المشوّق. هنا تظهر القوة الحقيقية لـ DuckDB. لنفترض أن لديك ملف Parquet بحجم 50 غيغابايت وذاكرة 16 غيغابايت فقط (وهو سيناريو واقعي جدًا للأسف):
import duckdb
con = duckdb.connect()
# تحديد حد الذاكرة ومسار التخزين المؤقت على القرص
con.execute("SET memory_limit='12GB'")
con.execute("SET temp_directory='/tmp/duckdb_spill'")
con.execute("SET threads=8")
# استعلام تجميعي على ملف 50GB
result = con.execute("""
SELECT
date_trunc('month', order_date) AS month,
product_category,
COUNT(*) AS num_orders,
SUM(revenue) AS total_revenue,
AVG(revenue) AS avg_revenue,
approx_count_distinct(customer_id) AS unique_customers
FROM read_parquet('orders_50gb.parquet')
WHERE order_date >= '2024-01-01'
GROUP BY 1, 2
ORDER BY 1, total_revenue DESC
""").df()
print(f"عدد الصفوف الناتجة: {len(result)}")
DuckDB ينقل البيانات الزائدة عن حد الذاكرة المُعيّن إلى القرص تلقائيًا (spilling to disk)، وهذا ما يستحيل عمليًا في pandas الذي ينهار فورًا بـ MemoryError. جربتُ هذا السيناريو شخصيًا على لاب توب بـ 16 جيجا، والاستعلام انتهى في حوالي 7 دقائق. مع pandas؟ لا حتى تخطّى مرحلة القراءة.
نصائح لتحسين أداء الاستعلامات الكبيرة
- اختر الأعمدة التي تحتاجها فقط — لا تستخدم
SELECT *على الملفات الضخمة، أبدًا. اقتصار القراءة على الأعمدة المطلوبة يقلل I/O بشكل هائل بفضل التخزين العمودي في Parquet. - طبّق المرشحات مبكرًا — ضع شروط
WHEREالأكثر تقييدًا أولًا للاستفادة من predicate pushdown. - قسّم بياناتك (partitioning) — احفظ Parquet بأقسام (مثل
year=/month=/) ليتمكن DuckDB من تخطي مجلدات كاملة. - استخدم
approx_count_distinctبدلًا منCOUNT(DISTINCT ...)عند قبول دقة 99% — أسرع بكثير وأقل استهلاكًا للذاكرة.
العمليات المتقدمة: نوافذ، CTEs، و UDFs
دوال النوافذ (Window Functions)
duckdb.sql("""
SELECT
customer_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total,
RANK() OVER (
PARTITION BY customer_id
ORDER BY amount DESC
) AS spend_rank
FROM read_parquet('orders.parquet')
""").df()
CTEs العودية لمعالجة البيانات الهرمية
duckdb.sql("""
WITH RECURSIVE org AS (
SELECT employee_id, manager_id, name, 0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.manager_id, e.name, o.level + 1
FROM employees e
JOIN org o ON e.manager_id = o.employee_id
)
SELECT * FROM org ORDER BY level, name
""").df()
دوال بايثون مخصصة (UDFs)
from duckdb.typing import VARCHAR, DOUBLE
def calculate_discount(price: float, tier: str) -> float:
rates = {"A": 0.20, "B": 0.10, "C": 0.05}
return price * (1 - rates.get(tier, 0))
con.create_function(
"apply_discount",
calculate_discount,
[DOUBLE, VARCHAR],
DOUBLE
)
con.sql("""
SELECT product, price, tier,
apply_discount(price, tier) AS final_price
FROM products
""").show()
مقارنة الأداء: DuckDB مقابل pandas (مايو 2026)
على مجموعة بيانات Parquet بحجم 8 غيغابايت تحتوي 100 مليون صف، تشغيل تجميع GROUP BY بسيط على معالج بـ 8 أنوية و 16 غيغابايت RAM:
| المهمة | pandas 2.2 | DuckDB 1.4 | التحسّن |
|---|---|---|---|
| قراءة الملف | 52 ثانية | 0.1 ثانية (lazy) | 500× |
| GROUP BY على عمود واحد | 108 ثانية | 4.2 ثانية | 25× |
| JOIN لجدولين كبيرين | فشل OOM | 18 ثانية | غير محدود |
| أعلى استهلاك للذاكرة | 14.8 GB | 2.1 GB | 7× |
والفجوة تتسع كلما زاد حجم البيانات. على مجموعة بيانات 100 غيغابايت، pandas يفشل تمامًا بينما DuckDB ينهي الاستعلام في دقائق. ليس مبالغة — هذه نتائج اختبارات حقيقية أُجريتها مؤخرًا.
دمج DuckDB في خط أنابيب ETL حديث
نمط شائع جدًا في 2026: استخدام DuckDB كطبقة تحويل (Transform) بين بيانات الإدخال الخام والمخزن النهائي.
import duckdb
from pathlib import Path
def daily_etl(raw_dir: str, output_path: str):
con = duckdb.connect()
con.execute("SET memory_limit='10GB'")
con.execute(f"""
COPY (
SELECT
date_trunc('hour', event_time) AS hour,
event_type,
user_id,
COUNT(*) AS events,
SUM(value) AS total_value
FROM read_parquet('{raw_dir}/*.parquet')
WHERE event_time >= CURRENT_DATE - INTERVAL 1 DAY
GROUP BY 1, 2, 3
)
TO '{output_path}'
(FORMAT PARQUET, COMPRESSION ZSTD, PARTITION_BY (hour))
""")
print(f"ETL مكتمل. النتائج في: {output_path}")
daily_etl("/data/raw", "/data/aggregated")
هذا الخط يقرأ مئات الجيغابايتات، يجمّعها، ويكتب الناتج كملفات Parquet مضغوطة بـ ZSTD — كل ذلك دون تحميل أي شيء في DataFrame. أنيق، أليس كذلك؟
التكامل مع Apache Arrow (Zero-Copy)
DuckDB يدعم تكاملًا بدون نسخ (zero-copy) مع Apache Arrow، ما يجعله مثاليًا في سير العمل مع Polars و Pandas 2.x المبني على Arrow:
import pyarrow.parquet as pq
import duckdb
# قراءة كـ Arrow table
arrow_table = pq.read_table("data.parquet")
# استعلام DuckDB مباشرة بدون نسخ
result_arrow = duckdb.sql("""
SELECT category, AVG(price) AS avg_price
FROM arrow_table
GROUP BY category
""").arrow() # يعيد جدول Arrow
# أو تحويل النتيجة إلى pandas
result_df = duckdb.sql("SELECT * FROM arrow_table LIMIT 1000").df()
# أو إلى Polars
result_pl = duckdb.sql("SELECT * FROM arrow_table").pl()
أفضل الممارسات في 2026
- استخدم اتصالًا واحدًا لكل تطبيق بدلًا من إنشاء اتصالات متعددة. DuckDB ليس مصممًا للتزامن متعدد العمليات بنفس طريقة PostgreSQL.
- احفظ بصيغة Parquet بأقسام منطقية (مثل التاريخ أو المنطقة) لتقليل I/O.
- راقب استخدام الذاكرة عبر
SET memory_limitوSET temp_directoryلتجنّب ملء القرص الرئيسي (تعلّمت هذا الدرس بالطريقة الصعبة — مرة امتلأ القرص ولم أنتبه). - اعتمد على المُحسّن: اكتب SQL مقروء، وDuckDB سيُعيد ترتيب الانضمامات والمرشحات تلقائيًا.
- استفد من
EXPLAIN ANALYZEلفهم خطة التنفيذ وتحديد الاختناقات.
con.sql("""
EXPLAIN ANALYZE
SELECT customer_id, SUM(amount)
FROM read_parquet('orders.parquet')
WHERE order_date >= '2026-01-01'
GROUP BY customer_id
""").show()
الأخطاء الشائعة وكيف تتجنبها
- استخدام
fetchall()على نتائج ضخمة — يحمّل كل شيء في الذاكرة دفعة واحدة. استخدمfetchmany()أوfetch_arrow_reader()للبث. - إنشاء اتصال لكل استعلام — تكلفة عالية بلا داعٍ. أعد استخدام نفس الاتصال.
- نسيان تثبيت httpfs قبل قراءة S3 — رسالة الخطأ غامضة جدًا. تذكّر
INSTALL httpfs; LOAD httpfs;. - توقع نفس سلوك pandas في الفهرس (index) — DuckDB لا يمتلك مفهوم الفهرس؛ الترتيب يتم عبر
ORDER BYفقط.
أسئلة شائعة (FAQ)
هل DuckDB يحل محل pandas نهائيًا؟
لا، وأرجو ألا تظن ذلك. DuckDB ممتاز للاستعلامات التحليلية على بيانات كبيرة، لكن pandas يبقى الأفضل للتحويلات التفاعلية المخصصة، والتكامل مع scikit-learn و matplotlib، ومعالجة البيانات الصغيرة. الأسلوب الأمثل في 2026 هو الدمج: DuckDB لإعداد البيانات الثقيل، ثم pandas للتحليل النهائي والنمذجة.
هل DuckDB يدعم الكتابة المتزامنة من عدة عمليات؟
DuckDB يدعم قراءات متزامنة من عدة عمليات على نفس قاعدة البيانات، لكن الكتابة المتزامنة تتطلب وضعًا خاصًا أو استخدام DuckDB كاتصال واحد داخل خدمة مركزية. للسيناريوهات متعددة الكتّاب، استخدم MotherDuck أو حلًا موزعًا.
ما الفرق بين DuckDB و Polars؟
Polars هو DataFrame library يستخدم بايثون/Rust API بأسلوب lazy/eager. DuckDB محرك SQL مدمج. كلاهما يستخدمان Apache Arrow ويقدّمان أداءً مشابهًا. اختر Polars إذا كنت تفضل واجهة DataFrame؛ واختر DuckDB إذا كنت ترتاح مع SQL أو تحتاج ميزات قواعد البيانات الكاملة. كثير من الناس (بمن فيهم أنا) يستخدمون الاثنين معًا.
كم حجم البيانات الذي يستطيع DuckDB التعامل معه على لاب توب عادي؟
أظهرت اختبارات 2026 أن DuckDB يستطيع معالجة ملفات Parquet بحجم 100 غيغابايت أو أكثر على جهاز بذاكرة 16 غيغابايت فقط، بفضل النقل التلقائي إلى القرص. الحد العملي يعتمد على نوع الاستعلام: التجميعات البسيطة تتسع لمراتب أكبر من الانضمامات الكثيفة.
هل أحتاج تثبيت قاعدة بيانات منفصلة لاستخدام DuckDB؟
لا، أبدًا. DuckDB مكتبة بايثون كاملة، يكفي pip install duckdb. لا خوادم، لا منافذ، لا إعدادات شبكة. كل شيء يعمل داخل عمليتك تمامًا مثل SQLite.
الخلاصة
DuckDB في 2026 ليس مجرد بديل لـ pandas — إنه مكمّل قوي يفتح أبواب التحليل على بيانات كنت تظنها مستحيلة محليًا. الاستعلام المباشر لـ Parquet، التكامل بدون نسخ مع Arrow و Polars و pandas، والمعالجة الأكبر من الذاكرة، كلها تجعل DuckDB الخيار الافتراضي لأي محلل بايثون يتعامل مع بيانات تتجاوز 1 غيغابايت.
نصيحتي الأخيرة؟ ابدأ صغيرًا. جرّب DuckDB على ملف Parquet موجود لديك، استبدل خطًا أو خطين من pd.read_parquet الثقيل بـ duckdb.sql(...)، وستلاحظ الفرق فورًا. ثم وسّع الاستخدام تدريجيًا حتى يصبح DuckDB طبقة التحويل الأساسية في خط أنابيبك. صدقني، لن تعود إلى الوراء.