日志表应统一用TIMESTAMP类型并设为UTC时区,字段名用created_at,加DEFAULT CURRENT_TIMESTAMP;写入需异步或消息队列;优先单字段索引而非分区;删旧日志用DROP TABLE;JSON字段慎用,核心字段应单独建索引。

日志表设计要避开 datetime 类型的时区陷阱
MySQL 的 DATETIME 不带时区,写入值就是你传进去的字面量;而 TIMESTAMP 会自动转成 UTC 存储、查询时再转回系统时区。项目部署在多时区服务器(比如容器跨区域调度)时,用 DATETIME 容易导致日志时间错乱,排查问题时发现“同一请求的日志时间倒流”。
实操建议:
- 统一用
TIMESTAMP,并确保 MySQL 服务端和应用层都设为UTC(如 JDBC 连接串加serverTimezone=UTC) - 字段名直接叫
created_at,别用log_time这类模糊命名,避免和业务时间混淆 - 加
DEFAULT CURRENT_TIMESTAMP,省去应用层拼时间字符串的逻辑,也防止单条 SQL 忘写时间字段
日志写入不能直连主库,必须走异步或中间件
高频操作(如用户登录、接口调用)如果每条都同步 INSERT 到 MySQL,主库 I/O 和锁竞争会立刻成为瓶颈。你看到的 Lock wait timeout exceeded 或慢查询日志里大量 INSERT INTO log_*,基本都是这个原因。
常见可行路径:
- 应用内用线程池+内存队列(如 Java 的
BlockingQueue+ExecutorService),攒批写入,但要注意进程重启丢日志 - 接入轻量消息队列(如 Redis Stream、RabbitMQ),由独立消费者服务落库,解耦且可靠
- 用 MySQL 的
LOAD DATA INFILE批量导入文本日志,适合离线归档场景,但实时性差
分区表对日志查询性能提升有限,别盲目上 RANGE 分区
很多文档说“按天分区能加速查询”,但实际中,如果你查的是 WHERE created_at BETWEEN '2024-05-01' AND '2024-05-07',MySQL 确实能裁剪分区;可一旦加上 AND level = 'ERROR',又没给 level 建联合索引,就还是全分区扫描。
更务实的做法:
- 先建普通表,用
created_at单字段索引,观察慢查频率和执行计划 - 真出现单表超千万行且查询变慢,再按月分表(
log_202405,log_202406),比分区表更可控、备份恢复更简单 - 删除旧日志用
DROP TABLE,比DELETE FROM ... LIMIT快几个数量级,也避免长事务锁表
JSON 字段存扩展字段要谨慎评估查询需求
想兼容不同模块的日志结构,有人倾向用 JSON 类型存 extra_info。这确实灵活,但代价是:无法在 JSON 内部字段上建索引,WHERE JSON_EXTRACT(extra_info, '$.user_id') = '123' 会全表扫描;MySQL 8.0 虽支持生成列索引,但需提前定义好路径,改结构成本高。
替代方案:
- 核心检索字段(如
user_id,trace_id,status_code)单独拆成普通列,加索引 - 真正非结构化内容(如堆栈快照、原始请求体)才进
TEXT或JSON字段,只用于展示,不用于 WHERE / ORDER BY - 如果字段组合太多,考虑用 Elasticsearch 做日志检索,MySQL 只做归档存储
最常被忽略的一点:日志表的 ENGINE 别默认用 InnoDB。如果只是写多读少、不依赖事务,MyISAM 或 ARCHIVE 引擎写入更快、磁盘占用更小;但 ARCHIVE 不支持索引,得搭配外部检索方案。










