Skip to main content

المقدمة

يوفّر ClickHouse آليات متعددة لتسريع الاستعلامات التحليلية على كميات كبيرة من البيانات في سيناريوهات الوقت الفعلي. ومن هذه الآليات التي تُسرّع استعلاماتك استخدام الإسقاطات. تساعد الإسقاطات في تحسين الاستعلامات من خلال إعادة ترتيب البيانات وفقًا للسمات المهمة. ويمكن أن يكون ذلك على النحو التالي:
  1. إعادة ترتيب كاملة
  2. مجموعة فرعية من الجدول الأصلي بترتيب مختلف
  3. تجميع محسوب مسبقًا (يشبه العرض المادي) ولكن بترتيب يتوافق مع التجميع.

كيف تعمل الإسقاطات؟

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

تخزين أكثر ذكاءً باستخدام _part_offset

بدءًا من الإصدار 25.5، يدعم ClickHouse العمود الافتراضي _part_offset في الإسقاطات، مما يوفّر طريقة جديدة لتعريف الإسقاط. توجد الآن طريقتان لتعريف الإسقاط:
  • تخزين الأعمدة الكاملة (السلوك الأصلي): يحتوي الإسقاط على البيانات الكاملة ويمكن قراءته مباشرةً، مما يوفّر أداءً أسرع عندما تتوافق عوامل التصفية مع مفتاح الفرز الخاص بالإسقاط.
  • تخزين مفتاح الفرز + _part_offset فقط: يعمل الإسقاط مثل فهرس. يستخدم ClickHouse الفهرس الأساسي الخاص بالإسقاط لتحديد الصفوف المطابقة، لكنه يقرأ البيانات الفعلية من الجدول الأساسي. ويقلّل ذلك من العبء الإضافي للتخزين مقابل زيادة طفيفة في I/O وقت تنفيذ الاستعلام.
يمكن أيضًا المزج بين الأسلوبين أعلاه، بحيث تُخزَّن بعض الأعمدة في الإسقاط وتُخزَّن الأعمدة الأخرى بشكل غير مباشر عبر _part_offset.

متى تستخدم الإسقاطات؟

تُعدّ الإسقاطات ميزة جذابة للمستخدمين الجدد لأنها تُصان تلقائيًا عند إدراج البيانات. علاوة على ذلك، يمكن ببساطة إرسال الاستعلامات إلى جدول واحد، حيث تُستغل الإسقاطات، متى أمكن، لتسريع زمن الاستجابة. وهذا بخلاف العروض المادية، حيث يتعين على المستخدم اختيار الجدول الهدف المُحسَّن المناسب أو إعادة كتابة استعلامه، بحسب المرشحات. وهذا يفرض عبئًا أكبر على تطبيقات المستخدم ويزيد من التعقيد من جهة العميل. وعلى الرغم من هذه المزايا، فإن للإسقاطات بعض القيود المتأصلة التي ينبغي أن تكون على دراية بها، ولذلك يجب استخدامها بحذر.
  • لا تسمح الإسقاطات باستخدام TTL مختلف للجدول المصدر والجدول الهدف (المخفي)، بينما تتيح العروض المادية استخدام قيم TTL مختلفة.
  • لا تدعم الجداول التي تحتوي على إسقاطات التحديثات الخفيفة وعمليات الحذف.
  • يمكن ربط العروض المادية في سلسلة: إذ يمكن أن يكون الجدول الهدف لعرض مادي واحد هو الجدول المصدر لعرض مادي آخر، وهكذا. وهذا غير ممكن مع الإسقاطات.
  • لا تدعم تعريفات الإسقاطات عمليات JOIN، لكن العروض المادية تدعمها. ومع ذلك، يمكن للاستعلامات على الجداول التي تحتوي على إسقاطات استخدام joins بحرية.
  • لا تدعم تعريفات الإسقاطات المرشحات (عبارة WHERE)، لكن العروض المادية تدعمها. ومع ذلك، يمكن للاستعلامات على الجداول التي تحتوي على إسقاطات استخدام المرشحات بحرية.
