تحسين السلسلة في ClickHouse. تقرير ياندكس

يعالج مشغل قاعدة بيانات ClickHouse العديد من الخطوط المختلفة ، ويستهلك الموارد. لتسريع النظام ، يتم إضافة تحسينات جديدة باستمرار. يتحدث مطور ClickHouse نيكولاي كوتشيتوف عن نوع بيانات السلسلة ، بما في ذلك النوع الجديد ، LowCardinality ، ويشرح كيفية تسريع العمل مع الأوتار.


- أولاً ، دعنا نرى كيف يمكنك تخزين السلاسل.



لدينا أنواع بيانات السلسلة. تعمل السلسلة بشكل جيد افتراضيًا ، ويجب استخدامها دائمًا تقريبًا. يحتوي على حمل علوي صغير - 9 بايت لكل سطر. إذا أردنا أن يكون حجم الصف ثابتًا ومعروفًا مسبقًا ، فمن الأفضل استخدام FixedString. يمكنك فيه تعيين عدد وحدات البايت التي نحتاجها ، وهو مناسب للبيانات مثل عناوين IP أو وظائف التجزئة.



بالطبع ، أحيانًا يتباطأ شيء ما. افترض أنك تقوم بإجراء استعلام على جدول. يقرأ ClickHouse كمية كبيرة إلى حد ما من البيانات ، على سبيل المثال ، بسرعة 100 غيغابايت / ثانية ، مع معالجة بضعة أسطر. لدينا جدولان يخزنان نفس البيانات تقريبًا. يقرأ ClickHouse البيانات من الجدول الثاني بسرعة أعلى ، ولكنه يقرأ صفوفًا أقل بثلاث مرات في الثانية.



إذا نظرنا إلى حجم البيانات المضغوطة ، فستكون متساوية تقريبًا. في الواقع ، يتم كتابة نفس البيانات في الجداول - أول مليار رقم - فقط في العمود الأول يتم كتابتها في شكل UInt64 ، وفي العمود الثاني في سلسلة. وبسبب هذا ، يقرأ الاستعلام الثاني البيانات من القرص لفترة أطول ويفك ضغطها.



هنا مثال آخر. لنفترض أن هناك مجموعة من الخطوط المحددة سلفًا ، فهي تقتصر على ثابت 1000 أو 10000 ولا تتغير أبدًا. في هذه الحالة ، يكون نوع بيانات Enum مناسبًا لنا ، في ClickHouse هناك نوعان منهم - Enum8 و Enum16. نظرًا للتخزين في Enum ، نعالج الطلبات بسرعة.

لدى ClickHouse تسارعات لـ GROUP BY و IN و DISTINCT وتحسينات لبعض الوظائف ، على سبيل المثال ، للمقارنة مع سلسلة ثابتة. بالطبع ، لا يتم تحويل الأرقام في السلسلة ، ولكن ، على العكس من ذلك ، يتم تحويل السلسلة الثابتة إلى القيمة Enum. بعد ذلك ، تتم مقارنة كل شيء بسرعة.

ولكن هناك أيضًا عيوب. حتى إذا كنا نعرف مجموعة الخطوط الدقيقة ، في بعض الأحيان يجب تجديدها. لقد وصل خط جديد - علينا القيام ALTER.



يتم تنفيذ ALTER لـ Enum في ClickHouse على النحو الأمثل. لا نقوم بالكتابة فوق البيانات الموجودة على القرص ، ولكن ALTER يمكن أن يتباطأ بسبب حقيقة أن هياكل Enum يتم تخزينها في مخطط الجدول نفسه. لذلك ، يجب أن ننتظر طلبات القراءة من الجدول ، على سبيل المثال.

السؤال هو ، هل يمكن للمرء أن يفعل أفضل؟ ربما نعم. يمكنك حفظ بنية التعداد ليس في مخطط الجدول ، ولكن في ZooKeeper. ومع ذلك ، قد تحدث مشاكل التزامن. على سبيل المثال ، تلقت نسخة متماثلة بيانات ، والأخرى لم تتلق ، وإذا كان لديها تعداد قديم ، فسوف ينكسر شيء ما. (في ClickHouse ، أكملنا تقريبًا طلبات ALTER غير المحظورة. عندما ننهيها بالكامل ، لن نضطر إلى انتظار طلبات القراءة.)



حتى لا تعبث مع ALTER Enum ، يمكنك استخدام قواميس ClickHouse الخارجية. دعني أذكرك بأن هذه بنية بيانات ذات قيمة رئيسية داخل ClickHouse ، والتي يمكنك من خلالها الحصول على البيانات من مصادر خارجية ، على سبيل المثال ، من جداول MySQL.

في قاموس ClickHouse ، نقوم بتخزين العديد من الخطوط المختلفة ، وفي الجدول معرفاتهم في شكل أرقام. إذا كنا بحاجة إلى الحصول على سلسلة ، فإننا نسميها وظيفة dictGet والعمل معها. بعد ذلك لا ينبغي لنا أن نغير. لإضافة شيء إلى Enum ، نقوم بإدراج هذا في نفس جدول MySQL.

