直接 GROUP BY 无法正确汇总叶子节点,因为会将所有层级节点(含父类)一并统计,导致父类数据被子节点重复累加而失真;业务只关心无子节点的叶子,需用 NOT EXISTS 或 LEFT JOIN ... IS NULL 判断节点是否为叶子(即其 category_id 不作为任何记录的 parent_id 出现),再聚合销售数据。
多级分类树里,GROUP BY category_id 会把所有层级的节点都算进去,但业务上往往只关心“没有子类”的叶子节点。比如商品类目表里,electronics 是父类,smartphone 和 laptop 是它的子类,而 iphone-15 才是叶子——它下面再没 children。如果直接按 category_id 汇总销售数据,父类数据会被重复累加(因为子节点也归属它),结果就失真。
关键判断逻辑是:一个节点是叶子,当且仅当它在树结构中不作为任何其他节点的 parent_id 出现。
NOT EXISTS 或 LEFT JOIN ... IS NULL 判断是否为叶子IS NULL 直接查 parent_id 字段——根节点可能也是 NULL,但这不代表它是叶子level 或 path 字段,可辅助判断,但不可替代实际子节点存在性检查这是最通用、兼容性最好的写法,适用于 MySQL 5.7+、PostgreSQL、SQL Server 等主流数据库。
SELECT t.category_id, t.name, SUM(s.amount) AS total_salesFROM categories tINNER JOIN sales s ON t.category_id = s.category_idWHERE NOT EXISTS ( SELECT 1 FROM categories c WHERE c.parent_id = t.category_id)GROUP BY t.category_id, t.name;
注意点:
NOT EXISTS 子查询必须关联外层 t.category_id,否则变成全表扫描NOT EXISTS 放在 HAVING 里——HAVING 对的是分组后结果,而叶子判定是行级逻辑sales 表里存在未映射到 categories 的 category_id,需先 INNER JOIN 或加 WHERE s.category_id IN (SELECT category_id FROM categories)
语义更直观,执行计划有时更容易理解,但要注意连接条件写法。
SELECT t.category_id, t.name, SUM(s.amount) AS total_salesFROM categories tINNER JOIN sales s ON t.category_id = s.category_idLEFT JOIN categories c ON c.parent_id = t.category_idWHERE c.category_id IS NULLGROUP BY t.category_id, t.name;
常见错误:
WHERE c.parent_id IS NULL —— 错,应判断被左连接的表主键是否为 NULL
INNER JOIN sales,导致叶子节点即使无销售记录也被列出(而业务通常只要“有销售的叶子”)categories 无索引在 parent_id 上,该查询会明显变慢如果表里存了类似 path = '/1/5/23/' 的字段(如使用闭包表或嵌套集模型),可以跳过子查询,直接用字符串函数判断是否为末级。
例如 PostgreSQL:
SELECT t.category_id, t.name, SUM(s.amount)FROM categories tINNER JOIN sales s ON t.category_id = s.category_idWHERE t.path = (SELECT MAX(c.path) FROM categories c WHERE c.path LIKE t.path || '%')GROUP BY t.category_id, t.name;
但这个写法有陷阱:
MAX(path) 依赖字典序,若 path 是 /1/10/ 和 /1/2/,MAX 会错判 /1/10/ 更大/ 的数量:ARRAY_LENGTH(STRING_TO_ARRAY(TRIM(t.path, '/'), '/'), 1),再和最大深度比对NOT EXISTS 通用可靠叶子节点判定本身不复杂,但容易在“以为某字段为空就是叶子”或“把聚合和过滤顺序搞反”上栽跟头。实际写的时候,先单独跑一遍 SELECT * FROM categories WHERE NOT EXISTS (SELECT 1 FROM categories c WHERE c.parent_id = categories.category_id) 确认叶子集合是否符合预期,再加聚合——这步省不得。