الانتقال إلى المحتوى الرئيسي
هذا هو الجزء 3 من دليل عن ترحيل البيانات من PostgreSQL إلى ClickHouse. ومن خلال مثال عملي، يوضّح كيفية نمذجة البيانات في ClickHouse عند الترحيل من PostgreSQL.
نوصي المستخدمين الذين يرحّلون من Postgres بقراءة دليل نمذجة البيانات في ClickHouse. يستخدم هذا الدليل نفس Stack Overflow dataset، ويستعرض عدة أساليب بالاعتماد على ميزات ClickHouse.

المفاتيح الأساسية (الترتيبية) في ClickHouse

غالبًا ما يبحث المستخدمون القادمون من قواعد بيانات OLTP عن المفهوم المقابل في ClickHouse. وعندما يلاحظون أن ClickHouse يدعم صيغة PRIMARY KEY، فقد يميلون إلى تعريف مخطط الجدول لديهم باستخدام المفاتيح نفسها المستخدَمة في قاعدة بيانات OLTP المصدر. وهذا غير مناسب.

كيف تختلف المفاتيح الأساسية في ClickHouse؟

لفهم سبب عدم ملاءمة استخدام المفتاح الأساسي الخاص بـ OLTP في ClickHouse، ينبغي أولًا فهم أساسيات الفهرسة في ClickHouse. نستخدم Postgres هنا كمثال للمقارنة، لكن هذه المفاهيم العامة تنطبق أيضًا على قواعد بيانات OLTP الأخرى.
  • تكون المفاتيح الأساسية في Postgres، بحكم التعريف، فريدة لكل صف. ويتيح استخدام هياكل B-tree إجراء lookup فعّال للصفوف الفردية باستخدام هذا المفتاح. وبينما يمكن تهيئة ClickHouse لتحسين lookup لقيمة صف واحدة، فإن أعباء عمل analytics تتطلب عادةً قراءة عدد قليل من الأعمدة عبر عدد كبير من الصفوف. كما أن عامل التصفية سيحتاج في كثير من الأحيان إلى تحديد مجموعة فرعية من الصفوف لإجراء aggregation عليها.
  • تُعد كفاءة الذاكرة والتخزين على disk أمرًا بالغ الأهمية عند النطاق الذي يُستخدم فيه ClickHouse غالبًا. تُكتب البيانات في جداول ClickHouse على شكل chunks تُعرف باسم أجزاء، مع تطبيق قواعد لدمج هذه الأجزاء في الخلفية. في ClickHouse، لكل جزء فهرس أساسي خاص بها. وعند دمج الأجزاء، تُدمج أيضًا primary indexes الخاصة بالـ الجزء المدمج. وعلى خلاف Postgres، لا تُبنى هذه الفهارس لكل صف. وبدلًا من ذلك، يحتوي الفهرس الأساسي لكل جزء على index entry واحدة لكل مجموعة من الصفوف — وتُعرف هذه التقنية باسم الفهرسة المتفرقة.
  • تصبح الفهرسة المتفرقة ممكنة لأن ClickHouse يخزّن صفوف كل جزء على disk مرتبةً بحسب مفتاح محدد. وبدلًا من تحديد الصفوف الفردية مباشرةً (كما في الفهرس المعتمد على B-Tree)، يتيح الفهرس الأساسي المتفرق تحديد مجموعات الصفوف التي قد تطابق query بسرعة (عبر binary search على index entries). ثم تُبث مجموعات الصفوف المحتمل تطابقها، بالتوازي، إلى ClickHouse engine للعثور على التطابقات الفعلية. ويجعل تصميم الفهرس هذا الفهرس الأساسي صغيرًا (بحيث يتسع بالكامل في main memory)، مع تسريع execution time للاستعلامات بشكل ملحوظ، لا سيما استعلامات النطاق الشائعة في حالات استخدام analytics.
