在INT/BIGINT时间戳字段上建B-Tree索引有效,但需避免在WHERE中使用DATE()等函数包装,否则索引失效;应将日期查询重写为时间戳闭区间(如2024-01-01 → 1704067200000 <= created_at <= 1704153599999),并优先在应用层计算边界值;联合索引中等值字段(如user_id)须置于范围字段(created_at)之前。

MySQL 时间戳字段加索引前先确认类型和查询写法
直接在 created_at 这类 INT 或 BIGINT 类型的时间戳字段上建 B-Tree 索引是有效的,但前提是查询条件没用 FROM_UNIXTIME() 或 DATE() 包装——一旦包装,索引就失效了。
常见错误现象:EXPLAIN 显示 type=ALL,哪怕字段上有索引;WHERE DATE(created_at) = '2024-01-01' 这种写法必然全表扫描。
- 时间戳字段建议统一用
BIGINT存毫秒级(兼容性好、无时区歧义),避免用DATETIME再转 UNIX 时间戳 - 如果业务已用
DATETIME,且带时区,务必确认 MySQL 服务端时区和应用层一致,否则范围计算会偏移 - 索引本身不区分“时间戳”语义,只认值的有序性;所以只要查询条件能走范围比较(
>=/<=),就能命中索引
把 DATE() 范围查询重写成时间戳区间计算
核心不是“怎么去掉 DATE()”,而是“把日期粒度的意图翻译成时间戳的闭区间”。例如查 2024-01-01 当天的数据,不能写 DATE(created_at) = '2024-01-01',得算出当天起止毫秒值再查。
使用场景:后台导出某日订单、按天聚合统计、定时任务拉取增量数据。
- 假设
created_at是毫秒级BIGINT,2024-01-01 对应区间是1704067200000(含)到1704153599999(含) - SQL 应写成:
WHERE created_at >= 1704067200000 AND created_at <= 1704153599999 - 别用
<上界写成1704153600000——虽然数学等价,但易错且可读性差;显式写<=更稳妥 - 如果用的是秒级时间戳(
INT),对应值要少三位,别混用
应用层生成时间戳区间比数据库里算更可靠
在 WHERE 里调 UNIX_TIMESTAMP('2024-01-01') 或 STR_TO_DATE() 看似省事,实则埋雷:函数执行依赖 MySQL 的 time_zone 设置,跨环境容易结果不一致。
性能影响:每次查询都调时间函数,无法利用 Query Cache(即使开着也大概率失效),还增加服务端 CPU 开销。
- 推荐在应用代码里用本地时区或 UTC 统一算好两个边界值,作为参数传入 SQL
- Python 示例:
start = int(datetime(2024, 1, 1).timestamp() * 1000),注意timestamp()默认返回秒级,需乘 1000 - Java 用
LocalDate.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(),避免System.currentTimeMillis()那种模糊写法 - 千万别在 SQL 里拼接字符串生成时间戳,比如
CONCAT('1704067200', '000')—— 可维护性为零,且容易因空格/类型隐式转换失败
复合索引中时间戳字段的位置很关键
如果查询常带用户 ID + 时间范围(如查某用户最近 7 天操作),只给 created_at 单独建索引效果有限;必须考虑联合索引字段顺序。
原则:等值查询字段放前面,范围查询字段放后面。比如 WHERE user_id = 123 AND created_at >= X,索引应建为 (user_id, created_at)。
- 反过来建
(created_at, user_id),虽然也能用,但会先扫一大片时间范围,再过滤 user_id,效率差很多 - 如果还有其他等值条件(如
status = 'done'),把它也放在联合索引前面,顺序按选择性从高到低排 - 注意:MySQL 8.0+ 支持降序索引,但时间戳字段通常不需要显式写
DESC,B-Tree 本身支持反向扫描
最麻烦的其实是历史数据迁移时补索引——大表 ADD INDEX 会锁表或耗很长时间,得提前在低峰期做,别等到慢查询报警了才想起来。










