Skip to main content

مقدمة

تؤثر عوامل عديدة في أداء استعلامات ClickHouse. ويتمثل العامل الحاسم في معظم الحالات في ما إذا كان ClickHouse يستطيع استخدام المفتاح الأساسي عند تقييم شرط جملة WHERE في الاستعلام. لذلك، يُعد اختيار مفتاح أساسي يناسب أنماط الاستعلام الأكثر شيوعًا أمرًا أساسيًا لتصميم الجداول بفعالية. ومع ذلك، مهما جرى ضبط المفتاح الأساسي بعناية، فستظل هناك حتمًا حالات استخدام لاستعلامات لا يمكنها الاستفادة منه بكفاءة. يعتمد المستخدمون عادةً على ClickHouse لبيانات السلاسل الزمنية، لكنهم كثيرًا ما يرغبون أيضًا في تحليل هذه البيانات نفسها وفق أبعاد أعمال أخرى، مثل معرّف العميل أو URL الموقع الإلكتروني أو رقم المنتج. في هذه الحالة، قد يتراجع أداء الاستعلامات بدرجة ملحوظة، لأن تطبيق شرط جملة WHERE قد يتطلب فحصًا كاملًا لكل قيمة في كل عمود. ومع أن ClickHouse يظل سريعًا نسبيًا حتى في هذه الظروف، فإن تقييم ملايين أو مليارات القيم الفردية يجعل الاستعلامات “غير المفهرسة” أبطأ بكثير في التنفيذ من الاستعلامات التي تستند إلى المفتاح الأساسي. في قواعد البيانات العلائقية التقليدية، يتمثل أحد الأساليب لمعالجة هذه المشكلة في إضافة فهرس “ثانوي” واحد أو أكثر إلى جدول. وهذه بنية من نوع B-tree تتيح لقاعدة البيانات العثور على جميع الصفوف المطابقة على القرص في زمن O(log(n)) بدلًا من O(n) (أي فحص الجدول)، حيث تمثل n عدد الصفوف. لكن هذا النوع من الفهارس الثانوية لا يصلح مع ClickHouse (أو غيره من قواعد البيانات العمودية) لأنه لا توجد صفوف فردية على القرص يمكن إضافتها إلى الفهرس. بدلًا من ذلك، يوفّر ClickHouse نوعًا مختلفًا من الفهارس يمكنه، في ظروف معينة، تحسين سرعة الاستعلامات بشكل كبير. وتُسمّى هذه البنى فهارس “التخطي” لأنها تمكّن ClickHouse من تخطي قراءة كتل كبيرة من البيانات المضمون أنها لا تحتوي على أي قيم مطابقة.

آلية العمل الأساسية

لا يمكنك استخدام فهارس تخطي البيانات إلا مع الجداول التابعة لعائلة MergeTree. ويتضمن كل فهرس تخطي بيانات أربع معلمات أساسية:
  • اسم الفهرس. يُستخدم اسم الفهرس لإنشاء ملف الفهرس في كل قسم. كما يُطلب أيضًا كمعلمة عند حذف الفهرس أو تجسيده.
  • تعبير الفهرس. يُستخدم تعبير الفهرس لحساب مجموعة القيم المخزنة في الفهرس. ويمكن أن يكون مزيجًا من الأعمدة والعوامل البسيطة و/أو مجموعة فرعية من الدوال التي يحددها نوع الفهرس.
  • TYPE. يتحكم نوع الفهرس في الحساب الذي يحدد ما إذا كان من الممكن تخطي قراءة كل كتلة فهرس وتقييمها.
  • GRANULARITY. تتكون كل كتلة مفهرسة من GRANULARITY من الحبيبات. على سبيل المثال، إذا كانت درجة تحبيب الفهرس الأساسي للجدول 8192 صفًا، وكانت درجة تحبيب الفهرس 4، فستتكون كل “كتلة” مفهرسة من 32768 صفًا.
عندما ينشئ المستخدم فهرس تخطي بيانات، سيكون هناك ملفان إضافيان في دليل كل جزء بيانات للجدول.
  • skp_idx_{index_name}.idx، ويحتوي على قيم التعبير المرتبة
  • skp_idx_{index_name}.mrk2، ويحتوي على الإزاحات المقابلة داخل ملفات أعمدة البيانات المرتبطة.
