الانتقال إلى المحتوى الرئيسي

الخلفية

تتيح لك العروض المادية التزايدية (العروض المادية) نقل تكلفة المعالجة من وقت الاستعلام إلى وقت الإدراج، مما يؤدي إلى تسريع استعلامات SELECT. وخلافًا لقواعد البيانات المعاملاتية مثل Postgres، فإن العرض المادي في ClickHouse ليس سوى مُشغِّل ينفّذ استعلامًا على كتل البيانات أثناء إدراجها في جدول. ثم تُدرَج نتيجة هذا الاستعلام في جدول “الهدف” ثانٍ. وإذا أُدرجت صفوف إضافية، فستُرسَل النتائج مرة أخرى إلى جدول الهدف، حيث تُحدَّث النتائج الوسيطة وتُدمَج. وهذه النتيجة المدمجة تكافئ تنفيذ الاستعلام على كامل البيانات الأصلية. والسبب الرئيسي لاستخدام العروض المادية هو أن النتائج المُدرَجة في جدول الهدف تمثل ناتج عمليات تجميع أو تصفية أو تحويل تُجرى على الصفوف. وغالبًا ما تكون هذه النتائج تمثيلًا أصغر للبيانات الأصلية (ملخصًا جزئيًا في حالة عمليات التجميع). وهذا، إلى جانب بساطة الاستعلام الناتج لقراءة النتائج من جدول الهدف، يضمن أن تكون أوقات الاستعلام أسرع مما لو أُجريت المعالجة نفسها على البيانات الأصلية، إذ ينقل المعالجة (ومن ثم زمن استجابة الاستعلام) من وقت الاستعلام إلى وقت الإدراج. تُحدَّث العروض المادية في ClickHouse آنيًا مع تدفق البيانات إلى الجدول الذي تستند إليه، فتعمل بصورة أقرب إلى الفهارس التي تُحدَّث باستمرار. وهذا يختلف عن قواعد البيانات الأخرى، حيث تكون العروض المادية عادةً لقطات ثابتة لاستعلام يجب تحديثها (على نحو مشابه لـ العروض المادية القابلة للتحديث في ClickHouse).

مثال

لأغراض المثال، سنستخدم مجموعة بيانات Stack Overflow الموثقة في “تصميم المخطط”. لنفترض أننا نريد الحصول على عدد الأصوات الإيجابية والسلبية يومياً لمنشور معين.
يُعدّ هذا استعلامًا بسيطًا نسبيًا في ClickHouse بفضل الدالة toStartOfDay:
هذا الاستعلام سريع بالفعل بفضل ClickHouse، لكن هل بإمكاننا تحقيق أداء أفضل؟ إذا أردنا احتساب هذا عند وقت الإدراج باستخدام طريقة عرض مُجسَّدة، فنحتاج إلى جدول لاستقبال النتائج. يجب أن يحتفظ هذا الجدول بصف واحد فقط لكل يوم. وإذا وردت تحديثات ليوم موجود مسبقاً، وجب دمج الأعمدة الأخرى في صف ذلك اليوم. ولكي يتحقق هذا الدمج للحالات التراكمية، يجب تخزين الحالات الجزئية للأعمدة الأخرى. يتطلب هذا نوع محرك خاصًا في ClickHouse: وهو SummingMergeTree. يستبدل هذا المحرك جميع الصفوف التي تحمل نفس مفتاح الترتيب بصف واحد يحتوي على القيم المجمّعة للأعمدة الرقمية. سيدمج الجدول التالي أي صفوف تشترك في نفس التاريخ، مع جمع قيم الأعمدة الرقمية:
لتوضيح العرض المُجسَّد، لنفترض أن جدول votes فارغ ولم يستقبل أي بيانات بعد. يُنفّذ العرض المُجسَّد استعلام SELECT المذكور أعلاه على البيانات المُدرجة في votes، وتُرسَل النتائج إلى up_down_votes_per_day:
تُعدّ عبارة TO هنا أساسية، إذ تحدد الوجهة التي ستُرسَل إليها النتائج، أي up_down_votes_per_day. يمكننا إعادة ملء جدول Votes بالاستناد إلى عملية الإدراج السابقة:
عند الانتهاء، يمكننا التحقق من حجم up_down_votes_per_day — إذ ينبغي أن يحتوي على صف واحد لكل يوم:
لقد خفّضنا فعليًا عدد الصفوف هنا من 238 مليونًا (في votes) إلى 5000 عبر تخزين نتيجة الاستعلام. لكن الأهم هو أنه إذا أُدرجت أصوات جديدة في جدول votes، فستُرسَل قيم جديدة إلى up_down_votes_per_day لليوم المقابل لها، حيث ستُدمَج تلقائيًا بشكل غير متزامن في الخلفية، بحيث يبقى صف واحد فقط لكل يوم. وهكذا سيظل up_down_votes_per_day صغيرًا ومحدّثًا دائمًا. وبما أن دمج الصفوف يتم بشكل غير متزامن، فقد يوجد أكثر من صف واحد لليوم نفسه عند تنفيذ المستخدم لاستعلام. ولضمان دمج أي صفوف معلّقة وقت الاستعلام، لدينا خياران:
  • استخدام المعدِّل FINAL مع اسم الجدول. لقد فعلنا ذلك في استعلام العدّ أعلاه.
  • التجميع حسب مفتاح الترتيب المستخدم في جدولنا النهائي، أي CreationDate، ثم جمع المقاييس. ويكون هذا عادةً أكثر كفاءة ومرونة (إذ يمكن استخدام الجدول لأغراض أخرى)، لكن الخيار الأول قد يكون أبسط في بعض الاستعلامات. نعرض كلا الخيارين أدناه:
لقد سرّع هذا استعلامنا من 0.133s إلى 0.004s — أي تحسّن بأكثر من 25 مرة!
مهم: ORDER BY = GROUP BYفي معظم الحالات، يجب أن تتوافق الأعمدة المستخدمة في عبارة GROUP BY ضمن تحويل العروض المادية مع الأعمدة المستخدمة في عبارة ORDER BY للجدول الهدف عند استخدام محركَي الجدول SummingMergeTree أو AggregatingMergeTree. تعتمد هذه المحركات على أعمدة ORDER BY لدمج الصفوف ذات القيم المتطابقة أثناء عمليات الدمج في الخلفية. وقد يؤدي عدم التوافق بين أعمدة GROUP BY وORDER BY إلى تراجع أداء الاستعلامات، أو إلى عمليات دمج غير فعّالة، أو حتى إلى اختلافات في البيانات.

مثال أكثر تعقيدًا

يستخدم المثال أعلاه Materialized Views لحساب مجموعين والاحتفاظ بهما لكل يوم. وتمثل المجاميع أبسط أشكال التجميع التي يمكن الاحتفاظ بحالاتها الجزئية، إذ يمكننا ببساطة إضافة القيم الجديدة إلى القيم الحالية عند وصولها. ومع ذلك، يمكن استخدام Materialized Views في ClickHouse مع أي نوع من أنواع التجميع. لنفترض أننا نريد حساب بعض الإحصاءات للمنشورات لكل يوم: المئين 99.9 للحقل Score ومتوسط الحقل CommentCount. قد يبدو الاستعلام المستخدم لحساب ذلك كما يلي:
كما في السابق، يمكننا إنشاء عرض مادي ينفّذ الاستعلام أعلاه عند إدراج منشورات جديدة في جدول posts الخاص بنا. لأغراض المثال، ولتجنّب تحميل بيانات posts من S3، سننشئ جدولًا مكررًا باسم posts_null له البنية نفسها الخاصة بـ posts. ومع ذلك، لن يخزّن هذا الجدول أي بيانات، بل سيُستخدم فقط بواسطة العرض المادي عند إدراج صفوف. ولمنع تخزين البيانات، يمكننا استخدام نوع Null من محرك الجدول.
يُعد محرك الجدول Null تحسينًا قويًا — فكّر فيه على أنه /dev/null. سيحسب العرض المادي الإحصاءات الموجزة ويخزّنها عندما يستقبل جدول posts_null صفوفًا وقت الإدراج — فهو مجرد مُحفِّز. ومع ذلك، لن تُخزَّن البيانات الخام. ومع أننا في حالتنا على الأرجح ما زلنا نريد تخزين المنشورات الأصلية، يمكن استخدام هذا النهج لحساب التجميعات مع تجنّب أعباء تخزين البيانات الخام. وعليه، يصبح العرض المادي كما يلي:
لاحظ كيف نُضيف اللاحقة State إلى نهاية دوال التجميع. وهذا يضمن إرجاع حالة التجميع الخاصة بالدالة بدلًا من النتيجة النهائية. وتحتوي هذه الحالة على معلومات إضافية تتيح دمج هذه الحالة الجزئية مع حالات أخرى. على سبيل المثال، في حالة حساب المتوسط، سيتضمن ذلك عدد القيم ومجموع العمود.
تُعد حالات التجميع الجزئية ضرورية لحساب النتائج الصحيحة. فعلى سبيل المثال، عند حساب متوسط، فإن الاكتفاء بأخذ متوسط متوسطات النطاقات الفرعية يؤدي إلى نتائج غير صحيحة.
ننشىء الآن الجدول الهدف لهذا العرض post_stats_per_day، والذي يخزّن حالات التجميع الجزئية هذه:
في حين كان SummingMergeTree كافيًا سابقًا لتخزين عمليات العدّ، فإننا نحتاج إلى نوع محرك أكثر تقدمًا لوظائف أخرى: AggregatingMergeTree. ولضمان أن يعرف ClickHouse أنه ستُخزَّن حالات التجميع، نُعرّف Score_quantiles وAvgCommentCount بالنوع AggregateFunction، مع تحديد الدالة المصدر للحالات الجزئية ونوع الأعمدة المصدر الخاصة بها. وكما هو الحال مع SummingMergeTree، ستُدمج الصفوف التي لها قيمة المفتاح نفسها في ORDER BY (Day في المثال أعلاه). ولملء post_stats_per_day عبر العرض المادي الخاص بنا، يمكننا ببساطة إدراج جميع الصفوف من posts في posts_null:
في بيئة الإنتاج، من المرجّح أن تُلحِق العرض المادي بجدول posts. وقد استخدمنا posts_null هنا لتوضيح جدول Null.
يجب أن يستخدم استعلامنا النهائي اللاحقة Merge مع دوالنا (لأن الأعمدة تخزّن حالات التجميع الجزئية):
لاحظ أننا نستخدم GROUP BY هنا بدلًا من FINAL.

