الانتقال إلى المحتوى الرئيسي
يتم تناول عوامل التشغيل IN وNOT IN وGLOBAL IN وGLOBAL NOT IN بشكل منفصل، نظرًا لغنى وظائفها وتعدد إمكانياتها. الجانب الأيسر من المعامل هو إما عمود واحد أو Tuple. أمثلة:
SELECT UserID IN (123, 456) FROM ...
SELECT (CounterID, UserID) IN ((34, 123), (101500, 456)) FROM ...
إذا كان الجانب الأيسر عمودًا واحدًا مُدرَجًا في الفهرس، والجانب الأيمن مجموعةً من الثوابت، فإن النظام يستخدم الفهرس لمعالجة الاستعلام. لا تُدرج قيمًا كثيرة بشكل صريح (أي الملايين منها). إذا كانت مجموعة البيانات كبيرة، ضعها في جدول مؤقت (للاطلاع على مثال، راجع قسم البيانات الخارجية لمعالجة الاستعلامات)، ثم استخدم استعلامًا فرعيًا. يمكن أن يكون الجانب الأيمن من المعامل مجموعةً من التعبيرات الثابتة، أو مجموعةً من Tuples تحتوي على تعبيرات ثابتة (كما هو موضح في الأمثلة أعلاه)، أو اسم جدول في قاعدة البيانات، أو استعلاماً فرعياً SELECT داخل أقواس. للتوافق مع الإصدارات السابقة، عندما يكون الجانب الأيمن تعبير tuple واحدًا، يمكن تفسيره إما كمجموعة من القيم أو كقيمة tuple واحدة، وذلك بحسب الجانب الأيسر من عامل IN. إذا كان الجانب الأيسر قيمةً scalar، فإن ClickHouse يعامل عناصر تعبير tuple الواحد هذا في الجانب الأيمن كقيم IN منفصلة:
Query
SELECT
    1 IN (tuple(1, 2)) AS one_in_tuple,
    2 IN (tuple(1, 2)) AS two_in_tuple,
    3 IN (tuple(1, 2)) AS three_in_tuple;
Response
┌─one_in_tuple─┬─two_in_tuple─┬─three_in_tuple─┐
│            1 │            1 │              0 │
└──────────────┴──────────────┴────────────────┘
يعمل هذا مثل SELECT 1 IN (1, 2). إذا كان الجانب الأيسر أيضاً Tuple، فإن الجانب الأيمن يُفسَّر باعتباره مجموعة من قيم Tuple:
Query
SELECT tuple(1, 2) IN (tuple(1, 2)) AS tuple_in_tuple;
Response
┌─tuple_in_tuple─┐
│              1 │
└────────────────┘
تنطبق هذه المعالجة الخاصة فقط عندما يكون الجانب الأيمن تعبير tuple واحداً. لا يمكن مطابقة الجانب الأيسر القياسي (scalar) مع جانب أيمن يحتوي على قيم tuple متعددة:
Query
SELECT 1 IN (tuple(1, 2), tuple(3, 4));
Response
Code: 43. DB::Exception: Unsupported types for IN. First argument type UInt8. Second argument type Tuple(Tuple(UInt8, UInt8), Tuple(UInt8, UInt8)). (ILLEGAL_TYPE_OF_ARGUMENT)
يتيح ClickHouse اختلاف الأنواع بين الجانب الأيسر والجانب الأيمن في الاستعلام الفرعي IN. في هذه الحالة، يُحوِّل النظام قيمة الجانب الأيمن إلى نوع الجانب الأيسر، كما لو طُبِّقت الدالة accurateCastOrNull على الجانب الأيمن. يعني هذا أن نوع البيانات يصبح Nullable، وإذا تعذّر إجراء التحويل، فإنه يُرجع NULL. مثال
Query
SELECT '1' IN (SELECT 1);
Response
┌─in('1', _subquery49)─┐
│                    1 │
└──────────────────────┘
إذا كان الجانب الأيمن من المعامل هو اسم جدول (على سبيل المثال، UserID IN users)، فهذا يعادل الاستعلام الفرعي UserID IN (SELECT * FROM users). استخدم هذا عند التعامل مع البيانات الخارجية المُرسَلة مع الاستعلام. على سبيل المثال، يمكن إرسال الاستعلام مصحوبًا بمجموعة من معرّفات المستخدمين المحمَّلة في الجدول المؤقت ‘users’، والتي يجب تصفيتها. إذا كان الجانب الأيمن من المعامل اسمَ جدول يستخدم محرك Set (مجموعة بيانات جاهزة تُحفظ دائمًا في ذاكرة RAM)، فلن تُعاد إنشاء مجموعة البيانات من جديد عند كل استعلام. قد يحدد الاستعلام الفرعي أكثر من عمود واحد لتصفية المجموعات. مثال:
Query
SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...
يجب أن تكون الأعمدة الواقعة على يسار عامل التشغيل IN ويمينه من نفس النوع. يمكن أن يرد عامل التشغيل IN والاستعلام الفرعي في أي جزء من الاستعلام، بما في ذلك داخل الدوال التجميعية ودوال lambda. مثال:
Query
SELECT
    EventDate,
    avg(UserID IN
    (
        SELECT UserID
        FROM test.hits
        WHERE EventDate = toDate('2014-03-17')
    )) AS ratio
