订单表已经上千万行,一个按用户和时间查询的接口越来越慢,你会怎么设计和验证索引?
我的判断
我不会因为表到千万级就直接分库分表。先确认扫描、排序和分页成本,再让联合索引贴合用户按时间查订单的访问路径。
建索引前,我会从慢日志和调用链拿到真实 SQL、典型参数和执行计划。千万级只是现象,访问路径才决定这个接口快不快。 先确认慢的是所有用户,还是订单特别多的少数用户;时间究竟耗在扫描、排序、回表、深分页,还是连接池等待。
假设接口长期稳定地按租户、用户和时间范围过滤,再按时间倒序展示,我会建立下面这条联合索引:
CREATE INDEX idx_orders_tenant_user_time
ON orders (tenant_id, user_id, created_at DESC, id DESC);
tenant_id、user_id 负责等值定位,created_at 接住时间范围和倒序读取,id 解决同一时间多笔订单时翻页不稳定的问题。列表只取二十条,少量回表通常可以接受;我不会为了追求覆盖索引,把金额、状态等字段全部塞进去。
如果接口还在使用 LIMIT 100000, 20,仅仅加索引只解决了一半。我会改成基于上一页末尾记录的游标翻页:
AND (created_at < ?
OR (created_at = ? AND id < ?))
ORDER BY created_at DESC, id DESC
LIMIT 21
上线前不会只看 EXPLAIN 里是否出现目标索引,而会用普通用户、超级用户和大时间区间分别验证扫描行数、接口 P99、数据库 IO 和建索引期间的写入延迟。
如果单个用户几年的历史订单仍然很多,我会再做按时间归档或冷热分离。总表一千万行本身,不是立刻分库分表的理由。
思路拆解问题分析
这道题真正考察的不是能不能背出 B+Tree,而是能不能从线上接口反推出完整访问路径。“上千万行”只是背景,不是根因。
tenant_id、user_id是等值条件,先缩到单个用户;created_at承担时间过滤和倒序读取;id提供稳定排序,避免翻页重复或漏单。
status 是否进入联合索引要看它是否必传。可选状态夹在 user_id 与 created_at 中间,反而会破坏不带状态查询的范围与排序能力。
容易答偏踩坑误区
- 看到千万级就分库分表。 正确索引通常仍能很好支撑。
- 把 WHERE 每列都建单列索引。 完整访问路径比字段清单重要。
- 制造超宽覆盖索引。 会占缓存、磁盘并放大订单写入。
- 只测第一页。 深分页、超级用户和大时间范围才是风险点。