Báo cáo lệch đúng một ngày: lần theo múi giờ, DST và định dạng ngày trong dữ liệu khách
Khi doanh thu trên dashboard sai đúng một ngày, nguyên nhân thường không nằm ở công thức, mà ở một chuỗi thời gian không ai ghi rõ thuộc múi giờ nào.
Tóm tắt nhanh
- Thiếu múi giờ thì một chuỗi thời gian chỉ là phỏng đoán: Python gọi đó là datetime naive, còn JavaScript đọc chuỗi chỉ có ngày theo UTC và chuỗi ngày-giờ theo giờ địa phương.
- PostgreSQL lưu timestamptz ở UTC và bỏ đi múi giờ gốc, nên tên múi giờ IANA phải lưu ở một cột riêng, không dùng offset cố định hay viết tắt như CST.
- Gom báo cáo theo ngày địa phương của khách, và kiểm tra riêng giờ bị lặp lại khi DST kết thúc.
Sáng thứ Hai, khách gửi một ảnh chụp màn hình. Doanh thu thứ Sáu trên dashboard bạn vừa deploy thấp hơn số trong hệ thống bán hàng của họ, còn doanh thu thứ Bảy lại cao hơn. Tổng cả tuần vẫn khớp, chỉ có một phần đơn hàng ban đêm bị đẩy sang ngày hôm sau.
Gặp kiểu lỗi này, phản xạ đầu tiên của nhiều người là mở công thức SQL ra soát. Thường thì công thức đúng. Lỗi nằm ở chỗ một chuỗi như 03/04/2024 23:30 đã đi qua ba hệ thống, và hệ thống nào cũng tự đoán nó thuộc múi giờ nào.
Với FDE, chuyện này không hiếm. Bạn là người đầu tiên chạm vào dữ liệu thật của khách: file xuất từ ERP, log máy chủ đặt ở một châu lục khác, bảng tính ai đó gõ tay.
Nếu bạn làm đúng phần thời gian ngay từ đầu, khách sẽ tin con số của bạn. Nếu làm sai, mọi cuộc họp sau đó sẽ xoay quanh câu hỏi “sao số này lệch”.
Vì sao một chuỗi thời gian lại mơ hồ?
Lấy chuỗi 01/11/06 làm ví dụ. Theo thứ tự ngày-tháng-năm, đó là 1 tháng 11 năm 2006. Theo thứ tự tháng-ngày-năm của Mỹ, đó là 11 tháng 1 năm 2006.
Theo thứ tự năm-tháng-ngày, nó lại là 6 tháng 11 năm 2001. Không có thư viện nào đoán đúng giúp bạn được, vì đây là quy ước của người xuất file chứ không phải của dữ liệu.
Nếu định dạng đã rõ, câu hỏi tiếp theo là múi giờ. Tài liệu Python gọi một datetime không có múi giờ là “naive”: nó không mang đủ thông tin để xác định chính xác nó nằm ở đâu so với các mốc thời gian khác.
Chuyện nó là UTC hay giờ địa phương là do chương trình tự quy ước. Python cũng khuyên nên dùng datetime “aware”, tức có múi giờ, để biểu diễn UTC, vì nhiều phương thức coi datetime naive là giờ địa phương. utcnow() đã bị deprecate từ bản 3.12 cũng vì lý do này.
JavaScript còn khó lường hơn. Theo MDN, khi chuỗi không có offset, dạng chỉ có ngày được hiểu là UTC còn dạng ngày-giờ được hiểu là giờ địa phương. MDN thừa nhận đây là một lỗi đặc tả từ lâu, không khớp với ISO 8601, nhưng không sửa được vì sẽ làm hỏng tương thích web.
new Date("2024-03-01") // nửa đêm UTC
new Date("2024-03-01T00:00") // nửa đêm theo giờ máy đang chạy
Trên một trình duyệt ở múi giờ âm như ở Mỹ, dòng đầu tiên hiển thị thành tối 29 tháng 2. Một ô date picker trên dashboard chỉ cần vậy là đã lùi một ngày.
Một mốc thời gian cần mang theo những gì?
Muốn dữ liệu không bị mơ hồ, mỗi mốc thời gian cần ba thứ: thời điểm tuyệt đối, tên múi giờ nơi sự kiện xảy ra, và quy ước “ngày” của báo cáo. Thiếu một trong ba thì sớm muộn sẽ có lỗi.
Thời điểm tuyệt đối nên lưu ở UTC. Khi trao đổi dưới dạng chuỗi, dùng ISO 8601 với hậu tố Z, ví dụ 2024-03-05T05:30:00Z, để không ai phải đoán.
Cần nhớ một điều về PostgreSQL: kiểu timestamp with time zone lưu giá trị nội bộ ở UTC và không giữ lại múi giờ gốc. Nghĩa là nếu sau này bạn cần biết đơn hàng được đặt ở cửa hàng nào, theo giờ nào, bạn phải lưu múi giờ đó vào một cột riêng.
Cột đó nên chứa tên múi giờ IANA dạng Vùng/Địa điểm, như America/Chicago hay Asia/Ho_Chi_Minh, chứ không phải chữ viết tắt. Tài liệu tz của IANA nêu ví dụ CST: ở Trung Quốc nó mang một nghĩa, ở Bắc Mỹ lại mang nghĩa khác.
Offset cố định như -06:00 cũng không an toàn, vì chính phủ có thể đổi quy tắc giờ trong tương lai. Lưu tên múi giờ và cập nhật gói tzdata đều đặn thì hệ thống sẽ tự tính đúng khi quy tắc thay đổi.
Áp dụng vào chuỗi cửa hàng ở Chicago
Thử hình dung khách là một chuỗi cửa hàng ở Chicago. Hệ thống bán hàng của họ xuất CSV với cột order_time dạng 03/04/2024 23:30, không kèm múi giờ.
Việc đầu tiên không phải viết code mà là hỏi khách hai câu: thứ tự trong file là tháng/ngày hay ngày/tháng, và giờ trong file là giờ cửa hàng hay giờ máy chủ.
Giả sử khách xác nhận là tháng/ngày và giờ cửa hàng.
from datetime import datetime
from zoneinfo import ZoneInfo
STORE_TZ = "America/Chicago" # tên IANA khách xác nhận, không dùng "CST"
def parse_order_time(raw: str) -> datetime:
naive = datetime.strptime(raw, "%m/%d/%Y %H:%M") # MDY, theo xác nhận của khách
local = naive.replace(tzinfo=ZoneInfo(STORE_TZ)) # naive -> aware
return local.astimezone(ZoneInfo("UTC")) # lưu ở UTC
Khi ghi vào bảng, order_ts có kiểu timestamptz và source_tz là cột text chứa America/Chicago. Báo cáo theo ngày quy đổi về giờ địa phương trước rồi mới cắt ngày:
SELECT date(order_ts AT TIME ZONE source_tz) AS business_day,
sum(amount)
FROM orders
GROUP BY 1;
Lỗi trong ảnh chụp màn hình đầu bài thường xuất phát từ việc gom theo date(order_ts), tức theo ngày UTC. Đơn hàng 23:30 tối thứ Sáu ở Chicago khi đó đã sang thứ Bảy theo UTC. Đổi một dòng GROUP BY là doanh thu quay về đúng ngày.
Giờ lặp lại khi DST kết thúc
Còn một trường hợp cần tách riêng. PostgreSQL lưu ý rằng offset của một múi giờ thay đổi trong năm vì DST, nên giờ không kèm ngày gần như vô nghĩa. Vào đêm DST kết thúc, một khoảng giờ địa phương xảy ra hai lần. Hai đơn hàng cùng ghi 01:30 có thể cách nhau một tiếng thật.
Python xử lý chuyện này bằng thuộc tính fold: fold=0 dùng offset trước thời điểm chuyển, fold=1 dùng offset sau. Hàm strptime ở trên luôn cho fold=0.
Nếu file của khách có số thứ tự giao dịch hoặc log tăng dần, bạn có thể dùng chúng để quyết định dòng nào thuộc lần thứ hai và gán naive.replace(fold=1, ...).
Nếu không có, ít nhất hãy đánh dấu các dòng rơi vào khoảng giờ này và báo cho khách biết.
DST còn ảnh hưởng đến lịch làm việc. Việt Nam dùng UTC+7 quanh năm, không có DST.
Một nhóm làm việc theo giờ Việt Nam với khách ở Mỹ hoặc châu Âu sẽ thấy độ chênh giờ đổi theo mùa, nên cron job “chạy lúc 8 giờ sáng giờ khách” phải được cấu hình bằng tên múi giờ của khách, không phải một offset tính một lần rồi để đó.
Năm câu hỏi nên hỏi ngay buổi discovery đầu tiên
Hai câu hỏi trong ví dụ Chicago mới là khởi đầu. Mọi điểm mơ hồ ở trên đều quy về năm câu hỏi, và hỏi sớm thì rẻ hơn nhiều so với sửa dashboard sau khi khách đã chụp màn hình.
| Câu hỏi cho khách | Vì sao phải hỏi |
|---|---|
| Thứ tự ngày trong file là DMY, MDY hay YMD? | 01/11/06 có ba cách đọc, thư viện không đoán thay được |
| Giờ trong file là giờ cửa hàng, giờ máy chủ hay UTC? | Datetime naive không tự cho biết nó thuộc múi giờ nào |
| Mỗi địa điểm dùng múi giờ IANA nào? | Viết tắt như CST mơ hồ, offset cố định sai khi quy tắc đổi |
| “Một ngày” trong báo cáo cắt lúc nào, theo giờ ở đâu? | Ngày UTC và ngày cửa hàng khác nhau với đơn hàng ban đêm |
| Đêm DST kết thúc, dữ liệu có số thứ tự để phân biệt giờ lặp không? | Cùng một giờ địa phương có thể ứng với hai thời điểm thật |
Ghi câu trả lời vào tài liệu bàn giao, cạnh tên cột. Người tiếp quản dự án sau bạn sẽ không phải hỏi lại.
Lỗi hay gặp, và cách biến chúng thành một case để kể
Có vài lỗi lặp đi lặp lại. Để thư viện tự đoán thứ tự ngày/tháng. Gọi datetime.now() hoặc utcnow() rồi tưởng là đã có UTC. Lưu CST hoặc -06:00 vào cột múi giờ. Truyền chuỗi YYYY-MM-DD thẳng vào new Date() ở frontend.
Gom báo cáo theo ngày UTC trong khi khách đọc số theo ngày cửa hàng. Code mắc những lỗi này vẫn chạy bình thường trên máy bạn và chỉ lộ ra khi gặp dữ liệu thật ở múi giờ khác.
Cách học nhanh nhất là tự tái hiện ảnh chụp màn hình đầu bài. Tạo một bảng orders với vài chục đơn giả ở America/Chicago, trong đó có vài đơn sau 23:00 tối thứ Sáu và vài đơn rơi vào đêm DST kết thúc. Chạy báo cáo bằng date(order_ts), ghi lại thứ Sáu hụt bao nhiêu, rồi sửa sang AT TIME ZONE source_tz và so lại.
Bài tập đó cho bạn một câu chuyện trọn vẹn: triệu chứng, giả thuyết, câu hỏi đặt cho khách, dòng code đã sửa.
Khi đã làm được trên dữ liệu thật, hãy ghi vào CV một dòng cụ thể như “chuẩn hoá cột thời gian từ nhiều múi giờ về UTC kèm tên múi giờ IANA, sửa lỗi báo cáo lệch ngày”, và kể lại đúng trình tự đó khi phỏng vấn.
Lần tới khi khách hỏi vì sao số lệch đúng một ngày, đừng mở công thức ra trước. Hãy hỏi chuỗi thời gian đó được tạo ra ở đâu và theo giờ của ai.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!
8 nguồn
- Date - JavaScript | MDN
- datetime — Basic date and time types (Python 3.15.1 documentation)
- PostgreSQL: Documentation: 18: 8.5. Date/Time Types
- zoneinfo — IANA time zone support (Python 3.15.1 documentation)
- Theory and pragmatics of the tz code and data
- List of date formats by country - Wikipedia
- Time in Vietnam
- ISO 8601 - Wikipedia