Site logo

DateTime Trong MySQL: Tránh Bug Timezone Khi Report

5:00 read

Một trong những nơi bug timezone xuất hiện nhiều nhất không phải ở tầng Backend hay Frontend, mà ở ngay câu SQL query dữ liệu theo ngày. Một report tưởng chừng đơn giản như "lấy dữ liệu hôm nay" hay "lấy dữ liệu hôm qua" lại là nguồn gốc của rất nhiều bug âm thầm: không throw exception, không crash, chỉ lặng lẽ trả về sai tập dữ liệu — thiếu vài giờ đầu ngày, thừa vài giờ cuối ngày, hoặc lệch hẳn sang ngày kế bên.

Bài viết này tập trung vào một chủ đề hẹp nhưng cực kỳ thực tế: query và report DateTime trực tiếp trên MySQL — cách các hàm NOW(), CURDATE(), UTC_TIMESTAMP() thực sự hoạt động, vì sao "hôm nay" của Database chưa chắc là "hôm nay" của Business, và vì sao một câu WHERE DATE(created_at) = '...' tưởng vô hại lại có thể làm chậm report trên bảng hàng triệu record.

Audit Trước Khi Bắt Đầu: Cột Đang Là Kiểu Gì, Timezone Đang Set Gì

Trước khi viết bất kỳ report nào, nên dành 5 phút audit lại hiện trạng của database — rất nhiều bug timezone thực ra đã "nằm sẵn" trong schema hoặc trong cấu hình session từ trước, chỉ chưa bị phát hiện.

1. Kiểm tra kiểu dữ liệu của các audit field trên toàn bộ table:

SELECT
    TABLE_NAME,
    COLUMN_NAME,
    DATA_TYPE,
    COLUMN_TYPE,
    IS_NULLABLE,
    COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND COLUMN_NAME IN ('created_at', 'updated_at', 'deleted_at')
ORDER BY
    TABLE_NAME,
    FIELD(COLUMN_NAME, 'created_at', 'updated_at', 'deleted_at');

Kết quả cho biết ngay những cột nào đang là TIMESTAMP (dễ bị auto-convert khi đổi session timezone) so với DATETIME (lưu literal cố định), và cột nào đang dùng default DB-side (COLUMN_DEFAULT = CURRENT_TIMESTAMP) — loại default sinh giá trị trực tiếp từ MySQL, không đi qua application code, nên phụ thuộc hoàn toàn vào timezone của DB tại thời điểm insert.

2. Kiểm tra timezone hiện tại của session và của server:

-- Timezone của session hiện tại (kết nối đang dùng)
SELECT @@session.time_zone;

-- Timezone global của toàn server MySQL
SELECT @@global.time_zone;

-- Giá trị "bây giờ" mà DB đang hiểu, theo đúng timezone ở trên
SELECT NOW();

Ba giá trị này nên được đối chiếu lại với timezone mà business thực sự mong đợi. @@session.time_zone và @@global.time_zone không nhất thiết giống nhau — một connection pool hoặc tool báo cáo có thể override session timezone khác với global default của server.

3. Nếu timezone chưa đúng, set lại ở đúng cấp cần thiết:

-- Set timezone cho session hiện tại
SET time_zone = '+09:00';

-- Hoặc set global (cần quyền SUPER, hoặc cấu hình ở RDS Parameter Group)
SET GLOBAL time_zone = 'Asia/Tokyo';

Lưu ý: SET GLOBAL chỉ ảnh hưởng đến các session kết nối mới sau khi lệnh được chạy, không ảnh hưởng ngược lại các session đang mở sẵn. Với RDS/managed MySQL, thường phải cấu hình qua Parameter Group thay vì chạy SET GLOBAL trực tiếp, vì user quản trị của managed service thường không có quyền SUPER.

Trước Khi Query: Luôn Kiểm Tra Timezone Của Session

Trước khi tin vào bất kỳ kết quả nào từ NOW() hay CURDATE(), cần xác nhận session đang query đang hiểu "bây giờ" là mấy giờ:

SELECT NOW();
SELECT CURRENT_DATE();
SELECT CURRENT_TIME();
SELECT UTC_TIMESTAMP();

SELECT CONVERT_TZ(NOW(), @@session.time_zone, '+09:00');

Vài lưu ý quan trọng về các hàm này:

  • NOW() / CURRENT_TIMESTAMP: trả về ngày-giờ hiện tại theo @@session.time_zone — đây là điểm mấu chốt. Cùng một thời điểm thực tế, hai session có time_zone khác nhau sẽ cho ra hai giá trị NOW() khác nhau.
  • CURDATE() / CURRENT_DATE: lấy phần ngày từ NOW(), nên cũng phụ thuộc session timezone. Đây chính là hàm gây bug nhiều nhất trong report "hôm nay/hôm qua".
  • UTC_TIMESTAMP() / UTC_DATE(): không phụ thuộc session timezone — luôn trả về giờ UTC thực, dùng để đối chiếu khi nghi ngờ session đang lệch.
  • CONVERT_TZ(dt, from_tz, to_tz): chuyển một giá trị datetime từ timezone này sang timezone khác. Có một cạm bẫy hay bị bỏ sót: nếu dùng tên timezone dạng IANA ('Asia/Tokyo') thay vì offset ('+09:00'), MySQL cần bảng mysql.time_zone_name đã được nạp dữ liệu (mysql_tzinfo_to_sql) — nếu chưa nạp, CONVERT_TZ sẽ âm thầm trả về NULL thay vì báo lỗi, khiến toàn bộ điều kiện WHERE liên quan biến mất một cách khó hiểu.
-- An toàn trên mọi server, không cần load timezone tables
SELECT CONVERT_TZ(NOW(), '+00:00', '+09:00');

-- Chỉ hoạt động nếu server đã nạp timezone name tables (mysql_tzinfo_to_sql)
SELECT CONVERT_TZ(NOW(), 'UTC', 'Asia/Tokyo');

Yêu cầu đăng nhập

Vui lòng đăng nhập để truy cập nội dung này

Additional Resources

Course Guide

Comprehensive PDF guide with examples

GitHub Repository

Example code for all lessons

Discussion

Have a question about this lesson? Post it here and get answers from instructors and peers.