تطبيقات أخرى

يركّز ما سبق أساسًا على استخدام العروض المادية لتحديث التجميعات الجزئية للبيانات بصورة تدريجية، وبذلك يُنقَل عبء المعالجة من وقت الاستعلام إلى وقت الإدراج. وإلى جانب حالة الاستخدام الشائعة هذه، للعروض المادية تطبيقات أخرى عديدة.

التصفية والتحويل

في بعض الحالات، قد نرغب في إدراج مجموعة فرعية فقط من الصفوف والأعمدة عند الإدراج. في هذه الحالة، يمكن لجدول posts_null تلقّي عمليات إدراج، مع استخدام استعلام SELECT لتصفية الصفوف قبل إدراجها في جدول posts. على سبيل المثال، لنفترض أننا أردنا تحويل عمود Tags في جدول posts. يحتوي هذا العمود على قائمة بأسماء الوسوم مفصولة بالرمز pipe. ومن خلال تحويلها إلى مصفوفة، يمكننا التجميع حسب قيم الوسوم الفردية بسهولة أكبر.
يمكننا إجراء هذا التحويل عند تشغيل INSERT INTO SELECT. ويتيح لنا العرض المادي تضمين هذا المنطق في DDL الخاصة بـ ClickHouse والإبقاء على INSERT بسيطًا، مع تطبيق التحويل على أي صفوف جديدة.
يظهر أدناه العرض المادي المستخدم لهذا التحويل:

جدول مرجعي