لمزيد من التفاصيل، نوصي بهذا الدليل المتعمق. لا يحدد المفتاح المختار في ClickHouse الفهرس فحسب، بل يحدد أيضًا الترتيب الذي تُكتب به البيانات على disk. ولهذا السبب، يمكن أن يؤثر بدرجة كبيرة في مستويات Compression، مما قد ينعكس بدوره على query performance. فمفتاح الترتيب الذي يجعل قيم معظم الأعمدة تُكتب بترتيب متجاور يتيح لخوارزمية الضغط المختارة (والـ codecs) ضغط البيانات بكفاءة أعلى.
ستُرتَّب جميع الأعمدة في الجدول استنادًا إلى قيمة مفتاح الترتيب المحدد، سواء أكانت مُدرجة في المفتاح نفسه أم لا. فعلى سبيل المثال، إذا استُخدم CreationDate كمفتاح، فسيطابق ترتيب القيم في جميع الأعمدة الأخرى ترتيب القيم في العمود CreationDate. ويمكن تحديد عدة مفاتيح ترتيب، وعندها سيكون الترتيب بنفس دلالات عبارة ORDER BY في query من نوع SELECT.

اختيار مفتاح الترتيب

للاطلاع على الاعتبارات والخطوات المتعلقة باختيار مفتاح الترتيب، باستخدام جدول Posts كمثال، راجع هنا. عند استخدام النسخ المتماثل الآني مع CDC، توجد قيود إضافية ينبغي أخذها في الاعتبار. راجع هذه الوثائق للتعرّف على كيفية تخصيص مفاتيح الترتيب مع CDC.

التقسيمات

إذا كنت قادمًا من Postgres، فلا بد أنك تعرف مفهوم تقسيم الجداول لتحسين الأداء وسهولة الإدارة في قواعد البيانات الكبيرة، وذلك عبر تقسيم الجداول إلى أجزاء أصغر وأسهل في الإدارة تُسمى أقسامًا. ويمكن تحقيق هذا التقسيم باستخدام نطاق على عمود محدد (مثل التواريخ)، أو قوائم محددة، أو باستخدام hash على مفتاح. ويتيح ذلك للمسؤولين تنظيم البيانات وفق معايير محددة مثل النطاقات الزمنية أو المواقع الجغرافية. ويساعد التقسيم في تحسين أداء الاستعلامات من خلال تسريع الوصول إلى البيانات عبر استبعاد الأقسام غير اللازمة أثناء الاستعلام وفهرسة أكثر كفاءة. كما يفيد في مهام الصيانة، مثل النسخ الاحتياطية وحذف البيانات، إذ يتيح تنفيذ العمليات على أقسام فردية بدلًا من الجدول بالكامل. بالإضافة إلى ذلك، يمكن أن يحسّن التقسيم قابلية التوسع في قواعد بيانات PostgreSQL بشكل كبير من خلال توزيع الحمل على عدة أقسام. في ClickHouse، يُحدَّد التقسيم للجدول عند تعريفه أول مرة عبر العبارة PARTITION BY. ويمكن أن تحتوي هذه العبارة على تعبير SQL على أي أعمدة، وتحدّد نتيجته القسم الذي يُرسل إليه الصف. ترتبط أجزاء البيانات منطقيًا بكل قسم على القرص، ويمكن الاستعلام عنها بشكل مستقل. في المثال أدناه، نقسّم جدول posts حسب السنة باستخدام التعبير toYear(CreationDate). ومع إدراج الصفوف في ClickHouse، سيُقيَّم هذا التعبير لكل صف ويُوجَّه إلى القسم الناتج إذا كان موجودًا (وإذا كان هذا الصف هو الأول لتلك السنة، فسيُنشأ القسم).
 CREATE TABLE posts
