Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

فهم تنفيذ الاستعلامات باستخدام المحلل

يعالج ClickHouse الاستعلامات بسرعة فائقة، لكن تنفيذ الاستعلام ليس بهذه البساطة. لِنحاول فهم كيفية تنفيذ استعلام SELECT. ولتوضيح ذلك، دعونا نُضيف بعض البيانات إلى جدول في ClickHouse:

CREATE TABLE session_events(
   clientId UUID,
   sessionId UUID,
   pageId UUID,
   timestamp DateTime,
   type String
) ORDER BY (timestamp);

INSERT INTO session_events SELECT * FROM generateRandom('clientId UUID,
   sessionId UUID,
   pageId UUID,
   timestamp DateTime,
   type Enum(\'type1\', \'type2\')', 1, 10, 2) LIMIT 1000;

الآن بعد أن أصبحت لدينا بعض البيانات في ClickHouse، نريد تشغيل بعض الاستعلامات وفهم كيفية تنفيذها. يُقسَّم تنفيذ الاستعلام إلى العديد من الخطوات. ويمكن تحليل كل خطوة من خطوات تنفيذ الاستعلام واستكشاف أخطائها وإصلاحها باستخدام استعلام EXPLAIN المقابل. تُلخَّص هذه الخطوات في المخطط أدناه:

خطوات استعلام Explain

لنلقِ نظرة على كل مكوّن أثناء تنفيذ الاستعلام. سنأخذ بضعة استعلامات ثم نفحصها باستخدام عبارة EXPLAIN.

المُحلِّل

يهدف المُحلِّل إلى تحويل نص الاستعلام إلى AST (شجرة البنية المجرّدة). ويمكن توضيح هذه الخطوة باستخدام EXPLAIN AST:

EXPLAIN AST SELECT min(timestamp) AS minimum_date, max(timestamp) AS maximum_date FROM session_events;
┌─explain────────────────────────────────────────────┐
│ SelectWithUnionQuery (children 1)                  │
│  ExpressionList (children 1)                       │
│   SelectQuery (children 2)                         │
│    ExpressionList (children 2)                     │
│     Function min (alias minimum_date) (children 1) │
│      ExpressionList (children 1)                   │
│       Identifier timestamp                         │
│     Function max (alias maximum_date) (children 1) │
│      ExpressionList (children 1)                   │
│       Identifier timestamp                         │
│    TablesInSelectQuery (children 1)                │
│     TablesInSelectQueryElement (children 1)        │
│      TableExpression (children 1)                  │
│       TableIdentifier session_events               │
└────────────────────────────────────────────────────┘

الناتج هو شجرة البنية المجرّدة يمكن عرضها بصريًا كما هو موضح أدناه:

graph TD
    swuq["SelectWithUnionQuery"] --> el_root["ExpressionList"]
    el_root --> sq["SelectQuery"]
    sq --> el_select["ExpressionList"]
    sq --> tisq["TablesInSelectQuery"]
    el_select --> fmin["Function min (alias minimum_date)"]
    el_select --> fmax["Function max (alias maximum_date)"]
    fmin --> el_min["ExpressionList"]
    el_min --> id_min["Identifier timestamp"]
    fmax --> el_max["ExpressionList"]
    el_max --> id_max["Identifier timestamp"]
    tisq --> tisqe["TablesInSelectQueryElement"]
    tisqe --> te["TableExpression"]
    te --> ti["TableIdentifier session_events"]

لكل عقدة عُقد فرعية مرتبطة بها، وتمثل الشجرة بأكملها البنية العامة لاستعلامك. وهذه بنية منطقية تساعد في معالجة الاستعلام. ومن منظور المستخدم النهائي (ما لم يكن مهتمًا بتنفيذ الاستعلام)، فهي ليست مفيدة كثيرًا؛ إذ تُستخدم هذه الأداة أساسًا من قِبل المطورين.

المحلّل

لدى ClickHouse حاليًا معماريتان للمحلّل. يمكنك استخدام المعمارية القديمة عبر تعيين: enable_analyzer=0. المعمارية الحالية مفعّلة افتراضيًا منذ ClickHouse 24.3. وسنقتصر هنا على وصف المعمارية الحالية فقط، لأن القديمة أصبحت مهملة ويُحتفظ بها فقط من أجل التوافق مع الإصدارات السابقة.

يُعدّ المحلّل مرحلة مهمة في تنفيذ الاستعلام. فهو يأخذ AST ويحوّله إلى شجرة استعلام. والفائدة الأساسية لشجرة الاستعلام مقارنةً بـ AST هي أن كثيرًا من المكوّنات يكون قد جرى حلّها، مثل التخزين على سبيل المثال. كما يصبح معروفًا أيضًا من أي جدول ستتم القراءة، وتُحلّ الأسماء المستعارة، وتعرف الشجرة أنواع البيانات المختلفة المستخدمة. ومع هذه المزايا، يمكن للمحلّل تطبيق تحسينات. وتعمل هذه التحسينات عبر "تمريرات". وتبحث كل تمريرة عن نوع مختلف من التحسينات. يمكنك الاطلاع على جميع التمريرات هنا، ولنرَ ذلك عمليًا باستخدام استعلامنا السابق:

EXPLAIN QUERY TREE passes=0 SELECT min(timestamp) AS minimum_date, max(timestamp) AS maximum_date FROM session_events SETTINGS allow_experimental_analyzer=1;
┌─explain────────────────────────────────────────────────────────────────────────────────┐
│ QUERY id: 0                                                                            │
│   PROJECTION                                                                           │
│     LIST id: 1, nodes: 2                                                               │
│       FUNCTION id: 2, alias: minimum_date, function_name: min, function_type: ordinary │
│         ARGUMENTS                                                                      │
│           LIST id: 3, nodes: 1                                                         │
│             IDENTIFIER id: 4, identifier: timestamp                                    │
│       FUNCTION id: 5, alias: maximum_date, function_name: max, function_type: ordinary │
│         ARGUMENTS                                                                      │
│           LIST id: 6, nodes: 1                                                         │
│             IDENTIFIER id: 7, identifier: timestamp                                    │
│   JOIN TREE                                                                            │
│     IDENTIFIER id: 8, identifier: session_events                                       │
│   SETTINGS allow_experimental_analyzer=1                                               │
└────────────────────────────────────────────────────────────────────────────────────────┘
EXPLAIN QUERY TREE passes=20 SELECT min(timestamp) AS minimum_date, max(timestamp) AS maximum_date FROM session_events SETTINGS allow_experimental_analyzer=1;
┌─explain───────────────────────────────────────────────────────────────────────────────────┐
│ QUERY id: 0                                                                               │
│   PROJECTION COLUMNS                                                                      │
│     minimum_date DateTime                                                                 │
│     maximum_date DateTime                                                                 │
│   PROJECTION                                                                              │
│     LIST id: 1, nodes: 2                                                                  │
│       FUNCTION id: 2, function_name: min, function_type: aggregate, result_type: DateTime │
│         ARGUMENTS                                                                         │
│           LIST id: 3, nodes: 1                                                            │
│             COLUMN id: 4, column_name: timestamp, result_type: DateTime, source_id: 5     │
│       FUNCTION id: 6, function_name: max, function_type: aggregate, result_type: DateTime │
│         ARGUMENTS                                                                         │
│           LIST id: 7, nodes: 1                                                            │
│             COLUMN id: 4, column_name: timestamp, result_type: DateTime, source_id: 5     │
│   JOIN TREE                                                                               │
│     TABLE id: 5, alias: __table1, table_name: default.session_events                      │
│   SETTINGS allow_experimental_analyzer=1                                                  │
└───────────────────────────────────────────────────────────────────────────────────────────┘

بين عمليتَي التنفيذ، يمكنك ملاحظة كيفية معالجة الأسماء المستعارة والإسقاطات.

المُخطِّط

يأخذ المُخطِّط شجرة الاستعلام ويبني منها خطة استعلام. توضّح لنا شجرة الاستعلام ما الذي نريد فعله باستعلام معيّن، بينما توضّح لنا خطة الاستعلام كيف سننفّذ ذلك. كما ستُجرى تحسينات إضافية ضمن خطة الاستعلام. يمكنك استخدام EXPLAIN PLAN أو EXPLAIN لرؤية خطة الاستعلام (سينفّذ EXPLAIN الأمر EXPLAIN PLAN).

EXPLAIN PLAN WITH
   (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT type, min(timestamp) AS minimum_date, max(timestamp) AS maximum_date, count(*) /total_rows * 100 AS percentage FROM session_events GROUP BY type
┌─explain──────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))      │
│   Aggregating                                    │
│     Expression (Before GROUP BY)                 │
│       ReadFromMergeTree (default.session_events) │
└──────────────────────────────────────────────────┘

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

