كان مصطلح «بحيرة البيانات» (Data Lakehouse) يعني لفترة طويلة بنية تحتية ثقيلة: عنقود Spark، وخدمة كتالوج Iceberg أو Delta Lake، ومخزن كائنات، وفريق صغير لإبقاء كل ذلك يعمل. DuckLake يغيّر هذه المعادلة. إنه تنسيق Lakehouse مفتوح من فريق DuckDB يخزّن بيانات الجداول كملفات Parquet عادية، والفكرة الذكية هنا: يحتفظ بكل بيانات الكتالوج الوصفية في قاعدة بيانات SQL عادية بدلاً من سرب من ملفات JSON وAvro.
النتيجة: تحصل على معاملات ACID، ولقطات (Snapshots)، والسفر عبر الزمن، وتطوير المخطط فوق تنسيقات ملفات مفتوحة، ويمكن تشغيل كل ذلك على حاسوبك المحمول بأمر ATTACH واحد. وعندما تحتاج إلى وصول متعدد المستخدمين، تستبدل قاعدة البيانات الوصفية بـ PostgreSQL وتوجّه مسار البيانات إلى S3. نفس SQL، نفس سير العمل.
في هذا الدليل ستبني بحيرة بيانات عاملة من الصفر: إنشاء كتالوج DuckLake، وتحميل البيانات ضمن معاملات، والرجوع في الزمن عبر اللقطات، وتطوير المخطط بأمان، والاتصال من Python، وأخيراً توسيع الإعداد إلى كتالوج PostgreSQL مشترك مع تخزين سحابي.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- DuckDB 1.3 أو أحدث (امتداد
ducklakeمتوفر بدءاً من الإصدار 1.3) — التثبيت عبرbrew install duckdbأو التنزيل من duckdb.org - Python 3.10 أو أحدث مع
pipلخطوات التكامل مع Python - معرفة أساسية بلغة SQL (أوامر CREATE TABLE وINSERT وSELECT وJOIN)
- اختياري للخطوة 7: نسخة PostgreSQL ومستودع (Bucket) متوافق مع S3 مثل AWS S3 أو Cloudflare R2 أو MinIO
تحقق أولاً من إصدار DuckDB لديك:
duckdb --version
# v1.3.x أو أحدثما الذي ستبنيه
بحيرة بيانات «محلية أولاً» لسيناريو تحليلات تجارة إلكترونية:
- كتالوج DuckLake مدعوم بملف بيانات وصفية DuckDB محلي
- جدول
ordersمخزّن كملفات Parquet مع ضمانات ACID كاملة - السفر عبر الزمن باللقطات — استعلام الجدول كما كان في أي لحظة سابقة
- تطوير المخطط دون إعادة كتابة البيانات
- سكربت تحليلات Python يقرأ البحيرة باستخدام pandas
- إعداد للفريق: كتالوج PostgreSQL ومسار بيانات على S3
كل ما يسبق الخطوة 7 يعمل بالكامل على جهازك دون أي خدمات خارجية.
لماذا DuckLake بدلاً من Iceberg أو Delta Lake؟
دقيقة واحدة من السياق قبل كتابة الأوامر. تنسيقات مثل Apache Iceberg وDelta Lake تخزّن البيانات الوصفية للجداول (المخططات واللقطات وقوائم الملفات والإحصاءات) كملفات موضوعة بجانب البيانات: ملفات manifest بصيغة JSON، وقوائم manifest بصيغة Avro، وسجلات معاملات. كل استعلام يبدأ برحلة بحث عبر مخزن الكائنات لإعادة بناء حالة الجدول، وكل عملية Commit تعيد كتابة ملفات وصفية ببروتوكولات معقدة لتجنّب التعارضات.
ملاحظة DuckLake التصميمية بسيطة: إذا كنت بحاجة إلى قاعدة بيانات كتالوج على أي حال (نشر Iceberg الجاد يتضمن واحدة عملياً دائماً)، فلماذا لا نضع كل البيانات الوصفية فيها؟ يخزّن DuckLake المخططات واللقطات وقوائم الملفات وإحصاءات الأعمدة في جداول SQL عادية. تصبح عمليات الـ Commit معاملات SQL، ويصبح حل التعارضات ما أتقنته قواعد البيانات منذ أربعين عاماً.
النتائج العملية:
- جولات أقل. تخطيط الاستعلام يتوجه إلى قاعدة بيانات واحدة، لا إلى عشرات الملفات الصغيرة على S3.
- معاملات حقيقية. المعاملات متعددة الجداول والأوامر تعمل — وهي نقطة ضعف معروفة في التنسيقات المعتمدة على الملفات.
- إعداد محلي في غاية البساطة. يمكن أن تكون قاعدة البيانات الوصفية مجرد ملف DuckDB واحد على حاسوبك.
- بيانات مفتوحة. صفوفك محفوظة في Parquet قياسي يقرؤه أي محرك، إلى الأبد.
الخطوة 1: تثبيت DuckDB وامتداد DuckLake
افتح صدفة (Shell) DuckDB وثبّت الامتداد. يُنزَّل مرة واحدة ويبقى في الذاكرة المؤقتة:
INSTALL ducklake;
LOAD ducklake;هذا هو التثبيت بأكمله. يتضمن الامتداد كل ما يلزم لإدارة الكتالوج واللقطات وتتبع ملفات Parquet.
نصيحة: في السكربتات، يُنفَّذ
LOAD ducklakeضمنياً بمجرد إرفاق كتالوج DuckLake — إذ يحمّل DuckDB الامتدادات المعروفة تلقائياً. كتابته صراحةً تجعل السكربتات موثِّقة لنفسها فحسب.
الخطوة 2: إنشاء أول كتالوج DuckLake
يحتاج كتالوج DuckLake إلى موقعين: قاعدة بيانات وصفية (حيث تعيش حالة الجداول) ومسار بيانات (حيث تُكتب ملفات Parquet). أنشئ مجلد عمل ثم أرفق:
mkdir -p ~/lakehouse && cd ~/lakehouse
duckdbATTACH 'ducklake:metadata.ducklake' AS lake (DATA_PATH 'data/');
USE lake;ما الذي حدث للتو:
metadata.ducklakeملف قاعدة بيانات DuckDB يحمل الكتالوج: المخططات واللقطات وقوائم الملفات والإحصاءات.data/هو المجلد الذي ستُكتب فيه جميع ملفات Parquet لكل الجداول.USE lakeيجعل البحيرة الكتالوج الافتراضي، فتُحل أسماء الجداول غير المؤهلة داخله.
نفّذ ls في نافذة طرفية أخرى — سترى أن metadata.ducklake أُنشئ فوراً، وسيمتلئ data/ بمجرد أول إدراج.
الخطوة 3: تحميل البيانات وتنفيذ معاملات ACID
أنشئ جدولاً وأدرج بيانات تماماً كما تفعل في أي قاعدة بيانات:
CREATE TABLE orders (
order_id INTEGER,
customer VARCHAR,
amount DECIMAL(10, 2),
country VARCHAR,
ordered_at TIMESTAMP
);
INSERT INTO orders VALUES
(1, 'Amel', 129.90, 'TN', '2026-07-01 09:15:00'),
(2, 'Youssef', 89.00, 'TN', '2026-07-02 14:30:00'),
(3, 'Sarah', 240.50, 'FR', '2026-07-03 11:05:00');خلف الكواليس، كتب DuckLake ملف Parquet في data/ وسجّل لقطة جديدة في قاعدة البيانات الوصفية. كل أمر مُثبَّت ينتج لقطة — نسخة مرقّمة وغير قابلة للتغيير من الكتالوج بأكمله.
المعاملات الممتدة عبر عدة أوامر وعدة جداول تعمل بشكل أصيل:
CREATE TABLE refunds (
order_id INTEGER,
amount DECIMAL(10, 2),
refunded_at TIMESTAMP
);
BEGIN TRANSACTION;
INSERT INTO refunds VALUES (2, 89.00, '2026-07-05 10:00:00');
UPDATE orders SET amount = 0 WHERE order_id = 2;
COMMIT;إما أن يُطبَّق التغييران معاً، أو لا يُطبَّق أي منهما. هذه الذرّية عبر الجداول ميزة تنافسية حقيقية — التنسيقات المعتمدة على الملفات فقط تثبّت عموماً جدولاً واحداً في كل مرة.
التحديثات والحذف يعملان أيضاً ببساطة. يديرهما DuckLake عبر ملفات بيانات جديدة وتتبع للحذف داخل الكتالوج، بشكل شفاف تماماً:
DELETE FROM orders WHERE country = 'FR';
SELECT count(*) FROM orders; -- 2الخطوة 4: السفر عبر الزمن باللقطات
كل عملية Commit أنشأت لقطة. اعرضها:
SELECT * FROM lake.snapshots();سترى إصدارات لقطات مرقّمة مع طوابع زمنية وملخص لما تغيّر في كل منها. لنستعلم الآن عن الماضي. أتذكر الطلب الفرنسي الذي حذفناه؟ اقرأ الجدول كما كان في إصدار سابق:
-- برقم الإصدار (عدّله حسب قائمة اللقطات لديك)
SELECT * FROM orders AT (VERSION => 4);
-- بالطابع الزمني
SELECT * FROM orders AT (TIMESTAMP => now() - INTERVAL 10 MINUTE);الصفوف المحذوفة عادت — لم تُستعد فعلياً، بل أصبحت مرئية فقط، لأن ملفات Parquet القديمة وحالة الكتالوج القديمة ما زالت موجودة. يدعم السفر عبر الزمن ثلاثة استخدامات عملية:
- التدقيق: «ماذا كان يحتوي هذا الجدول عندما صدر التقرير في 3 يوليو؟»
- تصحيح خطوط المعالجة: قارن بين لقطتين لترى بالضبط ما غيّرته مهمة معينة.
- الاستعادة الفورية: أصلح حذفاً خاطئاً بالإدراج من إصدار سابق:
INSERT INTO orders
SELECT * FROM orders AT (VERSION => 4)
WHERE country = 'FR';الخطوة 5: تطوير المخطط دون إعادة كتابة
المتطلبات تتغير، وبحيرتك لا ينبغي أن تحتاج إلى عطلة نهاية أسبوع للترحيل. يدعم DuckLake تطوير المخطط كعمليات على البيانات الوصفية فقط — ملفات Parquet الموجودة لا يُعاد كتابتها:
ALTER TABLE orders ADD COLUMN channel VARCHAR DEFAULT 'web';
ALTER TABLE orders RENAME COLUMN customer TO customer_name;استعلم عن الجدول: الصفوف القديمة تعرض القيمة الافتراضية للعمود الجديد، والإدراجات الجديدة يمكنها تعبئته. الملفات القديمة على القرص لم تُمسّ؛ الكتالوج ببساطة يربط مخططات الملفات القديمة بالمخطط الحالي للجدول. ولأن تغييرات المخطط تُحفظ في لقطات أيضاً، يعيد السفر عبر الزمن المخطط كما كان:
SELECT * FROM orders AT (VERSION => 4); -- ما زال يعرض 'customer' وبلا 'channel'هذا أبسط بكثير من رقصة إعادة كتابة ملفات الـ manifest التي تفرضها التنسيقات الأخرى، وهو أحد أقوى حجج DuckLake لتطوير المخططات التحليلية بثقة.
الخطوة 6: استخدام البحيرة من Python
عميل Python الخاص بـ DuckDB يجعل البحيرة مواطناً من الدرجة الأولى في الدفاتر التفاعلية وخطوط المعالجة. التثبيت:
pip install duckdb pandasثم اقرأ واكتب في الكتالوج نفسه:
import duckdb
con = duckdb.connect()
con.sql("ATTACH 'ducklake:/Users/you/lakehouse/metadata.ducklake' AS lake (DATA_PATH '/Users/you/lakehouse/data/')")
con.sql("USE lake")
# استعلام تحليلي مباشرة إلى DataFrame
df = con.sql("""
SELECT country, count(*) AS orders, sum(amount) AS revenue
FROM orders
GROUP BY country
ORDER BY revenue DESC
""").df()
print(df)
# كتابة DataFrame كجدول جديد ضمن معاملة
import pandas as pd
returns = pd.DataFrame([
dict(order_id=1, reason="damaged", created_at="2026-07-06 08:00:00"),
])
con.sql("CREATE TABLE IF NOT EXISTS returns AS SELECT * FROM returns")النمط نفسه يعمل من Node.js عبر @duckdb/node-api، ومن سطر الأوامر في مهام cron، ومن أي أداة ذكاء أعمال تدعم DuckDB. كتالوج واحد، قرّاء وكتّاب متعددون، يتناسقون عبر معاملات الكتالوج.
الخطوة 7: التوسع — كتالوج PostgreSQL وبيانات على S3
الإعداد المحلي يعمل على جهاز واحد. لمشاركة البحيرة بين فريق أو مجموعة خدمات، غيّر شيئين اثنين بالضبط: ضع البيانات الوصفية في PostgreSQL والبيانات في مخزن كائنات.
أنشئ أولاً قاعدة PostgreSQL فارغة (هنا باسم lake_catalog)، ثم أرفق باستخدام سلسلة اتصال PostgreSQL كخلفية للبيانات الوصفية:
INSTALL postgres;
CREATE SECRET s3_creds (
TYPE s3,
KEY_ID 'YOUR_ACCESS_KEY',
SECRET 'YOUR_SECRET_KEY',
REGION 'eu-west-1'
);
ATTACH 'ducklake:postgres:dbname=lake_catalog host=db.internal user=lake password=***'
AS lake (DATA_PATH 's3://my-company-lakehouse/main/');
USE lake;كل ما فعلته في الخطوات من 3 إلى 6 يعمل بشكل مطابق. ما تغيّر عملياتياً:
- التزامن: عدة عملاء DuckDB — حواسيب محمولة وحاويات وعمّال Airflow — يرتبطون بالكتالوج نفسه. محرك المعاملات في PostgreSQL يحكّم عمليات الـ Commit؛ وتُحل الكتابات المتعارضة كما تحلها قواعد البيانات، لا بحلقات إعادة محاولة على مخزن الكائنات.
- المتانة: ملفات Parquet على S3، والكتالوج في Postgres مُدار ومنسوخ احتياطياً.
- التحكم في الوصول: امنح أو اسحب الوصول إلى الكتالوج بمستخدمي وأدوار قاعدة البيانات العاديين.
تحذير: لا تضع بيانات الاعتماد في سكربتات SQL أبداً. استخدم أسرار DuckDB كما هو موضح أعلاه، أو متغيرات البيئة، أو أدوار IAM حيثما أمكن.
تدرّج منطقي: ابدأ محلياً (الخطوات 2 إلى 6)، وعند ظهور مستهلك ثانٍ، رحّل بإرفاق الكتالوجين معاً ونسخ الجداول عبر CREATE TABLE lake_shared.orders AS SELECT * FROM lake_local.orders.
الخطوة 8: الصيانة — اللقطات وضغط الملفات
حقيقتان تشغيليتان في أي بحيرة بيانات: اللقطات القديمة تتراكم، والإدراجات الصغيرة المتكررة تنتج ملفات Parquet صغيرة كثيرة. يوفر DuckLake دوال صيانة للحالتين.
أنهِ صلاحية اللقطات الأقدم من نافذة الاحتفاظ لديك، ثم احذف ملفات Parquet التي لا تشير إليها أي لقطة باقية:
CALL ducklake_expire_snapshots('lake', older_than => now() - INTERVAL 7 DAY);
CALL ducklake_cleanup_old_files('lake', cleanup_all => true);اضغط الملفات الصغيرة المتجاورة في ملفات أكبر لعمليات مسح أسرع:
CALL ducklake_merge_adjacent_files('lake');جدوِل هذه العمليات في مهمة ليلية (cron أو Airflow أو مؤقّت systemd بسيط). مدة الاحتفاظ قرار عملي: سبعة أيام من السفر عبر الزمن كافية تماماً للتصحيح؛ أما البيئات الخاضعة للتنظيم فقد تحتفظ بأكثر من ذلك بكثير.
اختبار التنفيذ
تحقق من كل قدرة من البداية إلى النهاية:
- ACID: افتح صدفتي DuckDB مرتبطتين بالكتالوج نفسه. ابدأ معاملة إدراج في الأولى؛ وتأكد أن الثانية لا ترى الصفوف قبل
COMMIT. - السفر عبر الزمن: نفّذ
SELECT count(*)عند إصدارين مختلفين وتأكد أن العددين يختلفان كما هو متوقع. - تطوير المخطط: تأكد أن
SELECT * FROM orders AT (VERSION => 1)يعرض مجموعة الأعمدة الأصلية. - جولة Python الكاملة: شغّل سكربت الخطوة 6 وتأكد أن الـ DataFrame يطابق نتائج الاستعلام في الصدفة.
- فحص البيانات المفتوحة: أقوى ضمانة على الإطلاق — اقرأ ملف بيانات مباشرة متجاوزاً DuckLake كلياً:
SELECT * FROM read_parquet('data/**/*.parquet') LIMIT 5;بياناتك تبقى ملفات Parquet عادية. إذا تخليت عن DuckLake يوماً، ستبقى الملفات قابلة للقراءة من كل الأدوات.
استكشاف الأخطاء وإصلاحها
«Extension ducklake not found». إصدار DuckDB لديك أقدم من 1.3. حدّث البرنامج ثم أعد تنفيذ INSTALL ducklake.
فشل الإرفاق محلياً بخطأ قفل (Lock). الكتالوج المخزّن في ملف DuckDB يسمح بعملية كاتبة واحدة في كل مرة. أغلق الصدفة الأخرى، أو انتقل إلى كتالوج PostgreSQL (الخطوة 7) لتزامن حقيقي.
استعلام السفر عبر الزمن يرجع «snapshot not found». انتهت صلاحية الإصدار بواسطة ducklake_expire_snapshots. راجع SELECT * FROM lake.snapshots() لمعرفة الإصدارات الباقية وعدّل مهمة الاحتفاظ.
كتابات S3 تفشل بخطأ 403. تحقق من مفتاح السر والمنطقة، ومن أن سياسة المستودع تسمح بـ PutObject على بادئة مسار البيانات. اختبر عبر SELECT * FROM read_parquet('s3://bucket/path/*.parquet') لعزل صلاحيات القراءة عن الكتابة.
استعلامات بطيئة بعد إدراجات صغيرة كثيرة. مشكلة الملفات الصغيرة الكلاسيكية — نفّذ ducklake_merge_adjacent_files وجمّع الإدراجات المستقبلية في دفعات كلما أمكن.
الخطوات التالية
- شغّل التحليلات بالكامل داخل المتصفح مع دليل DuckDB-WASM وNext.js — تكامل ممتاز للوحات معلومات فوق مستخرجات البحيرة.
- غذِّ واجهة أمامية تفاعلية من جداول البحيرة باستخدام TanStack DB.
- استكشف إرفاق كتالوج DuckLake من MotherDuck أو من عدة مناطق سحابية عبر نسخ قراءة لكتالوج Postgres.
- أضف dbt فوق ذلك: تعمل dbt-duckdb مع كتالوجات DuckLake المرفقة لتحويلات نمذجة منظمة.
الخلاصة
لقد بنيت بحيرة بيانات متكاملة دون عنقود حوسبة، ودون JVM، ودون رحلة بحث في ملفات البيانات الوصفية: كتالوج في SQL، وبيانات في Parquet، ومعاملات ACID، وسفر عبر الزمن، وتطوير للمخطط، ووصول من Python، ومسار واضح من الحاسوب المحمول إلى نطاق الفريق مع PostgreSQL وS3. رهان DuckLake — أن البيانات الوصفية للبحيرة مكانها قاعدة بيانات لا ملفات — يجعل الحالة البسيطة في غاية السهولة والحالة الموسّعة «مملة» بأفضل معاني الكلمة. ابدأ محلياً، وقدّم قيمة فعلية، ولا توسّع الكتالوج إلا يوم يظهر كاتب ثانٍ فعلاً.