Skip to main content
يستعرض هذا الدليل الجانب الخاص بـ ClickHouse من dbt باستخدام مشروع Jaffle Shop for ClickHouse، وهو نسخة ClickHouse من المشروع النموذجي الكلاسيكي لـ dbt Labs. وانطلاقًا من مشروع جاهز للبناء بالفعل، يوضّح الدليل كيفية:
  1. فهم الطريقة التي تُنشأ بها عروض المشروع وجداوله في ClickHouse.
  2. تحميل البيانات باستخدام seeds والتحكم في أنواع ClickHouse وبنية الجدول.
  3. تهيئة نموذج جدول بمحرك ClickHouse ومفتاح فرز وتقسيم.
  4. تحويل جدول إلى نموذج incremental واختيار strategy تزايدية.
  5. إنشاء snapshot.
  6. استخدام العروض المجسدة في ClickHouse.
وقد صُمم ليُقرأ إلى جانب بقية التوثيق، وصفحة الميزات والتهيئات، ومرجع materializations.

قبل أن تبدأ

اتبع أولاً ملف README الخاص بـ ClickHouse/jaffle-shop-clickhouse، فهو يوضح كيفية إعداد المشروع باستخدام dbt Core 1.x أو dbt OSS أو dbt v2 أو منصة dbt، وكيفية توجيهه إلى ClickHouse محلي (docker) أو إلى ClickHouse Cloud، وكيفية تحميل بيانات العينة باستخدام dbt seed، وكيفية تشغيل أول dbt build. وبعد أن يكتمل dbt build بنجاح، عد إلى هنا للاطلاع على الأمثلة والتهيئات الخاصة بـ ClickHouse. بعد إتمام خطوات README، ينبغي أن تكون لديك قاعدتا بيانات في ClickHouse:
  • raw: جداول المصدر الستة المحمَّلة من ملفات CSV بواسطة dbt seed (raw_customers، raw_orders، raw_items، raw_products، raw_stores، raw_supplies).
  • jaffle_shop (قيمة schema في ملف الـ profile الخاص بك): ستة عروض staging (stg_*) وسبعة جداول mart (customers، orders، order_items، products، locations، supplies، metricflow_time_spine).
إذا كان الـ profile الخاص بك يستخدم schema مختلفاً، فاستبدل jaffle_shop في الاستعلامات أدناه بالقيمة الخاصة بك.
dbt Core 1.x وdbt OSS وdbt v2 ومنصة dbt. كل أمر وكل نموذج في هذا الدليل هو نفسه في جميعها. اختُبرت الأمثلة باستخدام dbt Core 1.12 مع dbt-clickhouse 1.10، وباستخدام dbt OSS 2.0 مع ClickHouse 26.8؛ ويعمل dbt v2 بالـ adapter نفسه، وتعمل منصة dbt بـ dbt v2. مخرجات الـ console المعروضة مأخوذة من dbt Core 1.x، وقد أُشير إلى المواضع القليلة التي يختلف فيها سلوك المحركين. راجع صفحة dbt OSS وdbt v2 ومنصة dbt لمعرفة الحالة الراهنة لـ adapter الإصدار v2، وراجع Connect ClickHouse في توثيق dbt للبدء على منصة dbt.
جميع عبارات SQL التي ليست أوامر dbt يُقصد تشغيلها مباشرةً على ClickHouse، على سبيل المثال عبر clickhouse client، أو SQL console في ClickHouse Cloud، أو عميل SQL الذي تختاره.

كيف يتم تجسيد المشروع

