有些情境下,在 MySQL 裡用 Unix time 存時間是合理的。
比如資料來自外部 API、訊息佇列、日誌、行動端 SDK、埋點系統、事件流,大家都用 epoch time 表示同一個 UTC 時刻。這種情況下,數字時間戳更利於跨系統比較和排序。
但資料表結構必須把約定寫清楚。最常見的坑是:用 INT 存會超過 2038 年的值、秒和毫秒混用、欄位名稱叫 expires_at 實際卻存一個整數。
欄位名稱必須寫出單位
不要把單位藏在程式碼記憶裡。
推薦:
expires_at_epoch_seconds BIGINT NULL
或者:
expires_at_epoch_ms BIGINT NULL
不推薦:
expires_at INT NOT NULL
expires_at 這個名稱通常表示一個日期時間值。如果實際存的是整數,就應該在欄位名稱裡寫出 epoch_seconds、epoch_ms 或類似後綴。
INT 通常不適合作為預設選擇
MySQL 有符號 INT 最大值是 2147483647。如果把它解釋成 Unix 秒,範圍會卡在 2038 年邊界。
所以這種設計很脆弱:
created_at_epoch_seconds INT NOT NULL
INT UNSIGNED 雖然能把範圍延後,但仍然是一套自訂約定,也不能存毫秒。它可以用於短期相容或歷史系統,但不適合作為新資料表時間欄位的預設選擇。
新資料表如果要存 Unix time,優先用 BIGINT。
秒還是毫秒?
MySQL 的 UNIX_TIMESTAMP() 和 FROM_UNIXTIME() 預設處理的是 Unix 秒。JavaScript、很多行動端 SDK 和埋點系統常用毫秒。
單位一定要體現在欄位名稱裡:
event_time_epoch_seconds BIGINT NOT NULL
event_time_epoch_ms BIGINT NOT NULL
不要把毫秒存到一個叫 event_time 或 event_time_epoch 的欄位裡,然後靠團隊記憶。
常見位數:
- 10 位:秒級時間戳,例如
1721385000 - 13 位:毫秒級時間戳,例如
1721385000000 - 16 位:微秒
- 19 位:奈秒
如果值是 13 位,不能直接丟給 MySQL FROM_UNIXTIME() 當秒處理。展示時要除以 1000。
事件資料表設計範例
事件日誌適合用 BIGINT 存時間,並給時間欄位建索引:
CREATE TABLE user_events (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
event_name VARCHAR(80) NOT NULL,
event_time_epoch_ms BIGINT NOT NULL,
payload JSON NULL,
INDEX idx_event_time (event_time_epoch_ms),
INDEX idx_user_time (user_id, event_time_epoch_ms)
);
查詢某個時間範圍:
SELECT *
FROM user_events
WHERE event_time_epoch_ms >= 1721347200000
AND event_time_epoch_ms < 1721433600000
ORDER BY event_time_epoch_ms;
這種查詢可以很好地利用 idx_event_time,因為比較的是原始數字。
過期時間欄位設計範例
如果是業務過期時間,很多時候 DATETIME(3) 更合適。但如果你的系統契約已經統一使用 epoch milliseconds,也可以一致地存 BIGINT:
CREATE TABLE api_tokens (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
token_hash VARBINARY(32) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
expires_at_epoch_ms BIGINT NULL,
revoked_at_epoch_ms BIGINT NULL,
created_at_epoch_ms BIGINT NOT NULL,
UNIQUE KEY uk_token_hash (token_hash),
INDEX idx_expires_at_epoch_ms (expires_at_epoch_ms),
INDEX idx_user_expires (user_id, expires_at_epoch_ms)
);
查詢有效 token:
SELECT *
FROM api_tokens
WHERE revoked_at_epoch_ms IS NULL
AND (expires_at_epoch_ms IS NULL OR expires_at_epoch_ms > 1721385000000);
這裡 NULL 表示永不過期或沒有過期時間。通常比存一個很遠的魔法數字更清晰。
用產生欄提高可讀性
如果開發者需要在 SQL 用戶端裡直接看懂時間,可以加一個產生欄。
秒級:
CREATE TABLE jobs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
run_at_epoch_seconds BIGINT NOT NULL,
run_at_utc DATETIME
GENERATED ALWAYS AS (FROM_UNIXTIME(run_at_epoch_seconds)) STORED,
INDEX idx_run_at_epoch_seconds (run_at_epoch_seconds),
INDEX idx_run_at_utc (run_at_utc)
);
毫秒級:
CREATE TABLE jobs_ms (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
run_at_epoch_ms BIGINT NOT NULL,
run_at_utc DATETIME(3)
GENERATED ALWAYS AS (FROM_UNIXTIME(run_at_epoch_ms / 1000.0)) STORED,
INDEX idx_run_at_epoch_ms (run_at_epoch_ms)
);
注意:FROM_UNIXTIME() 回傳值會受 MySQL session time zone 影響。如果你希望展示穩定為 UTC,應讓應用和遷移連線設定 session time zone 為 +00:00,或者在應用層做轉換。
WHERE 裡不要對欄套函式
這種寫法可能慢:
SELECT *
FROM user_events
WHERE FROM_UNIXTIME(event_time_epoch_ms / 1000.0) >= '2026-07-19 00:00:00';
因為函式套在欄上,MySQL 很可能不能高效使用原始數字索引。
更推薦先把查詢邊界轉換成 epoch,再比較原始欄位:
SELECT *
FROM user_events
WHERE event_time_epoch_ms >= 1784419200000
AND event_time_epoch_ms < 1784505600000;
也就是說,在應用程式碼裡先算好 epoch 邊界,再傳給 SQL。
時區規則
Unix time 表示一個基於 UTC 的絕對時刻,本身不存時區。
這對機器事件很方便,但對業務日曆概念不一定夠。比如「一家門市優惠券在當地時間午夜過期」,可能需要同時儲存:
- 本地日期時間
- 門市時區
- 用於比較的 UTC 時刻
如果只存 epoch milliseconds,你知道那個瞬間,但不知道它來自哪個業務時區。業務需要時,可以額外儲存 timezone_name,例如 Asia/Taipei 或 America/New_York。
從 INT 秒遷移到 BIGINT 毫秒
如果舊資料表用 INT 存秒,可以這樣遷移:
ALTER TABLE api_tokens
ADD COLUMN expires_at_epoch_ms BIGINT NULL;
UPDATE api_tokens
SET expires_at_epoch_ms = expires_at_epoch_seconds * 1000
WHERE expires_at_epoch_seconds IS NOT NULL;
ALTER TABLE api_tokens
ADD INDEX idx_expires_at_epoch_ms (expires_at_epoch_ms);
然後修改應用程式碼,讓新寫入統一使用毫秒。遷移視窗內可以暫時保留兩個欄位,但不要長期雙寫而不清理。
可以加一個約束防止秒/毫秒混寫:
ALTER TABLE api_tokens
ADD CONSTRAINT chk_expires_at_epoch_ms
CHECK (
expires_at_epoch_ms IS NULL
OR expires_at_epoch_ms BETWEEN 946684800000 AND 4102444800000
);
這個範例允許 2000-01-01 到 2100-01-01 之間的毫秒值。實際專案要按業務範圍調整。
推薦使用規則
適合用 BIGINT Unix time 的情境:
- 值來自日誌、SDK、事件流或分散式系統
- 需要快速按時間範圍做數字查詢
- 多個服務統一使用 epoch milliseconds
- 不依賴 MySQL 日期函式作為主要介面
更適合用 DATETIME(3) 的情境:
- 人需要直接在 SQL 裡讀寫時間
- 欄位是業務截止時間,不是機器事件
- 需要很遠的未來日期
- 希望預設可讀,並使用 MySQL 日期函式
如果決定在 MySQL 裡存 Unix time,至少把三件事寫清楚:
- 秒還是毫秒
- 新資料表用
BIGINT,不要預設 signedINT - 欄位表示 UTC 絕對時刻,還是帶業務本地時間含義
拿到一個數字時間戳時,可以先用 Unix 時間戳轉換工具 判斷它是秒還是毫秒,再寫查詢或遷移腳本。