يفترض هذا الدليل أن المستخدمين على دراية مسبقًا بمفهوم العروض المادية التزايدية وتحميل البيانات باستخدام دوال الجداول مثل S3 وGCS. كما نوصي المستخدمين بقراءة دليلنا حول تحسين أداء الإدراج من تخزين الكائنات، إذ يمكن تطبيق الإرشادات الواردة فيه على عمليات الإدراج المذكورة في هذا الدليل.
مجموعة بيانات مثال
pip.
على سبيل المثال، تغطي المجموعة الفرعية يومًا واحدًا فقط — 2024-12-17 — وهي متاحة للعامة على https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/2024-12-17/. ويمكنك إجراء استعلام باستخدام:
تتوفر مجموعة بيانات PyPI الكاملة، التي تضم أكثر من 1 تريليون صف، في بيئة العرض التوضيحي العامة لدينا clickpy.clickhouse.com. ولمزيد من التفاصيل حول مجموعة البيانات هذه، بما في ذلك كيفية استفادة العرض التوضيحي من العروض المادية لتحسين الأداء وكيفية تعبئة البيانات يوميًا، راجع هنا.
سيناريوهات استكمال البيانات التاريخية
- استكمال البيانات التاريخية مع وجود استيعاب بيانات قائم - يجري تحميل بيانات جديدة، وتحتاج البيانات التاريخية إلى الاستكمال. وقد جرى تحديد هذه البيانات التاريخية.
- إضافة عروض مادية إلى الجداول الحالية - يلزم إضافة عروض مادية جديدة إلى إعداد جرى فيه بالفعل ملء البيانات التاريخية، وكانت البيانات تتدفق فيه بالفعل.
استخدام الجداول المكررة وطرق العرض
pypi الرئيسي والعرض المُجسَّد التاليان، اللذان يحسبان عدد التنزيلات لكل مشروع بايثون:
{101..200}. وبينما يمكننا الإدراج مباشرةً في pypi، يمكننا تنفيذ عملية الملء بالبيانات التاريخية هذه بصورة معزولة عبر إنشاء جداول مكررة.
في حال فشل عملية الإدراج التاريخي، لن تتأثر جداولنا الرئيسية، ويمكننا ببساطة تطبيق TRUNCATE على الجداول المكررة وإعادة المحاولة.
لإنشاء نسخ جديدة من طرق العرض هذه، يمكننا استخدام جملة CREATE TABLE AS مع اللاحقة _v2:
pypi_v2 وpypi_downloads_v2 ثم إعادة تحميل البيانات.
بعد اكتمال تحميل البيانات، يمكننا نقل البيانات من الجداول المنسوخة مؤقتًا إلى الجداول الرئيسية باستخدام عبارة ALTER TABLE MOVE PARTITION.
أسماء الأقساميستخدم استدعاء
MOVE PARTITION أعلاه اسم القسم (). ويمثّل هذا القسم الوحيد لهذا الجدول (غير المُقسَّم). أما بالنسبة إلى الجداول المُقسَّمة، فستحتاج إلى تنفيذ عدة استدعاءات MOVE PARTITION — استدعاء واحد لكل قسم. ويمكن معرفة أسماء الأقسام الحالية من جدول system.parts، على سبيل المثال: SELECT DISTINCT partition FROM system.parts WHERE (table = 'pypi_v2').pypi وpypi_downloads يحتويان على البيانات كاملةً. ويمكن حذف pypi_downloads_v2 وpypi_v2 بأمان.
MOVE PARTITION خفيفة الوزن (باستغلال الروابط الصلبة) وذرّية في الوقت نفسه، أي إنها إما أن تفشل أو تنجح من دون أي حالة وسيطة.
نستفيد من هذه العملية كثيرًا في سيناريوهات التحميل الاستدراكي أدناه.
لاحظ أن هذه العملية تتطلب من المستخدمين اختيار حجم كل عملية إدراج.
إن عمليات الإدراج الأكبر، أي عددًا أكبر من الصفوف، تعني الحاجة إلى عدد أقل من عمليات MOVE PARTITION. ومع ذلك، يجب موازنة ذلك مع كلفة الاسترداد في حال فشل الإدراج، مثلًا بسبب انقطاع الشبكة. ويمكنك استكمال هذه العملية بتجميع الملفات على دفعات لتقليل المخاطر. ويمكن تنفيذ ذلك إما باستخدام استعلامات النطاق مثل WHERE timestamp BETWEEN 2024-12-17 09:00:00 AND 2024-12-17 10:00:00 أو أنماط glob. على سبيل المثال،
يستخدم ClickPipes هذا النهج عند تحميل البيانات من تخزين الكائنات، إذ ينشئ تلقائيًا جداول مكررة للجدول الهدف ولعروضه المادية، ما يجنّب المستخدم الحاجة إلى تنفيذ الخطوات المذكورة أعلاه. ومع استخدام عدة خيوط عاملة، يتعامل كل منها مع مجموعات فرعية مختلفة (عبر glob patterns) وتمتلك جداولها المكررة الخاصة بها، يمكن تحميل البيانات بسرعة مع ضمانات exactly-once semantics. ويمكن للمهتمين الاطلاع على مزيد من التفاصيل في هذه المدوّنة.
السيناريو 1: استكمال البيانات التاريخية مع استمرار إدخال البيانات
- حدِّد نقطة التحقق — إما طابعًا زمنيًا أو قيمة عمود يلزم استعادة البيانات التاريخية بدءًا منها.
- أنشئ نسخًا مكررة من الجدول الرئيسي والجداول الهدف الخاصة بالعروض المادية.
- أنشئ نسخًا من أي عروض مادية تشير إلى الجداول الهدف التي أُنشئت في الخطوة (2).
- أدرج البيانات في الجدول الرئيسي المكرر الذي أنشأناه في الخطوة (2).
- انقل جميع الأقسام من الجداول المكررة إلى نظيراتها الأصلية. ثم احذف الجداول المكررة.
2024-12-17 09:00:00. وباستخدام العملية نفسها المذكورة سابقًا، ننشئ جداول مكررة وطرق عرض، ونحمّل المجموعة الفرعية باستخدام عامل تصفية يعتمد على الطابع الزمني.
يمكن أن تكون التصفية حسب أعمدة الطابع الزمني في Parquet فعّالة للغاية. لن يقرأ ClickHouse سوى عمود الطابع الزمني لتحديد نطاقات البيانات الكاملة التي ينبغي تحميلها، مما يقلّل حركة مرور الشبكة إلى الحد الأدنى. ويمكن أيضًا لمحرك الاستعلام في ClickHouse الاستفادة من فهارس Parquet، مثل min-max.
استخدم ClickPipes في ClickHouse Cloud فقطإذا كنت تستخدم ClickHouse Cloud، فمن الأفضل استخدام ClickPipes لاستعادة النسخ الاحتياطية التاريخية إذا كان يمكن عزل البيانات في bucket خاص بها (ولا تكون هناك حاجة إلى عامل تصفية). وإضافةً إلى تقليل زمن التحميل عبر موازاة التحميل باستخدام عدة عمّال، تؤتمت ClickPipes العملية المذكورة أعلاه وتُنشئ جداول مكررة لكل من الجدول الرئيسي والعروض المادية.
السيناريو 2: إضافة العروض المادية إلى الجداول الحالية
timestamp أو عمود يزداد بشكل رتيب، بحيث يمكن استخدامه لتحديد نقطة في التدفق، مفيدًا هنا لأنه يجنّب إيقاف إدخال البيانات مؤقتًا. في الأمثلة أدناه، نفترض توفّر هاتين الحالتين، مع تفضيل الأساليب التي تتجنب الإيقاف المؤقت أثناء الإدخال.
تجنب POPULATEلا نوصي باستخدام الأمر
POPULATE لإجراء التحميل الاستدراكي للعروض المادية إلا مع مجموعات البيانات الصغيرة التي يكون فيها إدخال البيانات متوقفًا مؤقتًا. فقد يفوّت هذا الإجراء صفوفًا أُدرجت في الجدول المصدر، لأن العرض المادي يُنشأ بعد اكتمال hash الخاص بعملية populate. إضافةً إلى ذلك، يُنفَّذ هذا الـ populate على جميع البيانات، ويكون عرضةً للانقطاعات أو لحدود الذاكرة عند التعامل مع مجموعات بيانات كبيرة.توفّر طابع زمني أو عمود متزايد رتيبًا
- أنشئ العرض المادي مع عامل تصفية لا يأخذ في الاعتبار إلا الصفوف الأكبر من وقت اعتباطي في المستقبل القريب.
- شغّل استعلام
INSERT INTO SELECTيُدرج البيانات في الجدول الهدف للعرض المادي، مع القراءة من الجدول المصدر باستخدام استعلام تجميع المشاهدات.
SELECT الخاصة به لتتضمن عامل تصفية لا يأخذ في الحسبان إلا الصفوف التي يتجاوز وقتها وقتًا اعتباطيًا في المستقبل القريب — وفي هذه الحالة نفترض أن 2024-12-17 09:00:00 يوافق وقتًا بعد بضع دقائق من الآن.
INSERT INTO SELECT. على سبيل المثال، بالنسبة إلى العرض أعلاه:
في المثال أعلاه، يكون الجدول الهدف لدينا من نوع SummingMergeTree. في هذه الحالة، يمكننا ببساطة استخدام استعلام التجميع الأصلي. أما في حالات الاستخدام الأكثر تعقيدًا التي تستفيد من AggregatingMergeTree، فستستخدم دوال
-State للتجميعات. ويمكن العثور على مثال على ذلك في دليل التكامل هذا.pypi_downloads_per_day_v2، ثم الإدراج فيه، وإرفاق الأقسام الناتجة عنه بـ pypi_downloads_per_day.
غالبًا ما يكون استعلام الـ materialized view أكثر تعقيدًا (وهذا ليس نادرًا، وإلا لما استخدم المستخدمون view أصلًا!) ويستهلك موارد. وفي حالات أندر، قد تتجاوز الموارد المطلوبة لهذا الاستعلام موارد الخادم. وهذا يبرز إحدى مزايا materialized views في ClickHouse — فهي incremental ولا تعالج dataset بالكامل دفعة واحدة!
في هذه الحالة، تتوفر للمستخدمين عدة خيارات:
- عدّل استعلامك لإجراء
backfillللنطاقات، مثلWHERE timestamp BETWEEN 2024-12-17 08:00:00 AND 2024-12-17 09:00:00، وWHERE timestamp BETWEEN 2024-12-17 07:00:00 AND 2024-12-17 08:00:00، وهكذا. - استخدم محرك الجدول Null لتعبئة العرض المادي. يحاكي هذا الأسلوب آلية التعبئة التزايدية المعتادة للعرض المادي، إذ يُنفِّذ استعلامه على كتل من البيانات (بحجم قابل للتهيئة).
استخدام محرك الجدول Null لتعبئة العروض المادية
/dev/null في عالم محركات الجداول). ورغم أن هذا قد يبدو متناقضًا، فإن العروض المادية تظل تُنفَّذ على البيانات التي تُدرج في محرك الجدول هذا. ويتيح ذلك إنشاء العروض المادية من دون الاحتفاظ بالبيانات الأصلية، مما يتجنب عمليات I/O ومتطلبات التخزين المرتبطة بها.
ومن المهم أن أي عروض مادية مرتبطة بمحرك الجدول تظل تُنفَّذ على كتل من البيانات عند إدراجها، ثم ترسل نتائجها إلى الجدول الهدف. وتكون هذه الكتل بحجم قابل للتهيئة. ومع أن الكتل الأكبر قد تكون أكثر كفاءة (وأسرع في المعالجة)، فإنها تستهلك موارد أكثر (وخاصة الذاكرة). ويعني استخدام محرك الجدول هذا أنه يمكننا بناء العرض المادي بشكل تزايدي، أي كتلةً تلو الأخرى، مما يجنّب الحاجة إلى الاحتفاظ بالتجميع بالكامل في الذاكرة.
لننظر في المثال التالي:
pypi_v2, لاستقبال الصفوف التي ستُستخدم في إنشاء العرض المادي. لاحظ كيف نقتصر في المخطط على الأعمدة التي نحتاج إليها فقط. وينفّذ العرض المادي لدينا عملية aggregation على الصفوف المُدرجة في هذا الجدول (بمعدل block واحدة في كل مرة)، ثم يرسل النتائج إلى الجدول الهدف، pypi_downloads_per_day.
استخدمنا
pypi_downloads_per_day هنا بصفته الجدول الهدف. ولمزيد من المتانة، يمكن للمستخدمين إنشاء جدول مكرّر، pypi_downloads_per_day_v2، واستخدامه بصفته الجدول الهدف للعرض، كما هو موضح في الأمثلة السابقة. وبعد اكتمال عملية insert، يمكن بعد ذلك نقل partitionات pypi_downloads_per_day_v2 إلى pypi_downloads_per_day. ويتيح ذلك إمكانية recovery إذا فشلت عملية insert بسبب مشكلات في الذاكرة أو انقطاعات في server، أي يمكننا ببساطة تنفيذ TRUNCATE لـ pypi_downloads_per_day_v2، وضبط Settings، ثم إعادة المحاولة.pypi_v2 من pypi.
639.47 MiB.
ضبط الأداء والموارد
- توازي القراءة - عدد الخيوط المستخدمة للقراءة. يتم التحكم فيه عبر
max_threads. في ClickHouse Cloud، يتحدد ذلك بحسب حجم المثيل، ويكون افتراضيًا مساويًا لعدد وحدات vCPU. قد تؤدي زيادة هذه القيمة إلى تحسين أداء القراءة على حساب زيادة استخدام الذاكرة. - توازي الإدراج - عدد خيوط الإدراج المستخدمة لتنفيذ الإدراج. يتم التحكم فيه عبر
max_insert_threads. ملاحظة: هذه القيمة مقيّدة بالحدmax_threads، لذا فإن توازي الإدراج الفعلي هوmin(max_insert_threads, max_threads). في ClickHouse Cloud، يتحدد ذلك بحسب حجم المثيل (بين 2 و4)، ويتم ضبطه على 1 في OSS. قد تؤدي زيادة هذه القيمة إلى تحسين الأداء على حساب زيادة استخدام الذاكرة. - حجم كتلة الإدراج - تتم معالجة البيانات في حلقة، حيث يتم سحبها وتحليلها وتشكيلها في كتل إدراج داخل الذاكرة استنادًا إلى مفتاح التقسيم. ثم تُفرز هذه الكتل وتُحسَّن وتُضغط وتُكتب إلى التخزين على شكل أجزاء بيانات جديدة. يؤثر حجم كتلة الإدراج، الذي تتحكم فيه الإعدادات
min_insert_block_size_rowsوmin_insert_block_size_bytes(غير مضغوط)، في استخدام الذاكرة وعمليات I/O على القرص. تستخدم الكتل الأكبر ذاكرة أكثر، لكنها تنشئ أجزاءً أقل، مما يقلل من I/O وعمليات الدمج في الخلفية. تمثل هذه الإعدادات حدودًا دنيا (وأيهما يتم بلوغه أولًا يؤدي إلى تنفيذ عملية flush). - حجم كتلة العرض المادي - بالإضافة إلى الآليات المذكورة أعلاه لعملية الإدراج الرئيسية، تُدمج الكتل أيضًا قبل الإدراج في العروض المادية لتكون المعالجة أكثر كفاءة. يتحدد حجم هذه الكتل بواسطة الإعدادات
min_insert_block_size_bytes_for_materialized_viewsوmin_insert_block_size_rows_for_materialized_views. تتيح الكتل الأكبر معالجة أكثر كفاءة على حساب زيادة استخدام الذاكرة. افتراضيًا، تعود هذه الإعدادات إلى قيم إعدادات الجدول المصدرmin_insert_block_size_rowsوmin_insert_block_size_bytes، على التوالي.
نصيحة لاستعلامات
INSERT SELECT البسيطة: بالنسبة إلى الاستعلامات البسيطة من نوع INSERT INTO t1 SELECT * FROM t2 من دون تحويلات معقدة، فكّر في تفعيل optimize_trivial_insert_select=1. يضبط هذا الإعداد (وهو معطّل افتراضيًا منذ الإصدار 24.7) تلقائيًا درجة التوازي في SELECT لتتوافق مع max_insert_threads، مما يقلّل من استهلاك الموارد وعدد الأجزاء التي يتم إنشاؤها. ويكون هذا مفيدًا بشكل خاص في عمليات الترحيل المجمّعة للبيانات بين الجداول.min_insert_block_size_bytes_for_materialized_views وmin_insert_block_size_rows_for_materialized_views لتحسين الأداء. وإن أُجري تعديل عليهما، فاتبع أفضل الممارسات ذاتها المُشار إليها بشأن min_insert_block_size_rows وmin_insert_block_size_bytes.
لتقليل استهلاك الذاكرة، يمكنك تجربة هذه الإعدادات، مع العلم أن ذلك سيؤدي حتمًا إلى تراجع في الأداء. باستخدام الاستعلام السابق، نستعرض فيما يلي بعض الأمثلة.
يؤدي خفض max_insert_threads إلى 1 إلى تقليل الحمل الزائد على الذاكرة.
max_threads إلى 1.
min_insert_block_size_rows على 0 (ما يعطّله كعاملٍ مُحدِّد لحجم الـ block) وmin_insert_block_size_bytes على 10485760 (10MiB).
لا يوجد عمود timestamp أو عمود متزايد باطراد
timestamp لدى المستخدم أو عمود متزايد باطراد. وفي بعض الحالات، لا يكون ذلك متاحًا ببساطة. في هذه الحالة، نوصي بالعملية التالية، التي تستفيد من كثير من الخطوات الموضّحة سابقًا، لكنها تتطلب من المستخدمين إيقاف الاستيعاب مؤقتًا.
- أوقف عمليات الإدراج في جدولك الرئيسي مؤقتًا.
- أنشئ نسخة مكررة من جدولك الهدف الرئيسي باستخدام صيغة
CREATE AS. - أرفق الأقسام من الجدول الهدف الأصلي بالنسخة المكررة باستخدام
ALTER TABLE ATTACH. ملاحظة: تختلف عملية الإرفاق هذه عن عملية النقل المستخدمة سابقًا. ومع أنها تعتمد على الروابط الصلبة، فإن البيانات في الجدول الأصلي تظل محفوظة. - أنشئ عروضًا مادية جديدة.
- استأنف عمليات الإدراج. ملاحظة: لن تُحدِّث عمليات الإدراج إلا الجدول الهدف، وليس النسخة المكررة، التي لن تشير إلا إلى البيانات الأصلية.
- نفّذ إعادة تحميل البيانات التاريخية للعرض المادي، مع تطبيق العملية نفسها المستخدمة أعلاه على البيانات التي تحتوي على
timestamp، باستخدام الجدول المكرر بوصفه المصدر.
pypi_downloads_per_day (وسنفترض أننا لا نستطيع استخدام timestamp):
pypi_downloads_per_day باستخدام نهج INSERT INTO SELECT البسيط الذي شرحناه سابقًا. ويمكن أيضًا تحسين ذلك باستخدام نهج جدول Null الموثّق أعلاه، مع إمكانية استخدام جدول مطابق اختياريًا لزيادة المتانة.
ومع أن هذه العملية تتطلب بالفعل إيقاف عمليات الإدراج مؤقتًا، فإن الخطوات الوسيطة يمكن عادةً إنجازها بسرعة، مما يحدّ إلى أدنى حد من أي انقطاع في البيانات.