(
        `Id` Int32 CODEC(Delta(4), ZSTD(1)),
        `PostTypeId` Enum8('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
        `AcceptedAnswerId` UInt32,
        `CreationDate` DateTime64(3, 'UTC'),
...
        `ClosedDate` DateTime64(3, 'UTC')
)
ENGINE = MergeTree
ORDER BY (PostTypeId, toDate(CreationDate), CreationDate)
PARTITION BY toYear(CreationDate)
للاطلاع على وصف كامل للتقسيم، راجع “أقسام الجدول”.

استخدامات التقسيم

للتقسيم في ClickHouse استخدامات مشابهة لاستخداماته في Postgres، مع بعض الفروق الدقيقة. وبشكل أكثر تحديدًا:
  • إدارة البيانات - في ClickHouse، ينبغي أساسًا النظر إلى التقسيم على أنه ميزة لإدارة البيانات، وليس أسلوبًا لتحسين الاستعلامات. ومن خلال فصل البيانات منطقيًا استنادًا إلى مفتاح، يمكن التعامل مع كل قسم بصورة مستقلة، مثل حذفه. يتيح لك ذلك نقل الأقسام، وبالتالي المجموعات الفرعية، بين طبقات التخزين بكفاءة استنادًا إلى الوقت أو انتهاء صلاحية البيانات/حذفها بكفاءة من العنقود. في المثال أدناه، نحذف المنشورات من عام 2008.
SELECT DISTINCT partition
FROM system.parts
WHERE `table` = 'posts'
┌─partition─┐
│ 2008      │
│ 2009      │
│ 2010      │
│ 2011      │
│ 2012      │
│ 2013      │
│ 2014      │
│ 2015      │
│ 2016      │
│ 2017      │
│ 2018      │
│ 2019      │
│ 2020      │
│ 2021      │
│ 2022      │
│ 2023      │
│ 2024      │
└───────────┘

17 rows in set. Elapsed: 0.002 sec.
ALTER TABLE posts
(DROP PARTITION '2008')
Ok.

0 rows in set. Elapsed: 0.103 sec.
  • تحسين الاستعلامات - رغم أن الأقسام قد تساعد في تحسين أداء الاستعلامات، فإن ذلك يعتمد بدرجة كبيرة على أنماط الوصول. فإذا كانت الاستعلامات تستهدف عددًا قليلًا فقط من الأقسام (ويُفضَّل أن يكون قسمًا واحدًا)، فقد يتحسن الأداء. ويكون هذا مفيدًا عادةً فقط إذا لم يكن مفتاح التقسيم ضمن المفتاح الأساسي، وكانت التصفية تتم بناءً عليه. ومع ذلك، فإن الاستعلامات التي تحتاج إلى تغطية عدد كبير من الأقسام قد يكون أداؤها أسوأ مما لو لم يُستخدم التقسيم أصلًا (إذ قد يؤدي التقسيم إلى زيادة عدد الأجزاء). كما أن فائدة استهداف قسم واحد ستكون أقل وضوحًا، وقد تنعدم تمامًا، إذا كان مفتاح التقسيم بالفعل عنصرًا مبكرًا في المفتاح الأساسي. ويمكن أيضًا استخدام التقسيم لتحسين استعلامات GROUP BY إذا كانت القيم داخل كل قسم فريدة. لكن بشكل عام، ينبغي التأكد من تحسين المفتاح الأساسي، وعدم اللجوء إلى التقسيم كتقنية لتحسين الاستعلامات إلا في حالات استثنائية تكون فيها أنماط الوصول موجَّهة إلى مجموعة فرعية محددة ويمكن التنبؤ بها من البيانات، مثل التقسيم حسب اليوم، عندما تتركز معظم الاستعلامات على اليوم الأخير.

توصيات بشأن الأقسام

ينبغي النظر إلى التقسيم باعتباره أسلوبًا لإدارة البيانات. ويكون مثاليًا عندما تحتاج إلى حذف البيانات من العنقود عند العمل مع البيانات الزمنية؛ فعلى سبيل المثال، يمكن ببساطة حذف أقدم قسم. مهم: تأكد من أن تعبير مفتاح التقسيم لا ينتج عنه مجموعة عالية العددية؛ أي ينبغي تجنب إنشاء أكثر من 100 قسم. على سبيل المثال، لا تقسّم بياناتك حسب أعمدة عالية العددية مثل معرّفات العملاء أو الأسماء. وبدلًا من ذلك، اجعل معرّف العميل أو الاسم هو العمود الأول في تعبير ORDER BY.
داخليًا، ينشئ ClickHouse أجزاءً للبيانات المُدرجة. ومع إدراج المزيد من البيانات، يزداد عدد الأجزاء. ولمنع ارتفاع عدد الأجزاء بشكل مفرط، وهو ما سيؤدي إلى تراجع أداء الاستعلامات (بسبب زيادة عدد الملفات المطلوب قراءتها)، تُدمج الأجزاء معًا في عملية غير متزامنة تُجرى في الخلفية. وإذا تجاوز عدد الأجزاء حدًا مُعدًا مسبقًا، فسيطرح ClickHouse استثناءً عند insert على شكل خطأ “too many parts”. ولا ينبغي أن يحدث هذا في التشغيل المعتاد، ولا يقع إلا إذا كان ClickHouse مُهيأً على نحو غير صحيح أو استُخدم بشكل خاطئ، مثل تنفيذ عدد كبير من عمليات insert الصغيرة.
وبما أن الأجزاء تُنشأ لكل قسم بشكل مستقل، فإن زيادة عدد الأقسام تؤدي إلى زيادة عدد الأجزاء؛ أي إن عددها يصبح مضاعفًا لعدد الأقسام. لذلك، قد تتسبب مفاتيح التقسيم عالية العددية في هذا الخطأ، وينبغي تجنبها.

العروض المادية مقابل الإسقاطات

يتيح Postgres إنشاء عدة فهارس على جدول واحد، مما يسمح بتحسينه ليتلاءم مع مجموعة متنوعة من أنماط الوصول. وتمنح هذه المرونة المسؤولين والمطورين القدرة على مواءمة أداء قاعدة البيانات مع استعلامات محددة واحتياجات تشغيلية بعينها. ورغم أن مفهوم الإسقاطات في ClickHouse لا يطابق ذلك تمامًا، فإنه يتيح لك تحديد عدة عبارات ORDER BY لجدول واحد. في وثائق نمذجة البيانات الخاصة بـ ClickHouse، نستعرض كيف يمكن استخدام العروض المادية في ClickHouse للاحتساب المسبق للتجميعات، وتحويل الصفوف، وتحسين الاستعلامات لأنماط وصول مختلفة. وفي الحالة الأخيرة، قدمنا مثالًا يُرسل فيه العرض المادي الصفوف إلى جدول الهدف ذي مفتاح ترتيب مختلف عن الجدول الأصلي الذي يستقبل عمليات الإدراج. على سبيل المثال، تأمل الاستعلام التالي:
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
   ┌──────────avg(Score)─┐
1. │ 0.18181818181818182 │
   └─────────────────────┘

1 row in set. Elapsed: 0.040 sec. Processed 90.38 million rows, 361.59 MB (2.25 billion rows/s., 9.01 GB/s.)
Peak memory usage: 201.93 MiB.
يتطلب هذا الاستعلام فحص جميع الصفوف البالغ عددها 90 مليونًا (مع أن ذلك يتم بسرعة فعلًا) لأن UserId ليس مفتاح الترتيب. في السابق، عالجنا هذا باستخدام عرض مادي يعمل كآلية بحث عن PostId. ويمكن حل المشكلة نفسها باستخدام إسقاط. يضيف الأمر أدناه إسقاطًا لـ ORDER BY user_id.
ALTER TABLE comments ADD PROJECTION comments_user_id (
SELECT * ORDER BY UserId
)

ALTER TABLE comments MATERIALIZE PROJECTION comments_user_id
لاحظ أنه يجب أولًا إنشاء الإسقاط ثم تخزينه ماديًا. ويؤدي هذا الأمر الأخير إلى تخزين البيانات مرتين على القرص، بترتيبين مختلفين. ويمكن أيضًا تعريف الإسقاط عند إنشاء البيانات، كما هو موضح أدناه، وسيُحدَّث تلقائيًا عند إدراج البيانات.
CREATE TABLE comments
(
        `Id` UInt32,
        `PostId` UInt32,
        `Score` UInt16,
        `Text` String,
        `CreationDate` DateTime64(3, 'UTC'),
        `UserId` Int32,
        `UserDisplayName` LowCardinality(String),
        PROJECTION comments_user_id
        (
        SELECT *
        ORDER BY UserId
        )
)
ENGINE = MergeTree
ORDER BY PostId
إذا تم إنشاء الإسقاط عبر ALTER، فسيكون الإنشاء غير متزامن عند إصدار الأمر MATERIALIZE PROJECTION. يمكنك التحقق من تقدّم هذه العملية باستخدام الاستعلام التالي، مع انتظار is_done=1.
SELECT
        parts_to_do,
        is_done,
        latest_fail_reason
FROM system.mutations
WHERE (`table` = 'comments') AND (command LIKE '%MATERIALIZE%')
   ┌─parts_to_do─┬─is_done─┬─latest_fail_reason─┐
1. │           1 │       0 │                    │
   └─────────────┴─────────┴────────────────────┘

1 row in set. Elapsed: 0.003 sec.
إذا كررنا الاستعلام أعلاه، فسنلاحظ أن الأداء قد تحسّن بشكل ملحوظ، ولكن مقابل استخدام مساحة تخزين إضافية.
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
   ┌──────────avg(Score)─┐
1. │ 0.18181818181818182 │
   └─────────────────────┘

1 row in set. Elapsed: 0.008 sec. Processed 16.36 thousand rows, 98.17 KB (2.15 million rows/s., 12.92 MB/s.)
Peak memory usage: 4.06 MiB.
وباستخدام الأمر EXPLAIN، نؤكد أيضًا أنه تم استخدام الإسقاط لتنفيذ هذا الاستعلام:
EXPLAIN indexes = 1
SELECT avg(Score)
FROM comments
WHERE UserId = 8592047
    ┌─explain─────────────────────────────────────────────┐
 1. │ Expression ((Projection + Before ORDER BY))         │
 2. │   Aggregating                                       │
 3. │   Filter                                            │
 4. │           ReadFromMergeTree (comments_user_id)      │
 5. │           Indexes:                                  │
 6. │           PrimaryKey                                │
 7. │           Keys:                                     │
 8. │           UserId                                    │
 9. │           Condition: (UserId in [8592047, 8592047]) │
10. │           Parts: 2/2                                │
11. │           Granules: 2/11360                         │
    └─────────────────────────────────────────────────────┘

11 rows in set. Elapsed: 0.004 sec.

متى تستخدم إسقاطات

تُعد إسقاطات ميزة جذابة للمستخدمين الجدد لأنها تُصان تلقائيًا عند إدراج البيانات. علاوة على ذلك، يمكن ببساطة إرسال الاستعلامات إلى جدول واحد، حيث تُستغل إسقاطات، متى أمكن، لتسريع زمن الاستجابة. وهذا على النقيض من العروض المادية، حيث يتعين على المستخدم اختيار الجدول الهدف المُحسَّن المناسب أو إعادة كتابة استعلامه، بحسب عوامل التصفية. وهذا يحمّل تطبيقات المستخدم مسؤولية أكبر ويزيد التعقيد من جهة العميل. على الرغم من هذه المزايا، تأتي إسقاطات مع بعض القيود المتأصلة التي ينبغي أن تكون على دراية بها، ولذلك يجب استخدامها بحذر. نوصي باستخدام إسقاطات عندما:
  • تكون هناك حاجة إلى إعادة ترتيب كاملة للبيانات. وبينما يمكن للتعبير في إسقاط، من الناحية النظرية، أن يستخدم GROUP BY, فإن العروض المادية تكون أكثر فاعلية في الحفاظ على التجميعات. كما أن مُحسِّن الاستعلامات يكون أكثر احتمالًا للاستفادة من إسقاطات التي تستخدم إعادة ترتيب بسيطة، أي SELECT * ORDER BY x. يمكنك اختيار مجموعة فرعية من الأعمدة في هذا التعبير لتقليل البصمة التخزينية.
  • يكون المستخدمون متقبلين للزيادة المصاحبة في البصمة التخزينية وعبء كتابة البيانات مرتين. اختبر التأثير في سرعة الإدراج وقيّم العبء التخزيني.
اعتبارًا من الإصدار 25.5، يدعم ClickHouse العمود الافتراضي _part_offset في إسقاطات. يتيح ذلك طريقة أكثر كفاءة من حيث المساحة لتخزين إسقاطات.لمزيد من التفاصيل، راجع “Projections”

إزالة التطبيع

نظرًا إلى أن Postgres قاعدة بيانات علائقية، فإن نموذج بياناته يكون مُطبَّعًا بدرجة كبيرة، وغالبًا ما يضم مئات الجداول. في ClickHouse، قد تكون إزالة التطبيع مفيدة أحيانًا لتحسين أداء عمليات JOIN. يمكنك الرجوع إلى هذا الدليل الذي يوضح فوائد إزالة التطبيع لمجموعة بيانات Stack Overflow في ClickHouse. بهذا نختتم دليلنا الأساسي إذا كنت تُرحِّل من Postgres إلى ClickHouse. ونوصي بقراءة دليل نمذجة البيانات في ClickHouse للتعرّف أكثر على ميزات ClickHouse المتقدمة.
آخر تعديل في ٢٩ يونيو ٢٠٢٦