إذا تطابق جزء من شرط التصفية في جملة WHERE مع تعبير فهرس التخطي أثناء تنفيذ استعلام وقراءة ملفات الأعمدة ذات الصلة، فسيستخدم ClickHouse بيانات ملف الفهرس لتحديد ما إذا كان يجب معالجة كل كتلة بيانات ذات صلة أو يمكن تجاوزها (على افتراض أن الكتلة لم تكن قد استُبعدت بالفعل بتطبيق المفتاح الأساسي). ولاستخدام مثال مبسط جدًا، انظر إلى الجدول التالي المحمّل ببيانات يمكن التنبؤ بها.
عند تنفيذ استعلام بسيط لا يستخدم primary key، يتم فحص جميع القيم المئة مليون في العمود my_value:
أضِف الآن فهرسَ تخطٍّ بسيطًا جدًا:
عادةً لا تُطبَّق فهارس التخطي إلا على البيانات التي أُدرجت حديثًا، لذا فإن مجرد إضافة الفهرس لن يؤثر في الاستعلام أعلاه. لفهرسة البيانات الموجودة مسبقًا، استخدم هذه التعليمة:
أعِد تنفيذ الاستعلام باستخدام الفهرس المُنشأ حديثًا:
بدلًا من معالجة 100 مليون صف بحجم 800 ميغابايت، لم يقرأ ClickHouse ويحلّل سوى 32768 صفًا بحجم 360 كيلوبايت — أي أربع حبيبات، يحتوي كلٌّ منها على 8192 صفًا. وبصيغة أكثر وضوحًا، هكذا تمت قراءة 4096 صفًا ذات my_value يساوي 125 واختيارها، وكيف جرى تخطي الصفوف التالية من دون القراءة من disk: يمكنك الوصول إلى معلومات تفصيلية حول استخدام فهرس التخطي من خلال تمكين trace عند تنفيذ queries. من clickhouse-client، اضبط send_logs_level:
سيوفر هذا معلومات مفيدة لاستكشاف الأخطاء وإصلاحها عند محاولة ضبط استعلامات SQL وفهارس الجدول. ومن المثال أعلاه، يُظهر سجل التصحيح أن فهرس التخطي استبعد جميع الحبيبات باستثناء اثنتين:

أنواع فهارس التخطي

minmax

لا يتطلب هذا النوع من الفهارس خفيف الوزن أي معلمات. فهو يخزّن القيمتين الدنيا والعظمى لتعبير الفهرس لكل كتلة (إذا كان التعبير عبارة عن tuple، فإنه يخزّن قيم كل عضو من عناصر الـ tuple بشكل منفصل). ويُعد هذا النوع مثاليًا للأعمدة التي تميل إلى أن تكون مرتبة تقريبًا حسب القيمة. وعادةً ما يكون هذا النوع من الفهارس الأقل كلفةً عند تطبيقه أثناء معالجة الاستعلامات. لا يعمل هذا النوع من الفهارس بشكل صحيح إلا مع تعبير scalar أو tuple — ولن يُطبَّق الفهرس مطلقًا على التعبيرات التي تُرجع نوع بيانات array أو map.

set

يقبل نوع الفهرس خفيف الوزن هذا معاملًا واحدًا، وهو max_size لمجموعة القيم في كل كتلة (وتسمح القيمة 0 بعدد غير محدود من القيم المميزة). تحتوي هذه المجموعة على جميع القيم في الكتلة (أو تكون فارغة إذا تجاوز عدد القيم قيمة max_size). يعمل نوع الفهرس هذا جيدًا مع الأعمدة ذات الكاردينالية المنخفضة داخل كل مجموعة من الحبيبات (أي إنها، في الأساس، “متجمعة معًا”) ولكن ذات كاردينالية أعلى بشكل عام. تعتمد تكلفة هذا الفهرس وأداؤه وفعاليته على الكاردينالية داخل الكتل. إذا كانت كل كتلة تحتوي على عدد كبير من القيم الفريدة، فإما أن يكون تقييم شرط الاستعلام على مجموعة فهرس كبيرة مكلفًا جدًا، أو لن يُطبَّق الفهرس لأن الفهرس يكون فارغًا نتيجة تجاوز max_size.

text

بالنسبة إلى أعباء العمل التي تتضمن البحث باللغة الطبيعية أو البحث في النصوص الحرة (مثل البحث عن كلمات أو عبارات في أعمدة نصية كبيرة)، يوفّر ClickHouse فهرسًا نصيًا (وهو فهرس مقلوب حقيقي). يدعم الفهرس النصي دلالات البحث النصي الكامل بكفاءة وعمليات البحث عن الرموز المميّزة. وهو الخيار الموصى به لاستعلامات البحث النصي الكامل لأنه يوفّر فهرسة حتمية للرموز المميّزة وأداءً أفضل لوظائف البحث مثل hasAnyToken وhasAllTokens، كما يُحسّن أيضًا جميع وظائف البحث النصي الشائعة. اطّلع على توثيق الفهرس النصي لمزيد من التفاصيل هنا.