يهيّئ Jaffle Shop تجسيداته في dbt_project.yml: نماذج staging تكون views، أما marts فتكون tables.
تُعاد بناء نموذج العرض (view) باستخدام عبارة CREATE OR REPLACE VIEW في كل تشغيل. وهو لا يخزّن أي بيانات، لذا لا يكلّف بناؤه شيئًا، لكن كل استعلام يُنفَّذ عليه يُشغّل SQL الخاص بالنموذج على الجداول المصدر. ويحتفظ ClickHouse بكود SQL المُصرَّف للنموذج في تعريف العرض:
تُعاد بناء model من نوع table من الصفر في كل تشغيل: ينشئ adapter جدولًا جديدًا، ثم ينفّذ INSERT INTO ... SELECT باستخدام SQL الخاص بالـ model، ثم يبادله بشكل ذرّي مع الإصدار السابق. وأداء الاستعلام هنا أفضل بكثير منه في العرض، لكن على حساب التخزين وإعادة بناء الجدول بالكامل في كل مرة. ألقِ نظرة على الجدول الذي أنشأه dbt لمستودع orders:
هناك أمران خاصان بـ ClickHouse هنا. فالـ model لا يُعلِن عن table engine، لذا يستخدم الـ adapter المحرك MergeTree، كما أنه لا يُعلِن عن sorting key، لذا يستخدم الـ adapter العبارة ORDER BY tuple()، أي أن البيانات غير مُرتَّبة على الإطلاق. وهذا مقبول في مشروع تجريبي، أما في جدول حقيقي فستحتاج إلى تحديد كليهما، وهو ما تتناوله الأقسام التالية. وتسرد صفحة materializations كل تهيئة جدول يدعمها الـ adapter.

تحميل البيانات باستخدام seeds

يستخدم Jaffle Shop seeds في dbt لتحميل بياناته الخام من ملفات CSV الموجودة في seeds/jaffle-data. صُمّمت seeds للبيانات المرجعية الصغيرة والثابتة (جداول الرموز وعمليات الربط)، وليست مخصصة لتحميل warehouse؛ لكن المشروع يستخدمها لتسهيل الأمر عليك كي تبدأ دون الحاجة إلى أداة ingestion أخرى، ولهذا تبقى seeds معطّلة ما لم تمرّر --vars '{"load_source_data": true}'. ومع ذلك، تظل seeds مكانًا جيدًا لتتعلّم كيف يُنشئ dbt جداول ClickHouse. فهو يستنتج نوع column لكل column في ملف CSV، وتختلف الأنواع المستنتجة بين المحركات: وعندما يكون النوع مهمًا، ثبّته باستخدام column_types. وقد فعل المشروع ذلك بالفعل مع column المسمى opened_at في seed raw_stores ضمن dbt_project.yml:
تقبل الـ seeds أيضاً تهيئات جدول ClickHouse التالية: engine وorder_by وpartition_by. على سبيل المثال، لفرز الـ seed المسمى raw_orders حسب وقت الطلب وتقسيمه شهرياً، أضف ملف properties إلى جانب ملفات CSV، أي seeds/jaffle-data/_raw_orders.yml:
استخدم ملف properties لتهيئة seeds الخاصة بـ ClickHouse بدلاً من المفتاحين +order_by أو +engine ضمن seeds: في dbt_project.yml. يقبل dbt Core 1.x كلا الشكلين، أما dbt v2 فلا يتعرّف عليهما إلا في ملف properties ويرفض مفاتيح dbt_project.yml مع الرسالة Unrecognized key ... Custom keys must go under +meta.
أعد تحميل ذلك الـ seed وتحقّق من الجدول الناتج عنه:
يقوم الأمر dbt seed --full-refresh بحذف الجدول وإعادة إنشائه، لذا شغّله قبل بناء أي شيء يعتمد مباشرةً على بيانات الجدول، مثل الـ عرض مُجسَّد الوارد لاحقًا في هذا الدليل.

إعداد جدول لـ ClickHouse

يُعد الـ mart المسمى orders نقطة البداية الطبيعية: فهو الجدول الذي يستعلم منه mart المسمى customers ومقاييس المشروع، كما أنه جدول على نمط الأحداث يتضمن timestamp. أضف كتلة config في أعلى ملف models/marts/orders.sql لتحديد المحرك ومفتاح الفرز ومخطط التقسيم:
تبقى بقية النموذج كما هي. يكرّر materialized='table' ما يحدده dbt_project.yml أصلاً لطبقة marts، وهو ما يجعل النموذج ذاتي الوصف عند تحويله لاحقاً إلى الوضع التزايدي. أعد بناء هذا النموذج فقط:
أصبح للجدول الآن sorting key مناسب وتقسيم واحد لكل شهر:
إلى جانب engine وorder_by وpartition_by، تقبل نماذج الجداول أيضًا primary_key وttl وsettings وquery_settings وprojections وindexes، ويمكن أن تحمل الأعمدة codec وttl عبر عقد النموذج (model contract). وجميعها موضّحة في صفحة materializations.

