المناطق الزمنية
كيف يحوّل ClickHouse سلاسل DateTime
DateTime:
- إذا كان العمود معرّفًا بمنطقة زمنية (
DateTime64(9, ‘Asia/Tokyo’))، فستُعامَل قيمة السلسلة على أنها طابع زمني في تلك المنطقة الزمنية. وستصبح2026-01-01 13:00:00هي2026-01-01 04:00:00بتوقيتUTC. - إذا لم يكن للعمود تعريف لمنطقة زمنية، فستُستخدم فقط المنطقة الزمنية الخاصة بالخادم. مهم: لا يؤثر الإعداد
session_timezoneعلى ذلك. لذا، إذا كانت المنطقة الزمنية للخادم هيUTCوكانت المنطقة الزمنية للجلسة هيAmerica/Los_Angeles، فستُكتب2026-01-01 13:00:00بتوقيتUTC. - عند قراءة قيمة من عمود لا يحتوي على تعريف لمنطقة زمنية، تُستخدم
session_timezone، أو المنطقة الزمنية الخاصة بالخادم إذا لم تكن مضبوطة. ولهذا السبب، قد تتأثر قراءة الطوابع الزمنية كسلاسل نصية بـsession_timezone. لا مشكلة في ذلك، ولكن ينبغي أخذه في الاعتبار.
كتابة الطوابع الزمنية عبر المناطق الزمنية
us-west مع منطقة زمنية محلية هي UTC-8، ونحتاج إلى كتابة طابع زمني محلي 2026-01-01 02:00:00، والذي يقابل في UTC القيمة 2026-01-01 10:00:00:
- تتطلب كتابته كسلسلة نصية تحويله إلى المنطقة الزمنية للخادم أو المنطقة الزمنية للعمود.
- تتطلب كتابته باستخدام بنية زمنية أصلية في اللغة أن يكون برنامج التشغيل على علم بالمنطقة الزمنية المستهدفة، ولكن:
- لا يكون ذلك ممكنًا دائمًا
- واجهة برمجة تطبيقات برنامج التشغيل ليست مصممة جيدًا لهذا الغرض
- والطريقة الوحيدة هي توضيح التحويلات التي ستُجرى حتى يتمكن التطبيق من التعويض (أو كتابة
طابع زمني Unixكرقم)
واجهات Java وJDBC للطابع الزمني
- استخدم الفئة
Timestamp، وهي في الواقع طابع زمني Unix.- عند استخدامها مع الكائن
Calendar، يتيح ذلك إعادة تفسيرTimestampوفقًا للمنطقة الزمنية الخاصة بالتقويم. - تحتوي
Timestampعلى تقويم داخلي ليس من السهل ملاحظته.
- عند استخدامها مع الكائن
- استخدم الفئة
LocalDateTime، إذ يسهل تحويلها إلى أي منطقة زمنية، ولكن لا توجد طريقة تتيح تمرير منطقة زمنية مستهدفة. - استخدم الفئة
ZonedDateTime، التي تساعد في تحويل المنطقة الزمنية عند الكتابة إلىDateTimeمن دون منطقة زمنية (لأننا نعرف أنه يجب استخدام المنطقة الزمنية للخادم).- لكن كتابة
ZonedDateTimeفي عمود ذي منطقة زمنية محددة تتطلب من المستخدم تعويض التحويل الذي يجريه برنامج التشغيل.
- لكن كتابة
- استخدم
Longلكتابة طابع زمني Unix بالميلي ثانية. - استخدم
Stringلإجراء جميع التحويلات على جانب التطبيق (وهذا ليس عمليًا جدًا عبر البيئات المختلفة).
Date
Date وDate32 لتخزين التواريخ. ويستخدم كلا النوعين عدد الأيام منذ Epoch (1970-01-01). يستخدم Date أعدادًا موجبة من الأيام فقط، لذا ينتهي نطاقه عند 2149-06-06. أما Date32 فيتعامل مع أعداد سالبة من الأيام لتغطية التواريخ السابقة لـ 1970-01-01، لكن نطاقه أصغر (من 1900-01-01 إلى 2100-01-01، حيث تمثل القيمة 0 التاريخ 1970-01-01). ويتعامل ClickHouse مع 2026-01-01 على أنه 2026-01-01 في أي منطقة زمنية، ولا توجد معلمة منطقة زمنية في تعريفات الأعمدة.
استخدام java.time.LocalDate
java.time.LocalDate الأنسب لتمثيل قيم التاريخ. يستخدم العميل هذه الفئة لتخزين قيمة أعمدة Date وDate32 (عند القراءة: LocalDate.ofEpochDay((long)readUnsignedShortLE())).
نوصي باستخدام java.time.LocalDate لأنها لا تتأثر بتحويلات المناطق الزمنية، وهي جزء من واجهة برمجة تطبيقات الوقت والتاريخ الحديثة.
استخدام java.sql.Date
LocalDate في Java 8. وقبل ذلك، كان java.sql.Date يُستخدم لكتابة التواريخ وقراءتها. وداخليًا، تمثل هذه الفئة طبقة تغليف حول لحظة زمنية (أي قيمة وقت تمثل نقطة مطلقة في الزمن). لذلك، تُرجع toString() تاريخًا مختلفًا بحسب المنطقة الزمنية التي تعمل فيها JVM. وهذا يفرض على برنامج تشغيل إنشاء القيم بعناية، كما يتطلب من المستخدم أن يكون على دراية بذلك.
إعادة التفسير المستندة إلى التقويم
java.sql.ResultSet طريقة لجلب قيم التاريخ تقبل Calendar، وهناك طريقة مماثلة في java.sql.PreparedStatement. صُمِّم ذلك لإتاحة إعادة تفسير برنامج تشغيل JDBC لقيمة تاريخ في منطقة زمنية المحددة. على سبيل المثال، تحتوي قاعدة البيانات على القيمة 2026-01-01، لكن التطبيق يريد عرض هذا التاريخ على أنه منتصف الليل في Tokyo. وهذا يعني أن الكائن java.sql.Date المُعاد سيُسنَد إليه توقيت زمني محدد، وعند تحويله إلى منطقة زمنية المحلية قد يصبح تاريخًا مختلفًا بسبب فرق التوقيت. ويمكن تحقيق الأمر نفسه باستخدام LocalDate عبر java.time.LocalDate#atStartOfDay(java.time.ZoneId).
يعيد ClickHouse برنامج تشغيل JDBC دائمًا كائن java.sql.Date يشير إلى التاريخ المحلي عند منتصف الليل. وبعبارة أخرى، إذا كان التاريخ هو 2026-01-01، فنحن نقصد 2026-01-01 12:00 AM في منطقة زمنية الخاصة بـ JVM (وهو السلوك نفسه في PostgreSQL وMariaDB JDBC drivers).
Time
’6:30’ تبقى نفسها أينما قُرئت.
أنواع Time في ClickHouse
Time وTime64 في 25.6. قبل ذلك، كان يُستخدم بدلًا منهما نوعا الطابع الزمني DateTime وDateTime64 (وسنناقشهما لاحقًا في هذا الدليل). يُخزَّن Time كعدد صحيح من 32 بت يمثّل عدد الثواني، ويقع ضمن النطاق [-999:59:59, 999:59:59]. ويُرمَّز Time64 على أنه Decimal64 غير موقَّع، ويخزّن وحدات زمنية مختلفة حسب الدقة. والخيارات الشائعة هي 3 (مللي ثانية) و6 (ميكروثانية) و9 (نانوثانية). ويتراوح نطاق قيمة الدقة بين [0, 9].
ربط الأنواع في Java
Time وTime64 ويخزّنهما بصيغة LocalDateTime. ويتم ذلك لدعم النطاق الزمني السالب (إذ إن LocalTime لا يدعمه). في هذه الحالة، يكون جزء التاريخ هو تاريخ Epoch 1970-01-01، لذا ستكون القيم السالبة سابقة لهذا التاريخ.
يُنفَّذ الدعم الأساسي لأنواع الوقت باستخدام LocalTime (عندما تكون القيمة ضمن يوم واحد) وDuration للاستفادة من النطاق الكامل للقيم. ويمكن استخدام LocalDateTime للقراءة فقط.
استخدام java.sql.Time
java.sql.Time على نطاق LocalTime. داخليًا، يُحوَّل java.sql.Time إلى قيمة نصية حرفية. ويمكن تغيير هذه القيمة باستخدام المعلمة Calendar مع PreparedStatement#setTime().
الدالة toTime
- تتطلب
toTimeدائمًا النوعDateأوDateTimeأو نوعًا مشابهًا آخر. ولا تقبل السلاسل النصية. المشكلة ذات الصلة: https://github.com/ClickHouse/ClickHouse/issues/89896 - لها اسم مستعار هو
toTimeWithFixedDate. - توجد مشكلة متعلقة بالمنطقة الزمنية: https://github.com/ClickHouse/ClickHouse/pull/90310
الطابع الزمني
1970-01-01 00:00:00 UTC (ويمثّل العدد السالب من الثواني طابعًا زمنيًا يسبق زمن يونكس، بينما يمثّل العدد الموجب طابعًا زمنيًا يقع بعده). يسهل حساب هذا التمثيل والتعامل معه إذا كان المستخدم ضمن المنطقة الزمنية UTC أو يستخدمها بدلًا من منطقته الزمنية المحلية.
أنواع الطوابع الزمنية في ClickHouse
DateTime (عدد صحيح 32-بت، وتكون الدقة دائمًا بالثواني) وDateTime64 (عدد صحيح 64-بت، وتعتمد الدقة على التعريف). تُخزَّن القيم دائمًا كطوابع زمنية بتوقيت UTC. وهذا يعني أنه عند تمثيلها كأرقام، لا يُجرى أي تحويل للمنطقة الزمنية.
التمثيل النصي وسلوك المنطقة الزمنية
- إذا لم تُحدَّد منطقة زمنية في تعريف العمود، وتم تمرير سلسلة نصية عند الكتابة، فستُحوَّل من المنطقة الزمنية الخاصة بالخادم إلى رقم طابع زمني بتوقيت UTC. وعند قراءة قيمة من هذا العمود، ستُحوَّل من طابع زمني UTC إلى قيمة طابع زمني حرفية باستخدام المنطقة الزمنية الخاصة بالخادم أو الجلسة (ويُطبَّق نهج مماثل على قيم الطابع الزمني الحرفية في التعبيرات عندما لا تكون المنطقة الزمنية محددة صراحةً).
- إذا كانت المنطقة الزمنية محددة في تعريف العمود، فستُستخدم هذه المنطقة الزمنية وحدها في جميع التحويلات النصية. وهذا يتعارض مع المنطق المتبع عند عدم تحديد منطقة زمنية، لذا يتطلب الأمر فهمًا جيدًا لكيفية كتابة البيانات لكل عمود في الاستعلام.
- إذا تم تمرير تاريخ كسلسلة نصية بتنسيق يتضمن منطقة زمنية، فستكون هناك حاجة إلى دالة تحويل. وعادةً ما يُستخدم
parseDateTimeBestEffort.
كيفية تعامل برنامج تشغيل JDBC مع الطوابع الزمنية
DateTime وDateTime64 وتُخزَّنان لدى العميل بصيغة java.time.ZonedDateTime، مما يساعد على تحويل هذه القيم إلى أي منطقة زمنية أخرى (مع الاحتفاظ بمعلومات المنطقة الزمنية).
من الأخطاء الشائعة عند استخدام toDateTime64
toDateTime64 يستخدم المنطقة الزمنية للخادم ولا يأخذ المنطقة الزمنية للمصدر في الحسبان.
جداول التحويل
Date على أنها java.sql.Timestamp لعدم وجود جزء زمني فيها.
لا يحوّل برنامج التشغيل القيم الصحيحة إلى أي قيمة من قيم التاريخ/الوقت. سيؤدي استدعاء pstmt.setLong("timestamp", 1772132359L) إلى كتابة 1772132359 كرقم في الخادم، وسيُعامَل عندها على أنه
طابع زمني Unix بتوقيت UTC وبوحدة الثواني.
كتابة القيم باستخدام PreparedStatement#setObject
PreparedStatement#setObject(column, value):
يجب اعتبار نوع العمود غير معروف. ويعود إلى التطبيق تحديد ما يجب تمريره إلى العبارة المُحضَّرة.
قراءة القيم باستخدام ResultSet#getObject
ResultSet#getObject(column, class):
استخدام الطرائق المعتمدة على التقويم
ResultSet#getTime(column, calendar) وResultSet#getDate(column, calendar) إذا كانت القيم قد خُزّنت باستخدام PreparedStatement#setTime(param, value, calendar) وPreparedStatement#setDate(param, value, calendar)، على التوالي.