FROM test.hits
GROUP BY EventDate
ORDER BY EventDate ASC
Response
┌──EventDate─┬────ratio─┐
│ 2014-03-17 │        1 │
│ 2014-03-18 │ 0.807696 │
│ 2014-03-19 │ 0.755406 │
│ 2014-03-20 │ 0.723218 │
│ 2014-03-21 │ 0.697021 │
│ 2014-03-22 │ 0.647851 │
│ 2014-03-23 │ 0.648416 │
└────────────┴──────────┘
لكل يوم بعد 17 مارس، احسب النسبة المئوية لمرات عرض الصفحات التي جاءت من مستخدمين زاروا الموقع في 17 مارس. يُنفَّذ الاستعلام الفرعي في بند IN مرة واحدة فقط دائمًا وعلى خادم واحد. لا توجد استعلامات فرعية معتمدة.

معالجة NULL

أثناء معالجة الطلب، يفترض عامل التشغيل IN أن ناتج أي عملية على NULL يساوي دائمًا 0، بغض النظر عمّا إذا كانت NULL على الجانب الأيمن أو الأيسر من المعامل. لا تُدرَج قيم NULL في أي مجموعة بيانات، ولا تُعدّ متساوية فيما بينها، ولا يمكن مقارنتها إذا كانت transform_null_in = 0. فيما يلي مثال باستخدام الجدول t_null:
┌─x─┬────y─┐
│ 1 │ ᴺᵁᴸᴸ │
│ 2 │    3 │
└───┴──────┘
يُرجع تنفيذ الاستعلام SELECT x FROM t_null WHERE y IN (NULL,3) النتيجة التالية:
┌─x─┐
│ 2 │
└───┘
يمكنك ملاحظة أن الصف الذي تكون فيه y = NULL يُستبعَد من نتائج الاستعلام. ويعود ذلك إلى أن ClickHouse لا يستطيع تحديد ما إذا كانت NULL ضمن المجموعة (NULL,3)، فيُرجِع 0 نتيجةً لهذه العملية، ولذلك يستبعد SELECT هذا الصف من المخرجات النهائية.
SELECT y IN (NULL, 3)
FROM t_null
┌─in(y, tuple(NULL, 3))─┐
│                     0 │
│                     1 │
└───────────────────────┘

الاستعلامات الفرعية الموزعة