ينبغي مراعاة أنماط الوصول عند اختيار مفتاح الترتيب في ClickHouse. ويُفضَّل استخدام الأعمدة التي تُستخدَم كثيرًا في عبارات التصفية والتجميع. وقد يكون ذلك مقيِّدًا في الحالات التي تكون فيها لدى المستخدمين أنماط وصول أكثر تنوعًا لا يمكن حصرها في مجموعة واحدة من الأعمدة. على سبيل المثال، تأمّل جدول comments التالي:
يُحسِّن مفتاح الترتيب هنا الجدولَ للاستعلامات التي تُصفّي حسب PostId. لنفترض أن مستخدمًا يرغب في التصفية وفق UserId معيّن وحساب متوسط Score الخاص به:
على الرغم من أن هذا سريع (فالبيانات صغيرة بالنسبة إلى ClickHouse)، يمكننا أن نستدل من عدد الصفوف المُعالجة — 90.38 مليونًا — على أن ذلك يتطلب فحصًا كاملًا للجدول. وبالنسبة إلى مجموعات البيانات الأكبر، يمكننا استخدام عرض مادي للبحث عن قيم PostId الخاصة بنا من أجل تصفية العمود UserId. ويمكن بعد ذلك استخدام هذه القيم لإجراء عملية بحث فعّالة. في هذا المثال، يمكن أن يكون العرض المادي بسيطًا جدًا، إذ يحدد فقط PostId وUserId من comments on insert. ثم تُرسَل هذه النتائج بدورها إلى جدول comments_posts_users المرتّب حسب UserId. ننشئ أدناه إصدارًا من جدول Comments باستخدام Null ونستخدمه لتعبئة العرض وجدول comments_posts_users:
يمكننا الآن استخدام هذا العرض ضمن استعلام فرعي لتسريع الاستعلام السابق:

ربط / تتابع العروض المادية

يمكن ربط العروض المادية على شكل سلسلة (أو بشكل متتابع)، مما يتيح إنشاء مسارات عمل معقدة. لمزيد من المعلومات، راجع دليل “العروض المادية المتتابعة”.

العروض المادية وJOINs

العروض المادية القابلة للتحديثينطبق ما يلي على العروض المادية التزايدية فقط. تنفّذ العروض المادية القابلة للتحديث استعلامها دوريًا على كامل مجموعة البيانات المستهدفة، وتدعم JOINs بشكل كامل. يُنصح باستخدامها مع JOINs المعقدة إذا كان بالإمكان تقبّل انخفاض حداثة النتائج.
تدعم العروض المادية التزايدية في ClickHouse عمليات JOIN دعمًا كاملًا، ولكن مع قيد مهم واحد: لا يتم تشغيل العرض المادي إلا عند حدوث عمليات إدراج في الجدول المصدر (الجدول الواقع في أقصى اليسار في الاستعلام). أما الجداول الموجودة على الجانب الأيمن في JOINs فلا تؤدي إلى تشغيل تحديثات، حتى إذا تغيّرت بياناتها. ويكتسب هذا السلوك أهمية خاصة عند إنشاء العروض المادية التزايدية، حيث تُجمَّع البيانات أو تُحوَّل وقت الإدراج. عند تعريف عرض مادي تزايدي باستخدام JOIN، يعمل الجدول الواقع في أقصى اليسار ضمن استعلام SELECT بوصفه المصدر. وعند إدراج صفوف جديدة في هذا الجدول، ينفّذ ClickHouse استعلام العرض المادي فقط على هذه الصفوف المُدرجة حديثًا. وتُقرأ الجداول الموجودة على الجانب الأيمن في JOIN كاملةً أثناء هذا التنفيذ، لكن التغييرات التي تطرأ عليها وحدها لا تؤدي إلى تشغيل العرض. يجعل هذا السلوك JOINs في العروض المادية مشابهةً لعملية join على لقطة من البيانات مقابل بيانات أبعاد ثابتة. ويعمل هذا جيدًا عند إثراء البيانات باستخدام جداول مرجعية أو جداول أبعاد. ومع ذلك، فإن أي تحديثات على الجداول الموجودة على الجانب الأيمن (مثل البيانات الوصفية للمستخدم) لن تُحدِّث العرض المادي بأثر رجعي. ولمشاهدة البيانات المُحدَّثة، يجب أن تصل عمليات إدراج جديدة إلى الجدول المصدر.

مثال

