不能只靠created_at索引做范围查询,因其选择性低、易致全表扫描,且时间戳不唯一导致排序分页结果不稳定;必须用id与created_at的联合索引(id在前),并配合id > 上次最大值动态下界来确保高效、准确、无漏查询。

为什么不能只靠 created_at 索引做范围查询?
流水表数据量一旦过千万,仅对 created_at 建索引常会查得慢——尤其当查询时间范围跨度过大(比如“最近7天”),MySQL 可能放弃索引走全表扫描,因为优化器发现满足时间条件的行太多,回表成本高。更关键的是:created_at 不唯一,相同毫秒级时间戳可能有上百条记录,排序和分页时结果不稳定,ORDER BY created_at DESC LIMIT 100 每次执行都可能返回不同行。
id 和 created_at 联合索引怎么建才真正生效?
必须把 id 放在联合索引第一位:INDEX idx_id_created (id, created_at)。不是反过来,也不是两个单独索引。原因很实际:自增 id 天然有序、唯一、紧凑,用它做主过滤键,MySQL 能快速定位到某一段物理连续的数据块;再结合 created_at 做二次筛选,就能跳过大量无关记录。
- 错误写法:
INDEX idx_created_id (created_at, id)—— 时间范围大时仍要扫描大量索引项 - 正确写法:
INDEX idx_id_created (id, created_at),配合查询中明确给出id下界 - 典型查询模式:
SELECT * FROM log_table WHERE id > 12345678 AND created_at >= '2024-06-01' ORDER BY id DESC LIMIT 100
如何动态维护 id 下界避免漏数据?
不能硬编码一个固定 id 值。真实场景中,新数据持续写入,上次查到的最后一条 id(比如 12345678)之后,可能还有更早时间但 id 更大的记录(因写入延迟、批量导入、主从延迟等)。所以每次查询后,必须用实际返回的最大 id 作为下一次的起点,而不是依赖时间字段判断“是否已覆盖”。
- 安全做法:每次取完一批,记录
MAX(id),下轮用WHERE id > 上次MAX(id) - 不推荐做法:用
WHERE created_at > 上次最大时间—— 时间重复 + 写入乱序会导致漏 - 如果必须按时间语义分页(如“第3页,每页100条”),先用
id快速定位大致区间,再用created_at过滤并LIMIT,别直接OFFSET
注意 INSERT 写入节奏对双重过滤的影响
自增 id 的“时间相关性”其实只是经验假设。当存在批量导入、replace into、主从延迟、或多个写入源时,id 和 created_at 的单调关系会被打破。这时单纯依赖 id > X 可能跳过某些时间上应被包含的记录。
- 线上务必监控
SELECT COUNT(*) FROM t WHERE id > ? AND created_at 是否长期非零——这是乱序写入的信号 - 若乱序严重,需补查:在用
id快速捞出主集后,额外查一遍created_at落在目标范围内但id小于下界的“迟到数据”,代价可控 - 不要指望
AUTO_INCREMENT严格保序,它只保证唯一和递增,不保证与时间强一致
真正难处理的不是建索引,而是写入链路不可控时,如何让“按 id 过滤”不变成一道信任陷阱。


