هناك خياران لعوامل IN مع الاستعلامات الفرعية (على غرار عوامل JOIN): الصيغة العادية IN / JOIN والصيغة العامة GLOBAL IN / GLOBAL JOIN، ويختلفان في طريقة تنفيذهما عند معالجة الاستعلامات الموزعة.
تذكّر أن الخوارزميات الموضّحة أدناه قد تعمل بصورة مختلفة تبعًا لإعداد distributed_product_mode ضمن الإعدادات.
عند استخدام IN العادية، يُرسَل الاستعلام إلى الخوادم البعيدة، وتُنفِّذ كلٌّ منها الاستعلامات الفرعية الواردة في جملة IN أو JOIN. عند استخدام GLOBAL IN / GLOBAL JOIN، تُنفَّذ أولاً جميع الاستعلامات الفرعية الخاصة بـ GLOBAL IN / GLOBAL JOIN، وتُجمَّع نتائجها في جداول مؤقتة. ثم تُرسَل هذه الجداول المؤقتة إلى كل خادم بعيد، حيث تُنفَّذ الاستعلامات باستخدام هذه البيانات المؤقتة. بالنسبة لـ GLOBAL ... JOIN، يعتمد تحديد أي جانب من الـ join يُحسب كاستعلام فرعي على نوع الـ join: ففي حالتَي LEFT وINNER، يُحسب الجدول الأيمن؛ أما في حالة RIGHT، فيُحسب الجدول الأيسر بدلًا من ذلك، نظرًا لأن الجدول الأيمن هو الجانب المحفوظ وينبغي قراءته من الأجزاء (shards). بالنسبة للاستعلام غير الموزَّع، استخدم IN / JOIN العادي. توخَّ الحذر عند استخدام الاستعلامات الفرعية في عبارتَي IN / JOIN عند معالجة الاستعلامات الموزعة. لنستعرض بعض الأمثلة. افترض أن كل خادم في الكلستر يحتوي على local_table عادية. كما يحتوي كل خادم على جدول distributed_table من نوع Distributed، والذي يشمل جميع الخوادم في الكلستر. عند توجيه استعلام إلى distributed_table، يُرسَل الاستعلام إلى جميع الخوادم البعيدة وينفَّذ عليها باستخدام local_table. على سبيل المثال، الاستعلام
SELECT uniq(UserID) FROM distributed_table
سيتم إرساله إلى جميع الخوادم البعيدة بوصفه
SELECT uniq(UserID) FROM local_table
وتُنفَّذ على كلٍّ منها بالتوازي، حتى تصل إلى المرحلة التي يمكن فيها دمج النتائج الوسيطة. عندئذٍ، تُعاد النتائج الوسيطة إلى الخادم الطالب لتُدمج عليه، ثم تُرسَل النتيجة النهائية إلى العميل. لنفحص الآن استعلامًا يستخدم IN:
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
  • حساب تقاطع قيم audience لموقعين.
سيُرسَل هذا الاستعلام إلى جميع الخوادم البعيدة بوصفه
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
بمعنى آخر، سيتم تجميع مجموعة البيانات الموجودة في عبارة IN على كل خادم بصورة مستقلة، استناداً فقط إلى البيانات المخزنة محلياً على كل خادم من الخوادم. سيعمل هذا بشكل صحيح وأمثل إذا كنت مستعدًا لهذه الحالة وقد وزّعت البيانات على خوادم الـ cluster بحيث تقع بيانات كل UserID بالكامل على خادم واحد. في هذه الحالة، ستكون جميع البيانات اللازمة متاحةً محليًا على كل خادم. وإلا، فستكون النتيجة غير دقيقة. نشير إلى هذا النوع من الـ query بـ “local IN”. لتصحيح طريقة عمل الاستعلام عندما تكون البيانات موزَّعة عشوائيًا عبر خوادم الكلستر، يمكنك تحديد distributed_table داخل استعلام فرعي. سيبدو الاستعلام كالتالي:
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
سيُرسَل هذا الاستعلام إلى جميع الخوادم البعيدة بوصفه
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
سيبدأ الاستعلام الفرعي بالتنفيذ على كل خادم بعيد. ونظرًا لأن الاستعلام الفرعي يستخدم جدولًا موزعًا، فسيُعاد إرسال الاستعلام الفرعي الموجود على كل خادم بعيد إلى جميع الخوادم البعيدة بالشكل التالي:
SELECT UserID FROM local_table WHERE CounterID = 34
على سبيل المثال، إذا كان لديك مجموعة مكوّنة من 100 خادم، فإن تنفيذ الاستعلام بالكامل سيستلزم 10,000 طلب أولي، وهو أمر غير مقبول بشكل عام. في مثل هذه الحالات، يجب دائمًا استخدام GLOBAL IN بدلًا من IN. لنرَ كيف يعمل ذلك في الاستعلام:
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
سيُنفّذ الخادم الطالب الاستعلام الفرعي:
SELECT UserID FROM distributed_table WHERE CounterID = 34
وستُوضع النتيجة في جدول مؤقت في ذاكرة الوصول العشوائي (RAM). ثم يُرسَل الطلب إلى كل خادم بعيد على النحو التالي:
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID GLOBAL IN _data1
سيُرسَل الجدول المؤقت _data1 إلى كل خادم بعيد مع الاستعلام (اسم الجدول المؤقت يعتمد على التنفيذ). هذا أكثر كفاءة من استخدام IN العادي. ومع ذلك، ضع النقاط التالية في الاعتبار:
  1. عند إنشاء جدول مؤقت، لا تصبح البيانات مميّزة تلقائيًا. لتقليل حجم البيانات المنقولة عبر الشبكة، حدِّد DISTINCT في الاستعلام الفرعي. (لا تحتاج إلى فعل ذلك مع IN العادي.)
  2. سيُرسَل الجدول المؤقت إلى جميع الخوادم البعيدة. ولا يراعي الإرسال طوبولوجيا الشبكة. على سبيل المثال، إذا كانت 10 خوادم بعيدة موجودة في مركز بيانات بعيد جدًا عن خادم الطالب، فستُرسَل البيانات 10 مرات عبر القناة إلى مركز البيانات البعيد. حاول تجنب مجموعات البيانات الكبيرة عند استخدام GLOBAL IN.
  3. عند نقل البيانات إلى الخوادم البعيدة، لا يمكن تهيئة القيود المفروضة على عرض نطاق الشبكة. وقد يؤدي ذلك إلى إرهاق الشبكة.
  4. حاول توزيع البيانات على الخوادم بحيث لا تضطر إلى استخدام GLOBAL IN بانتظام.
  5. إذا كنت بحاجة إلى استخدام GLOBAL IN كثيرًا، فخطط لموقع عنقود ClickHouse بحيث لا توجد مجموعة واحدة من النسخ المتماثلة إلا في مركز بيانات واحد، مع توفر شبكة سريعة بينها، بحيث يمكن معالجة الاستعلام بالكامل داخل مركز بيانات واحد.