إنشاء model تزايدي

إعادة بناء orders من الصفر في كل تشغيل أمر مقبول مع 62,000 row، لكنه غير مناسب لـ table ينمو بملايين الـ rows يوميًا. أما التجسيد التزايدي في dbt فلا يعالج سوى الـ rows التي تغيّرت منذ التشغيل الأخير. ويتطلب تحويل model الخاص بـ orders إضافتين:
  1. unique_key: الـ column الذي يعرّف الـ row، وهو order_id هنا. ويستخدمه الـ adapter لاستبدال الـ rows التي تُعالج مرة أخرى بدلًا من تكرارها.
  2. filter تزايدي: عبارة where مُحاطة بـ {% if is_incremental() %} تختار الـ rows المطلوب معالجتها فقط. وتُطبَّق في التشغيلات التزايدية، لا عند بناء الـ table للمرة الأولى (أو عند إعادة بنائه باستخدام --full-refresh). وبما أن الطلبات تحمل timestamp، فإن الـ filter يقارن قيمة ordered_at بأحدث قيمة موجودة بالفعل في الـ table، والتي يُشار إليها عبر المتغير {{ this }}.
حدّث models/marts/orders.sql بحيث تصبح كتلة config ونهاية الـ model كما يلي:
يقتطع stg_orders القيمة ordered_at إلى مستوى اليوم، لذا يستخدم الـ filter المعامل >=: ففي كل تشغيل تُعاد معالجة اليوم الأخير بالكامل، وبفضل unique_key تُستبدل الـ rows المحمَّلة مسبقًا بدلًا من تكرارها. وهذا ما يجعله آمنًا للطلبات التي تصل لاحقًا في اليوم نفسه. شغّل الـ model. الـ table موجود بالفعل، لذا فإن هذا التشغيل الأول يُعد تشغيلًا incremental أصلًا: تُعاد معالجة اليوم الأخير فقط.
والآن أضف بعض البيانات الجديدة. تنتهي بيانات Jaffle Shop عند أغسطس 2025، لذا سنُدخل عميلًا جديدًا هو Clicky McClickHouse، الذي طلب جافل أمس. أدرج عميلًا وطلبًا وعنصر الطلب الخاص به في الجداول الخام:
معرّف المتجر هو Philadelphia، والعنصر هو جافل nutellaphone who dis? بسعر 11.00، والضريبة هي 6% الخاصة بـ Philadelphia، لذا تبقى اختبارات البيانات في المشروع ناجحة. شغّل المشروع بالكامل حتى تتعرّف عروض staging وجدول order_items على الصفوف الجديدة قبل أن يتعرّف عليها orders:
الطلب الجديد موجود في جدول incremental، كما أن mart الخاص بـ customers، الذي أُعيد بناؤه منه، يتعرّف على العميل الجديد:

البنية الداخلية

يُظهر سجل الاستعلامات في ClickHouse العبارات التي نفّذها الـ adapter لإجراء التحديث التزايدي:
تعمل الاستراتيجية التزايدية الافتراضية للـ adapter كما يلي. في مخططات هذا القسم، يعني السهم المتجه من جدول إلى عبارة أن العبارة تقرأ ذلك الجدول؛ أما السهم المتجه من عبارة إلى جدول فيعني أنها تكتب إليه أو تُجري عليه تحويلاً (mutate) أو تعيد تسميته أو تحذفه:
  1. يُنشأ جدول orders__dbt_new_data ويُدرج فيه استعلام SQL الخاص بالـ model، بما في ذلك الـ filter التزايدي. في التشغيل أعلاه، كُتب 378 صفاً: 377 طلباً من آخر يوم تم تحميله مسبقاً، إضافةً إلى الطلب الجديد.
  2. يُنشأ جدول orders__dbt_tmp بالبنية نفسها لجدول orders، وتُنسخ إليه جميع صفوف orders التي لا يوجد order_id الخاص بها في orders__dbt_new_data.
  3. تُدرج جميع صفوف orders__dbt_new_data في orders__dbt_tmp. والخطوتان 2 و3 هما ما يستبدل صفوف آخر يوم بدلاً من تكرارها.
  4. يُحذف orders__dbt_new_data.
  5. يُبدَّل orders__dbt_tmp مع orders باستخدام عبارة EXCHANGE TABLES الذرية (عبر إعادة تسمية وسيطة إلى orders__dbt_backup)، وبذلك يحتوي orders الآن على النسخة الجديدة.
  6. تُحذف النسخة القديمة.
