الخلفية
SELECT.
وخلافًا لقواعد البيانات المعاملاتية مثل Postgres، فإن العرض المادي في ClickHouse ليس سوى مُشغِّل ينفّذ استعلامًا على كتل البيانات أثناء إدراجها في جدول. ثم تُدرَج نتيجة هذا الاستعلام في جدول “الهدف” ثانٍ. وإذا أُدرجت صفوف إضافية، فستُرسَل النتائج مرة أخرى إلى جدول الهدف، حيث تُحدَّث النتائج الوسيطة وتُدمَج. وهذه النتيجة المدمجة تكافئ تنفيذ الاستعلام على كامل البيانات الأصلية.
والسبب الرئيسي لاستخدام العروض المادية هو أن النتائج المُدرَجة في جدول الهدف تمثل ناتج عمليات تجميع أو تصفية أو تحويل تُجرى على الصفوف. وغالبًا ما تكون هذه النتائج تمثيلًا أصغر للبيانات الأصلية (ملخصًا جزئيًا في حالة عمليات التجميع). وهذا، إلى جانب بساطة الاستعلام الناتج لقراءة النتائج من جدول الهدف، يضمن أن تكون أوقات الاستعلام أسرع مما لو أُجريت المعالجة نفسها على البيانات الأصلية، إذ ينقل المعالجة (ومن ثم زمن استجابة الاستعلام) من وقت الاستعلام إلى وقت الإدراج.
تُحدَّث العروض المادية في ClickHouse آنيًا مع تدفق البيانات إلى الجدول الذي تستند إليه، فتعمل بصورة أقرب إلى الفهارس التي تُحدَّث باستمرار. وهذا يختلف عن قواعد البيانات الأخرى، حيث تكون العروض المادية عادةً لقطات ثابتة لاستعلام يجب تحديثها (على نحو مشابه لـ العروض المادية القابلة للتحديث في ClickHouse).
مثال
toStartOfDay:
SELECT المذكور أعلاه على البيانات المُدرجة في votes، وتُرسَل النتائج إلى up_down_votes_per_day:
TO هنا أساسية، إذ تحدد الوجهة التي ستُرسَل إليها النتائج، أي up_down_votes_per_day.
يمكننا إعادة ملء جدول Votes بالاستناد إلى عملية الإدراج السابقة:
up_down_votes_per_day — إذ ينبغي أن يحتوي على صف واحد لكل يوم:
votes) إلى 5000 عبر تخزين نتيجة الاستعلام. لكن الأهم هو أنه إذا أُدرجت أصوات جديدة في جدول votes، فستُرسَل قيم جديدة إلى up_down_votes_per_day لليوم المقابل لها، حيث ستُدمَج تلقائيًا بشكل غير متزامن في الخلفية، بحيث يبقى صف واحد فقط لكل يوم. وهكذا سيظل up_down_votes_per_day صغيرًا ومحدّثًا دائمًا.
وبما أن دمج الصفوف يتم بشكل غير متزامن، فقد يوجد أكثر من صف واحد لليوم نفسه عند تنفيذ المستخدم لاستعلام. ولضمان دمج أي صفوف معلّقة وقت الاستعلام، لدينا خياران:
- استخدام المعدِّل
FINALمع اسم الجدول. لقد فعلنا ذلك في استعلام العدّ أعلاه. - التجميع حسب مفتاح الترتيب المستخدم في جدولنا النهائي، أي
CreationDate، ثم جمع المقاييس. ويكون هذا عادةً أكثر كفاءة ومرونة (إذ يمكن استخدام الجدول لأغراض أخرى)، لكن الخيار الأول قد يكون أبسط في بعض الاستعلامات. نعرض كلا الخيارين أدناه:
مثال أكثر تعقيدًا
Score ومتوسط الحقل CommentCount. قد يبدو الاستعلام المستخدم لحساب ذلك كما يلي:
posts الخاص بنا.
لأغراض المثال، ولتجنّب تحميل بيانات posts من S3، سننشئ جدولًا مكررًا باسم posts_null له البنية نفسها الخاصة بـ posts. ومع ذلك، لن يخزّن هذا الجدول أي بيانات، بل سيُستخدم فقط بواسطة العرض المادي عند إدراج صفوف. ولمنع تخزين البيانات، يمكننا استخدام نوع 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بسيطًا، مع تطبيق التحويل على أي صفوف جديدة.
جدول مرجعي
comments التالي:
PostId.
لنفترض أن مستخدمًا يرغب في التصفية وفق UserId معيّن وحساب متوسط Score الخاص به:
PostId الخاصة بنا من أجل تصفية العمود UserId. ويمكن بعد ذلك استخدام هذه القيم لإجراء عملية بحث فعّالة.
في هذا المثال، يمكن أن يكون العرض المادي بسيطًا جدًا، إذ يحدد فقط PostId وUserId من comments on insert. ثم تُرسَل هذه النتائج بدورها إلى جدول comments_posts_users المرتّب حسب UserId. ننشئ أدناه إصدارًا من جدول Comments باستخدام Null ونستخدمه لتعبئة العرض وجدول comments_posts_users:
ربط / تتابع العروض المادية
العروض المادية وJOINs
العروض المادية القابلة للتحديثينطبق ما يلي على العروض المادية التزايدية فقط. تنفّذ العروض المادية القابلة للتحديث استعلامها دوريًا على كامل مجموعة البيانات المستهدفة، وتدعم JOINs بشكل كامل. يُنصح باستخدامها مع JOINs المعقدة إذا كان بالإمكان تقبّل انخفاض حداثة النتائج.
JOIN دعمًا كاملًا، ولكن مع قيد مهم واحد: لا يتم تشغيل العرض المادي إلا عند حدوث عمليات إدراج في الجدول المصدر (الجدول الواقع في أقصى اليسار في الاستعلام). أما الجداول الموجودة على الجانب الأيمن في JOINs فلا تؤدي إلى تشغيل تحديثات، حتى إذا تغيّرت بياناتها. ويكتسب هذا السلوك أهمية خاصة عند إنشاء العروض المادية التزايدية، حيث تُجمَّع البيانات أو تُحوَّل وقت الإدراج.
عند تعريف عرض مادي تزايدي باستخدام JOIN، يعمل الجدول الواقع في أقصى اليسار ضمن استعلام SELECT بوصفه المصدر. وعند إدراج صفوف جديدة في هذا الجدول، ينفّذ ClickHouse استعلام العرض المادي فقط على هذه الصفوف المُدرجة حديثًا. وتُقرأ الجداول الموجودة على الجانب الأيمن في JOIN كاملةً أثناء هذا التنفيذ، لكن التغييرات التي تطرأ عليها وحدها لا تؤدي إلى تشغيل العرض.
يجعل هذا السلوك JOINs في العروض المادية مشابهةً لعملية join على لقطة من البيانات مقابل بيانات أبعاد ثابتة.
ويعمل هذا جيدًا عند إثراء البيانات باستخدام جداول مرجعية أو جداول أبعاد. ومع ذلك، فإن أي تحديثات على الجداول الموجودة على الجانب الأيمن (مثل البيانات الوصفية للمستخدم) لن تُحدِّث العرض المادي بأثر رجعي. ولمشاهدة البيانات المُحدَّثة، يجب أن تصل عمليات إدراج جديدة إلى الجدول المصدر.
مثال
users.
للتذكير، فمخططات جداولنا هي:
users مملوء بالبيانات مسبقًا:
محاذاة التجميع والترتيبيجب أن تتضمن عبارة
GROUP BY في العرض المادي DisplayName وUserId وDay لكي تتطابق مع ORDER BY في الجدول الهدف SummingMergeTree. وهذا يضمن تجميع الصفوف ودمجها على نحو صحيح. وقد يؤدي إغفال أيٍّ منها إلى نتائج غير صحيحة أو إلى عمليات دمج غير فعّالة.daily_badges_by_user الخاص بنا.
أفضل الممارسات لعمليات 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
مثال توضيحي
الشرح
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.
badges، مثلًا.
users باستخدام معرّفات المستخدم في صفوف الشارات التي أُدرجت:
badge مستقبلًا ستكون فعّالة:
2936484. كما جرى تحسين عملية البحث هذه باستخدام مفتاح ترتيب الجدول Id.
العروض المادية وعمليات UNION
UNION ALL عادةً لدمج البيانات من عدة جداول مصدر في مجموعة نتائج واحدة.
ومع أن UNION ALL غير مدعومة مباشرةً في العروض المادية التزايدية، يمكنك تحقيق النتيجة نفسها من خلال إنشاء عرض مادي منفصل لكل فرع SELECT وكتابة نتائجه في جدول هدف مشترك.
في مثالنا، سنستخدم مجموعة بيانات Stack Overflow. تأمّل الجدولين badges وcomments أدناه، اللذين يمثّلان الشارات التي يحصل عليها المستخدم والتعليقات التي يضيفها إلى المنشورات:
INSERT INTO التالية:
badges أو comments، فقد يكون النهج المباشر لهذه المشكلة هو محاولة إنشاء عرض مادي باستخدام استعلام union السابق:
comments. على سبيل المثال:
badges إلى تشغيل الـ view، مما يمنع user_activity من تلقّي التحديثات:
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)
تعبيرات الجدول الشائعة ليست مُجسَّدةلا يقوم ClickHouse بتخزين تعبيرات CTE تخزينًا ماديًا؛ بل يستبدل تعريف CTE مباشرةً داخل الاستعلام، ما قد يؤدي إلى تقييم التعبير نفسه عدة مرات (إذا استُخدم CTE أكثر من مرة).
- إذا كان تعبير الجدول المشترك (CTE) يشير إلى جدول مختلف عن الجدول المصدر (أي الجدول المرتبط به العرض المادي)، وكان مستخدمًا في عبارة
JOINأوIN، فسيتصرف كاستعلام فرعي أو كعملية join، وليس كمُشغِّل. - سيظل العرض المادي يُفعَّل فقط عند تنفيذ عمليات insert في الجدول المصدر الرئيسي، لكن سيُعاد تنفيذ تعبير الجدول المشترك (CTE) مع كل عملية insert، ما قد يسبب عبئًا إضافيًا غير ضروري، خصوصًا إذا كان الجدول المشار إليه كبيرًا.