لنستعرض مثالًا عمليًا باستخدام مجموعة بيانات Stack Overflow. سنستخدم عرضًا ماديًا لحساب الشارات اليومية لكل مستخدم، بما في ذلك اسم العرض الخاص بالمستخدم من جدول users. للتذكير، فمخططات جداولنا هي:
سنفترض أن جدول users مملوء بالبيانات مسبقًا:
يُعرَّف العرض المادي والجدول الهدف المرتبط به على النحو التالي:
محاذاة التجميع والترتيبيجب أن تتضمن عبارة GROUP BY في العرض المادي DisplayName وUserId وDay لكي تتطابق مع ORDER BY في الجدول الهدف SummingMergeTree. وهذا يضمن تجميع الصفوف ودمجها على نحو صحيح. وقد يؤدي إغفال أيٍّ منها إلى نتائج غير صحيحة أو إلى عمليات دمج غير فعّالة.
إذا قمنا الآن بتعبئة بيانات الشارات، فسيُفعَّل العرض، مما سيملأ جدول daily_badges_by_user الخاص بنا.
لنفترض أننا نريد عرض الشارات التي حصل عليها مستخدم معيّن؛ يمكننا كتابة الاستعلام التالي:
الآن، إذا تلقّى هذا المستخدم شارة جديدة وأُدرِج صف جديد، فسيُحدَّث العرض لدينا:
لاحظ زمن الاستجابة لعملية الإدراج هنا. إذ يُجرى JOIN لصف المستخدم المُدرَج مع جدول users بالكامل، ما يؤثر بشكل كبير في أداء الإدراج. نقترح أدناه أساليب لمعالجة ذلك في “استخدام الجدول المصدر في عوامل التصفية وعمليات JOIN”.
في المقابل، إذا أدرجنا شارةً لمستخدم جديد ثم أدرجنا بعد ذلك صف هذا المستخدم، فسيفشل العرض المادي لدينا في التقاط مقاييس المستخدمين.
في هذه الحالة، لا يُنفَّذ العرض إلا عند إدراج الشارة قبل وجود صفّ المستخدم. وإذا أدرجنا شارة أخرى للمستخدم، فسيُدرَج صفّ، كما هو متوقَّع:
لكن تجدر الإشارة إلى أن هذه النتيجة غير صحيحة.

أفضل الممارسات لعمليات JOIN في العروض المادية

  • استخدم الجدول الواقع في أقصى اليسار كمُحفّز. لا يُحفّز العرض المادي إلا الجدول الموجود على الجانب الأيسر من عبارة SELECT. أما التغييرات التي تطرأ على الجداول الموجودة على اليمين فلن تُحفّز التحديثات.
  • أدرِج بيانات الجداول المنضمّة مسبقًا. تأكد من وجود البيانات في الجداول المنضمّة قبل إدراج الصفوف في الجدول المصدر. إذ يُقيَّم JOIN وقت الإدراج، لذا ستؤدي البيانات المفقودة إلى صفوف غير متطابقة أو قيم NULL.
  • قلّل الأعمدة المسحوبة من عمليات JOIN. حدّد فقط الأعمدة المطلوبة من الجداول المنضمّة لتقليل استخدام الذاكرة وخفض زمن الاستجابة وقت الإدراج (انظر أدناه).
  • قيّم الأداء وقت الإدراج. تزيد عمليات JOIN من تكلفة عمليات الإدراج، خاصةً مع الجداول الكبيرة على الجانب الأيمن. قِس معدلات الإدراج باستخدام بيانات ممثلة من بيئة الإنتاج.
  • فضّل استخدام Dictionaries لعمليات البحث البسيطة. استخدم Dictionaries لعمليات البحث من نوع key-value (مثل ربط معرّف المستخدم بالاسم) لتجنّب عمليات JOIN المكلفة.
  • حاذِ بين GROUP BY وORDER BY لتحقيق كفاءة الدمج. عند استخدام SummingMergeTree أو AggregatingMergeTree، تأكد من أن GROUP BY يطابق عبارة ORDER BY في الجدول الهدف للسماح بدمج الصفوف بكفاءة.
  • استخدم أسماءً مستعارة صريحة للأعمدة. عندما تحتوي الجداول على أسماء أعمدة متداخلة، استخدم الأسماء المستعارة لتجنّب الالتباس وضمان صحة النتائج في الجدول الهدف.
  • راعِ حجم الإدراج وتكراره. تعمل عمليات JOIN جيدًا مع أعباء عمل الإدراج المتوسطة. أما في حالات استيعاب البيانات عالي الإنتاجية، ففكّر في استخدام staging tables أو عمليات الربط المسبق أو أساليب أخرى مثل Dictionaries وRefreshable Materialized Views.