تنسخ الخطوة 2 الجدول بأكمله، ولذلك تكون تكلفة هذه الاستراتيجية مماثلة لإعادة بناء الجدول في النماذج الضخمة جدًا؛ راجع القيود. أما الاستراتيجيات الواردة أدناه فهي تتجنب عملية النسخ هذه.

Append strategy

تُدرج استراتيجية append الصفوف التي يحددها الـ model مباشرةً في الـ target table. فلا تُنشأ أي temporary tables ولا يُنسخ أي شيء، ما يجعلها أقل تكلفة ما يمكن أن يكون عليه تشغيل incremental. لكن الثمن هو عدم إجراء أي deduplicate أيضًا: فإذا حدّد الـ filter الخاص بـ incremental صفًا موجودًا بالفعل في الـ table، فستحصل عليه مرتين. استخدمها للبيانات غير القابلة للتغيير ذات النمط الحدثي، وتأكد من أن الـ filter لا يحدد إلا الصفوف الجديدة فعليًا. ومع ordered_at المقتطع على مستوى اليوم، يعني ذلك تغيير الـ filter إلى >. عدّل الـ model:
أضف عميلاً جديداً ثانياً، Danny DeBito، مع طلب تم تقديمه اليوم في Brooklyn (ضريبة 4%) يتضمّن jaffle وقهوة:
استغرق تشغيل الـ incremental model جزءًا بسيطًا من زمن التشغيل السابق. ولكلٍّ من العميلين الجديدين طلب واحد فقط في الـ table:
يؤكّد سجل الاستعلامات (query log) الفرق: هذه المرة العبارة الوحيدة التي تمسّ orders هي عبارة INSERT INTO jaffle_shop.orders ... SELECT ... واحدة تتضمّن SQL الخاص بالـ model والـ filter التزايدي، وقد كتبت صفًّا واحدًا.
عند استخدام > مع طابع زمني مقتطع إلى مستوى اليوم، فإن أي طلب يصل لاحقًا في اليوم نفسه الذي وصل فيه آخر طلب محمَّل لن يُلتقط أبدًا. في المشاريع الحقيقية، طبّق الـ filter على طابع زمني بدقة كاملة، أو على وقت ingestion متزايد باستمرار، عند استخدام الـ append strategy.

استراتيجية الحذف والإدراج

تاريخيًا، لم يقدّم ClickHouse سوى دعم محدود لعمليات التحديث والحذف، على هيئة mutations غير متزامنة. وقد تكون هذه العمليات مكثفة جدًا من ناحية IO ويُفضّل تجنّبها عمومًا. وقد أضاف ClickHouse 22.8 ميزة lightweight deletes، وأضاف ClickHouse 25.7 ميزة lightweight updates. وبفضلهما، يظهر أثر عبارة الحذف أو التحديث الواحدة فورًا من منظور المستخدم، رغم أن تجسيدها يتم بشكل غير متزامن. تعتمد استراتيجية delete+insert على lightweight deletes وتُهيَّأ عبر المعلمة incremental_strategy:
تعمل هذه الاستراتيجية مباشرةً على الجدول الهدف، لذا إذا فشل شيء في منتصف العملية فمن المرجّح أن تكون بيانات النموذج التزايدي في حالة غير صحيحة، إذ لا يوجد تبديل (swap) ذرّي. وبإيجاز:
  1. يُنشأ جدول مؤقت (orders__dbt_new_data_<run_id>) وتُدرج فيه الصفوف التي يحددها النموذج.
  2. يُنفَّذ أمر DELETE على orders لكل order_id موجود في الجدول المؤقت.
  3. تُدرج صفوف الجدول المؤقت في orders.
  4. يُحذف الجدول المؤقت.

استراتيجية insert overwrite (تجريبية)