لكن هناك مشاكل أخرى. أولاً ، البنية المحرجة. إذا أردنا الحصول على سلسلة ، فيجب استدعاء dictGet. ثانياً ، عدم وجود بعض التحسينات. المقارنة مع السلسلة الثابتة للقواميس ليست سريعة أيضًا.

قد لا تزال هناك مشاكل في التحديث. افترض أننا طلبنا سطرًا في قاموس ذاكرة التخزين المؤقت ، ولكنه لم يدخل في ذاكرة التخزين المؤقت. ثم علينا الانتظار حتى يتم تحميل البيانات من مصدر خارجي.



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

ترميز سلسلة المفردات


لذلك جئنا إلى إنشاء نوع بيانات جديد في ClickHouse - LowCardinality. هذا هو تنسيق لتخزين البيانات: كيف يتم كتابتها على القرص وكيف يتم قراءتها ، وكيف يتم تقديمها في الذاكرة ومخطط معالجتها.



يوجد عمودين على الشريحة. على اليمين ، يتم تخزين السلاسل بشكل قياسي في نوع السلاسل. يمكن ملاحظة أن هذه هي بعض أنواع طرازات الهواتف المحمولة. على اليسار يوجد نفس العمود بالضبط ، فقط في نوع LowCardinality. يتكون من قاموس يحتوي على العديد من الأسطر المختلفة (خطوط من العمود على اليمين) وقائمة بالمواضع (أرقام الأسطر).

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

LowCardinality هو نوع بيانات معلمي. يمكن أن يكون إما رقمًا ، أو شيئًا تم تخزينه كرقم ، أو سلسلة ، أو Nullable منها.



تكمن خصوصية LowCardinality في أنه يمكن حفظها لبعض الوظائف. يظهر طلب مثال على الشريحة. في السطر الأول ، قمت بإنشاء عمود من النوع LowCardinality من String ، وأطلق عليه اسم S. ثم طلبت اسمها - قال ClickHouse أنه كان LowCardinality من String. حسنا.

السطر الثالث هو نفسه تقريبًا ، فقط سمينا دالة الطول. في ClickHouse ، تُرجع الدالة length نوع بيانات UInt64. لكننا حصلنا على LowCardinality من UInt64. ما هي النقطة؟



تم تخزين أسماء الهواتف المحمولة في القاموس ، وقمنا بتطبيق دالة الطول. الآن لدينا قاموس مشابه يتكون فقط من أرقام وهذه أطوال السلاسل. العمود مع المواقف لم يتغير. ونتيجة لذلك ، قمنا بمعالجة بيانات أقل ، تم توفيرها في وقت الطلب.

قد تكون هناك تحسينات أخرى ، مثل إضافة ذاكرة تخزين مؤقت بسيطة. عند حساب قيمة دالة ، يمكنك تذكرها وجعلها هي نفسها ، لا تعيد الحساب.

يمكن أيضًا إجراء التحسين GROUP BY ، لأن عمودنا مع القاموس مجمع بالفعل جزئيًا - يمكنك بسرعة حساب قيمة وظائف التجزئة والعثور على المجموعة تقريبًا لوضع السطر التالي. يمكنك أيضًا تخصيص بعض الوظائف المجمعة ، على سبيل المثال uniq ، لأنه يمكنك فقط إرسال قاموس إليها ، وترك المواقف دون تغيير - بهذه الطريقة سيعمل كل شيء بشكل أسرع. أول تحسينين أضفناهما بالفعل إلى ClickHouse.



ولكن ماذا لو أنشأنا عمودًا بنوع بياناتنا وأدخلنا العديد من الصفوف السيئة المختلفة فيه؟ هل ذاكرتنا ممتلئة؟ لا ، هناك إعدادان خاصان لهذا في ClickHouse. الأول هو low_cardinality_max_dictionary_size. هذا هو الحد الأقصى لحجم القاموس الذي يمكن كتابته على القرص. يحدث الإدراج على النحو التالي: عندما ندرج البيانات ، يأتي إلينا دفق من الخطوط ، ونشكل منها قاموسًا عامًا كبيرًا. إذا أصبح القاموس أكبر من قيمة الإعداد ، نكتب القاموس الحالي إلى القرص ، وبقية الأسطر في مكان ما في مكان ما على الجانب ، بجوار الفهارس. ونتيجة لذلك ، لن نقوم بإعادة سرد قاموس كبير ولن نواجه أي مشاكل في الذاكرة.

الإعداد الثاني يسمى low_cardinality_use_single_dictionary_for_part. تخيل أنه في المخطط السابق ، عندما أدخلنا البيانات ، كان قاموسنا ممتلئًا ، وكتبهنا على القرص. السؤال الذي يطرح نفسه ، لماذا لا تشكل الآن نفس القاموس بالضبط؟

عندما يفيض ، سنكتبه مرة أخرى إلى القرص ونبدأ في تشكيل واحد ثالث. هذا الإعداد يعطل هذه الميزة فقط بشكل افتراضي.

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



