اگر در سال ۲۰۲۶ با دادههای بزرگ در پایتون دست و پنجه نرم میکنید و pandas شما را روی ۱۰۰ میلیون ردیف زمین میگذارد، احتمالاً جواب همان چیزی است که این روزها از زبان خیلی از تیمهای داده میشنوید: DuckDB. این موتور تحلیلی درونفرآیندی (in-process) ستونی، در دو سال گذشته آرام آرام جای خودش را در استک مدرن داده پایتون باز کرده و حالا با انتشار نسخه ۱.۵.۲ در آوریل ۲۰۲۶ و بیش از ۳۷٬۵۰۰ ستاره گیتهاب، تقریباً به یکی از ابزارهای پیشفرض دانشمندان داده تبدیل شده است.
راستش را بخواهید، اولین باری که DuckDB را امتحان کردم، باور نکردم همان کوئری GROUP BY که در pandas دو دقیقه طول میکشید، در ۴ ثانیه تمام شد. در این راهنما همان مسیر را با هم طی میکنیم: از نصب و اولین کوئری تا کوئری مستقیم روی Parquetهای چند گیگابایتی بدون لود کردن در RAM، ترکیب هوشمندانه با pandas و Polars، و چند نکته بهینهسازی که در بنچمارکهای واقعی تفاوت ۵۰ برابری میسازد.
DuckDB چیست و چرا در ۲۰۲۶ همه دارند دربارهاش حرف میزنند؟
خب، خیلی ساده بگویم: DuckDB یک سیستم مدیریت پایگاه داده تحلیلی (OLAP) درونفرآیندی است که با ++C نوشته شده و کاملاً درون فرآیند پایتون شما اجرا میشود. بدون سرور جداگانه، بدون احراز هویت، بدون پیکربندی شبکه. اگر SQLite را برای کارهای تراکنشی میشناسید، DuckDB دقیقاً همان «SQLite برای تحلیل داده» است — همان فلسفه، فقط با هدف متفاوت.
تفاوت بنیادی DuckDB با pandas
تفاوت معماری این دو ابزار اساسی است، و دانستنش کلید استفاده درست از هرکدام:
- pandas یک کتابخانه DataFrame است که داده را به صورت ردیفی در حافظه نگه میدارد و عملیات را روی یک ترد پایتون اجرا میکند.
- DuckDB یک موتور SQL برداری (vectorized) ستونی است با پشتیبانی از اجرای موازی، فشار دادن predicate به سطح ذخیرهسازی، و توانایی پردازش دادههای بزرگتر از RAM.
نتیجه عملی این تفاوت در بنچمارکهای ۲۰۲۶ کاملاً روشن است: یک GROUP BY روی ۱۰۰ میلیون ردیف ممکن است در pandas بیش از ۱۰۰ ثانیه طول بکشد یا کلاً برنامه را کرش کند، در حالی که DuckDB همان کوئری را زیر ۳۰ ثانیه تمام میکند. روی یک دیتاست فروش با ۱ میلیون ردیف هم اختلاف به ۵۰ برابر میرسد، چون DuckDB از یک پاس AVX-512 استفاده میکند، در حالی که pandas همچنان دارد ردیف به ردیف کار میکند.
کِی DuckDB، کِی pandas؟
یک نکته مهم را همین اول بگویم تا سوءتفاهمی پیش نیاید: DuckDB جایگزین pandas نیست، مکمل آن است. این جدول کوچک کمک میکند تصمیم درستی بگیرید:
- برای دادههای کوچک تا متوسط (کمتر از چند صد مگابایت) که در RAM جا میشوند: pandas با اکوسیستم بالغش (scikit-learn، matplotlib، seaborn) همچنان بهترین انتخاب برای پاکسازی تعاملی، مهندسی ویژگی و یکپارچگی با ML است.
- برای دادههای بزرگ، Parquetهای چند گیگابایتی، joinهای سنگین، و agregationهای پیچیده: DuckDB با چندنخی خودکار، کوئری مستقیم روی فایل، و توانایی out-of-core، چندین برابر سریعتر و کممصرفتر است.
- روند ۲۰۲۶ — استک هیبرید: ترکیب «DuckDB + Polars + pandas» دارد به استاندارد تبدیل میشود. DuckDB برای ingestion و آمادهسازی، Polars برای تبدیلهای میانی، و pandas برای مدلسازی نهایی.
نصب و راهاندازی DuckDB در پایتون
نصب DuckDB سادهترین قسمت ماجراست. صادقانه بگویم، بهندرت ابزاری دیدهام که نصبش اینقدر بدون درد باشد. فقط یک دستور pip و خبری از سرویس پسزمینه نیست:
pip install duckdb
اگر کاربر conda هستید:
conda install python-duckdb -c conda-forge
DuckDB به پایتون ۳.۹ یا بالاتر نیاز دارد. برای کار با Parquet، CSV، JSON و ادغام با pandas/Polars/Arrow هیچ بسته اضافهای نمیخواهد — همه چیز توی همان یک نصب میآید.
دو نوع اتصال: درونحافظهای و فایلی
DuckDB دو حالت اتصال دارد و انتخاب بین این دو به ماهیت کارتان بستگی دارد:
import duckdb
# اتصال درونحافظهای (موقت — با پایان سشن از بین میرود)
con = duckdb.connect()
# اتصال فایلی (پایدار — داده در دیسک ذخیره میشود)
con = duckdb.connect("analytics.duckdb")
برای کارهای اکتشافی و تحلیل ad-hoc، اتصال درونحافظهای کفایت میکند. ولی اگر پروژهای دارید که نیاز به ذخیرهسازی پایدار است (مثلاً یک data mart کوچک یا کش تحلیلی)، اتصال فایلی را انتخاب کنید.
اولین کوئری SQL در DuckDB
سادهترین راه اجرای کوئری، تابع duckdb.sql() است که یک رابطه (Relation) برمیگرداند. نکته جالب اینکه این یک نمایش نمادین از کوئری است که تا وقتی نتیجه را درخواست نکردهاید اجرا نمیشود — همان lazy evaluation معروف:
import duckdb
# اولین کوئری
duckdb.sql("SELECT 42 AS answer").show()
# گرفتن نتیجه به فرمتهای مختلف
result_list = duckdb.sql("SELECT 42").fetchall() # لیست پایتون
result_df = duckdb.sql("SELECT 42").df() # pandas DataFrame
result_pl = duckdb.sql("SELECT 42").pl() # Polars DataFrame
result_arrow = duckdb.sql("SELECT 42").arrow() # Arrow Table
result_np = duckdb.sql("SELECT 42").fetchnumpy() # آرایه NumPy
این انعطاف در فرمت خروجی، به نظر من یکی از قدرتمندترین ویژگیهای DuckDB است. میتوانید از SQL برای کوئری استفاده کنید و نتیجه را مستقیماً به فرمت دلخواه اکوسیستم پایتون خود تبدیل کنید — هیچجای دیگر چنین چیزی به این روانی نمیبینید.
کوئری مستقیم روی pandas DataFrame با SQL
یکی از جذابترین قابلیتهای DuckDB این است که میتوانید روی DataFrameهای pandas که در حافظه پایتون دارید، مستقیماً SQL بزنید. بدون register دستی، بدون کپی داده، و عملاً بدون سربار. اسم این قابلیت replacement scan است:
import duckdb
import pandas as pd
# ساخت یک DataFrame نمونه
sales_df = pd.DataFrame({
'order_id': range(1, 11),
'product': ['کتاب', 'لپتاپ', 'هدفون', 'کتاب', 'موس',
'کیبورد', 'لپتاپ', 'کتاب', 'هدفون', 'مانیتور'],
'amount': [50, 1200, 80, 45, 25, 60, 1500, 30, 90, 350],
'region': ['تهران', 'اصفهان', 'تهران', 'مشهد', 'تهران',
'شیراز', 'اصفهان', 'تهران', 'مشهد', 'اصفهان']
})
# DuckDB به طور خودکار sales_df را میبیند — نیازی به register نیست
query = '''
SELECT
region,
product,
SUM(amount) AS total_sales,
COUNT(*) AS order_count,
AVG(amount) AS avg_order
FROM sales_df
WHERE amount > 40
GROUP BY region, product
ORDER BY total_sales DESC
'''
result = duckdb.sql(query).df()
print(result)
چرا این قابلیت اهمیت دارد؟ چون میتوانید قدرت SQL برای aggregation و join را با راحتی pandas برای دستکاری ستونها ترکیب کنید. کوئری بالا در pandas به چند خط groupby و agg با dict پیچیده نیاز دارد، اما در SQL هم خواناست هم سریع.
کوئری مستقیم روی Parquet بدون لود کردن در حافظه
این بخش جایی است که DuckDB واقعاً میدرخشد و pandas را کاملاً پشت سر میگذارد. باور کنید یا نه، DuckDB میتواند یک Parquet چند گیگابایتی را مستقیماً کوئری بزند بدون اینکه حتی یک بایت اضافی را وارد RAM کند:
import duckdb
# کوئری مستقیم روی فایل Parquet — بدون لود
sql = """
SELECT pickup_borough, AVG(fare_amount) AS avg_fare
FROM read_parquet('yellow_tripdata_2026-03.parquet')
WHERE trip_distance > 1
GROUP BY pickup_borough
ORDER BY avg_fare DESC
"""
result = duckdb.sql(sql).df()
چرا این کوئری ممکن است حتی روی یک فایل ۲ گیگابایتی در چند ثانیه تمام شود؟ به سه بهینهسازی کلیدی برمیگردد:
- Column pruning: DuckDB فقط ستونهایی را میخواند که در کوئری استفاده شدهاند. اگر فایل ۵۰ ستون داشته باشد و شما فقط ۳ ستون بخواهید، حدود ۹۴٪ از I/O صرفهجویی میشود.
- Predicate pushdown: شرط
WHERE trip_distance > 1به سطح اسکن Parquet فشار داده میشود؛ DuckDB از metadata موجود در footer فایل استفاده میکند تا row groupهایی که حتماً مطابقت ندارند را کلاً رد کند. - اجرای موازی: DuckDB به صورت خودکار از همه هستههای CPU استفاده میکند. نیازی نیست خودتان کد multiprocessing بنویسید (که خدا میداند چند ساعت از وقت همه ما را گرفته).
کوئری روی چند فایل با glob pattern
اگر دادههایتان ماهانه پارتیشن شدهاند، DuckDB میتواند دهها (یا حتی صدها) فایل را در یک کوئری به عنوان یک جدول مجازی ببیند:
sql = """
SELECT
DATE_TRUNC('month', tpep_pickup_datetime) AS month,
COUNT(*) AS trips,
SUM(total_amount) AS revenue
FROM read_parquet('yellow_tripdata_2026-*.parquet')
GROUP BY month
ORDER BY month
"""
yearly_summary = duckdb.sql(sql).df()
ساخت View برای کوئریهای مکرر
اگر قرار است چندین کوئری روی یک مجموعه فایل بزنید، یک view بسازید تا مسیر فایل را مدام تکرار نکنید (و شب راحت بخوابید):
con = duckdb.connect()
con.execute(
"CREATE VIEW taxi_trips AS "
"SELECT * FROM read_parquet('yellow_tripdata_2026-*.parquet')"
)
# حالا taxi_trips مثل یک جدول معمولی رفتار میکند
con.sql("SELECT COUNT(*) FROM taxi_trips WHERE passenger_count > 4").show()
con.sql("SELECT AVG(tip_amount) FROM taxi_trips").show()
پردازش دادههای بزرگتر از RAM (Out-of-Core)
یکی از قدرتمندترین قابلیتهای DuckDB، پشتیبانی شفاف از پردازش دادههای بزرگتر از حافظه است. اگر کوئری شما به حافظهای بیش از RAM موجود نیاز داشته باشد، DuckDB به صورت خودکار دادههای میانی را به دیسک سرریز (spill) میکند و کوئری را، به جای کرش وحشتناک، با کمی کاهش سرعت تکمیل میکند.
این یعنی روی یک لپتاپ با ۱۶ گیگابایت RAM، میتوانید روی یک دیتاست ۱۰۰ گیگابایتی کار کنید. (بله، خودم هم اولین بار باور نکردم.) برای کنترل دقیقتر:
con = duckdb.connect("analytics.duckdb")
# تعیین مسیر temp برای spill (پیشفرض: کنار فایل دیتابیس)
con.execute("SET temp_directory = '/path/to/fast/ssd/duckdb_temp'")
# محدود کردن مصرف RAM (مفید روی سیستمهای مشترک)
con.execute("SET memory_limit = '8GB'")
# تنظیم دستی تعداد ترد
con.execute("SET threads = 8")
یک نکته از تجربه شخصی: قرار دادن دایرکتوری temp روی یک SSD سریع، تفاوت بزرگی در سرعت کوئریهای out-of-core ایجاد میکند. روی HDD این کار کار میکند، اما لذتبخش نیست.
استک هیبرید ۲۰۲۶: DuckDB + Polars + pandas
روند غالب در ۲۰۲۶ این نیست که یک ابزار را برای همه چیز استفاده کنید، بلکه ترکیب نقاط قوت هر سه ابزار است. در یک پایپلاین تحلیلی واقعی، این تقسیم کار به طرز عجیبی خوب جواب میدهد:
import duckdb
import polars as pl
# مرحله ۱: DuckDB برای ingestion سنگین و فیلتر اولیه روی فایل خام
duck_con = duckdb.connect()
ingest_sql = """
SELECT customer_id, product_id, order_date, amount, quantity
FROM read_parquet('orders_*.parquet')
WHERE order_date >= '2026-01-01'
AND amount > 0
"""
duck_result = duck_con.sql(ingest_sql)
# تبدیل به Polars بدون کپی داده (از طریق Arrow)
pl_df = duck_result.pl()
# مرحله ۲: Polars برای feature engineering با lazy execution
features = (
pl_df.lazy()
.with_columns([
(pl.col("amount") / pl.col("quantity")).alias("unit_price"),
pl.col("order_date").dt.weekday().alias("dow"),
pl.col("amount").rolling_mean(window_size=7).over("customer_id").alias("ma7"),
])
.filter(pl.col("unit_price") < pl.col("unit_price").quantile(0.99))
.collect()
)
# مرحله ۳: pandas برای مدلسازی با scikit-learn
pd_df = features.to_pandas()
from sklearn.ensemble import RandomForestRegressor
X = pd_df[['unit_price', 'dow', 'ma7']].fillna(0)
y = pd_df['amount']
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X, y)
هر مرحله از ابزاری استفاده میکند که در آن کار قویترین است: DuckDB برای SQL declarative و فیلترهای روی فایل، Polars برای تبدیلهای موازی با lazy plan، و pandas برای پل به scikit-learn و کتابخانههای ML. ساده، شفاف، و مهمتر از همه، سریع.
پنج نکته کلیدی بهینهسازی عملکرد در DuckDB
-
هرگز
SELECT *روی Parquetهای بزرگ نزنید. فقط ستونهای موردنیاز را بنویسید تا column pruning فعال شود. این یک قانون بهتنهایی میتواند کوئریتان را ۱۰ برابر سریعتر کند. - فیلترهای WHERE را تا حد ممکن زود اعمال کنید. DuckDB از این فیلترها برای رد کردن row groupهای کامل در Parquet استفاده میکند. اگر دادهتان روی ستونی sort یا partition شده باشد که در WHERE استفاده میشود، صرفهجویی I/O واقعاً چشمگیر خواهد بود.
-
برای دیتاستهای بزرگ، Parquet را پارتیشن کنید. اگر ۹۹٪ کوئریهایتان روی یک ماه خاص است، فایلها را به فرمت
year=2026/month=03/data.parquetسازماندهی کنید. DuckDB از Hive partitioning به طور خودکار پشتیبانی میکند — هیچ پیکربندی اضافهای نمیخواهد. -
تعداد ترد را تنظیم کنید. پیشفرض DuckDB استفاده از همه هستههاست. اگر کنار فرآیندهای دیگر اجرا میشود، با
SET threads = 4کنترل دستی بگیرید (مخصوصاً روی سرورهای اشتراکی). -
برای کوئریهای تکرارشونده از
duckdb.connect(file)استفاده کنید. اتصال فایلی به DuckDB اجازه میدهد آماری از داده ذخیره کند که کوئریهای بعدی را به طرز محسوسی سریعتر میکند.
یک مثال کامل از ابتدا تا انتها
برای جمعبندی، بیایید یک سناریوی واقعی را با هم پیاده کنیم: تحلیل ۱۲ ماه داده فروش که هر ماه در یک فایل Parquet جدا است، حجم کل ۸ گیگابایت، و RAM ما فقط ۸ گیگ. (بله، روی یک لپتاپ معمولی.)
import duckdb
con = duckdb.connect("sales_analytics.duckdb")
con.execute("SET memory_limit = '6GB'")
# ساخت view بدون کپی داده
view_sql = """
CREATE OR REPLACE VIEW sales AS
SELECT * FROM read_parquet('sales/year=2026/month=*/data.parquet',
hive_partitioning = true)
"""
con.execute(view_sql)
# تحلیل ۱: درآمد ماهانه و رشد ماه به ماه
monthly_sql = """
WITH m AS (
SELECT month,
SUM(revenue) AS rev,
COUNT(DISTINCT customer_id) AS unique_customers
FROM sales
GROUP BY month
)
SELECT month, rev, unique_customers,
rev - LAG(rev) OVER (ORDER BY month) AS mom_change,
ROUND(100.0 * (rev - LAG(rev) OVER (ORDER BY month))
/ LAG(rev) OVER (ORDER BY month), 2) AS mom_pct
FROM m
ORDER BY month
"""
monthly = con.sql(monthly_sql).df()
# تحلیل ۲: ۱۰ مشتری برتر بر اساس درآمد سالانه
top_sql = """
SELECT customer_id,
SUM(revenue) AS total,
COUNT(*) AS orders,
AVG(revenue) AS avg_order
FROM sales
GROUP BY customer_id
ORDER BY total DESC
LIMIT 10
"""
top_customers = con.sql(top_sql).df()
# تحلیل ۳: cohort retention (ماه اول خرید vs ماههای بعد)
cohort_sql = """
WITH first_order AS (
SELECT customer_id, MIN(month) AS cohort_month
FROM sales
GROUP BY customer_id
)
SELECT f.cohort_month,
s.month,
COUNT(DISTINCT s.customer_id) AS active_users
FROM first_order f
JOIN sales s USING (customer_id)
GROUP BY f.cohort_month, s.month
ORDER BY f.cohort_month, s.month
"""
cohorts = con.sql(cohort_sql).df()
print("درآمد ماهانه:\n", monthly)
print("\nبرترین مشتریان:\n", top_customers)
این کوئریها روی یک لپتاپ معمولی در کمتر از یک دقیقه اجرا میشوند، در حالی که نوشتن همین تحلیلها در pandas نیاز به chunking دستی، multiprocessing، و مدیریت دقیق حافظه داشت. تجربهای که، خب، هیچکس دلش نمیخواهد دو بار از سر بگذراند.
سؤالات متداول درباره DuckDB در پایتون
آیا DuckDB واقعاً جایگزین pandas میشود؟
خیر. DuckDB و pandas مکمل یکدیگرند، نه جایگزین. DuckDB برای کوئریهای تحلیلی روی دادههای بزرگ، joinهای سنگین، و agregationهای SQL بهتر است. pandas برای دستکاری تعاملی، یکپارچگی با scikit-learn و matplotlib، و کارهای سفارشی روی دادههای متوسط بهتر باقی میماند. در ۲۰۲۶، استک هیبرید استاندارد است: DuckDB برای ingestion، Polars برای تبدیلهای میانی، و pandas برای مدلسازی نهایی.
تفاوت DuckDB و SQLite چیست؟
هر دو embedded هستند، اما برای کاربردهای کاملاً متفاوت. SQLite یک موتور OLTP ردیفی است که برای تراکنشهای زیاد و کوچک (یک رکورد در زمان) بهینه شده. DuckDB یک موتور OLAP ستونی برداری است که برای کوئریهای تحلیلی روی دادههای زیاد (میلیونها ردیف) بهینه شده. اگر اپلیکیشن وب میسازید، SQLite. اگر تحلیل داده میکنید، DuckDB. به همین سادگی.
آیا DuckDB میتواند داده بزرگتر از RAM را پردازش کند؟
بله. DuckDB از سال ۲۰۲۳ پشتیبانی کامل از پردازش out-of-core دارد و از نسخه ۱.۵ این قابلیت بهبود چشمگیری یافته. وقتی داده میانی از حافظه فراتر برود، DuckDB به صورت شفاف بخشهایی را به دایرکتوری temp روی دیسک سرریز میکند. میتوانید با SET memory_limit = 'XGB' سقف را تعیین کنید. روی یک لپتاپ ۱۶ گیگ، کوئری روی فایلهای ۱۰۰ گیگ کاملاً ممکن است (و من خودم چندین بار آزمایش کردهام).
آیا برای استفاده از DuckDB باید SQL بلد باشم؟
SQL کمک زیادی میکند، ولی الزامی نیست. DuckDB یک Relational API هم دارد که به سبک متد چینینگ مانند pandas کار میکند: con.table('x').filter('a > 1').aggregate('count(*)'). اما واقعیت تلخ این است که حتی SQL پایه (SELECT، WHERE، GROUP BY، JOIN) برای ۹۰٪ کارهای تحلیلی کافی است و یادگیریاش چند ساعت بیشتر طول نمیکشد. سرمایهگذاری ارزشمندی است، باور کنید.
DuckDB با Polars چه فرقی دارد؟ کدام را انتخاب کنم؟
Polars یک کتابخانه DataFrame با Rust است که API به سبک pandas اما با اجرای lazy و موازی ارائه میدهد. DuckDB یک موتور SQL است. در بنچمارکهای ۲۰۲۶ سرعتشان نزدیک است؛ انتخاب بیشتر سلیقهای است: اگر SQL را ترجیح میدهید و با Parquet/CSV روی دیسک کار میکنید، DuckDB. اگر API دیتافریم را ترجیح میدهید و بیشتر تبدیلهای ستونی پیچیده انجام میدهید، Polars. در عمل، خیلی از تیمها از هر دو در پایپلاین استفاده میکنند، چون با Arrow بدون کپی داده با هم تبادل میکنند.
جمعبندی و گام بعدی
DuckDB در ۲۰۲۶ یکی از سریعترین راههای ارتقای استک تحلیل داده پایتون شماست. نه نیاز به سرور دارد، نه پیکربندی پیچیده، و فقط با یک pip install duckdb میتوانید کوئریهایی که در pandas دقیقهها طول میکشیدند را در ثانیهها اجرا کنید. مهمتر از همه، با pandas و Polars بدون اصطکاک کار میکند، پس مجبور نیستید کل کدبیستان را بازنویسی کنید.
پیشنهاد عملی من؟ همین امروز یک Parquet از دیتاست واقعی خودتان بردارید، با read_parquet یک کوئری بزنید و زمان اجرا را با pandas مقایسه کنید. به احتمال خیلی زیاد، خودتان متقاعد میشوید که چرا تیمهای داده در سراسر دنیا دارند DuckDB را به استک خود اضافه میکنند. و اگر مثل من پنج سال است با pandas کار میکنید، احتمالاً کمی هم آه میکشید که چرا زودتر با DuckDB آشنا نشدید.