تستبدل استراتيجية insert_overwrite تقسيمات كاملة، لذا فهي تتطلّب تهيئة partition_by مثل التهيئة الشهرية المستخدمة في orders. وهي تنفّذ الخطوات التالية:
  1. إنشاء staging table (‏orders__dbt_new_data_<run_id>) بالبنية نفسها المستخدمة في orders.
  2. إدخال الصفوف التي حدّدها الـ model فقط في الـ staging table.
  3. سرد التقسيمات الموجودة في الـ staging table من system.parts.
  4. استبدال تلك التقسيمات تحديدًا في orders باستخدام ALTER TABLE ... REPLACE PARTITION ... FROM من الـ staging table.
  5. حذف الـ staging table.
ويوفّر هذا الأسلوب المزايا التالية:
  • أسرع من الاستراتيجية الافتراضية لأنه لا ينسخ الجدول بالكامل.
  • أكثر أمانًا من الاستراتيجيات الأخرى لأنه لا يعدّل الجدول الأصلي إلى أن تكتمل عملية INSERT بنجاح؛ فإذا حدث فشل في منتصف العملية يبقى الجدول الأصلي كما هو.
  • يطبّق أفضل ممارسات هندسة البيانات المتمثلة في “ثبات التقسيمات”، ما يبسّط معالجة البيانات التزايدية والمتوازية وعمليات التراجع وغيرها.
تتناول صفحة التجسيدات بقية خيارات التجسيد التزايدي، بما في ذلك استراتيجية microbatch وon_schema_change.

إنشاء snapshot

تسجّل snapshots في dbt كيفية تغيّر صفوف جدول قابل للتعديل بمرور الوقت، بما يتيح للمحللين الاطلاع على حالة البيانات في أي لحظة سابقة. وهي تُطبّق الأبعاد بطيئة التغيّر من النوع 2: إذ يُخزَّن كل إصدار من الصف مع الفاصل الزمني الذي كان صالحًا خلاله. يُعد مستودع البيانات customers مرشحًا جيدًا لذلك: فالحقول count_lifetime_orders وlifetime_spend وcustomer_type تتغير جميعها كلما قدّم العميل طلبًا جديدًا. قبل المتابعة، أعد ضبط الـ model المسمى orders على استراتيجية الـ incremental الافتراضية الواردة في قسم incremental (بإزالة incremental_strategy='append' وإعادة الـ filter إلى >=)، حتى يتم التقاط الطلبات التي تُقدَّم لاحقًا في اليوم نفسه. تُعرَّف اللقطات (snapshots) بصيغة YAML اعتبارًا من dbt 1.9. أنشئ الملف snapshots/customers_snapshot.yml:
تقارن الـ strategy check الـ columns المدرجة بين اللقطة الحالية والـ source في كل تشغيل، وتسجّل version جديدة عند تغيّر أي منها. وإذا كان الـ model يحتوي على column من نوع timestamp موثوق يمثّل “آخر تحديث”، فإن الـ strategy timestamp أقل تكلفة: اضبط strategy: timestamp وupdated_at: <column>. أما last_ordered_at في Jaffle Shop فهو مقتطع إلى مستوى اليوم، لذا لن يرصد طلبًا ثانيًا في اليوم نفسه، ولهذا يستخدم هذا المثال check. خُذ أول snapshot:
يُنشأ جدول الـ snapshot بجوار الـ models. ويضع ماكرو generate_schema_name الخاص بالمشروع كل relation في schema الهدف بالنسبة للأهداف غير الإنتاجية، لذا فإن إعداد schema على الـ snapshot لا يسري إلا مع الهدف prod. ويحتوي الجدول على صف واحد لكل عميل، إلى جانب عمودَي التتبع الخاصين بـ dbt وهما dbt_valid_from وdbt_valid_to؛ ويكون الأخير NULL بالنسبة للإصدار الحالي من الصف:
يعود Clicky اليوم لاحتساء القهوة:
شغّل الـ models حتى يعكس كل من orders وcustomers الطلب الجديد، ثم خُذ snapshot ثانيًا:
أصبح لدى Clicky الآن صفّان في اللقطة. فقد أُغلقت النسخة الأولى بتعيين قيمة dbt_valid_to الخاصة بها، أما النسخة الجديدة، التي باتت تمثّل عميلاً returning لديه طلبان، فهي مفتوحة. ولم يطرأ أي تغيير على Danny، لذا بقي صفّه كما هو:
داخليًا، يبني الـ adapter النسخة الجديدة من الـ snapshot في جدول customers_snapshot__snapshot_upsert ثم يستبدلها باستخدام EXCHANGE TABLES (أو عبر drop وrename في الحالات التي لا يستطيع فيها الخادم تبديل الجداول)، بحيث يرى القُرّاء إمّا النسخة السابقة أو النسخة الجديدة من الـ snapshot. راجع قسم snapshot في صفحة materializations للاطلاع على مرجع التهيئة.

