مقدمة
- كيف تختلف الفهرسة في ClickHouse عن أنظمة إدارة قواعد البيانات العلائقية التقليدية
- كيف يُنشئ ClickHouse الفهرس الأساسي المتناثر للجدول ويستخدمه
- ما بعض أفضل ممارسات الفهرسة في ClickHouse
مجموعة البيانات
- سنستخدم مجموعة فرعية تضم 8.87 مليون صف (حدث) من مجموعة البيانات النموذجية.
- يبلغ حجم البيانات غير المضغوطة 8.87 مليون حدث ونحو 700 ميغابايت. وينخفض هذا إلى 200 ميغابايت عند تخزينه في ClickHouse.
- في مجموعتنا الفرعية، يحتوي كل صف على ثلاثة أعمدة تمثل مستخدم إنترنت (عمود
UserID) نقر على عنوان URL (عمودURL) في وقت محدد (عمودEventTime).
- “ما أكثر 10 عناوين URL نقرًا من قِبل مستخدم محدد؟”
- “من هم أكثر 10 مستخدمين نقرًا على عنوان URL محدد بشكل متكرر؟”
- “ما أكثر الأوقات شيوعًا (مثل أيام الأسبوع) التي ينقر فيها المستخدم على عنوان URL محدد؟“
جهاز الاختبار
مسح كامل للجدول
تصميم الفهارس في ClickHouse
تصميم فهرس لنطاقات بيانات هائلة
B(+)-Tree مقدار O(log n)؛ وبصورة أدق، log_b n = log_2 n / log_2 b، حيث إن b هو عامل التفريع في B(+)-Tree وn هو عدد الصفوف المفهرسة. ونظرًا إلى أن b يكون عادةً بين عدة مئات وعدة آلاف، فإن أشجار B(+)-Tree تكون ضحلة جدًا، ولا يتطلب تحديد السجلات سوى عدد قليل من عمليات seek على القرص. ومع وجود 8.87 مليون صف وعامل تفريع مقداره 1000، يلزم في المتوسط 2.3 عملية seek على القرص. لكن هذه الإمكانية لها تكلفة: أعباء إضافية على القرص والذاكرة، وارتفاع تكلفة الإدراج عند إضافة صفوف جديدة إلى الجدول وإدخالات فهرس جديدة إلى الفهرس، وأحيانًا إعادة موازنة شجرة B-Tree.
ونظرًا إلى التحديات المرتبطة بفهارس B-Tree، تعتمد محركات الجداول في ClickHouse نهجًا مختلفًا. فقد صُممت عائلة محركات MergeTree في ClickHouse وحُسّنت للتعامل مع أحجام بيانات هائلة. وقد صُممت هذه الجداول لاستقبال ملايين عمليات إدراج الصفوف في الثانية وتخزين كميات ضخمة جدًا من البيانات (مئات البيتبايتات). وتُكتب البيانات بسرعة إلى الجدول جزءًا بعد جزء، مع تطبيق قواعد لدمج الأجزاء في الخلفية. وفي ClickHouse، لكل جزء فهرسه الأساسي الخاص. وعند دمج الأجزاء، تُدمج أيضًا الفهارس الأساسية للجزء الناتج عن الدمج. وعلى النطاقات الكبيرة جدًا التي صُمم ClickHouse من أجلها، تكون الكفاءة في استخدام القرص والذاكرة أمرًا بالغ الأهمية. لذلك، وبدلًا من فهرسة كل صف، يحتوي الفهرس الأساسي للجزء على إدخال فهرس واحد (يُعرف باسم ‘mark’) لكل مجموعة من الصفوف (تُسمى ‘granule’) — وتُعرف هذه التقنية باسم الفهرس المتناثر.
ويصبح استخدام الفهرسة المتناثرة ممكنًا لأن ClickHouse يخزّن صفوف الجزء على القرص مرتبةً حسب أعمدة المفتاح الأساسي. وبدلًا من تحديد مواقع الصفوف المفردة مباشرةً (كما في الفهرس المعتمد على B-Tree)، يتيح الفهرس الأساسي المتناثر له أن يحدّد بسرعة، عبر بحث ثنائي في إدخالات الفهرس، مجموعات الصفوف التي قد تطابق الاستعلام. ثم تُبث مجموعات الصفوف التي جرى تحديدها، والتي قد تحتوي على مطابقات (الحبيبات)، بالتوازي إلى ClickHouse engine من أجل العثور على المطابقات. ويجعل تصميم الفهرس هذا الفهرسَ الأساسي صغيرًا (إذ يمكنه، بل ويجب عليه، أن يتسع بالكامل في الذاكرة الرئيسية)، مع الاستمرار في تسريع أزمنة تنفيذ الاستعلامات بدرجة كبيرة، ولا سيما في استعلامات النطاق الشائعة في حالات استخدام تحليلات البيانات.
يوضح ما يلي بالتفصيل كيف يبني ClickHouse فهرسه الأساسي المتناثر ويستخدمه. وفي موضع لاحق من المقالة، سنناقش بعض أفضل الممارسات لاختيار أعمدة الجدول المستخدمة في بناء الفهرس (أعمدة المفتاح الأساسي) وإزالتها وترتيبها.
جدول ذو مفتاح أساسي
تفاصيل عبارة DDL
تفاصيل عبارة DDL
لتبسيط المناقشات الواردة لاحقًا في هذا الدليل، وكذلك لجعل المخططات والنتائج قابلة للتكرار، فإن عبارة DDL:
- تحدد مفتاح فرز مركبًا للجدول عبر عبارة
ORDER BY. - تتحكم بشكل صريح في عدد إدخالات الفهرس التي سيحتوي عليها الفهرس الأساسي من خلال الإعدادات التالية:
index_granularity: يُضبط صراحةً على قيمته الافتراضية 8192. وهذا يعني أنه لكل مجموعة من 8192 صفًا، سيحتوي الفهرس الأساسي على إدخال فهرس واحد. على سبيل المثال، إذا كان الجدول يحتوي على 16384 صفًا، فسيحتوي الفهرس على إدخالَي فهرس.index_granularity_bytes: يُضبط على 0 من أجل تعطيل حبيبية الفهرس التكيفية. وتعني حبيبية الفهرس التكيفية أن ClickHouse ينشئ تلقائيًا إدخال فهرس واحدًا لمجموعة تضمnصفًا إذا تحقق أيٌّ مما يلي:- إذا كانت
nأقل من 8192 وكان الحجم الإجمالي لبيانات الصفوف لتلك الصفوف وعددهاnأكبر من أو يساوي 10 ميغابايت (القيمة الافتراضية لـindex_granularity_bytes). - إذا كان الحجم الإجمالي لبيانات الصفوف لعدد
nمن الصفوف أقل من 10 ميغابايت، لكنnيساوي 8192.
- إذا كانت
compress_primary_key: يُضبط على 0 لتعطيل ضغط الفهرس الأساسي. سيسمح لنا هذا بفحص محتوياته لاحقًا عند الحاجة.
بعد ذلك، أدرِج البيانات:
وحسِّن الجدول:
يمكننا استخدام الاستعلام التالي للحصول على بيانات وصفية عن جدولنا:
- تُخزَّن بيانات الجدول بتنسيق التنسيق الواسع في دليل محدد على القرص، ما يعني أنه سيكون هناك ملف بيانات واحد (وملف علامة واحد) لكل عمود في الجدول داخل ذلك الدليل.
- يحتوي الجدول على 8.87 مليون صف.
- يبلغ حجم البيانات غير المضغوطة لجميع الصفوف معًا 733.28 MB.
- يبلغ الحجم المضغوط على القرص لجميع الصفوف معًا 206.94 MB.
- يحتوي الجدول على فهرس أساسي يضم 1083 إدخالًا (تُسمى ‘علامات’)، ويبلغ حجم هذا الفهرس 96.93 KB.
- إجمالًا، تشغل بيانات الجدول وملفات العلامات وملف الفهرس الأساسي معًا مساحة 207.07 MB على القرص.
تُخزَّن البيانات على القرص مرتبةً بحسب أعمدة المفتاح الأساسي
- مفتاح أساسي مركب
(UserID, URL)، و - مفتاح فرز مركب
(UserID, URL, EventTime).
- لو كنا قد حدّدنا مفتاح الفرز فقط، لكان المفتاح الأساسي قد عُرِّف ضمنيًا على أنه مطابق لمفتاح الفرز.
- لتحقيق كفاءة في استخدام الذاكرة، حدّدنا صراحةً مفتاحًا أساسيًا لا يتضمن إلا الأعمدة التي تُصفّي الاستعلامات بناءً عليها. ويُحمَّل الفهرس الأساسي المبني على المفتاح الأساسي بالكامل إلى الذاكرة الرئيسية.
- وللحفاظ على الاتساق في مخططات الدليل وتعظيم نسبة الضغط، عرّفنا مفتاح فرز منفصلًا يشمل جميع أعمدة الجدول (فإذا وُضعت البيانات المتشابهة في عمود ما بالقرب من بعضها، مثلًا عبر الفرز، فسيكون ضغط تلك البيانات أفضل).
- يجب أن يكون المفتاح الأساسي بادئةً لمفتاح الفرز إذا جرى تحديدهما معًا.
EventTime من مفتاح الفرز).
EventTime.- في التمثيل على القرص، يوجد ملف بيانات واحد (*.bin) لكل عمود في الجدول، وتُخزَّن فيه جميع قيم ذلك العمود بتنسيق مضغوط، و
- تُخزَّن الصفوف البالغ عددها 8.87 مليون صف على القرص بترتيب معجمي تصاعدي بحسب أعمدة المفتاح الأساسي (وأعمدة مفتاح الفرز الإضافية)، أي في هذه الحالة:
- أولًا بحسب
UserID، - ثم بحسب
URL، - وأخيرًا بحسب
EventTime:
- أولًا بحسب
UserID.bin وURL.bin وEventTime.bin ملفات البيانات الموجودة على القرص والتي تُخزَّن فيها قيم الأعمدة UserID وURL وEventTime.
- بما أن المفتاح الأساسي يحدد الترتيب المعجمي للصفوف على القرص، فلا يمكن أن يكون للجدول إلا مفتاح أساسي واحد.
- نرقّم الصفوف بدءًا من 0 ليتوافق ذلك مع مخطط ترقيم الصفوف الداخلي في ClickHouse، والذي يُستخدم أيضًا في رسائل التسجيل.
تُنظَّم البيانات في حبيبات لمعالجتها بالتوازي
index_granularity (المضبوط على قيمته الافتراضية البالغة 8192).
تنتمي منطقيًا أول 8192 صفًا (قيم أعمدتها) — استنادًا إلى الترتيب الفعلي على القرص — إلى الحبيبة 0، ثم تنتمي الـ 8192 صفًا التالية (قيم أعمدتها) إلى الحبيبة 1، وهكذا.
- الحبيبة الأخيرة (الحبيبة 1082) “تحتوي” على أقل من 8192 صفًا.
- ذكرنا في بداية هذا الدليل، في قسم “تفاصيل تعليمة DDL”، أننا عطّلنا درجة تحبّب الفهرس التكيفية (لتبسيط النقاشات في هذا الدليل، وكذلك لجعل المخططات والنتائج قابلة لإعادة الإنتاج). لذلك، فإن جميع الحبيبات (باستثناء الأخيرة) في جدول المثال لدينا لها الحجم نفسه.
- بالنسبة إلى الجداول ذات درجة تحبّب الفهرس التكيفية (تكون درجة تحبّب الفهرس تكيفية بشكل افتراضي)، قد يكون حجم بعض الحبيبات أقل من 8192 صفًا بحسب أحجام بيانات الصفوف.
-
وضعنا علامة برتقالية على بعض قيم الأعمدة من أعمدة المفتاح الأساسي لدينا (
UserID,URL). وتمثل قيم الأعمدة المعلَّمة بالبرتقالي هذه قيم أعمدة المفتاح الأساسي لأول صف في كل حبيبة. وكما سنرى أدناه، ستكون قيم الأعمدة المعلَّمة بالبرتقالي هذه هي الإدخالات في الفهرس الأساسي للجدول. - نرقّم الحبيبات بدءًا من 0 ليتوافق ذلك مع مخطط الترقيم الداخلي في ClickHouse، والذي يُستخدم أيضًا في رسائل التسجيل.
يحتوي الفهرس الأساسي على إدخال واحد لكل حبيبة
primary.idx) يحتوي على ما يُعرف بعلامات الفهرس الرقمية، بدءًا من 0.
يوضح المخطط أدناه أن الفهرس يخزّن قيم أعمدة المفتاح الأساسي (القيم المميزة باللون البرتقالي في المخطط أعلاه) للصف الأول في كل حبيبة.
وبعبارة أخرى: يخزّن الفهرس الأساسي قيم أعمدة المفتاح الأساسي من كل صف رقم 8192 في الجدول (استنادًا إلى الترتيب المادي للصفوف الذي تحدده أعمدة المفتاح الأساسي).
على سبيل المثال
- يخزّن إدخال الفهرس الأول (‘العلامة 0’ في المخطط أدناه) قيم أعمدة المفتاح للصف الأول من الحبيبة 0 في المخطط أعلاه،
- ويخزّن إدخال الفهرس الثاني (‘العلامة 1’ في المخطط أدناه) قيم أعمدة المفتاح للصف الأول من الحبيبة 1 في المخطط أعلاه، وهكذا.
- بالنسبة إلى الجداول التي تستخدم درجة تحبّب الفهرس التكيفية، تُخزَّن أيضًا علامة إضافية “نهائية” في الفهرس الأساسي تسجّل قيم أعمدة المفتاح الأساسي لآخر صف في الجدول. ولكن لأننا عطّلنا درجة تحبّب الفهرس التكيفية (لتبسيط الشرح في هذا الدليل، وكذلك لجعل المخططات والنتائج قابلة لإعادة الإنتاج)، فإن فهرس جدول المثال لدينا لا يتضمن هذه العلامة النهائية.
- يُحمَّل ملف الفهرس الأساسي بالكامل إلى الذاكرة الرئيسية. وإذا كان الملف أكبر من مساحة الذاكرة الحرة المتاحة، فسيُصدر ClickHouse خطأً.
فحص محتوى الفهرس الأساسي
فحص محتوى الفهرس الأساسي
في عنقود ClickHouse مُدار ذاتيًا، يمكننا استخدام دالة الجدول file لفحص محتوى الفهرس الأساسي لجدول المثال الخاص بنا.وللقيام بذلك، نحتاج أولًا إلى نسخ ملف الفهرس الأساسي إلى user_files_path على إحدى عقد العنقود قيد التشغيل:
- الخطوة 1: احصل على مسار الجزء الذي يحتوي على ملف الفهرس الأساسي
- الخطوة 2: احصل على user_files_path إن user_files_path الافتراضي على Linux هو
- الخطوة 3: انسخ ملف الفهرس الأساسي إلى user_files_path
SELECT path FROM system.parts WHERE table = 'hits_UserID_URL' AND active = 1يعيد /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4 على جهاز الاختبار./var/lib/clickhouse/user_files/وعلى Linux، يمكنك التحقق مما إذا كان قد تغيّر: $ grep user_files_path /etc/clickhouse-server/config.xmlعلى جهاز الاختبار، كان المسار هو /Users/tomschreiber/Clickhouse/user_files/cp /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4/primary.idx /Users/tomschreiber/Clickhouse/user_files/primary-hits_UserID_URL.idxيمكننا الآن فحص محتوى الفهرس الأساسي عبر SQL:
- احصل على عدد الإدخالات
- احصل على أول علامتي فهرس
- احصل على آخر علامة فهرس
SELECT count( )<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String');
يعيد 1083SELECT UserID, URL<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 0, 2;يعيد240923, http://showtopics.html%3...<br/> 4073710, http://mk.ru&pos=3_0SELECT UserID, URL FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 1082, 1;
يعيد
4292714039 │ http://sosyal-mansetleri...وهذا يطابق تمامًا مخططنا لمحتوى الفهرس الأساسي لجدول المثال:
-
علامات فهرس UserID:
قيم
UserIDالمخزنة في الفهرس الأساسي مرتبة ترتيبًا تصاعديًا.
لذلك تشير ‘العلامة 1’ في المخطط أعلاه إلى أن قيمUserIDلجميع صفوف الجدول في granule 1، وفي جميع الـ granules التالية، مضمونة أن تكون أكبر من أو تساوي 4.073.710.
-
علامات فهرس URL:
إن التقارب الكبير في cardinality لعمودَي المفتاح الأساسي
UserIDوURLيعني أن علامات الفهرس لجميع أعمدة المفتاح بعد العمود الأول، بوجه عام، لا تحدد نطاق بيانات إلا ما دام مقدار عمود المفتاح السابق يظل ثابتًا لجميع صفوف الجدول ضمن الحبيبة الحالية على الأقل.
على سبيل المثال، لأن قيم UserID للعلامة 0 والعلامة 1 مختلفة في المخطط أعلاه، لا يمكن لـ ClickHouse أن يفترض أن جميع قيم URL لكل صفوف الجدول في الحبيبة 0 أكبر من أو تساوي'http://showtopics.html%3...'. ومع ذلك، إذا كانت قيم UserID للعلامة 0 والعلامة 1 متطابقة في المخطط أعلاه (أي إن قيمة UserID تظل ثابتة لجميع صفوف الجدول داخل الحبيبة 0)، فسيكون بإمكان ClickHouse أن يفترض أن جميع قيم URL لكل صفوف الجدول في الحبيبة 0 أكبر من أو تساوي'http://showtopics.html%3...'. سنناقش لاحقًا بمزيد من التفصيل ما يترتب على ذلك من حيث أداء تنفيذ الاستعلام.
يُستخدم الفهرس الأساسي لاختيار الحبيبات
749927693. ويتطلّب ذلك 19 خطوة بمتوسط تعقيد زمني قدره O(log2 n):
تفاصيل سجل التتبّع
تفاصيل سجل التتبّع
جرى تحديد العلامة 176 (إذ إن ‘علامة الحد الأيسر التي عُثر عليها’ شاملة، بينما ‘علامة الحد الأيمن التي عُثر عليها’ غير شاملة)، ولذلك تُمرَّر بعد ذلك جميع الصفوف الـ8192 من الحبيبة 176 (التي تبدأ عند الصف 1.441.792 — وسنرى ذلك لاحقًا في هذا الدليل) إلى ClickHouse من أجل العثور على الصفوف الفعلية التي تكون فيها قيمة العمود UserID هي 749927693.
كما نوقش أعلاه، يستخدم ClickHouse فهرسه الأساسي المتناثر لاختيار الحبيبات بسرعة (عبر البحث الثنائي) التي قد تحتوي على صفوف تطابق الاستعلام. هذه هي المرحلة الأولى (اختيار الحبيبات) من تنفيذ استعلام ClickHouse. في المرحلة الثانية (قراءة البيانات)، يحدّد ClickHouse مواقع الحبيبات المختارة من أجل تمرير جميع صفوفها إلى محرك ClickHouse للعثور على الصفوف المطابقة فعليًا للاستعلام. نناقش هذه المرحلة الثانية بمزيد من التفصيل في القسم التالي.
تُستخدم ملفات العلامات لتحديد مواقع الحبيبات
تفاصيل اختيار الحبيبة
تفاصيل اختيار الحبيبة
يوضح المخطط أعلاه أن العلامة 176 هي أول إدخال في الفهرس تكون فيه القيمة الدنيا لـ UserID في الحبيبة 176 المرتبطة أصغر من 749.927.693، وفي الوقت نفسه تكون القيمة الدنيا لـ UserID في الحبيبة 177 الخاصة بالعلامة التالية (العلامة 177) أكبر من هذه القيمة. لذلك، فإن الحبيبة 176 المقابلة للعلامة 176 وحدها هي التي قد تحتوي على صفوف تكون قيمة عمود UserID فيها 749.927.693.
UserID.mrk وURL.mrk وEventTime.mrk التي تخزّن المواقع الفعلية للحبيبات الخاصة بأعمدة الجدول UserID وURL وEventTime.
لقد ناقشنا أن الفهرس الأساسي عبارة عن ملف مصفوفة مسطّح غير مضغوط (primary.idx) يحتوي على علامات فهرس يبدأ ترقيمها من 0.
وبالمثل، فإن ملف العلامات هو أيضًا ملف مصفوفة مسطّح غير مضغوط (*.mrk) يحتوي على علامات يبدأ ترقيمها من 0.
وبمجرد أن يحدّد ClickHouse علامة الفهرس الخاصة بحبيبة قد تحتوي على صفوف مطابقة للاستعلام ويختارها، يمكن إجراء وصول موضعي داخل المصفوفة في ملفات العلامات للحصول على المواقع الفعلية للحبيبة.
يخزّن كل إدخال في ملف العلامات لعمود معيّن موقعين على شكل إزاحتين:
-
الإزاحة الأولى (
block_offsetفي المخطط أعلاه) تحدد موقع الكتلة في ملف بيانات العمود المضغوط التي تحتوي على النسخة المضغوطة من الحبيبة المحددة. وقد تحتوي هذه الكتلة المضغوطة على بضع حبيبات مضغوطة. وتُفكّ الكتلة المضغوطة المحددة إلى الذاكرة الرئيسية عند القراءة. -
الإزاحة الثانية (
granule_offsetفي المخطط أعلاه) من ملف العلامات توفّر موقع الحبيبة داخل بيانات الكتلة غير المضغوطة.
- بالنسبة إلى الجداول ذات التنسيق الواسع ومن دون درجة تحبّب الفهرس التكيفية، يستخدم ClickHouse ملفات العلامات
.mrkكما هو موضح أعلاه، والتي تحتوي على إدخالات تضم عنوانين، طول كل منهما 8 بايت، لكل إدخال. وتمثل هذه الإدخالات المواقع الفعلية لحبيبات لها جميعًا الحجم نفسه.
-
بالنسبة إلى الجداول ذات التنسيق الواسع ومع درجة تحبّب الفهرس التكيفية، يستخدم ClickHouse ملفات العلامات
.mrk2، التي تحتوي على إدخالات مشابهة لملفات العلامات.mrkولكن مع قيمة ثالثة إضافية لكل إدخال: عدد الصفوف في الحبيبة المرتبطة بالإدخال الحالي. -
بالنسبة إلى الجداول ذات التنسيق المدمج، يستخدم ClickHouse ملفات العلامات
.mrk3.
EventTime.بالنسبة إلى استعلامنا النموذجي، يحتاج ClickHouse فقط إلى إزاحتي الموقع الفعلي للحبيبة 176 في ملف بيانات UserID (UserID.bin) وإزاحتي الموقع الفعلي للحبيبة 176 في ملف بيانات URL (URL.bin).ويؤدي المستوى الوسيط الذي توفره ملفات العلامات إلى تجنب تخزين إدخالات المواقع الفعلية لجميع الحبيبات البالغ عددها 1083 عبر الأعمدة الثلاثة كلها مباشرةً داخل الفهرس الأساسي، مما يتجنب وجود بيانات غير ضرورية (وقد لا تُستخدم) في الذاكرة الرئيسية.استخدام فهارس أولية متعددة
قد تكون أعمدة المفتاح الثانوية غير فعّالة (أو لا)
نستخدم استعلامًا يحسب أفضل 10 مستخدمين نقروا على عنوان URL “http://public_search” بأعلى تكرار:
خوارزمية البحث بالاستبعاد العامة
- استعلامًا يبحث عن الصفوف التي تكون فيها قيمة URL = “W3”.
- نسخة مجردة من جدول hits لدينا، مع قيم مبسطة لكل من UserID وURL.
- المفتاح الأساسي المركب نفسه (UserID, URL) للفهرس. وهذا يعني أن الصفوف تُرتَّب أولًا حسب قيم UserID، ثم تُرتَّب الصفوف التي لها قيمة UserID نفسها حسب URL.
- حجم حبيبة يساوي صفَّين، أي إن كل حبيبة تحتوي على صفَّين.
- يمكن استبعاد علامة الفهرسة 0 التي تكون فيها قيمة URL أصغر من W3 وتكون فيها أيضًا قيمة URL لعلامة الفهرسة التالية مباشرة أصغر من W3، لأن العلامتين 0 و1 لهما قيمة UserID نفسها. لاحظ أن هذا الشرط المسبق للاستبعاد يضمن أن الحبيبة 0 تتكون بالكامل من قيم UserID تساوي U1، بحيث يمكن لـ ClickHouse أن يفترض أيضًا أن القيمة العظمى لـ URL في الحبيبة 0 أصغر من W3، ومن ثم يستبعد الحبيبة.
- تُحدَّد علامة الفهرسة 1 التي تكون فيها قيمة URL أصغر من W3 (أو مساوية له) وتكون فيها قيمة URL لعلامة الفهرسة التالية مباشرة أكبر من W3 (أو مساوية له)، لأن ذلك يعني أن الحبيبة 1 قد تحتوي على صفوف تكون قيمة URL فيها W3.
- يمكن استبعاد علامتي الفهرسة 2 و3 اللتين تكون فيهما قيمة URL أكبر من W3، لأن علامات الفهرسة في الفهرس الأساسي تخزّن قيم أعمدة المفتاح لأول صف في الجدول لكل حبيبة، وبما أن صفوف الجدول مرتبة على القرص حسب قيم أعمدة المفتاح، فلا يمكن أن تحتوي الحبيبتان 2 و3 على قيمة URL تساوي W3.
ملاحظة حول فهرس تخطي البيانات
GRANULARITY 4 في تعليمة ALTER TABLE أعلاه) — الحد الأدنى والحد الأقصى لقيمة URL:
يخزّن إدخال الفهرس الأول (mark 0 في المخطط أعلاه) الحد الأدنى والحد الأقصى لقيم URL الخاصة بالصفوف التي تنتمي إلى أول 4 حبيبات في جدولنا.
ويخزّن إدخال الفهرس الثاني (mark 1) الحد الأدنى والحد الأقصى لقيم URL الخاصة بالصفوف التي تنتمي إلى الحبيبات الأربع التالية في جدولنا، وهكذا.
(أنشأ ClickHouse أيضًا ملف علامات خاصًا بفهرس تخطّي البيانات من أجل تحديد مواقع مجموعات الحبيبات المرتبطة بعلامات الفهرس.)
وبسبب الارتفاع المتشابه في الكاردينالية لكلٍّ من UserID وURL، لا يمكن لفهرس تخطّي البيانات الثانوي هذا أن يساعد في استبعاد الحبيبات من الاختيار عند تنفيذ الاستعلام الذي يرشّح بناءً على URL.
ومن المرجّح جدًا أن تكون قيمة URL المحددة التي يبحث عنها الاستعلام (أي http://public_search) واقعة بين الحد الأدنى والحد الأقصى اللذين يخزنهما الفهرس لكل مجموعة من الحبيبات، مما يضطر ClickHouse إلى اختيار مجموعة الحبيبات (لأنها قد تحتوي على صفوف تطابق الاستعلام).
الحاجة إلى استخدام عدة فهارس أساسية
خيارات إنشاء فهارس أساسية إضافية
UserID معيّن، والاستعلام الذي يصفّي الصفوف ذات URL معيّن — فعلينا استخدام عدة فهارس أساسية عبر أحد هذه الخيارات الثلاثة:
- إنشاء جدول ثانٍ بمفتاح أساسي مختلف.
- إنشاء عرض مادي على جدولنا الحالي.
- إضافة إسقاط إلى جدولنا الحالي.
الخيار 1: الجداول الثانوية
UserIDs لن يعمل بكفاءة كبيرة مع الجدول الإضافي الجديد، لأن UserID أصبح الآن عمود المفتاح الثاني في primary index لهذا الجدول، ولذلك سيستخدم ClickHouse خوارزمية البحث بالاستبعاد العامة لاختيار granules، وهو ليس فعّالًا كثيرًا مع الارتفاع المتشابه في الكاردينالية لكلٍّ من UserID وURL.
افتح مربع التفاصيل للاطلاع على مزيد من المعلومات.
الاستعلام الذي يطبّق تصفية على UserIDs أصبح أداؤه سيئًا الآن
الاستعلام الذي يطبّق تصفية على UserIDs أصبح أداؤه سيئًا الآن
UserIDs، والآخر لتسريع الاستعلامات التي تطبّق تصفية على عناوين URL:
الخيار 2: العروض المادية
- نبدّل ترتيب أعمدة المفتاح (مقارنةً بـ الجدول الأصلي) في المفتاح الأساسي للعرض
- يستند العرض المادي إلى جدول مُنشأ ضمنيًا يعتمد ترتيب صفوفه وفهرسه الأساسي على تعريف المفتاح الأساسي المعطى
- يظهر الجدول المُنشأ ضمنيًا في الاستعلام
SHOW TABLESويكون اسمه بادئًا بـ.inner - من الممكن أيضًا إنشاء الجدول الداعم للعرض المادي صراحةً أولًا، ثم يمكن للعرض أن يستهدف ذلك الجدول عبر العبارة
TO [db].[table] - نستخدم الكلمة المفتاحية
POPULATEلملء الجدول المُنشأ ضمنيًا فورًا بجميع الصفوف البالغ عددها 8.87 مليون صف من جدول المصدر hits_UserID_URL - إذا أُدرجت صفوف جديدة في جدول المصدر hits_UserID_URL، فستُدرج هذه الصفوف أيضًا تلقائيًا في الجدول المُنشأ ضمنيًا
- عمليًا، يكون للجدول المُنشأ ضمنيًا ترتيب الصفوف نفسه والفهرس الأساسي نفسه كما في الجدول الثانوي الذي أنشأناه صراحةً:
الخيار 3: الإسقاطات
- ينشئ الإسقاط جدولًا مخفيًا يعتمد ترتيب صفوفه وفهرسه الأساسي على عبارة
ORDER BYالمحددة له - لا يظهر الجدول المخفي في استعلام
SHOW TABLES - نستخدم الكلمة المفتاحية
MATERIALIZEلملء الجدول المخفي فورًا بجميع الصفوف البالغ عددها 8.87 مليون صف من جدول المصدر hits_UserID_URL - إذا أُدرِجت صفوف جديدة في جدول المصدر hits_UserID_URL، فستُدرَج هذه الصفوف تلقائيًا أيضًا في الجدول المخفي
- يستهدف الاستعلام دائمًا، من ناحية الصياغة، جدول المصدر hits_UserID_URL، ولكن إذا كان ترتيب الصفوف والفهرس الأساسي للجدول المخفي يتيحان تنفيذ الاستعلام بكفاءة أكبر، فسيُستخدَم هذا الجدول المخفي بدلًا منه
- يُرجى ملاحظة أن الإسقاطات لا تجعل الاستعلامات التي تستخدم
ORDER BYأكثر كفاءة، حتى إذا كانتORDER BYمطابقة لعبارةORDER BYالخاصة بالإسقاط (راجع https://github.com/ClickHouse/ClickHouse/issues/47333) - عمليًا، يمتلك الجدول المخفي المُنشأ ضمنيًا ترتيب الصفوف نفسه والفهرس الأساسي نفسه اللذين يمتلكهما الجدول الثانوي الذي أنشأناه صراحةً:
الملخص
ترتيب أعمدة المفتاح بكفاءة
- كفاءة التصفية على أعمدة المفتاح الثانوية في الاستعلامات، و
- نسبة الضغط لملفات بيانات الجدول.
UserID) إلى عنوان URL (العمود URL) قد وُسِم على أنه حركة مرور بوت (العمود IsRobot) أم لا.
سنستخدم مفتاحًا أساسيًا مركّبًا يضم الأعمدة الثلاثة المذكورة آنفًا، ويمكن استخدامه لتسريع استعلامات تحليلات الويب المعتادة التي تحسب:
- مقدار حركة المرور إلى عنوان URL معيّن القادمة من البوتات (كنسبة مئوية)، أو
- مدى ثقتنا في أن مستخدمًا معيّنًا بوت أو ليس بوتًا (أي ما النسبة المئوية من حركة المرور الصادرة عن ذلك المستخدم التي يُفترض أنها حركة مرور بوت أو ليست كذلك)
clickhouse client:
URL وIsRobot، ولذلك فإن ترتيب هذه الأعمدة في مفتاح أساسي مركب مهمٌّ لكلٍّ من التسريع الفعّال للاستعلامات التي تُجري تصفيةً على تلك الأعمدة، وتحقيق نسب ضغط مثالية لملفات بيانات أعمدة الجدول.
ولتوضيح ذلك، سننشئ إصدارين من جدول لبيانات تحليل زيارات البوت لدينا:
- جدولًا باسم
hits_URL_UserID_IsRobotمع مفتاح أساسي مركب(URL, UserID, IsRobot)، حيث نرتب أعمدة المفتاح حسب cardinality ترتيبًا تنازليًا - جدولًا باسم
hits_IsRobot_UserID_URLمع مفتاح أساسي مركب(IsRobot, UserID, URL)، حيث نرتب أعمدة المفتاح حسب cardinality ترتيبًا تصاعديًا
hits_URL_UserID_IsRobot بالمفتاح الأساسي المركب (URL, UserID, IsRobot):
hits_IsRobot_UserID_URL بمفتاح أساسي مركب (IsRobot, UserID, URL):
التصفية بكفاءة على أعمدة المفاتيح الثانوية
UserID في table رتّبنا فيه أعمدة المفتاح (URL, UserID, IsRobot) حسب كاردينالية ترتيبًا تنازليًا:
(IsRobot, UserID, URL) حسب الكاردينالية بترتيب تصاعدي:
أفضل نسبة ضغط لملفات البيانات
UserID بين الجدولين اللذين أنشأناهما أعلاه:
UserID أعلى بكثير في الجدول الذي رتّبنا فيه أعمدة المفتاح (IsRobot, UserID, URL) حسب الكاردينالية بترتيب تصاعدي.
مع أن البيانات نفسها تمامًا مخزَّنة في كلا الجدولين (أدرجنا 8.87 مليون صف نفسها في كلا الجدولين)، فإن ترتيب أعمدة المفتاح في المفتاح الأساسي المركّب يؤثر بدرجة كبيرة في مقدار مساحة القرص التي تتطلبها البيانات المضغوطة في ملفات بيانات الأعمدة الخاصة بالجدول:
- في الجدول
hits_URL_UserID_IsRobotذي المفتاح الأساسي المركّب(URL, UserID, IsRobot)، حيث نرتّب أعمدة المفتاح حسب الكاردينالية بترتيب تنازلي، يشغل ملف البياناتUserID.binمساحة 11.24 MiB على القرص - في الجدول
hits_IsRobot_UserID_URLذي المفتاح الأساسي المركّب(IsRobot, UserID, URL)، حيث نرتّب أعمدة المفتاح حسب الكاردينالية بترتيب تصاعدي، لا يشغل ملف البياناتUserID.binسوى 877.47 KiB من مساحة القرص
cl، ثم تُرتَّب الصفوف التي لها قيمة cl نفسها حسب قيمة ch. وبما أن عمود المفتاح الأول cl منخفض الكاردينالية، فمن المرجّح وجود صفوف تحمل قيمة cl نفسها. ونتيجة لذلك، فمن المرجّح أيضًا أن تكون قيم ch مرتبة محليًا، أي ضمن الصفوف التي لها قيمة cl نفسها.
إذا وُضعت البيانات المتشابهة في عمود ما متقاربة من بعضها بعضًا، على سبيل المثال عن طريق الفرز، فإن ضغط هذه البيانات سيكون أفضل.
وبوجه عام، تستفيد خوارزمية الضغط من طول التتابعات في البيانات (فكلما زادت كمية البيانات التي تراها، كان ذلك أفضل للضغط)
ومن المحلية (فكلما كانت البيانات أكثر تشابهًا، كانت نسبة الضغط أفضل).
وعلى النقيض من المخطط أعلاه، يوضح المخطط أدناه ترتيب الصفوف على القرص لمفتاح أساسي تكون فيه أعمدة المفتاح مرتبة حسب الكاردينالية بترتيب تنازلي:
تُرتَّب صفوف الجدول الآن أولًا حسب قيمة ch، وتُرتَّب الصفوف التي لها قيمة ch نفسها حسب قيمة cl.
لكن بما أن عمود المفتاح الأول ch ذو كاردينالية عالية، فمن غير المرجّح أن توجد صفوف لها قيمة ch نفسها. ونتيجةً لذلك، فمن غير المرجّح أيضًا أن تكون قيم cl مرتبة (محليًا، أي ضمن الصفوف التي لها قيمة ch نفسها).
لذلك، فعلى الأرجح أن تكون قيم cl بترتيب عشوائي، وبالتالي تكون خاصية التجاور الموضعي لديها ونسبة الضغط ضعيفتين.
الملخص
تحديد الصفوف الفردية بكفاءة
مثال ملموس
- ترتيب insert للصفوف عند تغيّر المحتوى (على سبيل المثال بسبب ضغطات المفاتيح أثناء كتابة النص في منطقة النص) و
- الترتيب على القرص لبيانات الصفوف المُدرجة عند استخدام
PRIMARY KEY (hash):
hash يُستخدم بوصفه عمود المفتاح الأساسي
- يمكن استرجاع صفوف معيّنة بسرعة كبيرة، لكن
- تُخزَّن صفوف الجدول (أي بيانات أعمدتها) على القرص بترتيب تصاعدي حسب قيم
hash(الفريدة والعشوائية). لذلك تُخزَّن أيضًا قيم عمود المحتوى بترتيب عشوائي ومن دون تقارب في البيانات، مما يؤدي إلى نسبة ضغط دون المستوى الأمثل لملف بيانات عمود المحتوى.
- تجزئة للمحتوى، كما نوقش أعلاه، تختلف باختلاف البيانات، و
- تجزئة حساسة للتقارب (بصمة) لا تتغير عند حدوث تغييرات صغيرة في البيانات.
- ترتيب insert للصفوف عند تغيّر المحتوى (على سبيل المثال بسبب ضغطات المفاتيح أثناء كتابة النص في منطقة النص) و
- الترتيب على القرص لبيانات الصفوف المُدرجة عند استخدام
PRIMARY KEY (fingerprint, hash)المركب:
fingerprint، وبالنسبة إلى الصفوف التي لها قيمة fingerprint نفسها، تحدد قيمة hash الترتيب النهائي.
وبما أن البيانات التي تختلف فقط باختلافات صغيرة تحصل على قيمة fingerprint نفسها، فإن البيانات المتشابهة تُخزَّن الآن متقاربة على القرص داخل عمود المحتوى. وهذا مفيد جدًا لنسبة ضغط عمود المحتوى، لأن خوارزميات الضغط تستفيد عمومًا من تقارب البيانات (فكلما كانت البيانات أكثر تشابهًا، كانت نسبة الضغط أفضل).
أما المقايضة هنا فهي أن استرجاع صف معيّن يتطلب حقلين (fingerprint وhash) لتحقيق الاستفادة المثلى من المفتاح الأساسي الناتج عن PRIMARY KEY (fingerprint, hash) المركب.