يوفّر هذا المحرك تكامل بيانات مع جداول Apache Iceberg في Amazon S3 وAzure وHDFS والجداول المخزّنة محليًا.
إنشاء جدول
من دون مخطط صريح، يجب أن يكون جدول Iceberg موجودًا بالفعل في التخزين. لإنشاء جدول Iceberg مستقل جديد على خلفية قابلة للكتابة، حدّد مخططه في عبارة CREATE TABLE.
CREATE TABLE iceberg_table_s3
ENGINE = IcebergS3(url, [, NOSIGN | access_key_id, secret_access_key, [session_token]], format, [,compression], [,extra_credentials])
CREATE TABLE iceberg_table_azure
ENGINE = IcebergAzure(connection_string|storage_account_url, container_name, blobpath, [account_name, account_key, format, compression])
CREATE TABLE iceberg_table_hdfs
ENGINE = IcebergHDFS(path_to_table, [,format] [,compression_method])
CREATE TABLE iceberg_table_local
ENGINE = IcebergLocal(path_to_table, [,format] [,compression_method])وسيطات المحرك
وصف هذه الوسيطات مطابق لوصف الوسيطات في المحرّكات S3 وAzureBlobStorage وHDFS وFile، على الترتيب.
يشير format إلى تنسيق ملفات البيانات في جدول Iceberg.
بالنسبة إلى IcebergS3، يمكن استخدام المعامل الاختياري extra_credentials لتمرير role_arn من أجل الوصول المستند إلى الأدوار في ClickHouse Cloud. راجع تأمين S3 للاطلاع على خطوات الإعداد.
يمكن تحديد معاملات المحرك باستخدام المجموعات المُسمّاة
مثال
CREATE TABLE iceberg_table ENGINE=IcebergS3('http://test.s3.amazonaws.com/clickhouse-bucket/test_table', 'test', 'test')باستخدام المجموعات المُسمّاة:
<clickhouse>
<named_collections>
<iceberg_conf>
<url>http://test.s3.amazonaws.com/clickhouse-bucket/</url>
<access_key_id>test</access_key_id>
<secret_access_key>test</secret_access_key>
</iceberg_conf>
</named_collections>
</clickhouse>CREATE TABLE iceberg_table ENGINE=IcebergS3(iceberg_conf, filename = 'test_table')الأسماء البديلة
يكتشف محرك الجدول Iceberg خلفية التخزين تلقائيًا من إعداد disk، ثم يوجّه إلى IcebergS3 أو IcebergAzure أو IcebergLocal وفقًا لذلك. وعند عدم تحديد disk، يستخدم تنفيذ IcebergS3 افتراضيًا.
أنواع البيانات
يوضح الجدول التالي كيفية مطابقة أنواع بيانات Iceberg مع أنواع بيانات ClickHouse أثناء استنتاج المخطط (لأغراض القراءة).
الأنواع الأولية
| نوع Iceberg | نوع ClickHouse | ملاحظات |
|---|---|---|
boolean |
Bool |
|
int |
Int32 |
|
long, bigint |
Int64 |
|
float |
Float32 |
|
double |
Float64 |
|
date |
Date32 |
|
time |
Int64 |
ميكروثانية منذ منتصف الليل |
timestamp |
DateTime64(6) |
ميكروثانية، من دون منطقة زمنية |
timestamptz |
DateTime64(6, 'UTC') |
ميكروثانية، بتوقيت UTC |
timestamp_ns |
DateTime64(9) |
نانوثانية، من دون منطقة زمنية (بدءًا من Iceberg v3 فقط) |
timestamptz_ns |
DateTime64(9, 'UTC') |
نانوثانية، بتوقيت UTC (بدءًا من Iceberg v3 فقط) |
string, binary |
String |
|
uuid |
UUID |
|
fixed(N) |
FixedString(N) |
|
decimal(P, S) |
Decimal(P, S) |
الأنواع المركبة
| نوع Iceberg | نوع ClickHouse |
|---|---|
list |
Array |
map |
Map |
struct |
Tuple |
قيود توافق المخطط والكتابة
تنطبق تعيينات أنواع البيانات المذكورة أعلاه على عمليات القراءة. وتنطبق القيود التالية عند إنشاء ClickHouse لمخططات Iceberg أو تطويرها وكتابة البيانات:
- لا يستطيع ClickHouse إنشاء مخطط Iceberg يحتوي على
BoolأوDecimalأوFixedStringأوInt8أوUInt8أوInt16أوUInt16، ولا إضافة عمود أو تعديل نوعه إلى أحد هذه الأنواع. تفشل العملية باستثناء نوع غير مدعوم. لا يمنع ذلك إدراج قيمBoolأوDecimalفي أعمدة Iceberg الموجودة. - عند إنشاء ClickHouse لمخطط Iceberg أو تطويره، فإنه يعيّن كل عمود من نوع
DateTimeوDateTime64إلى نوعtimestampفي Iceberg، الذي يتميز بدقة ميكروثانية ولا يتضمن منطقة زمنية. لا يستطيع ClickHouse إنشاء أنواع مخططاتtimestamptzأوtimestamp_nsأوtimestamptz_ns، لذا لا تُمثَّل الدقة الأعلى ودلالات المنطقة الزمنية في مخطط Iceberg. - لا يستطيع ClickHouse الكتابة إلى جدول Iceberg يستخدم عمودًا من نوع
decimalأوfixedأوtimestamp_nsأوtimestamptz_nsكحقل تقسيم مباشر. تفشل العملية باستثناء نوع غير مدعوم. - بالنسبة إلى ملفات البيانات التي تحتوي على أنواع لا يستطيع ClickHouse إجراء تسلسل لحدودها، بما في ذلك
BoolوDecimal، يحذف ClickHouse جميع الحدود الدنيا والعليا للأعمدة من إدخال بيان Iceberg. تظل أحجام الأعمدة وأعداد قيم null مضمنة، وتبقى البيانات صحيحة، لكن لا تستطيع القارئات استخدام تقليم min-max على مستوى البيان لهذه الملفات.
تطور المخطط
يدعم ClickHouse قراءة جداول Iceberg التي تطور مخططها بمرور الوقت. ويشمل ذلك الجداول التي أُضيفت إليها أعمدة أو أُزيلت منها أو أُعيد ترتيبها، وكذلك الأعمدة التي تغيّرت من required إلى Nullable. بالإضافة إلى ذلك، تكون تحويلات الأنواع التالية مدعومة:
- int -> long
- float -> double
- decimal(P, S) -> decimal(P', S) where P' > P.
حاليًا، لا يمكن تغيير البُنى المتداخلة أو أنواع العناصر داخل Array وMap.
لقراءة جدول تغيّر مخططه بعد إنشائه باستخدام الاستدلال الديناميكي على المخطط، اضبط allow_dynamic_metadata_for_data_lakes = true عند إنشاء الجدول.
تشذيب الأقسام
يدعم ClickHouse تشذيب الأقسام أثناء استعلامات SELECT على جداول Iceberg، مما يساعد على تحسين أداء الاستعلام من خلال تخطي ملفات البيانات غير ذات الصلة. لتمكين تشذيب الأقسام، اضبط use_iceberg_partition_pruning = 1. لمزيد من المعلومات حول تشذيب أقسام Iceberg، راجع https://iceberg.apache.org/spec/#partitioning
السفر عبر الزمن
يدعم ClickHouse ميزة السفر عبر الزمن في جداول Iceberg، ما يتيح لك الاستعلام عن البيانات التاريخية باستخدام طابع زمني محدد أو معرّف لقطة.
دمج ملفات البيان الوصفي
مع مرور الوقت، قد تؤدي عمليات الكتابة المتكررة على جدول Iceberg إلى تراكم عدد كبير من ملفات البيان الصغيرة في قائمة البيان الخاصة باللقطة الحالية. وتؤدي قائمة البيان الطويلة إلى إبطاء تخطيط الاستعلام، إذ يجب قراءة كل ملف بيان لاكتشاف ملفات البيانات. ويمكن لـ ClickHouse دمج ملفات البيان هذه في عدد أقل من الملفات الأكبر حجمًا باستخدام عبارة OPTIMIZE TABLE ... MANIFEST:
OPTIMIZE TABLE example_table MANIFEST SETTINGS allow_experimental_iceberg_compaction = 1;ينتج عن ذلك لقطة جديدة (عملية replace) تشير إلى ملفات البيانات نفسها من خلال مجموعة موحّدة من ملفات البيان. لا يُعاد كتابة أي ملفات بيانات، ولا تُضاف أي صفوف أو تُحذف أو تُزال ازدواجيتها — بل يُعاد فقط ترتيب طبقة البيان.
المتطلبات والسلوك
- هذه الميزة تجريبية، ولا تتاح إلا عند تفعيل الإعداد
allow_experimental_iceberg_compaction. وتُطلق العبارة استثناءً إذا لم يكن الإعداد مفعّلًا. - لا تتم محاولة الدمج إلا عندما يتجاوز عدد ملفات البيان في قائمة البيان الخاصة باللقطة الحالية العتبة التي يحددها الإعداد
iceberg_manifest_min_count_to_compact(القيمة الافتراضية30). وإذا كان العدد الحالي أقل من العتبة أو مساويًا لها، يُتخطى الدمج ولا تُنشأ لقطة جديدة. اضبط العتبة على قيمة أقل لإجراء الدمج بشكل أكثر سرعة. - لا يكون
OPTIMIZE TABLE ... MANIFESTمدعومًا إلا لجداول Iceberg. ويؤدي تشغيله على أي محرك جدول آخر إلى إطلاق استثناء. - لا يكون
OPTIMIZE TABLE ... MANIFESTمدعومًا إلا لجداول Iceberg ذات format-version 2. ويؤدي تشغيله على جدول ذي format-version 1 إلى إطلاق استثناء، وكذلك تشغيله على جدول ذي format-version 3، لأن بياناتfirst_row_idالوصفية الخاصة بـ row-lineage في v3 لا تزال غير مدعومة بشكل صحيح عند إعادة كتابة manifest ذهابًا وإيابًا. - لا يكون
OPTIMIZE TABLE ... MANIFESTمدعومًا لجداول Iceberg المشفّرة التي تحتوي data files فيها علىkey_metadataلكل ملف. ولم يُنفَّذ بعد الحفاظ على بيانات التشفير الوصفية هذه عبر إعادة كتابة manifest، لذا تُطلق العبارة استثناءNOT_IMPLEMENTED.
معالجة الجداول ذات الصفوف المحذوفة
يدعم ClickHouse قراءة جداول Iceberg التي تستخدم طرق الحذف التالية:
- الحذف بحسب الموضع
- الحذف بحسب المساواة (مدعوم بدءًا من الإصدار 25.8+)
طريقة الحذف التالية غير مدعومة:
- متجهات الحذف (طُرحت في v3)
الاستخدام الأساسي
SELECT * FROM example_table ORDER BY 1
SETTINGS iceberg_timestamp_ms = 1714636800000 SELECT * FROM example_table ORDER BY 1
SETTINGS iceberg_snapshot_id = 3547395809148285433ملاحظة: لا يمكنك تحديد كلٍ من iceberg_timestamp_ms وiceberg_snapshot_id كمعاملَين في الاستعلام نفسه.
اعتبارات مهمة
-
تُنشأ اللقطات عادةً في الحالات التالية:
- عند كتابة بيانات جديدة إلى الجدول
- عند إجراء نوع من دمج البيانات
-
لا تؤدي تغييرات المخطط عادةً إلى إنشاء لقطات - وهذا يترتب عليه سلوكيات مهمة عند استخدام السفر عبر الزمن مع الجداول التي خضعت لتطور المخطط.
سيناريوهات نموذجية
تستخدم هذه السيناريوهات Spark لتوضيح تغييرات المخطط التي يجريها كاتب Iceberg خارجي.
السيناريو 1: تغييرات المخطط دون لقطات جديدة
ضع في اعتبارك تسلسل العمليات التالي:
-- Create a table with two columns
CREATE TABLE IF NOT EXISTS spark_catalog.db.time_travel_example (
order_number int,
product_code string
)
USING iceberg
OPTIONS ('format-version'='2')
-- Insert data into the table
INSERT INTO spark_catalog.db.time_travel_example VALUES
(1, 'Mars')
ts1 = now() // A piece of pseudo code
-- Alter table to add a new column
ALTER TABLE spark_catalog.db.time_travel_example ADD COLUMN (price double)
ts2 = now()
-- Insert data into the table
INSERT INTO spark_catalog.db.time_travel_example VALUES (2, 'Venus', 100)
ts3 = now()
-- Query the table at each timestamp
SELECT * FROM spark_catalog.db.time_travel_example TIMESTAMP AS OF ts1;
+------------+------------+
|order_number|product_code|
+------------+------------+
| 1| Mars|
+------------+------------+
SELECT * FROM spark_catalog.db.time_travel_example TIMESTAMP AS OF ts2;
+------------+------------+
|order_number|product_code|
+------------+------------+
| 1| Mars|
+------------+------------+
SELECT * FROM spark_catalog.db.time_travel_example TIMESTAMP AS OF ts3;
+------------+------------+-----+
|order_number|product_code|price|
+------------+------------+-----+
| 1| Mars| NULL|
| 2| Venus|100.0|
+------------+------------+-----+نتائج الاستعلام عند طوابع زمنية مختلفة:
- عند ts1 وts2: لا يظهر سوى العمودين الأصليين
- عند ts3: تظهر الأعمدة الثلاثة كلها، وتكون قيمة السعر في الصف الأول NULL
السيناريو 2: الاختلافات بين المخطط التاريخي والمخطط الحالي
قد يُظهر استعلام السفر عبر الزمن في اللحظة الحالية مخططًا يختلف عن مخطط الجدول الحالي:
-- Create a table
CREATE TABLE IF NOT EXISTS spark_catalog.db.time_travel_example_2 (
order_number int,
product_code string
)
USING iceberg
OPTIONS ('format-version'='2')
-- Insert initial data into the table
INSERT INTO spark_catalog.db.time_travel_example_2 VALUES (2, 'Venus');
-- Alter table to add a new column
ALTER TABLE spark_catalog.db.time_travel_example_2 ADD COLUMN (price double);
ts = now();
-- Query the table at a current moment but using timestamp syntax
SELECT * FROM spark_catalog.db.time_travel_example_2 TIMESTAMP AS OF ts;
+------------+------------+
|order_number|product_code|
+------------+------------+
| 2| Venus|
+------------+------------+
-- Query the table at a current moment
SELECT * FROM spark_catalog.db.time_travel_example_2;
+------------+------------+-----+
|order_number|product_code|price|
+------------+------------+-----+
| 2| Venus| NULL|
+------------+------------+-----+يحدث هذا لأن ALTER TABLE لا يُنشئ لقطة جديدة، لكن بالنسبة إلى الجدول الحالي، يأخذ Spark قيمة schema_id من أحدث ملف البيانات الوصفية، وليس من لقطة.
السيناريو 3: الاختلافات بين المخطط التاريخي والمخططط الحالي
أما الحالة الثانية فهي أنه عند استخدام السفر عبر الزمن، لا يمكنك الوصول إلى حالة الجدول قبل كتابة أي بيانات فيه:
-- Create a table
CREATE TABLE IF NOT EXISTS spark_catalog.db.time_travel_example_3 (
order_number int,
product_code string
)
USING iceberg
OPTIONS ('format-version'='2');
ts = now();
-- Query the table at a specific timestamp
SELECT * FROM spark_catalog.db.time_travel_example_3 TIMESTAMP AS OF ts; -- Finises with error: Cannot find a snapshot older than ts.في ClickHouse، يكون السلوك متوافقًا مع Spark. يمكنك ببساطة اعتبار استعلامات Select في Spark بمثابة استعلامات Select في ClickHouse، وسيعمل الأمر بالطريقة نفسها.
تحديد موقع ملف metadata
عند استخدام محرك الجدول Iceberg في ClickHouse، يحتاج النظام إلى تحديد موقع ملف metadata.json الصحيح الذي يصف بنية جدول Iceberg. وتعمل عملية التحديد هذه على النحو التالي:
البحث عن الملفات المرشحة
- تحديد المسار مباشرةً:
- إذا قمت بتعيين
iceberg_metadata_file_path، فسيستخدم النظام هذا المسار كما هو بدمجه مع مسار دليل جدول Iceberg. - عند توفير هذا الإعداد، يتم تجاهل جميع إعدادات الاستدلال الأخرى.
- مطابقة UUID الجدول:
- إذا تم تحديد
iceberg_metadata_table_uuid، فسيقوم النظام بما يلي:- النظر فقط في ملفات
.metadata.jsonداخل دليلmetadata - تصفية الملفات التي تحتوي على حقل
table-uuidيطابق UUID الذي حددته (غير حساس لحالة الأحرف)
- النظر فقط في ملفات
- البحث الافتراضي:
- إذا لم يتم توفير أيٍّ من الإعدادين أعلاه، فستصبح جميع ملفات
.metadata.jsonداخل دليلmetadataملفات مرشحة
اختيار أحدث ملف
بعد تحديد الملفات المرشحة وفقًا للقواعد المذكورة أعلاه، يحدّد النظام الملف الأحدث بينها:
-
إذا كان
iceberg_recent_metadata_file_by_last_updated_ms_fieldمفعّلًا:- يُختار الملف الذي يحمل أكبر قيمة لـ
last-updated-ms
- يُختار الملف الذي يحمل أكبر قيمة لـ
-
بخلاف ذلك:
- يُختار الملف ذو أعلى رقم إصدار
- (يظهر الإصدار على شكل
Vفي أسماء الملفات المنسّقة بالشكلV.metadata.jsonأوV-uuid.metadata.json)
ملاحظة: جميع الإعدادات المذكورة (ما لم يُنصّ صراحةً على خلاف ذلك) هي إعدادات على مستوى المحرّك، ويجب تحديدها أثناء إنشاء الجدول كما هو موضّح أدناه:
CREATE TABLE example_table ENGINE = Iceberg(
's3://bucket/path/to/iceberg_table'
) SETTINGS iceberg_metadata_table_uuid = '6f6f6407-c6a5-465f-a808-ea8900e35a38';ملاحظة: بينما تتولى كتالوجات Iceberg عادةً تحديد البيانات الوصفية، فإن محرك الجدول Iceberg في ClickHouse يفسّر مباشرةً الملفات المخزّنة في S3 باعتبارها جداول Iceberg، لذلك من المهم فهم قواعد التحديد هذه.
ذاكرة التخزين المؤقت للبيانات
يدعم محرك الجدول Iceberg ودالة الجدول التخزين المؤقت للبيانات، تمامًا مثل أنظمة التخزين S3 وAzureBlobStorage وHDFS. راجع هنا.
ذاكرة التخزين المؤقت للبيانات الوصفية
يدعم كلٌّ من محرك الجدول ودالة الجدول Iceberg ذاكرةً مؤقتة للبيانات الوصفية تخزّن معلومات ملفات البيان وقائمة البيان وملف JSON للبيانات الوصفية. تُخزَّن هذه الذاكرة المؤقتة في الذاكرة. ويجري التحكّم في هذه الميزة عبر الإعداد use_iceberg_metadata_files_cache، وهو مفعّل افتراضيًا.
الجلب المسبق غير المتزامن للبيانات الوصفية
يمكن تمكين الجلب المسبق غير المتزامن للبيانات الوصفية عند إنشاء جدول Iceberg من خلال ضبط iceberg_metadata_async_prefetch_period_ms. إذا ضُبطت هذه القيمة على 0 (الافتراضي)، أو إذا لم تكن ذاكرة التخزين المؤقت للبيانات الوصفية مُمكّنة، فسيُعطَّل الجلب المسبق غير المتزامن.
ولتمكين هذه الميزة، يجب تحديد قيمة غير صفرية بالميلي ثانية. وتمثل هذه القيمة الفاصل الزمني بين دورات الجلب المسبق.
إذا كانت هذه الميزة مُمكّنة، فسيشغّل الخادم عملية دورية في الخلفية لعرض الكتالوج البعيد واكتشاف إصدار جديد من البيانات الوصفية. ثم سيُحلّلها ويجتاز اللقطة بشكل تكراري، جالبًا ملفات manifest list النشطة وملفات manifest. أما الملفات المتوفرة بالفعل في ذاكرة التخزين المؤقت للبيانات الوصفية، فلن يُعاد تنزيلها. وفي نهاية كل دورة جلب مسبق، تصبح أحدث لقطة للبيانات الوصفية متاحة في ذاكرة التخزين المؤقت للبيانات الوصفية.
CREATE TABLE example_table ENGINE = Iceberg(
's3://bucket/path/to/iceberg_table'
) SETTINGS
iceberg_metadata_async_prefetch_period_ms = 60000;للاستفادة إلى أقصى حد من الجلب المسبق غير المتزامن للبيانات الوصفية في عمليات القراءة، يجب تحديد المعلمة iceberg_metadata_staleness_ms كمعلمة على مستوى الاستعلام أو الجلسة. افتراضيًا (0 - غير محددة)، وفي سياق كل استعلام، سيجلب الخادم أحدث البيانات الوصفية من الكتالوج البعيد.
ومن خلال تحديد مقدار التحمّل لتقادم البيانات الوصفية، يُسمح للخادم باستخدام النسخة المخزنة مؤقتًا من لقطة البيانات الوصفية من دون استدعاء الكتالوج البعيد. إذا توفرت نسخة من البيانات الوصفية في الذاكرة المؤقتة، وكانت قد نُزِّلت ضمن نافذة التقادم المحددة، فستُستخدم لمعالجة الاستعلام.
وإلا فسيتم جلب أحدث نسخة من الكتالوج البعيد.
SELECT count() FROM icebench_table WHERE ...
SETTINGS iceberg_metadata_staleness_ms=120000ملاحظة: يعمل الجلب المسبق غير المتزامن للبيانات الوصفية ضمن ICEBERG_SCEDULE_POOL، وهي مجموعة مؤشرات ترابط على مستوى الخادم لعمليات الخلفية على جداول Iceberg النشطة. ويُتحكَّم في حجم مجموعة مؤشرات الترابط هذه بواسطة معلمة إعداد الخادم iceberg_background_schedule_pool_size (القيمة الافتراضية هي 10).
ملاحظة: التوقع الحالي هو أن يكون حجم ذاكرة التخزين المؤقت للبيانات الوصفية كافيًا للاحتفاظ بالكامل بأحدث لقطة للبيانات الوصفية لجميع الجداول النشطة، إذا كان الجلب المسبق غير المتزامن مُمكّنًا.