ما هي أقسام الجدول في ClickHouse؟
تُجمِّع أقسام الجدول أجزاء البيانات الخاصة بجدول ضمن عائلة محركات MergeTree في وحدات منطقية منظَّمة، وهي طريقة لتنظيم البيانات بما يجعلها ذات دلالة مفاهيمية ومتوافقة مع معايير محددة، مثل النطاقات الزمنية أو الفئات أو السمات الرئيسية الأخرى. وتُسهِّل هذه الوحدات المنطقية إدارة البيانات والاستعلام عنها وتحسينها.
يمكن تفعيل التقسيم عند تعريف الجدول مبدئيًا عبر عبارة PARTITION BY. ويمكن أن تتضمن هذه العبارة تعبير SQL على أيٍّ من الأعمدة، بحيث تحدد نتائج هذا التعبير القسم الذي ينتمي إليه الصف.
ولتوضيح ذلك، نقوم بتعديل جدول المثال في ما هي أجزاء الجدول بإضافة عبارة PARTITION BY toStartOfMonth(date)، التي تنظّم أجزاء بيانات الجدول بحسب أشهر مبيعات العقارات:
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
ORDER BY (town, street)
PARTITION BY toStartOfMonth(date);
يمكنك الاستعلام عن هذا الجدول في ClickHouse SQL Playground التابع لنا.
عند إدراج مجموعة من الصفوف في الجدول، لا ينشئ ClickHouse جزء بيانات واحدًا يحتوي على جميع الصفوف المُدرجة (على الأقل)، كما هو موضح هنا، بل ينشئ جزء بيانات جديدًا لكل قيمة فريدة لمفتاح القسم بين الصفوف المُدرجة:
يقوم ClickHouse server أولًا بتقسيم الصفوف في مثال الإدراج، الذي يتضمن 4 صفوف كما هو موضح في المخطط أعلاه، وفقًا لقيمة مفتاح القسم toStartOfMonth(date).
ثم تُعالَج الصفوف، لكل قسم تم تحديده، كالمعتاد من خلال تنفيذ عدة خطوات متسلسلة (① الفرز، ② التقسيم إلى أعمدة، ③ الضغط، ④ الكتابة إلى القرص).
لاحظ أنه عند تمكين التقسيم، ينشئ ClickHouse تلقائيًا فهارس MinMax لكل جزء بيانات. وهي ببساطة ملفات لكل عمود في الجدول مستخدم في تعبير مفتاح القسم، وتحتوي على القيمتين الصغرى والكبرى لذلك العمود داخل جزء البيانات.
عند تمكين التقسيم، لا يَدمج ClickHouse أجزاء البيانات إلا داخل القسم نفسه، وليس عبر الأقسام. نوضّح ذلك في مثال الجدول أعلاه:
كما يوضّح المخطط أعلاه، لا تُدمَج الأجزاء التابعة لأقسام مختلفة مطلقًا. وإذا اخترت مفتاح تقسيم ذي عددية مرتفعة، فستتوزع الأجزاء على آلاف الأقسام، ولن تصبح مرشحة للدمج أبدًا، ما يؤدي إلى تجاوز الحدود المضبوطة مسبقًا والتسبب في ظهور الخطأ المزعج Too many parts. وحل هذه المشكلة بسيط: اختر مفتاح تقسيم مناسبًا ذي عددية أقل من 1000..10000.
يمكنك الاستعلام عن قائمة جميع الأقسام الفريدة الموجودة في جدول المثال باستخدام العمود الافتراضي _partition_value:
بدلاً من ذلك، يتتبع ClickHouse جميع الأجزاء والأقسام لكل الجداول في جدول النظام system.parts، ويُرجع الاستعلام التالي لجدول المثال أعلاه قائمةً بجميع الأقسام، بالإضافة إلى العدد الحالي من الأجزاء النشطة وإجمالي الصفوف في هذه الأجزاء لكل قسم:
فيما تُستخدم أقسام الجدول؟
في ClickHouse، يُعد التقسيم بالأساس ميزة لإدارة البيانات. فمن خلال تنظيم البيانات منطقيًا استنادًا إلى تعبير التقسيم، يمكن إدارة كل قسم على حدة. على سبيل المثال، يتيح مخطط التقسيم في جدول Example أعلاه حالات استخدام لا يُحتفَظ فيها إلا ببيانات آخر 12 شهرًا في الجدول الرئيسي، مع إزالة البيانات الأقدم تلقائيًا باستخدام قاعدة TTL (راجع الصف الأخير المُضاف في عبارة DDL):
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH DELETE;
نظرًا لأن الجدول مُقسَّم باستخدام toStartOfMonth(date)، فستُحذف الأقسام كاملةً (وهي مجموعات من أجزاء الجدول) التي تستوفي شرط TTL، مما يجعل عملية التنظيف أكثر كفاءة، من دون الحاجة إلى إعادة كتابة الأجزاء.
وبالمثل، بدلًا من حذف البيانات القديمة، يمكن نقلها تلقائيًا وبكفاءة إلى فئة تخزين أقل تكلفة:
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH TO VOLUME 'slow_but_cheap';
يمكن أن تساعد الأقسام في تحسين أداء الاستعلام، لكن ذلك يعتمد بدرجة كبيرة على أنماط الوصول. فإذا كانت الاستعلامات تستهدف عددًا قليلًا فقط من الأقسام (ومن الأفضل أن يكون قسمًا واحدًا)، فقد يتحسن الأداء. ويكون هذا مفيدًا عادةً فقط إذا لم يكن مفتاح التقسيم جزءًا من المفتاح الأساسي وكنت تُجري تصفية بناءً عليه، كما هو موضح في الاستعلام المثال أدناه.
يُنفَّذ هذا الاستعلام على جدول المثال المذكور أعلاه، ويقوم بحساب أعلى سعر لجميع العقارات المبيعة في لندن خلال ديسمبر 2020، وذلك عبر التصفية على كلٍّ من عمود (date) المُستخدَم في مفتاح تقسيم الجدول وعمود (town) المُستخدَم في المفتاح الأساسي للجدول (مع العلم أن date ليس جزءًا من المفتاح الأساسي).
يعالج ClickHouse هذا الاستعلام من خلال تطبيق سلسلة من تقنيات تقليم البيانات لتجنّب فحص البيانات غير ذات الصلة:
① تقليم الأقسام: تُستخدَم فهارس MinMax لتجاهل أقسام كاملة (مجموعات من الأجزاء) لا يمكن منطقيًا أن تطابق عامل التصفية في الاستعلام على الأعمدة المستخدمة في مفتاح تقسيم الجدول.
② تقليم الحبيبات: بالنسبة إلى أجزاء البيانات المتبقية بعد الخطوة ①، يُستخدَم الفهرس الأساسي لتجاهل جميع الحبيبات (كتل من الصفوف) التي لا يمكن منطقيًا أن تطابق عامل التصفية في الاستعلام على الأعمدة المستخدمة في المفتاح الأساسي للجدول.
يمكننا ملاحظة خطوات تقليم البيانات هذه من خلال فحص خطة تنفيذ الاستعلام الفعلية لاستعلام المثال المذكور أعلاه باستخدام عبارة EXPLAIN:
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE date >= '2020-12-01'
AND date <= '2020-12-31'
AND town = 'LONDON';
┌─explain──────────────────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple_partitioned) │
6. │ Indexes: │
7. │ MinMax │
8. │ Keys: │
9. │ date │
10. │ Condition: and((date in (-Inf, 18627]), (date in [18597, +Inf))) │
11. │ Parts: 1/436 │
12. │ Granules: 11/3257 │
13. │ Partition │
14. │ Keys: │
15. │ toStartOfMonth(date) │
16. │ Condition: and((toStartOfMonth(date) in (-Inf, 18597]), (toStartOfMonth(date) in [18597, +Inf))) │
17. │ Parts: 1/1 │
18. │ Granules: 11/11 │
19. │ PrimaryKey │
20. │ Keys: │
21. │ town │
22. │ Condition: (town in ['LONDON', 'LONDON']) │
23. │ Parts: 1/1 │
24. │ Granules: 1/11 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
يوضح الناتج أعلاه ما يلي:
① تقليم الأقسام: تُظهر الأسطر من 7 إلى 18 من ناتج EXPLAIN أعلاه أن ClickHouse يستخدم أولًا MinMax index الخاص بالحقل date لتحديد 11 من أصل 3257 حبيبة (كتل من الصفوف) مخزَّنة في جزء بيانات نشط واحد من أصل 436 جزء بيانات نشط موجود، وتحتوي على صفوف تطابق عامل التصفية date الخاص بالاستعلام.
② تقليم الحبيبة: تشير الأسطر من 19 إلى 24 من ناتج EXPLAIN أعلاه إلى أن ClickHouse يستخدم بعد ذلك الفهرس الأساسي (الذي أُنشئ على الحقل town) لجزء البيانات الذي جرى تحديده في الخطوة ①، لتقليل عدد الحبيبات أكثر (التي تحتوي على صفوف قد تطابق أيضًا عامل التصفية town الخاص بالاستعلام) من 11 إلى 1. وينعكس ذلك أيضًا في ناتج ClickHouse-client الذي طبعناه أعلاه للاستعلام المُنفَّذ:
... Elapsed: 0.006 sec. Processed 8.19 thousand rows, 57.34 KB (1.36 million rows/s., 9.49 MB/s.)
Peak memory usage: 2.73 MiB.
ما يعني أن ClickHouse فحص وعالج granule واحدة (كتلة من 8192 صفًا) في غضون 6 مللي ثانية لحساب نتيجة الاستعلام.
يُعد التقسيم في المقام الأول ميزة لإدارة البيانات
انتبه إلى أن تنفيذ الاستعلام عبر جميع الأقسام يكون عادةً أبطأ من تنفيذ الاستعلام نفسه على جدول غير مُقسَّم.
مع التقسيم، تتوزع البيانات عادةً على عدد أكبر من أجزاء البيانات، ما يؤدي غالبًا إلى أن يقوم ClickHouse بمسح حجم أكبر من البيانات ومعالجته.
يمكننا توضيح ذلك عبر تشغيل الاستعلام نفسه على كلٍّ من جدول المثال ما هي أجزاء الجدول (من دون تمكين التقسيم)، وجدول المثال الحالي أعلاه (مع تمكين التقسيم). يحتوي الجدولان على على البيانات نفسها وعدد الصفوف نفسه:
ومع ذلك، فإن الجدول الذي فُعِّل فيه التقسيم يحتوي على على عدد أكبر من أجزاء البيانات النشطة، لأن ClickHouse، كما ذُكر أعلاه، لا يدمج أجزاء البيانات إلا داخل الأقسام، وليس عبرها:
كما هو موضح أعلاه، يحتوي الجدول المُقسَّم uk_price_paid_simple_partitioned على أكثر من 600 قسم، وبالتالي على 600 306 من أجزاء البيانات النشطة. أما جدولنا غير المُقسَّم uk_price_paid_simple، فقد أمكن دمج جميع أجزاء البيانات الأولية فيه في جزء نشط واحد عبر عمليات الدمج في الخلفية.
عندما نتحقق من خطة التنفيذ الفعلية للاستعلام باستخدام عبارة EXPLAIN لاستعلام المثال أعلاه، من دون مُرشِّح للقسم، عند تشغيله على الجدول المُقسَّم، يمكننا أن نرى في الصفين 19 و20 من المخرجات أدناه أن ClickHouse حدّد 671 من أصل 3257 حبيبة موجودة (كتل من الصفوف)، موزعة على 431 من أصل 436 جزء بيانات نشط موجود، قد تحتوي على صفوف تطابق مُرشِّح الاستعلام، ولذلك سيجري محرك الاستعلام مسحها ومعالجتها:
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE town = 'LONDON';
┌─explain─────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple_partitioned) │
6. │ Indexes: │
7. │ MinMax │
8. │ Condition: true │
9. │ Parts: 436/436 │
10. │ Granules: 3257/3257 │
11. │ Partition │
12. │ Condition: true │
13. │ Parts: 436/436 │
14. │ Granules: 3257/3257 │
15. │ PrimaryKey │
16. │ Keys: │
17. │ town │
18. │ Condition: (town in ['LONDON', 'LONDON']) │
19. │ Parts: 431/436 │
20. │ Granules: 671/3257 │
└─────────────────────────────────────────────────────────────────┘
تُظهر خطة تنفيذ الاستعلام المادية للاستعلام نفسه عند تشغيله على الجدول من دون تقسيمات، في الصفَّين 11 و12 من المخرجات أدناه، أن ClickHouse حدَّد 241 من أصل 3083 كتلة صفوف موجودة ضمن جزء البيانات النشط الوحيد في الجدول، وقد تحتوي على صفوف تطابق عامل التصفية الخاص بالاستعلام:
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─explain───────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Keys: │
9. │ town │
10. │ Condition: (town in ['LONDON', 'LONDON']) │
11. │ Parts: 1/1 │
12. │ Granules: 241/3083 │
└───────────────────────────────────────────────────────┘
عند تشغيل الاستعلام على النسخة المُقسَّمة من الجدول، يفحص ClickHouse 671 كتلة من الصفوف (~ 5.5 مليون صف) ويعالجها خلال 90 مللي ثانية:
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594.30 million
└───────────────┘
1 row in set. Elapsed: 0.090 sec. Processed 5.48 million rows, 27.95 MB (60.66 million rows/s., 309.51 MB/s.)
Peak memory usage: 163.44 MiB.
أما عند تنفيذ الاستعلام على الجدول غير المُقسَّم، فإن ClickHouse يفحص 241 كتلة (~ مليوني صف) ويعالجها خلال 12 ميلي ثانية:
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594.30 million
└───────────────┘
1 row in set. Elapsed: 0.012 sec. Processed 1.97 million rows, 9.87 MB (162.23 million rows/s., 811.17 MB/s.)
Peak memory usage: 62.02 MiB.
آخر تعديل في ٢٩ يونيو ٢٠٢٦