طابعك الزمني كذبة
ما علمتني إياه تذكرة قطار عن تخزين الوقت في قواعد البيانات
كنت أحجز قطارًا من نيويورك إلى شيكاغو عندما أدركت لماذا أنواع الطوابع الزمنية في Postgres مربكة للغاية. أظهرت التذكرة:
- المغادرة: 8:00 صباحًا بتوقيت شرق الولايات المتحدة
- الوصول: 7:30 مساءً بتوقيت وسط الولايات المتحدة
- المدة: 11 ساعة و30 دقيقة
ثلاث طرق مختلفة للتحدث عن الوقت، كلها على نفس التذكرة. وكل واحدة منها تحتاج إلى تخزين مختلف في قاعدة البيانات.
السؤال الذي لا يسأله أحد أولاً
كلا النوعين TIMESTAMP و TIMESTAMPTZ في Postgres يشغلان بالضبط 8 بايت بنفس الدقة الميكروثانية. فلماذا يوجد نوعان على الإطلاق؟
لأن “كم الساعة؟” يعتمد كليًا على ما تحاول إخبار شخص ما به.
عندما أستقل ذلك القطار في نيويورك، أحتاج إلى معرفة أنه يغادر الساعة 8:00 صباحًا بالتوقيت الشرقي. هذا هو الرقم الموجود على ساعة المحطة الذي أحتاج إلى مطابقته. عندما تأتي صديقتي لاصطحابي في شيكاغو، تحتاج إلى معرفة أنني أصل الساعة 7:30 مساءً بالتوقيت المركزي—هذا هو الرقم الموجود على ساعتها هي. وإذا كنت أحاول معرفة ما إذا كان لدي وقت لقراءة كتابي، أحتاج إلى معرفة أن الرحلة تستغرق 11 ساعة ونصف.
نفس القطار. نفس الرحلة. ثلاثة تمثيلات مختلفة تمامًا للوقت.
ما يفعله TIMESTAMPTZ فعليًا
إليك الحيلة مع TIMESTAMPTZ—وهي ليست ما يعتقده معظم الناس. إنه لا يخزن المنطقة الزمنية. الاسم مضلل.
ما يفعله هو تحويل أي وقت تعطيه إياه إلى توقيت UTC قبل تخزينه، ثم تحويله مرة أخرى إلى المنطقة الزمنية لجلسة المستخدم عند قراءته. جزء “TZ” لا يتعلق بالتخزين، بل يتعلق بدعم التحويل.
لنقل أنك تخزن موعد مغادرة ذلك القطار. شخص في طوكيو يستعلم قاعدة البيانات ويرى موعد المغادرة بتوقيت اليابان (JST). شخص في لندن يراه بتوقيت غرينتش (GMT). الجميع ينظر إلى نفس اللحظة المطلقة، معبرًا عنها بمنطقتهم الزمنية المحددة. هذا مثالي لتسجيل الأحداث: “متى تمت معالجة هذه الدفعة؟” أو “متى حدث طلب API هذا؟”
لكن ماذا عن تذكرة القطار تلك؟ لا تريد أن يتغير موعد المغادرة لمجرد أن شخصًا ما استعلم عنها من منطقة زمنية مختلفة. القطار يغادر الساعة 8:00 صباحًا بالتوقيت الشرقي، نقطة. هذه ليست لحظة مطلقة في الزمن—إنها وعد حول ما ستظهره الساعة في محطة غراند سنترال.
تخزين ما تعنيه حقًا
لرحلة القطار تلك، تحتاج إلى تخزين أشياء مختلفة لأغراض مختلفة:
- اللحظات المطلقة (
departs_atوarrives_atكـTIMESTAMPTZ) - سياق العرض (
origin_timezoneوdestination_timezoneكنص) - المدة (
INTERVALبين اللحظتين)
الآن يمكن لتطبيقك أن يفعل ما تفعله تذكرة القطار: يعرض “المغادرة 8:00 صباحًا EST” بتحويل اللحظة المطلقة إلى المنطقة الزمنية للمنشأ، ويعرض “الوصول 7:30 مساءً CST” بتحويلها إلى المنطقة الزمنية للوجهة، ويعرض “المدة: 11 ساعة و30 دقيقة” مباشرة من الفاصل الزمني.
الشخص الذي يحجز التذكرة من طوكيو يرى نفس الأوقات المحلية في كل محطة. هذا ما يحتاج إلى معرفته.
لماذا أخطأ تطبيق تتبع الرحلات الجوية
هل لاحظت كيف تظهر بعض تطبيقات تتبع الرحلات الجوية منطقتك الزمنية أثناء الرحلة؟ كأنك فوق المحيط الأطلسي وتقول “الوقت الحالي: 4:32 مساءً GMT.” من يهتم؟ أنت لست في غرينتش، أنت على ارتفاع 38,000 قدم في مكان ما فوق المحيط.
ما الذي تريد رؤيته فعليًا:
- الوقت المنقضي منذ الإقلاع
- الوقت المتبقي حتى الوصول
- كم ستكون الساعة هناك عند هبوطك
لا شيء من هذه تحويلات للمنطقة الزمنية. الأولان هما فترات زمنية — مدد، وليس لحظات. الأخير هو تحويل للمنطقة الزمنية، ولكن إلى مكان محدد، وليس “منطقتك الزمنية الحالية.”
أرأيت؟ عمليتا حساب فاصل زمني (NOW() - actual_departure و estimated_arrival - NOW())، وتحويل واحد للمنطقة الزمنية إلى مكان محدد (AT TIME ZONE destination_timezone). منطقتك الزمنية الحالية لا دخل لها في الأمر.
عندما يكون وقت الساعة المحلية هو ما تحتاجه بالفعل
الفنادق لا تهتم باللحظات المطلقة في الزمن. إنها تهتم بقراءات الساعة في موقعها.
“تسجيل الوصول بعد الساعة 3:00 مساءً” لا يعني “تسجيل الوصول بعد 15 ساعة من منتصف الليل بتوقيت UTC.” بل يعني “عندما تشير الساعة في بهو الفندق إلى 3:00 مساءً، يمكنك تسجيل الوصول.” إذا كانت خوادمك في فرجينيا ولكن الفندق في باريس، فستظل تريد أن يتم تفعيل هذه القاعدة في الساعة 3:00 مساءً بتوقيت باريس.
النوع TIME (بدون تاريخ أو منطقة زمنية) يمثل هذا بالضبط: “قراءة على ساعة.” قم بإقرانه بحقل نصي للمنطقة الزمنية (“Europe/Paris”)، ويمكنك تطبيق سياسات الساعة المحلية بغض النظر عن مكان وجود خوادمك. ولكنك ستحتاج أيضًا إلى أعمدة من نوع TIMESTAMPTZ لتسجيل أوقات تسجيل الوصول والمغادرة الفعلية للضيوف — فهذه لحظات مطلقة يحتاجها نظامك الخلفي لتتبعها.
مشكلة التقويم
لدي تذكير متكرر مضبوط على الساعة 9:00 صباحًا: “مراجعة الأولويات اليومية.” أريد هذا التذكير في الساعة 9:00 صباحًا أينما كنت. إذا كنت مسافرًا، يجب أن ينطلق في الساعة 9:00 صباحًا بالتوقيت المحلي.
ولكن لدي أيضًا حدث في التقويم: “الاجتماع اليومي للفريق في الساعة 10:00 صباحًا بتوقيت EST.” زميلي في برلين يحتاج إلى رؤية “4:00 مساءً بتوقيت CET” لنفس الحدث. نفس الاجتماع، أوقات عرض مختلفة، لأن هذا الحدث هو لحظة مطلقة ننضم إليها جميعًا.
نوعان مختلفان من الأحداث، استراتيجيتان مختلفتان للتخزين. الاجتماع يحصل على TIMESTAMPTZ. التذكير يحصل على TIME بالإضافة إلى إعداد المنطقة الزمنية الحالية. تجنب محاولة حشر كليهما في نفس الحقل.
الأمور التي تتعطل في الإنتاج
حتى مع الأنواع الصحيحة، يمكن أن تعضك الدقة. بوستجريس يخزن الميكروثانية: 10:00:00.123456. كائن Date في جافا سكريبت يستخدم الميلي ثانية: 10:00:00.123.
لذا قد يعيد هذا الاستعلام بلا صفوف بشكل غامض:
SELECT * FROM orders WHERE created_at = '2026-01-15 10:00:00.123';قاعدة البيانات تحتوي على 10:00:00.123456 ورمزك يمرر 10:00:00.123. اعتمادًا على كيفية معالجة برنامج التشغيل الخاص بك، قد لا تتطابق.
لا تستخدم المساواة التامة للطوابع الزمنية. استخدم استعلامات النطاق، أو -الأفضل- لا تبحث عن السجلات بواسطة طابع الإنشاء الزمني على الإطلاق. استخدم قيدًا فريدًا مناسبًا أو مفتاح تكافؤ.
قواعد عملية
الافتراضي هو TIMESTAMPTZ. عندما تكون في شك، استخدم TIMESTAMPTZ. يتعامل مع النشر متعدد المناطق، التوقيت الصيفي، وتغييرات المنطقة الزمنية المستقبلية تلقائيًا. حجم التخزين هو نفس حجم TIMESTAMP، لذا لا توجد عقوبة.
خزّن السياق بشكل منفصل. إذا كنت بحاجة إلى عرض “المغادرة 8:00 صباحًا بتوقيت EST” بجانب اللحظة الفعلية، قم بتخزين كل من TIMESTAMPTZ و origin_timezone كأعمدة منفصلة. لا تحاول ترميز كل شيء في حقل واحد.
فكر في الفواصل الزمنية. الكثير من المتطلبات المتعلقة بالوقت هي في الواقع حول المدة، وليس اللحظات. “كم مضى على هذا معلقًا؟” “متى سينتهي هذا؟” استخدم عمليات INTERVAL، وليس تحويلات المنطقة الزمنية.
شغّل كل شيء بتوقيت UTC. يجب ضبط خوادمك على UTC. يجب أن تكون جلسات قاعدة البيانات الخاصة بك افتراضية على UTC. قم بالتحويل إلى المناطق الزمنية المحلية فقط عند العرض للمستخدمين، وفقط عندما تعرف أي منطقة زمنية مهمة.
اطلب معلومات المنطقة الزمنية من العملاء. إذا أرسل عميل 2026-01-15T10:00:00 بدون إزاحة، ارفضه. اطلب تنسيق ISO-8601 إما مع Z أو إزاحة صريحة مثل -05:00. لا تخمن.
فرض الإعدادات الافتراضية الجيدة
إذا كان TIMESTAMPTZ هو الإعداد الافتراضي لديك (وينبغي أن يكون كذلك)، فكر في فرضه على مستوى قاعدة البيانات. قد يبدو وجود مشغل يرفض أعمدة TIMESTAMP WITHOUT TIME ZONE أمرًا متطرفًا، لكن اكتشاف “نسيت إضافة المنطقة الزمنية” في وقت إنشاء المخطط أفضل من تصحيح الأخطاء بعد ستة أشهر عندما يضيف شخص ما جدولًا جديدًا وينسى.
ما علمتني إياه تذكرة القطار تلك
الوقت في قواعد البيانات ليس صعبًا لأن الطوابع الزمنية معقدة. إنه صعب لأننا عادةً نخزن اهتمامات متعددة في حقل واحد، أو لا نفكر فيما نحاول فعلاً عرضه للمستخدمين.
كانت تذكرة القطار تلك على صواب: وقت المغادرة في المنطقة الزمنية للمنشأ، ووقت الوصول في المنطقة الزمنية للوجهة، والمدة كشيء منفصل تمامًا. ثلاث قطع مختلفة من المعلومات، كل منها ذو معنى بطريقته الخاصة.
يمكن لقاعدة البيانات الخاصة بك أن تفعل الشيء نفسه. قم بتخزين اللحظات المطلقة كـ TIMESTAMPTZ. قم بتخزين سياق العرض (المناطق الزمنية، المواقع) كأعمدة منفصلة. استخدم أنواع INTERVAL للمدد. دع Postgres يقوم بالتحويلات عندما تحتاجها، ولكن كن صريحًا بشأن أي منطقة زمنية مهمة لأي غرض.
في معظم الأوقات، هذا يعني استخدام TIMESTAMPTZ وUTC في كل مكان، مع تحويلات المنطقة الزمنية فقط في وقت العرض. ولكن عندما تحتاج إلى أوقات الساعة الثابتة أو الجداول المتكررة، فإن أنواع TIMESTAMP أو TIME موجودة لهذا السبب تحديدًا.
المفتاح هو معرفة السؤال الذي تحاول الإجابة عليه: “متى حدث هذا؟” مقابل “في أي وقت يجب أن أكون هناك؟” مقابل “كم من الوقت سيستغرق هذا؟” كلها أسئلة مختلفة عن الوقت، وغالبًا ما تحتاج إلى استراتيجيات تخزين مختلفة.
فكر فيما يحتاج مستخدموك إلى رؤيته. ثم خزّن البيانات التي تتيح لك عرض ذلك لهم بدقة.
الموارد
- توثيق أنواع التاريخ/الوقت في PostgreSQL
- أفضل الممارسات للطوابع الزمنية في PostgreSQL
- تنسيق التاريخ والوقت ISO 8601
- قاعدة بيانات المناطق الزمنية (IANA)
- التعامل مع الطوابع الزمنية في الأنظمة الموزعة