استخدام العروض المُجسَّدة

كل ما سبق يتطلّب تنفيذ dbt run لإدخال البيانات الجديدة إلى الـ models. أما العروض المُجسَّدة في ClickHouse فتعمل بطريقة مختلفة: فهي بمثابة insert triggers. فكل كتلة من الصفوف تُدرَج في الـ source table يحوّلها الـ SELECT الخاص بالـ view ثم تُكتب في الـ target table، دون أي scheduling. ويوفّر الـ adapter الوصول إليها عبر الـ materialization المسمّى materialized_view. أنشئ الملف models/marts/daily_store_revenue.sql ليحتوي على عدد الطلبات والإيرادات لكل متجر ولكل يوم، بالقراءة مباشرةً من جدول الطلبات الخام:
تُطبَّق قيمتا engine وorder_by على الجدول الهدف. ويجمع SummingMergeTree الأعمدة الرقمية للصفوف التي تتشارك نفس مفتاح الفرز (sorting key) عند دمج أجزاء البيانات، وهو تحديدًا ما يتطلبه التجميع لكل يوم ولكل متجر.
أنشأ الـ adapter كائنين: الـ target table الذي يحمل اسم الـ model، والـ materialized view نفسها مع الـ suffix _mv، والتي تشير إلى الـ target table عبر عبارة TO. وافتراضيًا (catchup=True)، تم أيضًا إجراء backfill للـ target table بالطلبات الموجودة:
الآن أدرج طلبًا خامًا آخر لـ Danny، دون تشغيل dbt بعد ذلك:
يعكس الجدول الهدف ذلك بالفعل. فلدى Brooklyn الآن طلبان اليوم:
يُجري الاستعلام التجميع باستخدام sum() وGROUP BY عن قصد: فمحرك SummingMergeTree لا يدمج الصفوف ذات المفتاح نفسه إلا عند دمج أجزاء البيانات في الخلفية، ولذلك يظل طلبا Brooklyn صفّين منفصلين في الجدول حتى ذلك الحين. لذا اجمع البيانات دائمًا عند القراءة (أو استخدم FINAL) مع محركات الجمع والتجميع. وفي الوقت نفسه، لا يزال النموذج التزايدي orders يحتوي على طلب واحد فقط لـ Danny حتى تنفيذ dbt run التالي. تُبقي عمليات dbt run اللاحقة على الجدول الهدف وبياناته، ولا تحدّث سوى تعريف العرض، باستخدام ALTER TABLE ... MODIFY QUERY عندما يسمح التغيير بذلك، لذا من الآمن الإبقاء على النموذج ضمن المشروع. أما dbt run --full-refresh فيعيد بناء الجدول الهدف ويملؤه بالبيانات التاريخية من جديد (ما لم تكن قيمة catchup هي False). وتغطي صفحة العروض المجسّدة ما تبقّى: تغييرات المخطط عبر on_schema_change، وتعطيل الملء التاريخي باستخدام catchup، والعروض المجسّدة القابلة للتحديث، وتغذية عدة عروض للهدف نفسه، وتعريف الجدول الهدف كنموذج مستقل بذاته.

معلومات إضافية

لا يتطرق هذا الدليل إلا إلى أساسيات dbt. وتُعد وثائق dbt المرجع لكل ما هو غير خاص بـ ClickHouse. أما بالنسبة إلى adapter، فراجع صفحة features and configurations للاطلاع على إعدادات profile والميزات العامة، وصفحة تجسيدات لكل تهيئة وردت أعلاه، وصفحة dbt OSS وdbt v2 ومنصة dbt إذا كنت تستخدم dbt OSS أو dbt v2 أو منصة dbt. ونرحّب بالمساهمات بأمثلة جديدة في Jaffle Shop for ClickHouse.
آخر تعديل في ٢٦ سبتمبر ٢٠٢٦