أنواع Bloom filter

يُعد Bloom filter بنية بيانات تتيح اختبار الانتماء إلى مجموعة بكفاءة من حيث المساحة، مقابل احتمال طفيف لحدوث إيجابيات كاذبة. ولا تمثل الإيجابيات الكاذبة مشكلة كبيرة في حالة فهارس التخطي، لأن العيب الوحيد هو قراءة بعض الكتل غير الضرورية. ومع ذلك، فإن احتمال الإيجابيات الكاذبة يعني أنه ينبغي أن يكون من المتوقع أن يُقيَّم التعبير المفهرس إلى true، وإلا فقد يتم تخطي بيانات صحيحة. ولأن Bloom filters أكثر كفاءة في التعامل مع اختبار عدد كبير من القيم المنفصلة، فقد تكون مناسبة للتعبيرات الشرطية التي تنتج عددًا أكبر من القيم المطلوب اختبارها. وعلى وجه الخصوص، يمكن تطبيق فهرس Bloom filter على المصفوفات، حيث تُختبر كل قيمة في المصفوفة، وكذلك على الخرائط، وذلك بتحويل المفاتيح أو القيم إلى مصفوفة باستخدام الدالة mapKeys أو mapValues. توجد ثلاثة أنواع من فهارس تخطي البيانات تعتمد على Bloom filters:
  • الفهرس الأساسي bloom_filter، ويأخذ معاملًا اختياريًا واحدًا لمعدل “الإيجابيات الكاذبة” المسموح به بين 0 و1 (إذا لم يُحدَّد، تُستخدم القيمة .025).
  • الفهرس المتخصص tokenbf_v1 (مهمل). يأخذ ثلاثة معاملات، وكلها مرتبطة بضبط Bloom filter المستخدم: (1) حجم المرشح بالبايت (المرشحات الأكبر تقل فيها الإيجابيات الكاذبة، لكن على حساب قدر من مساحة التخزين)، و(2) عدد دوال hash المطبقة (ومرة أخرى، تؤدي زيادة دوال hash إلى تقليل الإيجابيات الكاذبة)، و(3) قيمة seed لدوال hash الخاصة بـ Bloom filter. راجع الحاسبة هنا لمزيد من التفاصيل حول كيفية تأثير هذه المعاملات في عمل Bloom filter. يعمل هذا الفهرس فقط مع أنواع البيانات String وFixedString وMap. ويُقسَّم تعبير الإدخال إلى تسلسلات من المحارف تفصل بينها محارف غير أبجدية رقمية. على سبيل المثال، ستحتوي قيمة العمود This is a candidate for a "full text" search على الرموز This is a candidate for full text search. وهو مخصص للاستخدام في عمليات البحث LIKE وEQUALS وIN وhasToken() وعمليات البحث المشابهة عن الكلمات والقيم الأخرى داخل السلاسل النصية الأطول. على سبيل المثال، قد يكون أحد الاستخدامات الممكنة هو البحث عن عدد صغير من أسماء الأصناف أو أرقام الأسطر في عمود يحتوي على أسطر سجلات تطبيقات حرة التنسيق.
  • الفهرس المتخصص ngrambf_v1 (مهمل). يعمل هذا الفهرس بالطريقة نفسها التي يعمل بها فهرس token. ويأخذ معاملًا إضافيًا واحدًا قبل إعدادات Bloom filter، وهو حجم ngrams المطلوب فهرستها. والـ ngram هي سلسلة محارف طولها n من أي محارف، لذا فإن السلسلة A short string مع حجم ngram مقداره 4 ستُفهرس على النحو التالي:
يمكن أن يكون هذا الفهرس مفيدًا أيضًا في عمليات البحث النصي، ولا سيما في اللغات التي لا توجد فيها فواصل بين الكلمات، مثل الصينية.
بالنسبة لأعباء عمل البحث النصي الكامل، يُوصى باستخدام الفهرس النصي المخصص (انظر Text index for full-text search) بدلًا من الفهرسين المهملين tokenbf_v1 أو ngrambf_v1. يوفر الفهرس النصي inverted index حقيقيًا مع أداء بحث أفضل، وسلوك أكثر قابلية للتنبؤ، ومرونة وأداء أعلى مقارنةً بفهارس Bloom filter المعتمدة على الرموز.