EXPLAIN header = 1
WITH (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT
   type,
   min(timestamp) AS minimum_date,
   max(timestamp) AS maximum_date,
   (count(*) / total_rows) * 100 AS percentage
FROM session_events
GROUP BY type
┌─explain──────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))      │
│ Header: type String                              │
│         minimum_date DateTime                    │
│         maximum_date DateTime                    │
│         percentage Nullable(Float64)             │
│   Aggregating                                    │
│   Header: type String                            │
│           min(timestamp) DateTime                │
│           max(timestamp) DateTime                │
│           count() UInt64                         │
│     Expression (Before GROUP BY)                 │
│     Header: timestamp DateTime                   │
│             type String                          │
│       ReadFromMergeTree (default.session_events) │
│       Header: timestamp DateTime                 │
│               type String                        │
└──────────────────────────────────────────────────┘

إذًا، أنت تعرف الآن أسماء الأعمدة التي يجب إنشاؤها للإسقاط الأخير (minimum_date وmaximum_date وpercentage)، لكن قد ترغب أيضًا في الاطّلاع على تفاصيل جميع الإجراءات التي يجب تنفيذها. يمكنك فعل ذلك بتعيين actions=1.

EXPLAIN actions = 1
WITH (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT
   type,
   min(timestamp) AS minimum_date,
   max(timestamp) AS maximum_date,
   (count(*) / total_rows) * 100 AS percentage
FROM session_events
GROUP BY type
┌─explain────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))                                                                                                │
│ Actions: INPUT :: 0 -> type String : 0                                                                                                     │
│          INPUT : 1 -> min(timestamp) DateTime : 1                                                                                          │
│          INPUT : 2 -> max(timestamp) DateTime : 2                                                                                          │
│          INPUT : 3 -> count() UInt64 : 3                                                                                                   │
│          COLUMN Const(Nullable(UInt64)) -> total_rows Nullable(UInt64) : 4                                                                 │
│          COLUMN Const(UInt8) -> 100 UInt8 : 5                                                                                              │
│          ALIAS min(timestamp) :: 1 -> minimum_date DateTime : 6                                                                            │
│          ALIAS max(timestamp) :: 2 -> maximum_date DateTime : 1                                                                            │
│          FUNCTION divide(count() :: 3, total_rows :: 4) -> divide(count(), total_rows) Nullable(Float64) : 2                               │
│          FUNCTION multiply(divide(count(), total_rows) :: 2, 100 :: 5) -> multiply(divide(count(), total_rows), 100) Nullable(Float64) : 4 │
│          ALIAS multiply(divide(count(), total_rows), 100) :: 4 -> percentage Nullable(Float64) : 5                                         │
│ Positions: 0 6 1 5                                                                                                                         │
│   Aggregating                                                                                                                              │
│   Keys: type                                                                                                                               │
│   Aggregates:                                                                                                                              │
│       min(timestamp)                                                                                                                       │
│         Function: min(DateTime) → DateTime                                                                                                 │
│         Arguments: timestamp                                                                                                               │
│       max(timestamp)                                                                                                                       │
│         Function: max(DateTime) → DateTime                                                                                                 │
│         Arguments: timestamp                                                                                                               │
│       count()                                                                                                                              │
│         Function: count() → UInt64                                                                                                         │
│         Arguments: none                                                                                                                    │
│   Skip merging: 0                                                                                                                          │
│     Expression (Before GROUP BY)                                                                                                           │
│     Actions: INPUT :: 0 -> timestamp DateTime : 0                                                                                          │
│              INPUT :: 1 -> type String : 1                                                                                                 │
│     Positions: 0 1                                                                                                                         │
│       ReadFromMergeTree (default.session_events)                                                                                           │
│       ReadType: Default                                                                                                                    │
│       Parts: 1                                                                                                                             │
│       Granules: 1                                                                                                                          │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

