Nguyên tắc: lưu UTC, chuyển đổi ở biên. Trong DB dùng timestamptz (Postgres lưu về UTC), API trả ISO 8601 có offset (2026-08-06T14:30:00+07:00), client hiển thị theo múi giờ của người xem bằng Intl.DateTimeFormat.
Dùng timestamp (không tz) là lỗi hay gặp nhất: nó lưu một chuỗi ngày giờ không mang thông tin múi giờ, nên hai server đặt TZ khác nhau sẽ hiểu khác nhau.
Báo cáo theo ngày Việt Nam: đừng cắt theo ngày UTC rồi hiểu nhầm là ngày VN — lệch 7 tiếng, đơn đặt lúc 23h ngày 5 sẽ rơi vào báo cáo ngày 6.
SELECT (created_at AT TIME ZONE 'Asia/Ho_Chi_Minh')::date AS day, count(*)
FROM orders GROUP BY 1;Hàm AT TIME ZONE trong GROUP BY làm mất index trên created_at; với bảng lớn thì lọc khoảng thời gian bằng mốc UTC đã tính sẵn (created_at >= $from AND created_at < $to) rồi mới gom nhóm.
Vài lưu ý bị hỏi vặn:
- Lưu tên vùng IANA (Asia/Ho_Chi_Minh), không lưu offset +07:00. Offset của nhiều vùng đổi theo mùa, tên vùng thì bền.
- Sự kiện tương lai (lịch hẹn, lịch chạy job) nên lưu giờ địa phương + tên vùng, vì quy định giờ có thể thay đổi trước khi sự kiện diễn ra.
- Việt Nam không có DST nên dễ chủ quan; hệ thống có user nước ngoài thì phải kiểm thử cả mốc chuyển DST.