
游标查询只在简单单表、升序、有索引字段时才真正生效
ThinkPHP 的 cursor() 不是万能分页替代品。它底层依赖数据库游标语义,要求排序字段严格单调递增(如 id 或 create_time),且必须建好索引;降序(desc)直接不支持——因为游标靠“上一页最后一条的值”作为下一页起点,降序会导致起点不可靠。
常见错误是加了 ->order(‘id’, ‘desc’) 还硬套 cursor(),结果查到一半就中断或重复。正确写法只能是:->order(‘id’, ‘asc’),且确保该字段无 NULL 值(否则 MIN(id) 失效,首请求可能漏数据)。
不能用 whereRaw 干扰主键/排序字段的可预测性,比如 whereRaw(‘id % 2 = 0’) 会破坏游标连续性关联查询(with())、子查询、聚合字段(count(*) as total)会让 cursor() 自动退化为普通查询SQLite 驱动下 cursor() 等价于 select(),因为 SQLite 不支持服务器端无缓冲游标
必须用 foreach 直接遍历,不能调 toArray() 或 all()
cursor() 返回的是 PHP 生成器(Generator),内存友好全靠“边 fetch 边 yield”。一旦你调 toArray()、all()、count(),或者赋值给变量再循环,框架就会把全部结果一次性加载进内存,和普通查询没区别。
正确姿势只有一种:foreach ($query->cursor() as $row) { … }。中间不能打断、不能缓存、不能二次遍历。
立即学习“PHP免费学习笔记(深入)”;
事务中使用需格外小心:未消费完所有行就结束事务,可能导致 MySQL 连接卡住、锁未释放,甚至触发 wait_timeout如果处理逻辑复杂(比如要前后行对比),别硬扛游标,改用 chunk() 更稳妥导出 CSV 等场景适合游标,但记得在循环内做 flush 或 set_time_limit(0),避免超时
chunk() 更通用,但默认按主键升序,可手动指定字段和方向
相比 cursor(),chunk() 兼容性更好、适用场景更广,底层是模拟分页(WHERE id > ? LIMIT N),支持任意字段和升降序。
例如按时间倒序分批处理:Db::table(‘log’)->where(‘status’, 1)->chunk(500, $callback, ‘create_time’, ‘desc’)。注意:指定字段必须有索引,否则性能急剧下降。
闭包里返回 false 可中断后续批次,适合条件提前退出WEB 请求慎用大批次(如 chunk(10000)),容易超时;命令行脚本更合适若主键不是自增整型(比如 UUID),必须显式指定带索引的数值型字段,否则分页逻辑错乱
游标不自动释放连接,事务+长耗时处理要加断点续传
cursor() 不会主动 close 游标或释放 PDO 连接。尤其在事务中逐行更新数据时,如果某次处理卡住或失败,已打开的结果集会一直占着连接,后续请求可能被阻塞。
真实线上环境建议:把游标遍历逻辑移出事务;如必须事务内执行,至少做到两点——记录最后成功处理的 id(用于断点续传),并给循环加最大执行时间限制(microtime(true) 判断)。
最容易被忽略的一点:游标查询对「数据实时一致性」无保障。若遍历过程中其他进程插入/删除了排序字段范围内的数据,会出现漏查或重复——这不是 bug,是游标语义本身决定的。需要强一致性的场景,请回归传统分页或加应用层锁。

评论(0)