当Yii应用数据量超十万行时,应改用游标分页或覆盖索引优化分页性能。游标分页基于主键ID实现毫秒级响应;覆盖索引方案需建立联合索引并重写查询避免回表,或用子查询缩小扫描范围。

当Yii应用中数据量超过十万行,直接用limit+offset做分页会导致MySQL全表扫描,第100页查询可能耗时3秒以上。必须改用游标分页或覆盖索引优化方案。
避免offset偏移的性能陷阱
MySQL执行SELECT * FROM user ORDER BY id LIMIT 10000, 20时,会先扫描前10020行再丢弃前10000行。数据量越大,offset越往后,性能越差。
这一步不能跳过:检查当前分页SQL是否含OFFSET关键字,尤其是CActiveDataProvider或ActiveQuery中调用->offset()的地方。
用EXPLAIN分析原SQL,如果type列显示ALL或rows值远大于limit数,说明已触发全表扫描。
改用主键游标分页(推荐)
适用于按主键或唯一有序字段(如created_at)分页,且前端能接受“上一页/下一页”而非“跳转指定页码”的场景。
方法一:基于主键ID的游标分页
第一步:查询第一页时,记录最后一条记录的id值,例如SELECT * FROM user ORDER BY id ASC LIMIT 20;拿到第20条的id=1056。
第二步:下一页请求传入last_id=1056,执行SELECT * FROM user WHERE id > 1056 ORDER BY id ASC LIMIT 20;数据库直接走主键索引范围扫描,毫秒级响应。
注意:WHERE条件必须严格对应ORDER BY字段方向,升序用>,降序用
方法二:Yii2中封装游标查询逻辑
在ActiveQuery中添加自定义方法:public function cursorPagination($lastId, $limit = 20) { return $this->andWhere(['>', 'id', $lastId])->orderBy(['id' => SORT_ASC])->limit($limit); }。
调用时:User::find()->cursorPagination(1056)->all();这比原生offset写法快10倍以上。
强制使用覆盖索引优化offset分页
如果业务强依赖页码跳转(如用户手动输入页码),且无法改造成游标分页,必须让MySQL只扫描索引不回表。
方法一:建立联合索引覆盖排序与查询字段
执行ALTER TABLE user ADD INDEX idx_status_created_id (status, created_at, id);其中status是常用筛选条件,created_at是ORDER BY字段,id是SELECT中需要的主键。
方法二:重写查询只SELECT索引字段
把SELECT * 改为 SELECT id, status, created_at;确保所有返回字段都在idx_status_created_id索引中,避免回表。
【关键前提】ORDER BY字段必须是索引最左前缀或连续前缀,否则索引失效。例如索引是(status, created_at, id),ORDER BY created_at, id可以走索引;但ORDER BY id, created_at不行。
方法三:用子查询缩小offset范围
SELECT u.* FROM user u INNER JOIN (SELECT id FROM user WHERE status = 1 ORDER BY created_at LIMIT 10000, 20) t ON u.id = t.id;外层JOIN只查20条完整记录,子查询仅扫描索引。



