ومن المنطقي أيضًا تحديد جدول محلي في عبارة GLOBAL IN، إذا كان هذا الجدول المحلي متاحًا فقط على خادم الطالب وكنت تريد استخدام بياناته على الخوادم البعيدة.

الاستعلامات الفرعية الموزعة و max_rows_in_set

يمكنك استخدام max_rows_in_set و max_bytes_in_set للتحكم في مقدار البيانات المنقولة أثناء الاستعلامات الموزعة. وتزداد أهمية ذلك بشكل خاص إذا كان استعلام GLOBAL IN يُرجِع كمية كبيرة من البيانات. انظر عبارة SQL التالية:
SELECT * FROM table1 WHERE col1 GLOBAL IN (SELECT col1 FROM table2 WHERE <some_predicate>)
إذا لم يكن some_predicate انتقائيًا بدرجة كافية، فسيُرجع كمية كبيرة من البيانات ويتسبب في مشكلات بالأداء. في مثل هذه الحالات، من الحكمة الحدّ من نقل البيانات عبر الشبكة. لاحظ أيضًا أن set_overflow_mode مُعيَّن إلى throw (افتراضيًا)، ما يعني أنه يتم رفع استثناء عند بلوغ هذه العتبات.

الاستعلامات الفرعية الموزعة و max_parallel_replicas

عندما تكون قيمة max_parallel_replicas أكبر من 1، تخضع الاستعلامات الموزعة لمزيد من التحويل. على سبيل المثال، ما يلي:
SELECT CounterID, count() FROM distributed_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
SETTINGS max_parallel_replicas=3
يُحوَّل على كل خادم إلى:
SELECT CounterID, count() FROM local_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
SETTINGS parallel_replicas_count=3, parallel_replicas_offset=M
حيث تكون M بين 1 و3 بحسب النسخة المتماثلة التي يُنفَّذ عليها الاستعلام المحلي. تؤثر هذه الإعدادات في كل جدول من عائلة MergeTree ضمن الاستعلام، ولها التأثير نفسه كما لو جرى تطبيق SAMPLE 1/3 OFFSET (M-1)/3 على كل جدول. لذلك، فإن إضافة الإعداد max_parallel_replicas لن تُنتج نتائج صحيحة إلا إذا كان للجدولين مخطط النسخ المتماثل نفسه، وكان أخذ العينات فيهما يتم باستخدام UserID أو مفتاح فرعي له. وعلى وجه الخصوص، إذا لم يكن لدى local_table_2 مفتاح أخذ عينات، فستنتج نتائج غير صحيحة. وتنطبق القاعدة نفسها على JOIN. أحد الحلول البديلة، إذا كان local_table_2 لا يستوفي المتطلبات، هو استخدام GLOBAL IN أو GLOBAL JOIN. إذا لم يكن لدى الجدول مفتاح أخذ عينات، فيمكن استخدام خيارات أكثر مرونة لـ parallel_replicas_custom_key، وقد ينتج عنها سلوك مختلف وأكثر ملاءمة.
آخر تعديل في ٢٥ يونيو ٢٠٢٦