يمكنك الآن رؤية جميع المدخلات والدوال والأسماء المستعارة وأنواع البيانات المستخدمة. كما يمكنك الاطلاع هنا على بعض التحسينات التي سيطبّقها المُخطِّط.

مسار تنفيذ الاستعلام

يُشتق مسار تنفيذ الاستعلام من خطة الاستعلام. وهو يشبه خطة الاستعلام إلى حد كبير، إلا أنه ليس شجرة بل رسمًا بيانيًا. ويُظهر كيف سينفّذ ClickHouse الاستعلام وما الموارد التي ستُستخدم. ويُعد تحليل مسار تنفيذ الاستعلام مفيدًا جدًا لتحديد موضع الاختناق من حيث المدخلات/المخرجات. لنأخذ الاستعلام السابق ونلقِ نظرة على تنفيذ مسار تنفيذ الاستعلام:

EXPLAIN PIPELINE
WITH (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT
   type,
   min(timestamp) AS minimum_date,
   max(timestamp) AS maximum_date,
   (count(*) / total_rows) * 100 AS percentage
FROM session_events
GROUP BY type;
┌─explain────────────────────────────────────────────────────────────────────┐
│ (Expression)                                                               │
│ ExpressionTransform × 2                                                    │
│   (Aggregating)                                                            │
│   Resize 1 → 2                                                             │
│     AggregatingTransform                                                   │
│       (Expression)                                                         │
│       ExpressionTransform                                                  │
│         (ReadFromMergeTree)                                                │
│         MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread) 0 → 1 │
└────────────────────────────────────────────────────────────────────────────┘

