聚合分页返回的条数比预期少,而单独count看起来正常,先检查过滤究竟发生在分页之前,还是只作用于已经切出的一页。MongoDB管道中,每个阶段接收前一阶段的输出;顺序本身就是查询语义。聚合管道说明
当时的顺序:先切页,再筛选
2020年的代码把主集合过滤放在最后:
List<Bson> pipeline = new ArrayList<>();
pipeline.add(Aggregates.lookup(
announceColName, "announceId", "_id", "announceData"));
pipeline.add(Aggregates.sort(
new BasicDBObject("announceCreateTime", -1)));
pipeline.add(Aggregates.skip((pageNum - 1) * pageSize + plus));
pipeline.add(Aggregates.limit(pageSize));
pipeline.add(Aggregates.match(condition));
假设排序后的主记录依次是A不匹配、B匹配、C不匹配、D匹配,页大小为2:
- 先limit再match:先选A、B,最后只剩B。
- 先match再limit:候选变成B、D,再选出B、D。
这只是固定样本的逻辑推演,不是数据库运行日志。count若针对匹配全集统计,就会与错误分页使用的候选范围不一致。
主集合条件应在分页前生效
当condition仅依赖主集合字段时,可改为:
List<Bson> pipeline = new ArrayList<>();
pipeline.add(Aggregates.match(condition));
pipeline.add(Aggregates.lookup(
announceColName, "announceId", "_id", "announceData"));
pipeline.add(Aggregates.sort(
new BasicDBObject("announceCreateTime", -1).append("_id", -1)));
pipeline.add(Aggregates.skip((pageNum - 1) * pageSize + plus));
pipeline.add(Aggregates.limit(pageSize));
AggregateIterable<Document> rows = collection.aggregate(pipeline);
这里沿用旧Java Driver构建器形式;需要项目已有的collection、condition等上下文。pageNum从1开始,pageSize应正数且有上限;先验证偏移计算及其是否落在所用驱动skip参数允许范围。额外的plus会改变分页起点,普通页码分页通常应为0,不要保留一个含义不明的修正量。
补 _id 作为次级键,是为相同时间值建立唯一排序;但它不解决两次请求之间有新增或删除记录的偏移漂移。MongoDB排序一致性
match并非永远第一,count也不能另写一套语义
若条件依赖lookup产生的关联字段,就必须先产生这些字段再过滤;unwind或group还可能改变记录数量。此时count要统计同样的业务单位:主记录、展开后的行,还是去重后的组。
是否把lookup移到分页后,也要看关联结果是否参与过滤或排序,不能只为性能改变结果含义。大页码、索引和执行计划是下一层优化,不应掩盖阶段顺序的正确性。
验证时使用固定数据比较筛选全集、每页ID和计数,尤其覆盖时间相同、无关联数据、边界页和空页。