دوال فهارس التخطي

الهدف الأساسي من فهارس تخطي البيانات هو تقليل كمية البيانات التي تحللها الاستعلامات الشائعة. ونظرًا للطبيعة التحليلية لبيانات ClickHouse، فإن نمط هذه الاستعلامات يتضمن في معظم الحالات تعبيرات دالية. لذلك، يجب أن تتكامل فهارس التخطي على نحو صحيح مع الدوال الشائعة حتى تكون فعّالة. ويمكن أن يحدث ذلك في إحدى الحالتين التاليتين:
  • عند إدراج البيانات ويُعرَّف الفهرس كتعبير دالي (مع تخزين ناتج التعبير في ملفات الفهرس)، أو
  • عند معالجة الاستعلام ويُطبَّق التعبير على قيم الفهرس المخزنة لتحديد ما إذا كان ينبغي استبعاد كتلة.
يعمل كل نوع من فهارس التخطي على مجموعة فرعية من دوال ClickHouse المتاحة والملائمة لتنفيذ الفهرس، والمذكورة هنا. وبوجه عام، فإن فهارس Set والفهارس المعتمدة على Bloom filter (وهو نوع آخر من فهارس Set) كلاهما غير مرتَّب، ولذلك لا يعملان مع النطاقات. وعلى النقيض من ذلك، تعمل فهارس MinMax بكفاءة عالية مع النطاقات لأن تحديد ما إذا كانت النطاقات تتقاطع سريع جدًا. وتعتمد فعالية دوال المطابقة الجزئية LIKE وstartsWith وendsWith وhasToken على نوع الفهرس المستخدم، وتعبير الفهرس، والبنية الفعلية للبيانات.

إعدادات فهارس التخطي

هناك إعدادان متاحان ينطبقان على فهارس التخطي.
  • use_skip_indexes (0 أو 1، والقيمة الافتراضية 1). لا يمكن لجميع الاستعلامات استخدام فهارس التخطي بكفاءة. إذا كان من المرجح أن يشمل شرط التصفية معظم الحبيبات، فإن تطبيق فهرس تخطي البيانات يفرض تكلفة غير ضرورية، وقد تكون كبيرة أحيانًا. اضبط القيمة على 0 للاستعلامات التي لا يُتوقع أن تستفيد من أي من فهارس التخطي.
  • force_data_skipping_indices (قائمة بأسماء الفهارس مفصولة بفواصل). يمكن استخدام هذا الإعداد لمنع بعض أنواع الاستعلامات غير الفعّالة. في الحالات التي يكون فيها الاستعلام عن جدول مكلفًا جدًا ما لم يُستخدم فهرس تخطٍ، فإن استخدام هذا الإعداد مع اسم فهرس واحد أو أكثر سيؤدي إلى إرجاع استثناء لأي استعلام لا يستخدم أيًا من الفهارس المُدرجة. وهذا من شأنه أن يمنع الاستعلامات المكتوبة بشكل سيئ من استهلاك موارد الخادم.

أفضل ممارسات فهارس التخطي