داخل القوسين تظهر خطوة من خطة الاستعلام، وبجوارها المعالج. هذه معلومات مفيدة جدًا، ولكن بما أن هذا رسم بياني، فمن الأفضل عرضه بهذه الصورة. لدينا إعداد graph يمكننا ضبطه على 1 وتحديد تنسيق الإخراج ليكون TSV:

EXPLAIN PIPELINE graph=1 WITH
   (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT type, min(timestamp) AS minimum_date, max(timestamp) AS maximum_date, count(*) /total_rows * 100 AS percentage FROM session_events GROUP BY type FORMAT TSV;
digraph
{
 rankdir="LR";
 { node [shape = rect]
   subgraph cluster_0 {
     label ="Expression";
     style=filled;
     color=lightgrey;
     node [style=filled,color=white];
     { rank = same;
       n5 [label="ExpressionTransform × 2"];
     }
   }
   subgraph cluster_1 {
     label ="Aggregating";
     style=filled;
     color=lightgrey;
     node [style=filled,color=white];
     { rank = same;
       n3 [label="AggregatingTransform"];
       n4 [label="Resize"];
     }
   }
   subgraph cluster_2 {
     label ="Expression";
     style=filled;
     color=lightgrey;
     node [style=filled,color=white];
     { rank = same;
       n2 [label="ExpressionTransform"];
     }
   }
   subgraph cluster_3 {
     label ="ReadFromMergeTree";
     style=filled;
     color=lightgrey;
     node [style=filled,color=white];
     { rank = same;
       n1 [label="MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread)"];
     }
   }
 }
 n3 -> n4 [label=""];
 n4 -> n5 [label="× 2"];
 n2 -> n3 [label=""];
 n1 -> n2 [label=""];
}

يمكنك بعد ذلك نسخ هذا الناتج ولصقه هنا، وسيؤدي ذلك إلى إنشاء الرسم البياني التالي:

مخرَج الرسم البياني

يشير المستطيل الأبيض إلى عقدة في مسار المعالجة، ويشير المستطيل الرمادي إلى خطوات خطة الاستعلام، أما الرمز x متبوعًا برقم فيشير إلى عدد المدخلات/المخرجات المستخدمة. وإذا كنت لا تريد عرضها بصيغة مضغوطة، فيمكنك دائمًا إضافة compact=0:

EXPLAIN PIPELINE graph = 1, compact = 0
WITH (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT
   type,
   min(timestamp) AS minimum_date,
   max(timestamp) AS maximum_date,
   (count(*) / total_rows) * 100 AS percentage
FROM session_events
GROUP BY type
FORMAT TSV
digraph
{
 rankdir="LR";
 { node [shape = rect]
   n0[label="MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread)"];
   n1[label="ExpressionTransform"];
   n2[label="AggregatingTransform"];
   n3[label="Resize"];
   n4[label="ExpressionTransform"];
   n5[label="ExpressionTransform"];
 }
 n0 -> n1;
 n1 -> n2;
 n2 -> n3;
 n3 -> n4;
 n3 -> n5;
}
مخرجات الرسم البياني المضغوط

لماذا لا يقرأ ClickHouse من الجدول باستخدام خيوط متعددة؟ لنجرب إضافة المزيد من البيانات إلى جدولنا:

INSERT INTO session_events SELECT * FROM generateRandom('clientId UUID,
   sessionId UUID,
   pageId UUID,
   timestamp DateTime,
   type Enum(\'type1\', \'type2\')', 1, 10, 2) LIMIT 1000000;

والآن لنشغّل استعلام EXPLAIN مجددًا:

EXPLAIN PIPELINE graph = 1, compact = 0
WITH (
       SELECT count(*)
       FROM session_events
   ) AS total_rows
SELECT
   type,
   min(timestamp) AS minimum_date,
   max(timestamp) AS maximum_date,
   (count(*) / total_rows) * 100 AS percentage
FROM session_events
GROUP BY type
FORMAT TSV
digraph
{
  rankdir="LR";
  { node [shape = rect]
    n0[label="MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread)"];
    n1[label="MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread)"];
    n2[label="ExpressionTransform"];
    n3[label="ExpressionTransform"];
    n4[label="StrictResize"];
    n5[label="AggregatingTransform"];
    n6[label="AggregatingTransform"];
    n7[label="Resize"];
    n8[label="ExpressionTransform"];
    n9[label="ExpressionTransform"];
  }
  n0 -> n2;
  n1 -> n3;
  n2 -> n4;
  n3 -> n4;
  n4 -> n5;
  n4 -> n6;
  n5 -> n7;
  n6 -> n7;
  n7 -> n8;
  n7 -> n9;
}
مخرجات الرسم البياني المتوازي

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

المنفّذ

أخيرًا، يتولى المنفّذ الخطوة الأخيرة من تنفيذ الاستعلام. إذ يأخذ مسار تنفيذ الاستعلام وينفّذه. وتوجد أنواع مختلفة من المنفّذات، بحسب ما إذا كنت تنفّذ SELECT أو INSERT أو INSERT SELECT.

Navigation