نوصي باستخدام الإسقاطات عندما:
  • تكون هناك حاجة إلى إعادة ترتيب كاملة للبيانات. وبينما يمكن للتعبير في الإسقاط، من الناحية النظرية، استخدام GROUP BY, فإن العروض المادية تكون أكثر فعالية في الاحتفاظ بالتجميعات. كما أن مُحسِّن الاستعلامات يكون على الأرجح أكثر ميلًا إلى الاستفادة من الإسقاطات التي تستخدم إعادة ترتيب بسيطة، أي SELECT * ORDER BY x. ويمكنك تحديد مجموعة فرعية من الأعمدة في هذا التعبير لتقليل البصمة التخزينية.
  • يكون المستخدمون مرتاحين للزيادة المحتملة في البصمة التخزينية وللكلفة الإضافية المترتبة على كتابة البيانات مرتين. اختبر التأثير على سرعة الإدراج و قيّم عبء التخزين.

أمثلة

التصفية على أعمدة ليست ضمن المفتاح الأساسي

في هذا المثال، سنوضح كيفية إضافة إسقاط إلى جدول. وسنرى أيضًا كيف يمكن استخدام إسقاط لتسريع الاستعلامات التي تُجري تصفية على أعمدة ليست ضمن المفتاح الأساسي للجدول. في هذا المثال، سنستخدم مجموعة بيانات New York Taxi Data المتاحة على sql.clickhouse.com والمرتبة حسب pickup_datetime. لنكتب استعلامًا بسيطًا للعثور على جميع معرّفات الرحلات التي قدّم فيها الركاب إكرامية للسائق تزيد على 200 دولار: لاحظ أنه لأننا نُجري التصفية على tip_amount، وهو ليس ضمن ORDER BY، اضطر ClickHouse إلى إجراء مسح كامل للجدول. لنعمل الآن على تسريع هذا الاستعلام. وللحفاظ على الجدول الأصلي والنتائج الأصلية، سننشئ جدولًا جديدًا وننسخ البيانات باستخدام INSERT INTO SELECT:
لإضافة إسقاط، نستخدم تعليمة ALTER TABLE مع تعليمة ADD PROJECTION التالية:
من الضروري، بعد إضافة إسقاط، استخدام تعليمة MATERIALIZE PROJECTION لكي تُرتَّب البيانات فيه فعليًا وتُعاد كتابتها وفقًا للاستعلام المحدد أعلاه:
لنُشغِّل الاستعلام مرة أخرى الآن بعد أن أضفنا الإسقاط: لاحظ كيف تمكّنا من تقليل وقت الاستعلام بشكل ملحوظ، واحتجنا إلى فحص عدد أقل من الصفوف. يمكننا التأكد من أن استعلامنا أعلاه استخدم بالفعل الإسقاط الذي أنشأناه، وذلك من خلال الاستعلام عن جدول system.query_log:

استخدام الإسقاط لتسريع استعلامات أسعار العقارات المدفوعة في المملكة المتحدة