استخدام الجدول المصدر في عوامل التصفية وعمليات JOIN

عند العمل مع العروض المادية في ClickHouse، من المهم فهم كيفية التعامل مع الجدول المصدر أثناء تنفيذ الاستعلام الخاص بالعرض المادي. وبالتحديد، يُستبدل الجدول المصدر في استعلام العرض المادي بكتلة البيانات المُدرَجة. وقد يؤدي هذا السلوك إلى بعض النتائج غير المتوقعة إذا لم يُفهم بالشكل الصحيح.

مثال توضيحي

لنفترض الإعداد التالي:

الشرح

في المثال أعلاه، لدينا عرضان ماديان mvw1 وmvw2 ينفذان عمليات متشابهة، مع اختلاف طفيف في كيفية إشارتهما إلى الجدول المصدر t0. في mvw1، يُشار إلى الجدول t0 مباشرةً داخل استعلام فرعي (SELECT * FROM t0) في الجانب الأيمن من عملية JOIN. وعند إدراج بيانات في t0، يُنفَّذ استعلام العرض المادي مع استبدال t0 بكتلة البيانات المُدرجة. وهذا يعني أن عملية JOIN تُجرى فقط على الصفوف المُدرجة حديثًا، لا على الجدول بأكمله. أما في الحالة الثانية عند الربط مع vt0، فيقرأ العرض جميع البيانات من t0. ويضمن ذلك أن عملية JOIN تأخذ جميع الصفوف في t0 في الحسبان، وليس فقط الكتلة المُدرجة حديثًا. ويكمن الفرق الأساسي في كيفية تعامل ClickHouse مع الجدول المصدر داخل استعلام العرض المادي. فعندما يُفعَّل عرض مادي بواسطة عملية insert، يُستبدل الجدول المصدر (t0 في هذه الحالة) بكتلة البيانات المُدرجة. ويمكن الاستفادة من هذا السلوك لتحسين الاستعلامات، لكنه يتطلب أيضًا الانتباه بعناية لتجنّب النتائج غير المتوقعة.

حالات الاستخدام والمحاذير

عمليًا، يمكنك استخدام هذا السلوك لتحسين العروض المادية التي تحتاج فقط إلى معالجة مجموعة فرعية من بيانات الجدول المصدر. على سبيل المثال، يمكنك استخدام استعلام فرعي لتصفية الجدول المصدر قبل ضمّه إلى جداول أخرى. وقد يساعد ذلك في تقليل حجم البيانات التي يعالجها العرض المادي وتحسين الأداء.
في هذا المثال، لا تحتوي المجموعة المُنشأة من الاستعلام الفرعي IN (SELECT id FROM t0) إلا على الصفوف المُدرجة حديثًا، مما قد يساعد في تصفية t1 بالاعتماد عليها.

مثال مع Stack Overflow

راجع مثال العرض المادي السابق لحساب الأوسمة اليومية لكل مستخدم، بما في ذلك الاسم المعروض للمستخدم من جدول users.
أثّر هذا العرض بشكل كبير في كمون عمليات insert على جدول badges، مثلًا.
باستخدام النهج أعلاه، يمكننا تحسين هذا العرض. سنضيف عامل تصفية إلى جدول users باستخدام معرّفات المستخدم في صفوف الشارات التي أُدرجت:
لا يقتصر الأمر على تسريع الإدراج الأولي للشارات:
لكن هذا يعني أيضًا أن عمليات إدراج badge مستقبلًا ستكون فعّالة:
في العملية المذكورة أعلاه، لا يُسترجَع سوى صف واحد من جدول users لمعرّف المستخدم 2936484. كما جرى تحسين عملية البحث هذه باستخدام مفتاح ترتيب الجدول Id.

العروض المادية وعمليات UNION

