تتضمن عمليات نشر ClickHouse لأغراض Observability دائمًا مجموعات بيانات كبيرة تحتاج إلى إدارة. ويوفّر ClickHouse عددًا من الميزات التي تساعد على إدارة البيانات.
الأقسام
يتيح التقسيم في ClickHouse فصل البيانات منطقيًا على القرص وفقًا لعمود أو تعبير SQL. ومن خلال هذا الفصل المنطقي للبيانات، يمكن التعامل مع كل قسم على حدة، مثل حذفه. ويتيح لك ذلك نقل الأقسام، وبالتالي المجموعات الفرعية، بين طبقات التخزين بكفاءة استنادًا إلى الوقت أو انتهاء صلاحية البيانات/الحذف بكفاءة من cluster.
يُحدَّد التقسيم في الجدول عند تعريفه لأول مرة عبر عبارة PARTITION BY. ويمكن أن تتضمن هذه العبارة تعبير SQL على أي عمود أو أعمدة، وتحدِّد نتائج هذا التعبير القسم الذي يُرسَل إليه الصف.

ترتبط أجزاء البيانات منطقيًا (من خلال بادئة مشتركة لاسم المجلد) بكل قسم على القرص، ويمكن الاستعلام عنها بشكل منفصل. في المثال أدناه، يُقسِّم مخطط otel_logs الافتراضي البيانات حسب اليوم باستخدام التعبير toDate(Timestamp). وعند إدراج الصفوف في ClickHouse، سيُقيَّم هذا التعبير على كل صف ويُوجَّه إلى القسم الناتج إذا كان موجودًا (وإذا كان هذا الصف هو الأول لذلك اليوم، فسيُنشأ القسم).
CREATE TABLE default.otel_logs
(
...
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)يمكن تنفيذ عدد من العمليات على الأقسام، بما في ذلك النسخ الاحتياطية، ومعالجة الأعمدة، وعمليات التغيير التي تعدّل/تحذف البيانات صفًا بصف) ومسح الفهارس (مثل الفهارس الثانوية).
على سبيل المثال، لنفترض أن جدول otel_logs لدينا مُقسَّم حسب اليوم. وإذا جرى ملؤه بمجموعة بيانات السجلات المنظَّمة، فسيحتوي على بيانات تمتد لعدة أيام:
SELECT Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC┌────────day─┬───────c─┐
│ 2019-01-22 │ 2333977 │
│ 2019-01-23 │ 2326694 │
│ 2019-01-26 │ 1986456 │
│ 2019-01-24 │ 1896255 │
│ 2019-01-25 │ 1821770 │
└────────────┴─────────┘
5 rows in set. Elapsed: 0.058 sec. Processed 10.37 million rows, 82.92 MB (177.96 million rows/s., 1.42 GB/s.)
Peak memory usage: 4.41 MiB.يمكن العثور على الأقسام الحالية باستخدام استعلام بسيط على أحد جداول النظام:
SELECT DISTINCT partition
FROM system.parts
WHERE `table` = 'otel_logs'┌─partition──┐
│ 2019-01-22 │
│ 2019-01-23 │
│ 2019-01-24 │
│ 2019-01-25 │
│ 2019-01-26 │
└────────────┘
5 rows in set. Elapsed: 0.005 sec.قد يكون لدينا جدول آخر، otel_logs_archive، نستخدمه لتخزين البيانات الأقدم. ويمكن نقل البيانات إلى هذا الجدول بكفاءة على مستوى الأقسام (إذ إن هذا مجرد تغيير في البيانات الوصفية).
CREATE TABLE otel_logs_archive AS otel_logs
--move data to archive table
ALTER TABLE otel_logs
(MOVE PARTITION tuple('2019-01-26') TO TABLE otel_logs_archive
--confirm data has been moved
SELECT
Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC┌────────day─┬───────c─┐
│ 2019-01-22 │ 2333977 │
│ 2019-01-23 │ 2326694 │
│ 2019-01-24 │ 1896255 │
│ 2019-01-25 │ 1821770 │
└────────────┴─────────┘
4 rows in set. Elapsed: 0.051 sec. Processed 8.38 million rows, 67.03 MB (163.52 million rows/s., 1.31 GB/s.)
Peak memory usage: 4.40 MiB.SELECT Timestamp::Date AS day,
count() AS c
FROM otel_logs_archive
GROUP BY day
ORDER BY c DESC┌────────day─┬───────c─┐
│ 2019-01-26 │ 1986456 │
└────────────┴─────────┘
1 row in set. Elapsed: 0.024 sec. Processed 1.99 million rows, 15.89 MB (83.86 million rows/s., 670.87 MB/s.)
Peak memory usage: 4.99 MiB.وهذا بخلاف التقنيات الأخرى التي تتطلب استخدام INSERT INTO SELECT وإعادة كتابة البيانات في الجدول الهدف الجديد.
وعلاوة على ذلك، يمكن حذف البيانات بكفاءة بحسب القسم. وهذا أكثر كفاءةً بكثير من حيث استهلاك الموارد من التقنيات البديلة (عمليات التغيير أو lightweight deletes)، لذا يُفضَّل اعتماده.
ALTER TABLE otel_logs
(DROP PARTITION tuple('2019-01-25'))
SELECT
Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC┌────────day─┬───────c─┐
│ 2019-01-22 │ 4667954 │
│ 2019-01-23 │ 4653388 │
│ 2019-01-24 │ 3792510 │
└────────────┴─────────┘التطبيقات
يوضح ما سبق كيف يمكن نقل البيانات ومعالجتها بكفاءة على مستوى الأقسام. عمليًا، من المرجّح أنك ستستخدم عمليات الأقسام في حالات استخدام Observability غالبًا ضمن سيناريوهين:
- المعماريات متعددة الطبقات - نقل البيانات بين طبقات التخزين (راجع طبقات التخزين)، مما يتيح بناء معماريات ساخنة-باردة.
- الحذف الفعّال - عند وصول البيانات إلى قيمة TTL محددة (راجع إدارة البيانات باستخدام TTL)
نستعرض كلاهما بالتفصيل أدناه.
أداء الاستعلامات
مع أن التقسيمات قد تساعد في تحسين أداء الاستعلامات، فإن ذلك يعتمد بدرجة كبيرة على أنماط الوصول. فإذا كانت الاستعلامات تستهدف عددًا قليلًا فقط من التقسيمات (ويُفضَّل أن يكون قسمًا واحدًا)، فقد يتحسن الأداء. ويكون هذا مفيدًا عادةً فقط إذا لم يكن مفتاح القسم ضمن المفتاح الأساسي وكنت تُجري التصفية بناءً عليه. أما الاستعلامات التي تحتاج إلى شمول عدد كبير من التقسيمات، فقد يكون أداؤها أسوأ مما لو لم يُستخدم التقسيم أصلًا (إذ قد يكون هناك عدد أكبر من الأجزاء). كما أن فائدة استهداف قسم واحد ستكون أقل وضوحًا، أو قد تنعدم تمامًا، إذا كان مفتاح القسم مكوّنًا مبكرًا بالفعل في المفتاح الأساسي. ويمكن أيضًا استخدام التقسيم لتحسين استعلامات GROUP BY إذا كانت القيم في كل قسم فريدة. ومع ذلك، وبوجه عام، ينبغي التأكد أولًا من تحسين المفتاح الأساسي، وعدم اللجوء إلى التقسيم كتقنية لتحسين الاستعلامات إلا في حالات استثنائية تكون فيها أنماط الوصول موجَّهة إلى مجموعة فرعية محددة ويمكن التنبؤ بها من البيانات، مثل التقسيم حسب اليوم عندما تكون معظم الاستعلامات على اليوم الأخير. راجع هنا للاطلاع على مثال على هذا السلوك.
إدارة البيانات باستخدام TTL (Time-to-live)
تُعد ميزة Time-to-Live (TTL) عنصرًا أساسيًا في حلول Observability المعتمدة على ClickHouse لتحقيق إدارة فعّالة للبيانات والاحتفاظ بها، لا سيما مع التوليد المستمر لكميات هائلة من البيانات. ويؤدي تطبيق TTL في ClickHouse إلى انتهاء صلاحية البيانات الأقدم وحذفها تلقائيًا، بما يضمن الاستخدام الأمثل للتخزين والحفاظ على الأداء من دون تدخل يدوي. وتُعد هذه الإمكانية ضرورية للحفاظ على قاعدة بيانات خفيفة، وخفض تكاليف التخزين، والإبقاء على الاستعلامات سريعة وفعّالة عبر التركيز على البيانات الأحدث والأكثر صلة. كما تساعد على الامتثال لسياسات الاحتفاظ بالبيانات من خلال إدارة دورة حياة البيانات بصورة منهجية، مما يعزز الاستدامة العامة لحل Observability وقابليته للتوسع.
يمكن تحديد TTL إما على مستوى الجدول أو على مستوى العمود في ClickHouse.
TTL على مستوى الجدول
يتضمن المخطط الافتراضي لكلٍّ من السجلات والتتبعات قيمة TTL بحيث تنتهي صلاحية البيانات بعد فترة محددة. ويُحدَّد ذلك في ClickHouse exporter تحت المفتاح ttl، على سبيل المثال.
exporters:
clickhouse:
endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1
ttl: 72hتدعم هذه الصياغة حاليًا صياغة Duration في Golang. نوصي باستخدام h والتأكد من أن ذلك يتوافق مع فترة التقسيم. على سبيل المثال، إذا كان التقسيم حسب اليوم، فتأكد من أن القيمة تمثل مضاعفات الأيام، مثل 24h و48h و72h. وسيضمن ذلك تلقائيًا إضافة عبارة TTL إلى الجدول، على سبيل المثال إذا كانت ttl: 96h.
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(4)
SETTINGS ttl_only_drop_parts = 1بشكل افتراضي، تُزال البيانات التي انتهت صلاحية TTL الخاصة بها عندما يقوم ClickHouse بدمج أجزاء البيانات. وعندما يكتشف ClickHouse أن البيانات انتهت صلاحيتها، فإنه يُجري عملية دمج خارج الجدول الزمني.
**مهم: نوصي باستخدام الإعداد ttl_only_drop_parts=1 ** (المطبّق بواسطة المخطط الافتراضي). عند تمكين هذا الإعداد، يحذف ClickHouse جزءًا كاملًا عندما تكون جميع الصفوف فيه منتهية الصلاحية. إن حذف الأجزاء كاملةً بدلًا من التنظيف الجزئي للصفوف التي انتهت صلاحية TTL الخاصة بها (وهو ما يتم عبر عمليات التغيير كثيفة الاستهلاك للموارد عندما تكون قيمة ttl_only_drop_parts=0) يتيح استخدام قيم merge_with_ttl_timeout أقصر وتقليل التأثير في أداء النظام. وإذا كانت البيانات مُقسّمة وفق الوحدة نفسها التي تُطبَّق عندها صلاحية TTL، مثل اليوم، فستحتوي الأجزاء بطبيعتها على بيانات من interval المحدد فقط. وهذا يضمن إمكانية تطبيق ttl_only_drop_parts=1 بكفاءة.
TTL على مستوى العمود
ينتهي المثال أعلاه صلاحية البيانات على مستوى الجدول. ويمكنك أيضًا إنهاء صلاحية البيانات على مستوى العمود. ومع تقادم البيانات، يمكن استخدام ذلك لحذف الأعمدة التي لا تبرر قيمتها في التحقيقات تكلفة الموارد الإضافية اللازمة للاحتفاظ بها. على سبيل المثال، نوصي بالاحتفاظ بالعمود Body تحسبًا لإضافة بيانات وصفية ديناميكية جديدة لم تُستخرج عند وقت الإدراج، مثل وسم Kubernetes جديد. وبعد فترة، مثل شهر واحد، قد يتضح أن هذه البيانات الوصفية الإضافية غير مفيدة، ما يقلل جدوى الاحتفاظ بالعمود Body.
فيما يلي، نوضح كيف يمكن حذف العمود Body بعد 30 يومًا.
CREATE TABLE otel_logs_v2
(
`Body` String TTL Timestamp + INTERVAL 30 DAY,
`Timestamp` DateTime,
...
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)إعادة ضغط البيانات
مع أننا نوصي عادةً باستخدام ZSTD(1) لمجموعات بيانات observability، يمكنك تجربة خوارزميات ضغط مختلفة أو مستويات ضغط أعلى مثل ZSTD(3). وإلى جانب إمكانية تحديد ذلك عند إنشاء المخطط، يمكن أيضًا ضبط إعدادات الضغط بحيث تتغير بعد فترة زمنية محددة. وقد يكون هذا مناسبًا إذا كان codec أو خوارزمية الضغط يحسّن الضغط لكنه يؤدي إلى تراجع في أداء الاستعلام. قد تكون هذه المفاضلة مقبولة للبيانات الأقدم، التي يقل الاستعلام عنها، لكنها قد لا تكون مناسبة للبيانات الحديثة، التي تُستخدَم بوتيرة أكبر في عمليات التحقيق.
يوضح المثال أدناه ذلك، حيث نضغط البيانات باستخدام ZSTD(3) بعد 4 أيام بدلًا من حذفها.
CREATE TABLE default.otel_logs_v2
(
`Body` String,
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`Status` UInt16,
`RequestProtocol` LowCardinality(String),
`RunTime` UInt32,
`Size` UInt32,
`UserAgent` String,
`Referer` String,
`RemoteUser` String,
`RequestType` LowCardinality(String),
`RequestPath` String,
`RemoteAddress` IPv4,
`RefererDomain` String,
`RequestPage` String,
`SeverityText` LowCardinality(String),
`SeverityNumber` UInt8,
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
TTL Timestamp + INTERVAL 4 DAY RECOMPRESS CODEC(ZSTD(3))يمكن العثور على مزيد من التفاصيل والأمثلة حول تهيئة TTL هنا. ويمكن العثور على أمثلة، مثل كيفية إضافة إعدادات TTL للجداول والأعمدة وتعديلها، هنا. ولمعرفة كيف تُمكّن إعدادات TTL طبقات تخزين متدرجة، مثل معماريات الساخن-الدافئ، راجع طبقات التخزين.
طبقات التخزين
في ClickHouse، يمكنك إنشاء طبقات تخزين على أقراص مختلفة، مثل تخزين البيانات الحديثة/الساخنة على SSD والبيانات الأقدم على تخزين مدعوم بـ S3. تتيح هذه المعمارية استخدام وسائط تخزين أقل تكلفة للبيانات الأقدم، والتي يمكن أن تكون لها اتفاقيات مستوى الخدمة الخاصة بالاستعلامات أعلى نظرًا لقلة استخدامها في التحقيقات.
يتطلب إنشاء طبقات التخزين من المستخدمين أولًا إنشاء أقراص، ثم تُستخدم هذه الأقراص لوضع سياسات التخزين، مع وحدات تخزين يمكن تحديدها أثناء إنشاء الجدول. ويمكن نقل البيانات تلقائيًا بين الأقراص استنادًا إلى معدلات الامتلاء، وأحجام الأجزاء، وأولويات وحدات التخزين. يمكن العثور على مزيد من التفاصيل هنا.
ومع أنه يمكن نقل البيانات يدويًا بين الأقراص باستخدام الأمر ALTER TABLE MOVE PARTITION، فإنه يمكن أيضًا التحكم في نقل البيانات بين وحدات التخزين باستخدام TTLs. يمكن العثور على مثال كامل هنا.
إدارة تغييرات المخططات
من المحتم أن تتغير مخططات السجلات والتتبعات على مدى عمر النظام، مثلًا عندما يراقب المستخدمون أنظمة جديدة لها بيانات وصفية أو تسميات جراب مختلفة. ومن خلال إنتاج البيانات باستخدام مخطط OTel، مع الاحتفاظ ببيانات الأحداث الأصلية بصيغة منظَّمة، ستكون مخططات ClickHouse أكثر قدرة على التكيف مع هذه التغييرات. ومع ذلك، مع توفر بيانات وصفية جديدة وتغير أنماط وصول الاستعلامات، ستحتاج إلى تحديث المخططات لتعكس هذه المستجدات.
ولتجنب فترة التوقف أثناء تغييرات المخططات، تتوفر للمستخدمين عدة خيارات نعرضها أدناه.
استخدام القيم الافتراضية
يمكن إضافة الأعمدة إلى المخطط باستخدام قيم DEFAULT. ستُستخدم القيمة الافتراضية المحددة إذا لم تُحدَّد أثناء INSERT.
يمكن إجراء تغييرات على المخطط قبل تعديل أي منطق تحويل في العرض المادي أو تهيئة OTel collector، ما يؤدي إلى إرسال هذه الأعمدة الجديدة.
بمجرد تغيير المخطط، يمكنك إعادة تهيئة OTel collectors. على افتراض أن المستخدمين يتبعون العملية الموصى بها الموضحة في "استخراج البنية باستخدام SQL"، حيث ترسل OTel collectors بياناتها إلى محرك الجدول Null، ويتولى عرض مادي استخراج المخطط المستهدف وإرسال النتائج إلى الجدول المستهدف للتخزين، يمكن تعديل العرض باستخدام صيغة ALTER TABLE ... MODIFY QUERY syntax. لنفترض أن لدينا أدناه الجدول المستهدف مع العرض المادي المقابل له (المشابه لما استُخدم في "استخراج البنية باستخدام SQL") لاستخراج المخطط المستهدف من السجلات المهيكلة الخاصة بـ OTel:
CREATE TABLE default.otel_logs_v2
(
`Body` String,
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`Status` UInt16,
`RequestProtocol` LowCardinality(String),
`RunTime` UInt32,
`UserAgent` String,
`Referer` String,
`RemoteUser` String,
`RequestType` LowCardinality(String),
`RequestPath` String,
`RemoteAddress` IPv4,
`RefererDomain` String,
`RequestPage` String,
`SeverityText` LowCardinality(String),
`SeverityNumber` UInt8
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
CREATE MATERIALIZED VIEW otel_logs_mv TO otel_logs_v2 AS
SELECT
Body,
Timestamp::DateTime AS Timestamp,
ServiceName,
LogAttributes['status']::UInt16 AS Status,
LogAttributes['request_protocol'] AS RequestProtocol,
LogAttributes['run_time'] AS RunTime,
LogAttributes['user_agent'] AS UserAgent,
LogAttributes['referer'] AS Referer,
LogAttributes['remote_user'] AS RemoteUser,
LogAttributes['request_type'] AS RequestType,
LogAttributes['request_path'] AS RequestPath,
LogAttributes['remote_addr'] AS RemoteAddress,
domain(LogAttributes['referer']) AS RefererDomain,
path(LogAttributes['request_path']) AS RequestPage,
multiIf(Status::UInt64 > 500, 'CRITICAL', Status::UInt64 > 400, 'ERROR', Status::UInt64 > 300, 'WARNING', 'INFO') AS SeverityText,
multiIf(Status::UInt64 > 500, 20, Status::UInt64 > 400, 17, Status::UInt64 > 300, 13, 9) AS SeverityNumber
FROM otel_logsلنفترض أننا نريد استخراج عمود جديد Size من LogAttributes. يمكننا إضافة ذلك إلى المخطط باستخدام ALTER TABLE، مع تحديد القيمة الافتراضية:
ALTER TABLE otel_logs_v2
(ADD COLUMN `Size` UInt64 DEFAULT JSONExtractUInt(Body, 'size'))في المثال أعلاه، نحدّد القيمة الافتراضية على أنها المفتاح size في LogAttributes (وستكون هذه القيمة 0 إذا لم يكن موجودًا). وهذا يعني أن الاستعلامات التي تصل إلى هذا العمود في الصفوف التي لم تُدرَج فيها هذه القيمة ستحتاج إلى الوصول إلى Map، وبالتالي ستكون أبطأ. ويمكننا أيضًا بسهولة تحديدها كثابت، مثل 0، مما يقلّل تكلفة الاستعلامات اللاحقة على الصفوف التي لا تحتوي على هذه القيمة. ويُظهر الاستعلام عن هذا الجدول أن القيمة قد مُلئت من Map كما هو متوقع:
SELECT Size
FROM otel_logs_v2
LIMIT 5┌──Size─┐
│ 30577 │
│ 5667 │
│ 5379 │
│ 1696 │
│ 41483 │
└───────┘
5 rows in set. Elapsed: 0.012 sec.لضمان إدراج هذه القيمة في كل البيانات المستقبلية، يمكننا تعديل العرض المادي باستخدام صيغة ALTER TABLE كما هو موضح أدناه:
ALTER TABLE otel_logs_mv
MODIFY QUERY
SELECT
Body,
Timestamp::DateTime AS Timestamp,
ServiceName,
LogAttributes['status']::UInt16 AS Status,
LogAttributes['request_protocol'] AS RequestProtocol,
LogAttributes['run_time'] AS RunTime,
LogAttributes['size'] AS Size,
LogAttributes['user_agent'] AS UserAgent,
LogAttributes['referer'] AS Referer,
LogAttributes['remote_user'] AS RemoteUser,
LogAttributes['request_type'] AS RequestType,
LogAttributes['request_path'] AS RequestPath,
LogAttributes['remote_addr'] AS RemoteAddress,
domain(LogAttributes['referer']) AS RefererDomain,
path(LogAttributes['request_path']) AS RequestPage,
multiIf(Status::UInt64 > 500, 'CRITICAL', Status::UInt64 > 400, 'ERROR', Status::UInt64 > 300, 'WARNING', 'INFO') AS SeverityText,
multiIf(Status::UInt64 > 500, 20, Status::UInt64 > 400, 17, Status::UInt64 > 300, 13, 9) AS SeverityNumber
FROM otel_logsستتضمن الصفوف اللاحقة عمود Size يُعبَّأ عند الإدراج.
إنشاء جداول جديدة
كبديل للعملية المذكورة أعلاه، يمكنك ببساطة إنشاء الجدول المستهدف الجديد باستخدام المخطط الجديد. بعد ذلك، يمكن تعديل أي materialized views لاستخدام الجدول الجديد باستخدام ALTER TABLE MODIFY QUERY. المذكور أعلاه. وبهذا النهج، يمكنك إصدار نسخ من جداولك، مثل otel_logs_v3.
يترك هذا النهج المستخدمين أمام عدة جداول للاستعلام عنها. وللاستعلام عبر عدة جداول، يمكنك استخدام الدالة merge، التي تقبل أنماط wildcard لاسم الجدول. نوضح ذلك أدناه من خلال الاستعلام عن الإصدارين v2 وv3 من جدول otel_logs:
SELECT Status, count() AS c
FROM merge('otel_logs_v[2|3]')
GROUP BY Status
ORDER BY c DESC
LIMIT 5┌─Status─┬────────c─┐
│ 200 │ 38319300 │
│ 304 │ 1360912 │
│ 302 │ 799340 │
│ 404 │ 420044 │
│ 301 │ 270212 │
└────────┴──────────┘
5 rows in set. Elapsed: 0.137 sec. Processed 41.46 million rows, 82.92 MB (302.43 million rows/s., 604.85 MB/s.)إذا أراد المستخدمون تجنب استخدام الدالة merge وتوفير جدول للمستخدمين النهائيين يجمع بين عدة جداول، فيمكن استخدام محرك جدول Merge. نوضح ذلك أدناه:
CREATE TABLE otel_logs_merged
ENGINE = Merge('default', 'otel_logs_v[2|3]')
SELECT Status, count() AS c
FROM otel_logs_merged
GROUP BY Status
ORDER BY c DESC
LIMIT 5┌─Status─┬────────c─┐
│ 200 │ 38319300 │
│ 304 │ 1360912 │
│ 302 │ 799340 │
│ 404 │ 420044 │
│ 301 │ 270212 │
└────────┴──────────┘
5 rows in set. Elapsed: 0.073 sec. Processed 41.46 million rows, 82.92 MB (565.43 million rows/s., 1.13 GB/s.)يمكن تحديث ذلك كلما أُضيف جدول جديد باستخدام صياغة EXCHANGE للجدول. على سبيل المثال، لإضافة جدول بالإصدار v4، يمكننا إنشاء جدول جديد واستبداله ذرّيًا بالإصدار السابق.
CREATE TABLE otel_logs_merged_temp
ENGINE = Merge('default', 'otel_logs_v[2|3|4]')
EXCHANGE TABLE otel_logs_merged_temp AND otel_logs_merged
SELECT Status, count() AS c
FROM otel_logs_merged
GROUP BY Status
ORDER BY c DESC
LIMIT 5┌─Status─┬────────c─┐
│ 200 │ 39259996 │
│ 304 │ 1378564 │
│ 302 │ 820118 │
│ 404 │ 429220 │
│ 301 │ 276960 │
└────────┴──────────┘
5 rows in set. Elapsed: 0.068 sec. Processed 42.46 million rows, 84.92 MB (620.45 million rows/s., 1.24 GB/s.)