لتوضيح كيف يمكن استخدام الإسقاط لتسريع أداء الاستعلامات، دعونا نلقِ نظرة على مثال يستخدم مجموعة بيانات من الواقع. في هذا المثال سنستخدم الجدول من دليلنا العملي UK Property Price Paid والذي يحتوي على 30.03 مليون صف. هذه المجموعة من البيانات متاحة أيضًا ضمن بيئة sql.clickhouse.com الخاصة بنا. إذا كنت ترغب في معرفة كيفية إنشاء الجدول وإدراج البيانات، فيمكنك الرجوع إلى صفحة “مجموعة بيانات أسعار العقارات في المملكة المتحدة”. يمكننا تشغيل استعلامين بسيطين على مجموعة البيانات هذه. يعرض الأول المقاطعات في لندن التي سُجلت فيها أعلى الأسعار المدفوعة، بينما يحسب الثاني متوسط السعر لكل مقاطعة: لاحظ أنه على الرغم من سرعتهما الكبيرة، فقد أُجري فحص كامل للجدول عبر جميع الصفوف البالغ عددها 30.03 مليونًا في كلٍ من الاستعلامين، وذلك لأن town وprice لم يكونا ضمن عبارة ORDER BY عند إنشاء الجدول:
لنرَ ما إذا كان بإمكاننا تسريع هذا الاستعلام باستخدام الإسقاط. للحفاظ على الجدول والنتائج الأصليين، سننشئ جدولًا جديدًا وننسخ البيانات باستخدام INSERT INTO SELECT:
ننشئ ونملأ الإسقاط prj_oby_town_price، الذي يُنتج جدولًا إضافيًا (مخفيًا) بفهرس أساسي، مرتّبًا حسب البلدة والسعر، من أجل تحسين الاستعلام الذي يسرد المقاطعات في بلدة محددة بحسب أعلى الأسعار المدفوعة:
يُستخدم الإعداد mutations_sync لفرض التنفيذ بشكل متزامن. ننشئ ونملأ الإسقاط prj_gby_county — وهو جدول إضافي (مخفي) يحسب مسبقًا وبشكل تزايدي قيم التجميع avg(price) لجميع مقاطعات المملكة المتحدة البالغ عددها 130:
إذا كانت هناك عبارة GROUP BY مستخدمة في إسقاط مثل الإسقاط prj_gby_county أعلاه، فإن محرك التخزين الأساسي للجدول (المخفي) يصبح AggregatingMergeTree، وتُحوَّل جميع الدوال التجميعية إلى AggregateFunction. وهذا يضمن تجميع البيانات التزايدي بشكل صحيح.
يعرض الشكل أدناه تصورًا للجدول الرئيسي uk_price_paid_with_projections والإسقاطين التابعين له: إذا شغّلنا الآن الاستعلام الذي يسرد المقاطعات في لندن ذات أعلى ثلاثة أسعار مرة أخرى، فسنلاحظ تحسنًا في أداء الاستعلام: وبالمثل، بالنسبة إلى الاستعلام الذي يسرد مقاطعات المملكة المتحدة ذات أعلى ثلاثة متوسطات للأسعار المدفوعة: لاحظ أن كلا الاستعلامين يستهدفان الجدول الأصلي، وأن كليهما أسفر عن فحص كامل للجدول (إذ جرى بثّ جميع الصفوف البالغ عددها 30.03 مليون صف من القرص) قبل أن ننشىء الإسقاطين. لاحظ أيضًا أن الاستعلام الذي يسرد المقاطعات في لندن لأعلى ثلاثة أسعار مدفوعة يبثّ 2.17 مليون صف. وعندما استخدمنا مباشرةً جدولًا ثانيًا مُحسّنًا لهذا الاستعلام، لم يُبثّ سوى 81.92 ألف صف من القرص. يرجع سبب هذا الاختلاف إلى أن التحسين optimize_read_in_order المذكور أعلاه غير مدعوم حاليًا للإسقاطات. نفحص جدول system.query_log لنرى أن ClickHouse استخدم تلقائيًا الإسقاطين للاستعلامين المذكورين أعلاه (انظر عمود projections أدناه):

أمثلة إضافية

تستخدم الأمثلة التالية مجموعة بيانات أسعار المملكة المتحدة نفسها، وتقارن بين الاستعلامات عند استخدام الإسقاطات وعند عدم استخدامها. وللحفاظ على جدولنا الأصلي (وعلى الأداء)، ننشئ مجددًا نسخة من الجدول باستخدام CREATE AS وINSERT INTO SELECT.

إنشاء إسقاط