تُستخدم استعلامات UNION ALL عادةً لدمج البيانات من عدة جداول مصدر في مجموعة نتائج واحدة. ومع أن UNION ALL غير مدعومة مباشرةً في العروض المادية التزايدية، يمكنك تحقيق النتيجة نفسها من خلال إنشاء عرض مادي منفصل لكل فرع SELECT وكتابة نتائجه في جدول هدف مشترك. في مثالنا، سنستخدم مجموعة بيانات Stack Overflow. تأمّل الجدولين badges وcomments أدناه، اللذين يمثّلان الشارات التي يحصل عليها المستخدم والتعليقات التي يضيفها إلى المنشورات:
يمكن ملؤها بأوامر INSERT INTO التالية:
لنفترض أننا نريد إنشاء عرض موحّد لنشاط المستخدمين، يُظهر آخر نشاط لكل مستخدم بدمج هذين الجدولين:
لنفترض أن لدينا جدولًا مستهدفًا لتلقّي نتائج هذا الاستعلام. لاحظ استخدام محرك الجدول AggregatingMergeTree وAggregateFunction لضمان دمج النتائج بشكل صحيح:
إذا كنت تريد أن يتحدّث هذا الجدول عند إدراج صفوف جديدة في badges أو comments، فقد يكون النهج المباشر لهذه المشكلة هو محاولة إنشاء عرض مادي باستخدام استعلام union السابق:
رغم أن هذا صحيح نحويًا، فإنه سيؤدي إلى نتائج غير مقصودة - إذ لن تُفعَّل طريقة العرض إلا عند عمليات الإدراج في جدول comments. على سبيل المثال:
لن تؤدي عمليات الإدراج في الجدول badges إلى تشغيل الـ view، مما يمنع user_activity من تلقّي التحديثات:
لحلّ هذه المشكلة، نُنشئ ببساطة عرضًا ماديًا لكل عبارة SELECT:
يُعطي الإدراج في أيٍّ من الجدولين الآن النتائج الصحيحة. على سبيل المثال، إذا أدرجنا بيانات في جدول comments:
وبالمثل، تنعكس عمليات الإدراج في جدول badges أيضًا في جدول user_activity:

المعالجة المتوازية مقابل المعالجة المتسلسلة

كما يتضح في المثال السابق، يمكن أن يكون الجدول مصدرًا لعدة عروض مادية. ويعتمد ترتيب تنفيذ هذه العروض على الإعداد parallel_view_processing. تكون قيمة هذا الإعداد افتراضيًا 0 (false)، ما يعني أن العروض المادية تُنفَّذ بشكل متسلسل وفق ترتيب uuid. على سبيل المثال، تأمّل جدول source التالي و3 عروض مادية، يرسل كلٌّ منها صفوفًا إلى جدول target:
لاحظ أن كل عرض من هذه العروض ينتظر ثانية واحدة قبل إدراج صفوفه في جدول target، مع تضمين اسمه ووقت الإدراج أيضًا. تستغرق عملية إدراج صف في الجدول source نحو 3 ثوانٍ، إذ يُنفَّذ كل عرض بالتسلسل:
يمكننا التحقق من وصول الصفوف من كل صف باستخدام SELECT:
يتوافق هذا مع uuid الخاص بالعروض:
في المقابل، لننظر في ما يحدث إذا أدرجنا صفًا مع تفعيل parallel_view_processing=1. عند تفعيل هذا الإعداد، تُنفَّذ طرق العرض بالتوازي، من دون أي ضمانات بشأن الترتيب الذي تصل به الصفوف إلى الجدول الهدف:
على الرغم من أن ترتيب وصول الصفوف من كل عرض متطابق هنا، فإن ذلك غير مضمون، كما يوضحه تقارب وقت الإدراج لكل صف. ولاحظ أيضًا تحسّن أداء الإدراج.

متى تستخدم المعالجة المتوازية

يمكن أن يؤدي تفعيل parallel_view_processing=1 إلى تحسين معدل نقل عمليات الإدراج بشكل كبير، كما هو موضح أعلاه، خاصةً عند إرفاق عدة عروض مادية بجدول واحد. ومع ذلك، من المهم فهم الموازنة بين الفوائد والتكاليف:
  • زيادة الضغط الناتج عن الإدراج: تُنفَّذ جميع العروض المادية بالتزامن، مما يزيد من استخدام CPU والذاكرة. وإذا كان كل عرض ينفّذ عمليات حسابية كثيفة أو عمليات JOIN، فقد يؤدي ذلك إلى زيادة الحمل على النظام.
  • الحاجة إلى ترتيب تنفيذ صارم: في حالات نادرة من سير العمل تكون فيها أولوية لترتيب تنفيذ العروض (مثل الاعتماديات المتسلسلة)، قد يؤدي التنفيذ المتوازي إلى حالة غير متسقة أو إلى race condition. ومع أنه يمكن تصميم النظام لتجاوز ذلك، فإن مثل هذه الإعدادات تكون هشة وقد تتعطل مع الإصدارات المستقبلية.
