他的回复:
补充一个已采纳回答之外、可以直接落地的思路:先把深分页的代价从回表降到只扫索引,再把任意跳页换成可缓存的页锚点。 一、先做覆盖索引,让延迟关联真正不回表你现在的查 order_no、user_id、amount,这些列不在 (created_at, id) 上。建议建一个只服务这个列表页的覆盖索引,把查询列都放进去:CREATE INDEX idx_order_list ON order_item (created_at, id, order_no, user_id, amount); 这样内层取 id 时可以只读索引,深分页往往能从 8 秒降到 1 秒量级。但注意它仍然要沿索引跳过 100 万条,只是每条代价变小了,所以它不是 100ms 的解法,只是兜底。 二、任意跳页不 offset 去数,用页锚点去定位MySQL 没有第 N 条的直接定位能力,offset 的语义就是逐条跳过,所以任意页 100ms 在纯 SQL 层面基本无解。可行的工程做法是:用户顺序翻页时,把每一页最后一条的 id 存下来,key 按筛选条件加页码存到 Redis,例如 list:order:筛选条件:page:5000 对应锚点 id,TTL 比如 10 分钟。用户点第 5000 页时,先查缓存有没有锚点,有就直接用 keyset 查询:WHERE created_at >= ? AND created_at ? AND id 锚点 id ORDER BY id DESC LIMIT 20,基本是毫秒级。缓存没有锚点时,不要直接跑 LIMIT 1000000,20,而是先跑一个只取 id 的边界查询拿到起始 id,再用 keyset 取整行;或者提示缩小时间范围或从附近页进入。管理后台的真实用户很少会凭空跳到第 5 万页,多数是误点或爬虫。 三、把时间范围变成第一道过滤器,而不是可选项你已经观察到时间范围越宽变慢越早。管理后台可以默认只 7 天,查更久必须显式选时间并走异步导出,而不是在线翻页。对历史数据查询,建议走导出 CSV 或异步任务加文件下载,不要让在线列表承担全量翻页。 四、总数不要和列表一起实时COUNT(*) 在大范围下同样要扫很多行。总数可以单独缓存,key 按筛选条件,TTL 5 分钟;或者只显示 10000+ 这种近似总数。把 SQL_CALC_FOUND_ROWS 拿掉是对的,它会让优化器多做一次计数。 推荐的落地组合是:覆盖索引 + keyset 顺序翻页 + 页锚点缓存支持跳页 + 默认窄时间范围 + 总数缓存。先上覆盖索引和 keyset,线上深分页的超时基本就能解决;跳页锚点是第二步,专门用来满足产品能跳页的要求,而不必为每一页都付出 offset 的全量扫描代价。兜底算查最近要用询要取