لننشئ إسقاطًا تجميعيًا استنادًا إلى الأبعاد toYear(date) وdistrict وtown:
عبِّئ الإسقاط للبيانات الموجودة. (من دون تخزينه ماديًا، لن يُنشأ الإسقاط إلا للبيانات المُدرجة حديثًا):
تُظهر الاستعلامات التالية الفرق في الأداء مع الإسقاطات وبدونها. ولتعطيل استخدام الإسقاطات، نستخدم الإعداد optimize_use_projections، وهو مُمكَّن افتراضيًا.

الاستعلام 1. متوسط السعر لكل سنة

يُفترض أن تكون النتائج نفسها، لكن الأداء سيكون أفضل في المثال الأخير!

الاستعلام 2. متوسط السعر سنويًا في لندن

الاستعلام 3. الأحياء الأكثر تكلفة

يجب تعديل الشرط (date >= '2020-01-01') بحيث يطابق بُعد الإسقاط (toYear(date) >= 2020): مرة أخرى، النتيجة هي نفسها، لكن لاحظ تحسّن أداء الاستعلام في الاستعلام الثاني.

دمج الإسقاطات في استعلام واحد

اعتبارًا من الإصدار 25.6، واستنادًا إلى دعم _part_offset الذي أُضيف في الإصدار السابق، يمكن لـ ClickHouse الآن استخدام عدة إسقاطات لتسريع استعلام واحد يتضمن عدة عوامل تصفية. ومن المهم أن ClickHouse لا يزال يقرأ البيانات من إسقاط واحد فقط (أو من الجدول الأساسي)، لكنه يستطيع استخدام الفهارس الأساسية للإسقاطات الأخرى لاستبعاد الأجزاء غير الضرورية قبل القراءة. ويكون هذا مفيدًا بشكل خاص للاستعلامات التي تُجري تصفية على عدة أعمدة، إذ قد يطابق كلٌّ منها إسقاطًا مختلفًا.
حاليًا، تقتصر هذه الآلية على استبعاد الأجزاء بالكامل. أما الاستبعاد على مستوى granule فغير مدعوم بعد.
لتوضيح ذلك، نعرّف الجدول (مع إسقاطات تستخدم أعمدة _part_offset) ونُدرج خمسة صفوف كمثال تطابق المخططات أعلاه.
ثم نُدخل البيانات إلى الجدول:
ملاحظة: يستخدم الجدول إعدادات مخصصة لأغراض التوضيح، مثل حبيبات من صف واحد وتعطيل عمليات دمج الأجزاء، وهي غير موصى بها للاستخدام في بيئات الإنتاج.
ينتج عن هذا الإعداد ما يلي:
  • خمسة أجزاء منفصلة (جزء واحد لكل صف مُدرَج)
  • مُدخل واحد في الفهرس الأساسي لكل صف (في الجدول الأساسي وفي كل إسقاط)
  • يحتوي كل جزء على صف واحد بالضبط
باستخدام هذا الإعداد، نشغّل استعلامًا يطبّق عامل تصفية على كلٍّ من region وuser_id. ونظرًا إلى أن الفهرس الأساسي للجدول الأساسي مبني على event_date وid، فهو غير مفيد هنا، لذا يستخدم ClickHouse ما يلي:
  • region_proj لتقليص الأجزاء حسب المنطقة
  • user_id_proj لتقليص الأجزاء أكثر حسب user_id
يمكن ملاحظة هذا السلوك باستخدام EXPLAIN projections = 1، الذي يوضّح كيف يحدّد ClickHouse الإسقاطات ويطبّقها.
يوضح ناتج EXPLAIN (المعروض أعلاه) خطة الاستعلام المنطقية من الأعلى إلى الأسفل: في النهاية، لا تتم القراءة إلا من جزء واحد فقط من أصل 5 أجزاء في الجدول الأساسي. ومن خلال الجمع بين تحليل الفهارس لعدة إسقاطات، يقلّل ClickHouse بشكل كبير كمية البيانات التي تُفحص، مما يحسّن الأداء مع إبقاء أعباء التخزين الإضافية منخفضة.
آخر تعديل في ١٤ أغسطس ٢٠٢٦