فهارس التخطي ليست بديهية، خاصةً لمن اعتادوا على الفهارس الثانوية المعتمدة على الصفوف في عالم RDMS أو الفهارس المعكوسة في مخازن المستندات. ولتحقيق أي فائدة منها، يجب أن يجنّب تطبيق فهرس تخطي البيانات في ClickHouse عددًا كافيًا من قراءات الحبيبات بما يعوّض تكلفة حساب الفهرس. والأهم من ذلك، إذا ظهرت قيمة ما ولو مرة واحدة داخل كتلة مفهرسة، فهذا يعني أنه يجب قراءة الكتلة كاملةً إلى الذاكرة وفحصها، وبذلك تكون تكلفة الفهرس قد تكبّدتها بلا داعٍ. ضع في اعتبارك توزيع البيانات التالي: افترض أن المفتاح الأساسي/مفتاح ORDER BY هو timestamp، وأن هناك فهرسًا على visitor_id. انظر الاستعلام التالي:
سيكون الفهرس الثانوي التقليدي مفيدًا للغاية مع هذا النوع من توزيع البيانات. فبدلًا من قراءة الصفوف الـ32768 كلها للعثور على الصفوف الخمسة التي تحتوي على visitor_id المطلوب، سيضم الفهرس الثانوي خمسة مواضع صفوف فقط، ولن تُقرأ من القرص إلا هذه الصفوف الخمسة فحسب. أما في ClickHouse فالأمر مع فهرس تخطي البيانات معاكس تمامًا. إذ ستُفحص جميع القيم الـ32768 في العمود visitor_id بغض النظر عن نوع فهرس التخطي. وبناءً على ذلك، فإن الميل الطبيعي لمحاولة تسريع استعلامات ClickHouse بمجرد إضافة فهرس إلى أعمدة المفتاح يكون غالبًا في غير محلّه. ولا ينبغي استخدام هذه الإمكانية المتقدمة إلا بعد دراسة بدائل أخرى، مثل تعديل المفتاح الأساسي (راجع How to Pick a Primary Key)، أو استخدام projections، أو استخدام العروض المادية. وحتى عندما يكون فهرس تخطي البيانات مناسبًا، فإن الضبط الدقيق لكلٍ من الفهرس والجدول يكون ضروريًا في كثير من الأحيان. في معظم الحالات، يتطلب فهرس التخطي المفيد وجود ارتباط قوي بين المفتاح الأساسي والعمود/التعبير غير الأساسي المستهدف. فإذا لم يوجد هذا الارتباط (كما في المخطط أعلاه)، فستكون احتمالية أن يستوفي شرط التصفية صف واحد على الأقل من الصفوف الموجودة في الكتلة التي تضم عدة آلاف من القيم مرتفعة، ولن يُتخطى سوى عدد قليل من الكتل. وعلى النقيض من ذلك، إذا كان نطاق من قيم المفتاح الأساسي (مثل وقت اليوم) مرتبطًا بقوة بالقيم الموجودة في العمود المرشح للفهرسة (مثل أعمار مشاهدي التلفاز)، فمن المرجح أن يكون الفهرس من نوع minmax مفيدًا. ولاحظ أنه قد يكون من الممكن زيادة هذا الارتباط عند إدراج البيانات، إما عبر تضمين أعمدة إضافية في مفتاح الفرز/ORDER BY، أو عبر تجميع عمليات الإدراج بطريقة تجعل القيم المرتبطة بالمفتاح الأساسي مجمّعة عند الإدراج. فعلى سبيل المثال، يمكن تجميع جميع الأحداث الخاصة بـsite_id معيّن وإدراجها معًا بواسطة عملية الاستيعاب، حتى لو كان المفتاح الأساسي عبارة عن timestamp يحتوي على أحداث من عدد كبير من المواقع. وسيؤدي ذلك إلى تكوين كثير من الحبيبات التي لا تحتوي إلا على عدد قليل من معرّفات المواقع، بحيث يمكن تخطي كثير من الكتل عند البحث باستخدام قيمة site_id محددة. ومن المرشحين الجيدين الآخرين لفهرس التخطي التعبيرات ذات الكاردينالية العالية حيث تكون أي قيمة واحدة متناثرة نسبيًا في البيانات. ومن الأمثلة على ذلك منصة observability تتعقب رموز الخطأ في طلبات API. فبعض رموز الخطأ، رغم ندرتها في البيانات، قد تكون مهمة للغاية لعمليات البحث. إذ يتيح فهرس تخطي من نوع set على العمود error_code تجاوز الغالبية العظمى من الكتل التي لا تحتوي على أخطاء، ومن ثم تحسين الاستعلامات التي تركز على الأخطاء تحسينًا كبيرًا. وأخيرًا، فإن أفضل ممارسة أساسية هي: اختبر، ثم اختبر، ثم اختبر. ومرة أخرى، وعلى خلاف فهارس b-tree الثانوية أو الفهارس المعكوسة المستخدمة في البحث داخل المستندات، فإن سلوك فهرس تخطي البيانات ليس سهل التنبؤ. فإضافته إلى جدول تترتب عليها تكلفة ملموسة سواء عند استيعاب البيانات أو عند تنفيذ الاستعلامات التي لا تستفيد من الفهرس لأي سبب من الأسباب. لذا ينبغي دائمًا اختباره على بيانات من النوع المستخدم في العالم الحقيقي، كما ينبغي أن يشمل الاختبار اختلافات في النوع، وحجم granularity، وغيرها من المعلمات. وغالبًا ما يكشف الاختبار عن أنماط ومزالق لا تكون واضحة من خلال التجارب الذهنية وحدها.
آخر تعديل في ٣ يوليو ٢٠٢٦