MySQL 中 Unix time 字段怎么设计:INT、BIGINT、秒、毫秒与索引
有些场景下,在 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/Shanghai 或 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 时间戳转换工具 判断它是秒还是毫秒,再写查询或迁移脚本。