TIMESTAMPTZ বনাম TIMESTAMP: PostgreSQL ড্যাশবোর্ড ও API
PostgreSQL-এ TIMESTAMPTZ বনাম TIMESTAMP: আপনি কোন টাইপ বাছেন তা ড্যাশবোর্ড, API রেসপন্স, টাইম জোন রূপান্তর এবং ডে-লাইট সেভিং টাইম বাগগুলোর উপর কীভাবে প্রভাব ফেলে।

প্রকৃত সমস্যা: এক স্মৃতি, অনেক ব্যাখ্যা
একটি ইভেন্ট একবার ঘটলেও সেটার রিপোর্ট অনেকভাবে শো হতে পারে। ডাটাবেস একটি মান সংরক্ষণ করে, একটি API এটিকে সিরিয়ালাইজ করে, একটি ড্যাশবোর্ড সেটিকে গ্রুপ করে, এবং প্রত্যেক ব্যবহারকারী তাদের নিজস্ব টাইম জোনে দেখে। কোনো স্তর যদি ভিন্ন অনুমান করে, একই সারি দুইটি ভিন্ন মুহূর্তের মতো দেখাতে পারে।
এই কারণেই TIMESTAMPTZ বনাম TIMESTAMP শুধু টাইপের পছন্দ নয়। এটা ঠিক করে যে যে ভ্যalueটি সংরক্ষিত আছে সেটা কি একটি নির্দিষ্ট মুহূর্তকে বোঝায়, নাকি একটি স্থানীয় ঘড়ির সময় যা নির্দিষ্ট স্থানের বাইরে অর্থহীন।
প্রথমে সাধারণত যা ভেঙে যায়: একটি সেলস ড্যাশবোর্ড নিউইয়র্ক ও বার্লিনে আলাদা দৈনিক মোট দেখায়। একটি ঘন্টাভিত্তিক চার্ট DST পরিবর্তনের সময় একটি ঘন্টা মিসিং বা ডুপ্লিকেট দেখায়। একটি অডিট লগ অর্ডারহীন দেখায় কারণ দুই সিস্টেম একই তারিখে “সম্মত” কিন্তু প্রকৃত মুহূর্তে সম্মত নয়।
একটি সহজ মডেল আপনাকে ঝামেলা থেকে বাঁচায়:
- Storage: আপনি PostgreSQL-এ কী সংরক্ষণ করছেন এবং সেটি কী বোঝায়।
- Display: UI, এক্সপোর্ট, বা রিপোর্টে কিভাবে এটি ফরম্যাট করবেন।
- User locale: ভিউয়ারের টাইম জোন এবং ক্যালেন্ডার নিয়ম, DST সহ।
এগুলো মিশলে আপনাকে নীরব রিপোর্টিং বাগ দেয়। একটি সাপোর্ট টিম ড্যাশবোর্ড থেকে "গতকাল তৈরি হওয়া টিকেট" এক্সপোর্ট করে, তারপর এটিকে API রিপোর্টের সাথে তুলনা করে। দুটোরই যৌক্তিক মনে হতে পারে, কিন্তু একটিতে ভিউয়ারের লোকাল মিডনাইট বাউন্ডারি ব্যবহৃত হয়েছে আর অন্যটিতে UTC ব্যবহার করা হয়েছে।
লক্ষ্যটি সোজা: প্রতিটি সময় মানের জন্য দুটি পরিষ্কার সিদ্ধান্ত নিন। আপনি কী সংরক্ষণ করবেন, এবং আপনি কী দেখাবেন। সেই স্পষ্টতা আপনার ডাটা মডেল, API রেসপন্স এবং ড্যাশবোর্ড জুড়ে বহন করতে হবে যাতে সবাই একই টাইমলাইন দেখে।
TIMESTAMP এবং TIMESTAMPTZ আসলে কী বোঝায়
PostgreSQL-এ নামগুলো ভ্রాంతিকর। এগুলো দেখতে মনে হয় কি স্টোর হচ্ছে তা বর্ণনা করে, কিন্তু আসলে এগুলো PostgreSQL কীভাবে ইনপুট ব্যাখ্যা করে এবং আউটপুট ফরম্যাট করে তা বর্ণনা করে।
TIMESTAMP (অর্থাৎ timestamp without time zone) কেবল একটি ক্যালেন্ডার তারিখ এবং ঘড়ির সময়, যেমন 2026-01-29 09:00:00। কোনো টাইম জোন সংযুক্ত নেই। PostgreSQL এটি আপনার জন্য রূপান্তর করবে না। বিভিন্ন টাইম জোনের মানুষ একই TIMESTAMP পড়ে ভিন্ন বাস্তব মুহূর্ত ধরে নিতে পারে।
TIMESTAMPTZ (অর্থাৎ timestamp with time zone) একটি বাস্তব মুহূর্ত প্রতিস্থাপন করে। এটিকে একটি ইনস্ট্যান্ট হিসেবে ভাবুন। PostgreSQL অভ্যন্তরে এটিকে সাধারণীকরণ করে (কার্যত UTC-এ), তারপর এটি আপনার সেশন টাইম জোন অনুযায়ী প্রদর্শন করে।
বেশিরভাগ বিস্ময়ের পিছনের আচরণ হলো:
- On input: PostgreSQL
TIMESTAMPTZমানগুলোকে একটি একক তুলনাযোগ্য মুহূর্তে রূপান্তর করে। - On output: PostgreSQL সেই মুহূর্তকে কারেন্ট সেশন টাইম জোন ব্যবহার করে ফরম্যাট করে।
- For
TIMESTAMP: ইনপুট বা আউটপুটে কোনো স্বয়ংক্রিয় রূপান্তর ঘটে না।
একটি ছোট উদাহরণ পার্থক্য দেখায়। ধরুন আপনার অ্যাপ ব্যবহারকারীর থেকে 2026-03-08 02:30 পায়। যদি আপনি এটি একটি TIMESTAMP কলামে ইনসার্ট করেন, PostgreSQL ঠিক সেই ওয়াল-ক্লক মানটি সংরক্ষণ করে। যদি ঐ লোকাল সময়টি DST জাম্পের কারণে বিদ্যমান না থাকে, আপনি রিপোর্টিং ভাঙা পর্যন্ত তা লক্ষ্য করতে নাও পারেন।
আপনি যদি TIMESTAMPTZ-এ ইনসার্ট করেন, PostgreSQL-কে মানটি ব্যাখ্যা করার জন্য একটি টাইম জোন দরকার। যদি আপনি 2026-03-08 02:30 America/New_York দেন, PostgreSQL এটিকে একটি ইনস্ট্যান্টে রূপান্তর করে (বা নিয়ম এবং নির্দিষ্ট মানের উপর নির্ভর করে ত্রুটি দেয়)। পরে, লন্ডনে থাকা একটি ড্যাশবোর্ড ভিন্ন লোকাল ক্লক টাইম দেখাবে, কিন্তু সেটা একই মুহূর্ত।
একটি সাধারণ ভুল ধারণা: মানুষ “with time zone” দেখে ধরে PostgreSQL মূল টাইম জোন লেবেলটি সংরক্ষণ করবে। করে না। PostgreSQL মুহূর্তটি সংরক্ষণ করে, লেবেল নয়। যদি আপনি ব্যবহারকারীর মূল টাইম জোন দেখাতে চান (উদাহরণ: “কাস্টমারের লোকাল টাইমে দেখান”), তাহলে জোনটি আলাদাভাবে টেক্সট ফিল্ডে সংরক্ষণ করুন।
সেশন টাইম জোন: অনেক বিস্ময়ের পিছনের লুকানো সেটিং
PostgreSQL-এ একটি সেটিং আছে যা চুপচাপ আপনি যা দেখেন তা বদলে দেয়: সেশন টাইম জোন। একই ডেটায় কেউ দুই জন একই কোয়ারি চালালে ভিন্ন ক্লক টাইম পেতে পারে কারণ তাদের সেশনগুলো ভিন্ন টাইম জোন ব্যবহার করে।
এটি প্রধানত TIMESTAMPTZ-কে প্রভাবিত করে। PostgreSQL একটি আপাতত্ মুহূর্ত সংরক্ষণ করে, তারপর সেটি সেশন টাইম জোনে প্রদর্শন করে। TIMESTAMP (টাইম জোন ছাড়া) মানটি সরল ক্যালেন্ডার সময় হিসেবে আচরণ করে; ডিসপ্লের জন্য এটি শিফট করে না, কিন্তু সেশন টাইম জোন আপনাকে কষ্ট দিতে পারে যখন আপনি এটিকে TIMESTAMPTZ-এ রূপান্তর করেন বা টাইম-জোন-সচেতন মানের সাথে তুলনা করেন।
সেশন টাইম জোন প্রায়ই লক্ষ্য ছাড়াই সেট হয়: অ্যাপ্লিকেশন স্টার্টআপ কনফিগ, ড্রাইভার প্যারামিটার, কানেকশন পুলের পুরানো সেশন পুনরায় ব্যবহার, BI টুলগুলোর নিজস্ব ডিফল্ট, ETL জবগুলো সার্ভারের লোকাল সেটিং উত্তরাধিকার সূত্রে পাওয়া, বা ম্যানুয়াল SQL কনসোল যেখানে আপনার ল্যাপটপের পছন্দ থাকে।
দলগুলো কিভাবে স্ববিবাদে পড়ে তা এখানে দেখা যায়। ধরুন একটি ইভেন্ট TIMESTAMPTZ কলামে 2026-03-08 01:30:00+00 হিসেবে সংরক্ষিত। একটি ড্যাশবোর্ড সেশন America/Los_Angeles-এ সেট থাকলে এটি পূর্ববর্তী সন্ধ্যার লোকাল টাইম দেখাবে, আর একটি API সেশন UTC-তে সেট থাকলে ভিন্ন ক্লক টাইম দেখাবে। যদি একটি চার্ট সেশন-লোকাল দিনের ব্যবহারে গ্রুপ করে, আপনি বিভিন্ন দৈনিক মোট পেতে পারেন।
-- Make your output consistent for a reporting job
SET TIME ZONE 'UTC';
SELECT created_at, date_trunc('day', created_at) AS day_bucket
FROM events;
যেকোনো কিছু যা রিপোর্ট বা API রেসপন্স তৈরি করে, টাইম জোনটি স্পষ্ট করুন। কানেক্টে সেট করুন (অথবা প্রথমেই SET TIME ZONE রান করুন), মেশিন আউটপুটের জন্য একটি স্ট্যান্ডার্ড নির্বাচন করুন (প্রায়ই UTC), এবং “লোকাল বিজনেস টাইম” রিপোর্টগুলোর জন্য জব-এর ভিতরেই বিজনেস জোন সেট করুন, কারো ল্যাপটপ না। যদি আপনি পুলড কানেকশন ব্যবহার করেন, একটি কানেকশন চেক আউট হলে সেশন সেটিংস রিসেট করুন।
ড্যাশবোর্ড কিভাবে ভেঙে যায়: গ্রুপিং, বালতি, এবং DST গ্যাপ
ড্যাশবোর্ডগুলো সহজ মনে হয়: দিনে অর্ডার গণনা করুন, প্রতি ঘন্টায় সাইনআপ দেখান, সপ্তাহের ভিত্তিতে তুলনা করুন। সমস্যা শুরু হয় যখন ডাটাবেস একটি “মুহূর্ত” সংরক্ষণ করে কিন্তু চার্ট সেটিকে অনেক বিভিন্ন “দিনে” পরিণত করে, যে কেউ দেখুক না কেন।
আপনি যদি ভিউয়ারের লোকাল টাইম জোন ব্যবহার করে দিন অনুযায়ী গ্রুপ করে থাকেন, একই ইভেন্ট দুজনের কাছে ভিন্ন তারিখে দেখা যাবে। লস অ্যাঞ্জেলেসে 23:30 এ দেয়া অর্ডার বার্লিনে ইতোমধ্যে “আগামীকাল”। আর যদি আপনার SQL DATE(created_at)-এ গ্রুপ করে একটি সাধারণ TIMESTAMP-এ, আপনি আসলে একটি প্রকৃত মুহূর্তের ভিত্তিতে গ্রুপ করছেন না। আপনি একটি ওয়াল-ক্লক রিডিং দিয়ে গ্রুপ করছেন যেটার সাথে কোনো টাইম জোন সংযুক্ত নেই।
ঘন্টাভিত্তিক চার্টরা DST-র সময় আরও জটিল হয়। বসন্তে এক লোকাল ঘন্টা কখনো ঘটে না, তাই চার্টে গ্যাপ দেখা যেতে পারে। শরতে এক লোকাল ঘন্টা দুইবার ঘটে, তাই আপনি স্পাইক বা ডবল বালতি পেতে পারেন যদি আপনার কোয়েরি এবং ড্যাশবোর্ড যে 01:30 বোঝায় তাতে অসম্মতি থাকে।
একটি ব্যবহারিক প্রশ্ন সাহায্য করে: আপনি কি প্রকৃত মুহূর্তগুলো চার্ট করছেন (রূপান্তর নিরাপদ), নাকি একটি লোকাল সময়সূচি (রূপান্তর করা চলবে না)? ড্যাশবোর্ডগুলো প্রায়ই প্রকৃত মুহূর্তই চায়।
কখন UTC বনাম বিজনেস টাইম জোনে গ্রুপ করবেন
একটি নিয়ম নির্বাচন করুন এবং তা প্রতিটি স্থানে প্রয়োগ করুন (SQL, API, BI টুল), নচেৎ মোটগুলো ঘোলে যাবে।
গ্লোবাল, কনসিস্টেন্ট সিরিজের জন্য UTC-তে গ্রুপ করুন (সিস্টেম হেলথ, API ট্র্যাফিক, গ্লোবাল সাইনআপ)। “বিজনেস ডে” যদি আইনি বা অপারেশনাল অর্থ বহন করে তাহলে বিজনেস টাইম জোনে গ্রুপ করুন (স্টোর দিন, সাপোর্ট SLA, ফাইনান্স ক্লোজ)। কেবল ব্যক্তিগতকরণের কারণেই ভিউয়ারের টাইম জোনে গ্রুপ করুন (পার্সোনাল অ্যাক্টিভিটি ফিড) যখন তুলনাযোগ্যতা কম গুরুত্বপূর্ণ।
বিসনেস ডে গ্রুপিংয়ের জন্য একটি প্যাটার্ন:
SELECT date_trunc('day', created_at AT TIME ZONE 'America/New_York') AS business_day,
count(*)
FROM orders
GROUP BY 1
ORDER BY 1;
বিভ্রান্তি রোধ করা লেবেল
জরা সংখ্যা কাঁপলেই মানুষ চার্টে বিশ্বাস হারায়। UI-তে নিয়মটি স্পষ্টভাবে লেবেল দিন: “Daily orders (America/New_York)” বা “Hourly events (UTC)”。 এক্সপোর্ট এবং API-তেও একই নিয়ম ব্যবহার করুন।
রিপোর্টিং এবং API-র জন্য একটি সহজ নিয়ম সরঞ্জাম
নির্ধারণ করুন আপনি কি একটি মুহূর্ত সংরক্ষণ করছেন নাকি একটি লোকাল ঘড়ির পাঠ্য রক্ষণ করছেন। এই দুইটি মিশালে ড্যাশবোর্ড এবং API-এর মধ্যে অসম্মতি শুরু হয়।
একটি নিয়ম সেট যা রিপোর্টিং পূর্বানুমানযোগ্য রাখে:
- বাস্তব-জগত ইভেন্টগুলোকে ইনস্ট্যান্ট হিসেবে
TIMESTAMPTZ-এ সংরক্ষণ করুন, এবং UTC-কে সত্যীর উৎস হিসেবে বিবেচনা করুন। - “বিজনেস” ধারণাগুলো যেমন “বিলিং ডে” আলাদাভাবে
DATEহিসেবে সংরক্ষণ করুন (অথবা যদি সত্যিই ওয়াল-ঘড়ির সময় ভাবে রাখেন তাহলে লোকাল-টাইম ফিল্ড)। - API-তে টাইমস্ট্যাম্প ISO 8601-এ রিটার্ন করুন এবং সঙ্গতিপূর্ণ থাকুন: সবসময় একটি অফসেট অন্তর্ভুক্ত করুন (যেমন
+02:00) অথবা সবসময় UTC-এZব্যবহার করুন। - রূপান্তর প্রান্তে (UI ও রিপোর্টিং লেয়ার) করুন। ডাটাবেস লজিক এবং ব্যাকগ্রাউন্ড জবগুলোর ভিতরে বারবার রূপান্তর এড়িয়ে চলুন।
কেন এটা টেকসই: ড্যাশবোর্ড বালতি করে, তুলনা করে। যদি আপনি ইনস্ট্যান্টগুলো (TIMESTAMPTZ) সংরক্ষণ করেন, PostgreSQL ক্রমান্বয়ে ইভেন্টগুলো অর্ডার ও ফিল্টার করতে পারে এমনকি DST-শিফটে। তারপর আপনি সিদ্ধান্ত নেবেন কিভাবে দেখাবেন বা গ্রুপ করবেন। যদি আপনি লোকাল ঘড়ির সময় (TIMESTAMP) সংরক্ষণ করেন এবং টাইম জোন না রাখেন, PostgreSQL এর কাছে মানের অর্থ জানা থাকবে না, ফলে সেশন টাইম জোন বদলে গেলে গ্রুপিংও বদলে যাবে।
“লোকাল বিজনেস ডে” আলাদাভাবে রাখুন কারণ সেগুলো ইনস্ট্যান্ট নয়। “2026-03-08-এ ডেলিভারি” একটি তারিখ সিদ্ধান্ত, মুহূর্ত নয়। যদি আপনি এটিকে একটি টাইমস্ট্যাম্পে জোর করে রাখেন, DST দিনের ফলে মিসিং বা ডুপ্লিকেট লোকাল ঘন্টাগুলি তৈরি হতে পারে, যা পরে গ্যাপ বা স্পাইক হয়ে ফিরে আসে।
ধাপে ধাপে: প্রতিটি সময় মানের জন্য সঠিক টাইপ নির্বাচন
TIMESTAMPTZ বনাম TIMESTAMP বেছে নেয়া একটি প্রশ্ন দিয়ে শুরু হয়: এই মান কি একটি বাস্তব মুহূর্ত বর্ণনা করে, না কি একটি লোকাল সময় যা ঠিক তেমনই থাকতে হবে?
1) বাস্তব ইভেন্ট আলাদা করুন নির্ধারিত লোকাল সময় থেকে
আপনার কলামগুলোর দ্রুত ইনভেন্টরি তৈরি করুন।
বাস্তব ইভেন্ট (ক্লিকস, পেমেন্ট, লগইন, শিপমেন্ট, সেন্সর রিডিং, সাপোর্ট মেসেজ) সাধারণত TIMESTAMPTZ-এ সংরক্ষণ করা উচিত। আপনি একটি অদ্বিতীয় ইনস্ট্যান্ট চান, এমনকি মানুষ ভিন্ন টাইম জোন থেকে দেখেন।
নির্ধারিত লোকাল সময় ভিন্ন: “স্টোর খুলবে 09:00”, “পিকআপ উইন্ডো 16:00 থেকে 18:00”, “বিলিং 1 তারিখে 10:00 লোকাল সময়ে চলে” — এগুলো প্রায়শই TIMESTAMP এবং আলাদা টাইম জোন ফিল্ড সহ ভালো কারণ উদ্দেশ্য লোকেশনের ওয়াল-ক্লকের সাথে সংযুক্ত।
2) একটি স্ট্যান্ডার্ড বেছে নিন এবং তা লিখে রাখুন
প্রায়শই ডিফল্ট: ইভেন্ট টাইম UTC-তে সংরক্ষণ করুন, ব্যবহারকারীর টাইম জোনে প্রদর্শন করুন। এটি স্কিমা নোট, API ডকস, এবং ড্যাশবোর্ড বিবরণে ডকুমেন্ট করুন। এছাড়াও নির্ধারণ করে রাখুন “বিজনেস ডে” কি মানে (UTC দিন, বিজনেস জোন দিন, বা ভিউয়ার-লোকাল দিন), কারণ এই সিদ্ধান্ত দৈনিক রিপোর্টিং চালিত করে।
একটি সংক্ষিপ্ত চেকলিস্ট যা বাস্তবে কাজ করে:
- প্রতিটি টাইম কলামকে “event instant” বা “local schedule” হিসেবে ট্যাগ করুন।
- ইভেন্ট ইনস্ট্যান্টগুলোর ডিফল্ট
TIMESTAMPTZএবং UTC-এ রাখুন। - স্কিমা পরিবর্তন করলে ব্যাকফিল সাবধানে করুন এবং নমুনা সারি হাতে যাচাই করুন।
- API ফরম্যাট স্ট্যান্ডার্ড করুন (ইনস্ট্যান্টগুলোর জন্য সবসময়
Zবা অফসেট অন্তর্ভুক্ত করুন)। - ETL জব, BI কানেক্টর, এবং ব্যাকগ্রাউন্ড ওয়ার্কার-এ সেশন টাইম জোন স্পষ্টভাবে সেট করুন।
“কনভার্ট এবং ব্যাকফিল” কাজ নিয়ে সাবধানে থাকুন। একটি কলামের টাইপ বদললে পুরনো মানগুলো যদি ভিন্ন সেশন টাইম জোনে ব্যাখ্যা করা হয়ে থাকে তাহলে অর্থ চুপচাপ বদলে যেতে পারে।
সাধারণ ভুল যা এক দিনের ব্যত্যয় এবং DST বাগ সৃষ্টি করে
বেশিরভাগ টাইম বাগ “PostgreSQL অদ্ভুত” থেকে আসে না। এগুলো আসে সঠিক-দেখা মানটি ভুল মানে সংরক্ষণ থেকে, তারপর বিভিন্ন স্তর অনুপস্থিত প্রেক্ষাপট অনুমান করে।
ভুল 1: ওয়াল-ক্লক টাইমকে আপেক্ষিকভাবে সংরক্ষণ করা
একটি সাধারণ ফাঁদ হলো লোকাল ওয়াল-ক্লক টাইম (যেমন বার্লিনে “2026-03-29 09:00”) TIMESTAMPTZ-এ সংরক্ষণ করা। PostgreSQL এটিকে একটি ইনস্ট্যান্ট হিসেবে আচরণ করে এবং সেশন টাইম জোনের উপর ভিত্তি করে রূপান্তর করে। যদি উদ্দেশ্য ছিল “সবসময় লোকালভাবে সকাল 9টা”, আপনি সেটি হারাতে পারবেন।
অ্যাপয়েন্টমেন্টগুলোর জন্য লোকাল সময় TIMESTAMP হিসেবে এবং আলাদা টাইম জোন (অথবা লোকেশন) ফিল্ড হিসেবে সংরক্ষণ করুন। বাস্তবে ঘটেছে এমন ইভেন্টগুলোর জন্য TIMESTAMPTZ ব্যবহার করুন।
ভুল 2: ভিন্ন পরিবেশে ভিন্ন অনুমান
আপনার ল্যাপটপ, স্টেজিং, এবং প্রোডাকশন একই টাইম জোন শেয়ার নাও করতে পারে। একটি পরিবেশ UTC-তে চলে, অন্যটি লোকাল টাইমে, আর “দিন অনুযায়ী গ্রুপ” রিপোর্ট ভিন্ন হতে শুরু করে। ডাটা বদলায়নি, সেশন সেটিং পরিবর্তিত হয়েছে।
ভুল 3: টাইম ফাংশন ব্যবহার করে তাদের প্রতিশ্রুতি না জানা
now() এবং current_timestamp একটি লেনদেনের মধ্যে স্থির থাকে। clock_timestamp() প্রত্যেক কলেই পরিবর্তিত হয়। যদি আপনি এক ট্রানজেকশনের ভিতরে একাধিক স্থানে টাইমস্ট্যাম্প তৈরি করেন এবং এই ফাংশনগুলো মিশিয়ে দেন, অর্ডারিং এবং সময়কাল অদ্ভুত দেখাতে পারে।
ভুল 4: দ্বিগুণ (বা শূন্য) রূপান্তর করা
একটি সাধারণ API বাগ: অ্যাপ একটি লোকাল সময় UTC-তে রূপান্তর করে, সেটি একটি নিরপেক্ষ স্ট্রিং হিসেবে পাঠায়, তারপর ডাটাবেস সেশন আবার রূপান্তর করে কারণ এটি ইনপুটকে লোকাল সময় ধরে নেয়। উল্টোটা ও ঘটে: অ্যাপ লোকাল সময় পাঠায় কিন্তু সেটি Z (UTC) হিসেবে লেবেল করে, ফলে রেন্ডার করলে SHIFT হয়ে যায়।
ভুল 5: নির্দিষ্ট টাইম জোন না বলে তারিখ দ্বারা গ্রুপ করা
“দৈনিক মোট” কোন দিন সীমা বোঝায় তা নির্ভর করে কোন টাইম জোন আপনি মানে করছেন। যদি আপনি date(created_at) দিয়ে গ্রুপ করেন একটি TIMESTAMPTZ-এ, ফলাফল সেশন টাইম জোন অনুসরণ করবে। গভীর রাতের ইভেন্টগুলো আগের বা পরের দিনে চলে যেতে পারে।
একটি ড্যাশবোর্ড বা API শিপ করার আগে বেসিকগুলো স্যানিটি-চেক করুন: প্রতিটি চার্টের জন্য একটি রিপোর্টিং টাইম জোন নির্বাচন করুন এবং তা সঙ্গতিপূর্ণভাবে প্রয়োগ করুন, API পে-লোডে অফসেট (বা Z) অন্তর্ভুক্ত করুন, স্টেজিং ও প্রোডাকশন টাইম জোন নীতিতে সঙ্গতি রাখুন, এবং গ্রুপিং বলতে কোন টাইম জোন বোঝায় তা স্পষ্ট করুন।
শিপ করার আগে দ্রুত চেকলিস্ট
টাইম বাগ কমই এক খারাপ কোয়েরি থেকে আসে। এগুলো ঘটে যখন স্টোরেজ, রিপোর্টিং, এবং API প্রতিটিই একটু ভিন্ন অনুমান করে।
শিপ করার আগে একটি সংক্ষিপ্ত চেকলিস্ট:
- বাস্তব-জগতের ইভেন্টগুলোর জন্য (সাইনআপ, পেমেন্ট, সেন্সর পিং) ইনস্ট্যান্ট
TIMESTAMPTZহিসেবে সংরক্ষণ করুন। - বিজনেস-লোকাল ধারণাগুলোর জন্য (বিলিং ডে, রিপোর্টিং তারিখ)
DATEবাTIMEরাখুন, একটি টাইমস্ট্যাম্প যা পরে রূপান্তর করার পরিকল্পনা করা হয়েছে তা নয়। - শিডিউল করা জব এবং রিপোর্ট রানার-এ সেশন টাইম জোন উদ্দেশ্যসহ সেট করুন।
- API রেসপন্সে একটি অফসেট বা
Zঅন্তর্ভুক্ত করুন, এবং নিশ্চিত করুন ক্লায়েন্ট তা টাইম-জোন-অ্যাওয়ার হিসাবে পার্স করে। - লক্ষ্য টাইম জোনগুলোর জন্য DST ট্রানজিশন সপ্তাহ টেস্ট করুন।
একটি দ্রুত end-to-end ভ্যালিডেশন: একটি পরিচিত এজ-কেস ইভেন্ট নিন (উদাহরণ: 2026-03-08 01:30 একটি DST-অবজার্ভিং জোনে) এবং এটিকে স্টোরেজ, কোয়েরি আউটপুট, API JSON, এবং ফাইনাল চার্ট লেবেলের মধ্য দিয়ে অনুসরণ করুন। যদি চার্ট সঠিক দিন দেখায় কিন্তু টুলটিপ ভুল ঘন্টা দেখায় (অথবা উল্টো), আপনার কাছে রূপান্তর অসামঞ্জস্য আছে।
উদাহরণ: কেন দুই দল একই দিনের সংখ্যা নিয়ে দ্বন্দ্ব করে
নিউইয়র্কে একটি সাপোর্ট টিম এবং বার্লিনে একটি ফাইন্যান্স টিম একই ড্যাশবোর্ড দেখে। ডাটাবেস সার্ভার UTC-এ চলে। সবাই তাদের সংখ্যাকে সঠিক বলা নিশ্চিত, কিন্তু “গতকাল” ভিন্ন হতে পারে কার কাছে।
ইভেন্ট: একটি কাস্টমার টিকেট নিউইয়র্কে মার্চ 10-এ 23:30 এ তৈরি হয়েছে। সেটা UTC-তে 04:30 মার্চ 11, আর বার্লিনে 05:30। এক বাস্তব মুহূর্ত, তিনটি ক্যালেন্ডার তারিখ।
যদি টিকেটের ক্রিয়েশন টাইম TIMESTAMP (নো টাইম জোন) হিসেবে রাখা হয় এবং আপনার অ্যাপ এটিকে “লোকাল” ধরছে, আপনি নীরবভাবে ইতিহাস ছাপিয়ে দিতে পারেন। নিউইয়র্ক 2026-03-10 23:30 কে নিউইয়র্ক টাইম ধরে নেবে, যখন বার্লিন সেই একই সংরক্ষিত মানকে বার্লিন টাইম ধরে নেবে। একই সারি বিভিন্ন দর্শকের জন্য বিভিন্ন দিনে পড়ে।
যদি এটি TIMESTAMPTZ হিসেবে সংরক্ষিত থাকে, PostgreSQL ইনস্ট্যান্টটি কনসিস্টেন্টভাবে স্টোর করে এবং কেবল যখন কেউ দেখবে তখন এটি রূপান্তর করে। এ কারণেই TIMESTAMPTZ বনাম TIMESTAMP রিপোর্টে “এক দিন” কী বোঝায় তা বদলে দেয়।
সমাধান হল দুটি ধারণা আলাদা করা: ইভেন্ট কখন ঘটেছে (ইনস্ট্যান্ট), এবং আপনি কোন রিপোর্টিং তারিখ ব্যবহার করতে চান।
একটি ব্যবহারিক প্যাটার্ন:
- ইভেন্ট সময়
TIMESTAMPTZহিসেবে সংরক্ষণ করুন। - রিপোর্টিং নিয়ম নির্দিষ্ট করুন: ভিউয়ার-লোকাল (পার্সোনাল ড্যাশবোর্ড) নাকি একটি বিজনেস টাইম জোন (কোম্পানি-স্তরের ফাইনান্স)।
- কুয়েরি সময় রিপোর্টিং তারিখ গণনা করুন: ইনস্ট্যান্টটিকে চাওয়া জোনে রূপান্তর করুন, তারপর তারিখ নিন।
পরবর্তী পদক্ষেপ: আপনার স্ট্যাক জুড়ে টাইম হ্যান্ডলিং স্ট্যান্ডার্ডাইজ করুন
যদি টাইম হ্যান্ডলিং লেখার মতো না থাকে, প্রতিটি নতুন রিপোর্ট একটি অনুমানমূলক খেলা হয়ে যাবে। ডাটাবেস, API, এবং ড্যাশবোর্ড জুড়ে এমন টাইম আচরণ লক্ষ্য করুন যা বিরক্তিমুক্ত এবং পূর্বানুমানযোগ্য।
একটি ছোট “টাইম কন্ট্রাক্ট” লিখুন যা তিনটি প্রশ্নের উত্তর দেয়:
- Event time standard: যদি না কোনো শক্তিশালী কারণ থাকে, ইভেন্ট ইনস্ট্যান্টগুলোকে
TIMESTAMPTZ(সাধারণত UTC) হিসেবে রাখুন। - Business time zone: রিপোর্টিংয়ের জন্য একটি জোন বেছে নিন, এবং এটিকে দিন, সপ্তাহ, মাস নির্ধারণে সঙ্গতিপূর্ণভাবে ব্যবহার করুন।
- API format: সবসময় অফসেট সহ টাইমস্ট্যাম্প পাঠান (ISO 8601
Zবা+/-HH:MM) এবং ডকুমেন্ট করুন কোন ফিল্ডগুলো “ইনস্ট্যান্ট” এবং কোনগুলো “লোকাল ওয়াল টাইম” বোঝায়।
DST শুরু ও শেষের চারপাশে ছোট টেস্ট যুক্ত করুন। এগুলো ব্যয়বহুল বাগগুলো দ্রুত ধরা দেয়। উদাহরণস্বরূপ, যাচাই করুন যে “দৈনিক মোট” একটি নির্দিষ্ট বিজনেস জোনে DST পরিবর্তনের সময় স্থির থাকে, এবং API ইনপুটগুলো যেমন 2026-11-01T01:30:00-04:00 এবং 2026-11-01T01:30:00-05:00 দুটি আলাদা ইনস্ট্যান্ট হিসেবে আচরণ করে।
মাইগ্রেশন পরিকল্পনা সাবধানে করুন। টাইপ ও অনুমান বদলালে চার্টে ইতিহাস নীরবভাবে বদলে যেতে পারে। নিরাপদ পদ্ধতি হচ্ছে একটি নতুন কলাম যোগ করা (উদাহরণ: created_at_utc TIMESTAMPTZ), পরিচিত সেশন টাইম জোনে রিভিউ করা রূপান্তর দিয়ে ব্যাকফিল করা, রিডগুলো আপডেট করা যাতে নতুন কলাম ব্যবহার করে, তারপর রাইটও আপডেট করা। পুরনো ও নতুন রিপোর্ট সাময়িকভাবে পাশাপাশি রাখুন যাতে দৈনিক সংখ্যায় যে কোনো পরিবর্তন স্পষ্ট হয়।
যদি আপনি এক জায়গায় এই “টাইম কন্ট্রাক্ট” প্রয়োগ করতে চান তাহলে একটি ইউনিফায়েড বিল্ড সেটআপ সাহায্য করে। AppMaster প্রজেক্ট থেকে ব্যাকএন্ড, ওয়েব অ্যাপ, এবং API জেনারেট করে, যা আপনার অ্যাপ বাড়তে থাকলে টাইমস্ট্যাম্প স্টোরেজ ও ডিসপ্লে নিয়মগুলো কনসিস্টেন্ট রাখতে সহজ করে।
প্রশ্নোত্তর
Use TIMESTAMPTZ for anything that happened at a real moment (signups, payments, logins, messages, sensor pings). It stores one unambiguous instant and can be safely sorted, filtered, and compared across systems. Use plain TIMESTAMP only when the value is meant to be a wall-clock time that should stay exactly as written, usually paired with a separate time zone or location field.
TIMESTAMPTZ represents a real instant in time; PostgreSQL normalizes it internally and then displays it in your session time zone. TIMESTAMP is just a date and clock time with no zone attached, so PostgreSQL won’t shift it automatically. The key difference is meaning: instant versus local wall time.
Because the session time zone controls how TIMESTAMPTZ is formatted on output and how some inputs are interpreted. Two tools can query the same row and show different clock times if one session is set to UTC and another to America/Los_Angeles. For reports and APIs, set the session time zone explicitly so results don’t depend on hidden defaults.
Because “a day” depends on a time zone boundary. If one dashboard groups by viewer-local time while another groups by UTC (or a business zone), late-night events can fall on different dates and change daily totals. Fix it by picking one grouping rule per chart (UTC or a specific business zone) and using it consistently in SQL, BI, and exports.
DST creates missing or duplicated local hours, which can produce gaps or double-counted buckets when grouping by local time. If your data represents real moments, store it as TIMESTAMPTZ and choose a clear chart time zone for bucketing. Also test the DST transition week for your target zones to catch surprises early.
No, PostgreSQL does not preserve the original time zone label with TIMESTAMPTZ; it stores the instant. When you query it, PostgreSQL displays it in the session time zone, which may differ from the user’s original zone. If you need “show it in the customer’s time zone,” store that zone separately in another column.
Return ISO 8601 timestamps that include an offset, and be consistent. A simple default is to always return UTC with Z for event instants, then let clients convert for display. Avoid sending “naive” strings like 2026-03-10 23:30:00 because clients will guess the zone differently.
Convert at the edges: store event instants as TIMESTAMPTZ, then convert to the desired zone when you display or bucket for reporting. Avoid converting back and forth inside triggers, background jobs, and ETL unless you have a clear contract. Most reporting problems come from double conversion or from mixing naive and time-zone-aware values.
Use DATE for business concepts that are truly dates, like “billing day,” “reporting date,” or “delivery date.” Use TIME (or TIMESTAMP plus a separate time zone) for schedules like “opens at 09:00 local time.” Don’t force these into TIMESTAMPTZ unless you really mean a single instant, because DST and zone changes can shift the intended meaning.
First, decide whether it’s an instant (TIMESTAMPTZ) or a local wall time (TIMESTAMP plus zone), then add a new column instead of rewriting in place. Backfill with a reviewed conversion under a known session time zone, and validate sample rows around midnight and DST boundaries. Run old and new reports side by side briefly so any shifts in totals are obvious before you remove the old column.