قبل التحدث عن مزايا LowCardinality ، سأقول على الفور أنه من غير المحتمل أن نحقق بيانات مخفضة على القرص (على الرغم من أن هذا يمكن أن يحدث) ، لأن ClickHouse يضغط البيانات. هناك خيار افتراضي - LZ4. يمكنك أيضًا الضغط باستخدام ZSTD. لكن كلا الخوارزميتين تنفذ بالفعل ضغط القاموس ، لذا فإن قاموس ClickHouse الخارجي لن يساعد كثيرًا.

لكي لا يكون لديك أساس من الصحة ، أخذت بعض البيانات من المقياس - String و LowCardinality (String) و Enum - وحفظتها في أنواع بيانات مختلفة. اتضح ثلاثة أعمدة ، حيث تم كتابة مليار صف. يحتوي العمود الأول ، CodePage ، على إجمالي 62 قيمة. ويمكنك أن ترى أنه في LowCardinality (سلسلة) ، قاموا بضغطهم بشكل أفضل. السلسلة أسوأ قليلاً ، ولكن هذا على الأرجح يرجع إلى حقيقة أن الأوتار قصيرة ، نقوم بتخزين أطوالها ، وتستهلك مساحة كبيرة ولا تنضغط بشكل جيد.

إذا كنت تأخذ PhoneModel ، فهناك أكثر من 48 ألفًا ، ولا توجد فروق تقريبًا بين السلسلة و LowCardinality (السلسلة). بالنسبة إلى عنوان URL ، وفرنا أيضًا 2 غيغابايت فقط - أعتقد أنه لا يجب الاعتماد على ذلك.

تقدير سرعة العمل



رابط من الشريحة

الآن دعنا نقيم سرعة العمل. لتقييم ذلك ، استخدمت مجموعة بيانات تصف ركوب سيارات الأجرة في نيويورك. وهومتاحعلى جيثب. لديها أكثر من مليار رحلة بقليل. يُظهر الموقع ، وأوقات البدء والانتهاء للرحلة ، وطريقة الدفع ، وعدد الركاب وحتى نوع التاكسي - الأخضر والأصفر وأوبر.



لقد قدمت الطلب الأول بشكل بسيط للغاية - سألت أين يتم طلب سيارات الأجرة في أغلب الأحيان. للقيام بذلك ، تحتاج إلى أخذ الموقع الذي طلبت منه ، وجعل GROUP BY عليه وحساب وظيفة العد. هنا ClickHouse يعطي شيئا.



لقياس سرعة معالجة الطلب ، قمت بإنشاء ثلاثة جداول بالبيانات نفسها ، لكنني استخدمت ثلاثة أنواع مختلفة من البيانات لموقع البداية - String و LowCardinality و Enum. LowCardinality و Enum أسرع خمس مرات من String. التعداد أسرع لأنه يعمل مع الأرقام. LowCardinality - لأنه يتم تنفيذ تحسين GROUP BY.



دعونا نعقد الطلب - اسأل أين يقع منتزه الأكثر شعبية في نيويورك. مرة أخرى ، سنقيس ذلك من خلال المكان الذي يتم فيه طلب سيارات الأجرة في أغلب الأحيان ، ولكن في نفس الوقت سنقوم فقط بتصفية تلك المواقع حيث توجد كلمة "منتزه". أضف أيضًا وظيفة مماثلة.



ننظر إلى الوقت - نرى أن Enum بدأ فجأة في التباطؤ. ويعمل بشكل أبطأ من نوع بيانات السلسلة القياسي. هذا لأن الوظيفة الشبيهة ليست مثالية على الإطلاق لـ Enum. يجب علينا تحويل خطوطنا من Enum إلى خطوط عادية - نقوم بالمزيد من العمل. لا يتم أيضًا تحسين LowCardinality (String) افتراضيًا ، ولكن مثلما هو الحال في القاموس ، لذلك يكون الاستعلام أسرع مقارنة بـ String.

هناك مشكلة عالمية مع Enum. إذا أردنا تحسينه ، يجب أن نقوم بذلك في كل مكان من الكود. لنفترض أننا كتبنا وظيفة جديدة - يجب أن تأتي بالتأكيد بتحسينات لـ Enum. وفي LowCardinality ، يتم تحسين كل شيء بشكل افتراضي.



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



لا يزال انخفاض السرعة أسرع ، على الرغم من عدم وجود تصفية. هذا يرجع إلى حقيقة أن وظائفنا تعمل فقط في القاموس. تحتوي وظيفة حساب التجزئة على وسيطة واحدة - يمكنها معالجة بيانات أقل ويمكنها أيضًا إرجاع LowCardinality.



خطتنا العالمية هي تحقيق سرعة لا تقل عن سرعة السلسلة في أي حال ، وتوفير التسارع. وربما سنستبدل في يوم من الأيام String بـ LowCardinality ، وسوف تقوم بتحديث ClickHouse ، وسيعمل كل شيء بشكل أسرع قليلاً.

All Articles