应使用COUNT(DISTINCT product_id)统计每单商品种类数,而非COUNT(*);SQL语句为SELECT order_id, COUNT(DISTINCT product_id) AS product_count FROM order_items GROUP BY order_id;NULL值被自动忽略,主流数据库均支持。

用 GROUP BY + COUNT(DISTINCT) 统计每单商品种类数
直接对订单分组,再对商品 ID 去重计数即可。关键不是 COUNT(*),而是 COUNT(DISTINCT product_id)——否则会统计总行数,不是种类数。
假设订单明细表叫 order_items,字段含 order_id 和 product_id:
SELECT order_id, COUNT(DISTINCT product_id) AS product_count FROM order_items GROUP BY order_id;
- 如果
product_id允许为 NULL,COUNT(DISTINCT ...)会自动忽略 NULL,符合多数业务预期 - MySQL 5.7+、PostgreSQL、SQL Server 2017+、Oracle 都支持该写法;SQLite 也支持,但旧版(DISTINCT 在
COUNT中 - 注意别写成
COUNT(product_id)——那只是非 NULL 的行数,不是去重后的种类数
遇到重复商品但需按 SKU 统计时怎么办
有些场景下,同一 product_id 可能对应不同规格(如颜色、尺寸),这时应基于更细粒度的唯一标识统计,比如 sku_id 或组合字段。
例如用 (product_id, variant_code) 作为组合键:
SELECT order_id,
COUNT(DISTINCT CONCAT(product_id, '-', variant_code)) AS sku_count
FROM order_items
GROUP BY order_id;
- 用
CONCAT拼接是常见做法,但要注意 NULL 会导致整条结果为 NULL;可用COALESCE防御:CONCAT(COALESCE(product_id, '0'), '-', COALESCE(variant_code, 'N/A')) - PostgreSQL 可直接用
COUNT(DISTINCT (product_id, variant_code))(元组去重),MySQL 不支持元组形式的 DISTINCT,必须拼接或用子查询 - 性能上,
COUNT(DISTINCT)在大数据量时可能较慢,如有索引,优先建在(order_id, product_id)上
当需要排除已取消的商品行时加 WHERE 过滤
订单明细里常混有已取消、退货等无效记录,直接 GROUP 会污染统计结果。务必先过滤再聚合。
- 典型无效状态字段:如
status IN ('cancelled', 'refunded')或is_valid = 0 - 错误写法:
GROUP BY order_id HAVING status != 'cancelled'——HAVING 作用于分组后,无法剔除单行 - 正确方式:在
WHERE子句中提前筛掉,例如:WHERE status = 'confirmed' - 若状态逻辑复杂(如依赖关联表),建议先用 CTE 或子查询把有效明细拎出来,再 COUNT(DISTINCT)
MySQL 5.6 或旧版本不支持 COUNT(DISTINCT) 怎么办
MySQL 5.6 及更早版本虽支持 COUNT(DISTINCT),但在某些嵌套或联合场景下行为不稳定;部分老系统甚至禁用该语法。此时用子查询模拟是稳妥方案。
SELECT t1.order_id, COUNT(*) AS product_count FROM ( SELECT DISTINCT order_id, product_id FROM order_items WHERE status = 'confirmed' ) t1 GROUP BY t1.order_id;
- 本质是先去重(
SELECT DISTINCT order_id, product_id),再按订单计数 - 比直接
COUNT(DISTINCT)多一次临时表生成,大数据量时略慢,但语义清晰、兼容性强 - 注意子查询里也要带上必要的
WHERE条件,否则去重范围过大
product_id 是否真能代表“种类”——比如是否已归一化、有没有空值、有没有业务上视为同一类但 ID 不同的情况。这些细节不处理,统计结果看着对,其实已经偏了。

