القيم الافتراضية التاريخية والاستقرارظل التنفيذ التسلسلي هو الإعداد الافتراضي لفترة طويلة، ويعود ذلك جزئيًا إلى تعقيدات معالجة الأخطاء. تاريخيًا، كان الفشل في أحد materialized views قد يمنع تنفيذ العروض الأخرى. وقد حسّنت الإصدارات الأحدث هذا السلوك من خلال عزل الإخفاقات على مستوى كل block، لكن التنفيذ التسلسلي لا يزال يوفّر دلالات أوضح لحالات الفشل.
بوجه عام، فعّل parallel_view_processing=1 عندما:
  • تكون لديك عدة عروض مادية مستقلة
  • تسعى إلى تحقيق أقصى أداء لعمليات الإدراج
  • تكون مدركًا لقدرة النظام على التعامل مع التنفيذ المتزامن للعروض
اتركه معطّلًا عندما:
  • تكون هناك اعتماديات بين العروض المادية
  • تحتاج إلى تنفيذ متوقع ومرتّب
  • تكون بصدد استكشاف أخطاء سلوك الإدراج أو تدقيقه وتريد إعادة تشغيل حتمية

العروض المادية وتعبيرات الجدول الشائعة (CTE)

تدعم العروض المادية تعبيرات الجدول الشائعة (CTEs) غير التكرارية.
تعبيرات الجدول الشائعة ليست مُجسَّدةلا يقوم ClickHouse بتخزين تعبيرات CTE تخزينًا ماديًا؛ بل يستبدل تعريف CTE مباشرةً داخل الاستعلام، ما قد يؤدي إلى تقييم التعبير نفسه عدة مرات (إذا استُخدم CTE أكثر من مرة).
انظر إلى المثال التالي، الذي يحسب النشاط اليومي لكل نوع من المنشورات.
مع أن CTE ليس ضروريًا هنا إطلاقًا، فإن العرض سيعمل كما هو متوقع لأغراض المثال:
في ClickHouse، تُدرَج تعبيرات الجداول المشتركة (CTEs) مباشرةً، ما يعني أنها تُنسخ وتُلصق فعليًا داخل الاستعلام أثناء التحسين، ولا يتم تجسيدها. وهذا يعني ما يلي:
  • إذا كان تعبير الجدول المشترك (CTE) يشير إلى جدول مختلف عن الجدول المصدر (أي الجدول المرتبط به العرض المادي)، وكان مستخدمًا في عبارة JOIN أو IN، فسيتصرف كاستعلام فرعي أو كعملية join، وليس كمُشغِّل.
  • سيظل العرض المادي يُفعَّل فقط عند تنفيذ عمليات insert في الجدول المصدر الرئيسي، لكن سيُعاد تنفيذ تعبير الجدول المشترك (CTE) مع كل عملية insert، ما قد يسبب عبئًا إضافيًا غير ضروري، خصوصًا إذا كان الجدول المشار إليه كبيرًا.
على سبيل المثال،
في هذه الحالة، يُعاد تقييم تعبير الجدول الشائع الخاص بـ users عند كل عملية insert في posts، ولن يتم تحديث العرض المادي عند insert مستخدمين جدد، بل فقط عند insert في posts. بوجه عام، استخدم تعبيرات الجدول الشائعة للمنطق الذي يعمل على الجدول المصدر نفسه المرتبط به العرض المادي، أو تأكد من أن الجداول المشار إليها صغيرة ومن غير المرجح أن تتسبب في اختناقات بالأداء. بديلًا عن ذلك، يمكنك النظر في التحسينات نفسها الموصى بها لـ JOINs مع Materialized Views.
آخر تعديل في ١ يوليو ٢٠٢٦