我遇到过一个很迷惑的列表问题:页面实际渲染 18 个商品,工具栏却显示“共 42 条”。更怪的是,直接打印集合的 count() 是 18,而 getSize() 一直是 42。清缓存、重建索引都没有变化。

这类问题先别碰分页器。count() 和 getSize() 本来就不是一回事:前者统计当前已经加载到内存里的对象数量,后者会基于集合的 Select 另外生成一条计数 SQL。只要扩展代码对原查询做过 JOIN、GROUP BY 或 HAVING,两条数字就可能分开。

先把“列表 SQL”和“计数 SQL”并排打印

不要只看最终数字。临时在可控环境记录下面两条 SQL,通常一眼就能看出分叉发生在哪里:

$collection = $this->productCollectionFactory->create();
// 应用与页面相同的过滤条件

echo (string) $collection->getSelect();
echo "
--- COUNT SQL ---
";
echo (string) $collection->getSelectCountSql();

如果商品表 JOIN 了库存来源、分类关系或自定义一对多表,一个商品可能展开成多行。页面加载时 Magento 最终仍会按实体生成对象,但计数 SQL 可能在算展开后的行数。典型特征是:

  • 去掉某个 JOIN 后数字恢复;
  • COUNT(*) 明显大于 COUNT(DISTINCT e.entity_id);
  • 同一个商品在原始 SQL 结果中出现多次。

最常见的三个“制造重复行”现场

一对多表只用于过滤,却直接 join

例如一个商品有多个来源库存记录。需求只是“至少一个来源满足条件”,却把完整来源表连接进集合。更合适的写法往往是 EXISTS 子查询,或者先在子查询里聚合为每个 SKU 一行,再 JOIN 回主集合。

分类关系和自定义属性同时连接

商品属于多个分类时,catalog_category_product 会天然扩行。若页面不需要分类 position,尽量使用 Magento 已有的分类过滤能力;确实要手写 JOIN 时,必须确认每个实体最终只保留一行。

GROUP BY 修好了列表,却破坏了 size

有人发现重复后直接加 GROUP BY e.entity_id。列表看起来正常了,但集合生成计数 SQL 时可能保留或改写这个 GROUP,结果变成多个 count 行。此时不是再套一层 COUNT(*) 就万事大吉,而是要决定“业务上到底在数商品、组合还是分组”。

别忽略 getSize() 自己的缓存

getSize() 首次执行后会把结果保存在集合实例中。下面这种调用顺序很容易留下旧数字:

$size = $collection->getSize();
$collection->addFieldToFilter('status', 1);
// 此时再次 getSize() 可能仍返回前一次结果

正确做法不是到处调用内部方法清缓存,而是把所有过滤、JOIN 和作用域条件都组装完成后再读取 size。需要尝试不同条件时,重新创建集合比复用一个已计算过 size 的实例更清楚。

怎样修,取决于你真正要数什么

如果业务对象就是商品,优先从查询结构上消除一对多扩行;仅在确定语义正确时使用 COUNT(DISTINCT e.entity_id)。如果列表展示的是“商品 × 来源”这样的组合,就不应该强行按商品去重,工具栏也要明确显示组合数量。

我会用三组数据验收:一个没有关联记录的商品、一个只有一条关联记录的商品、一个有三条关联记录的商品。然后同时核对第一页、最后一页、空结果页以及应用筛选后的总数。只测第一页很容易把分页边界的漏项藏起来。

因此,遇到 getSize() 不准时,最有效的起点不是缓存和索引,而是把两条 SQL 放在一起看。它们从哪一段开始不同,根因通常就在那里。