如何筛选各分类子分类下start为null且日期最早的库存记录?
问题原因分析
初始查询的问题
你的初始查询存在非聚合列未包含在GROUP BY中的核心问题:
SELECT MIN(date) as date, category, subcategory, description, code, inventory.index FROM inventory WHERE start is null GROUP BY category, subcategory
当你按category和subcategory分组时,只有这两个列和聚合函数MIN(date)的结果是确定的。而description、code、index这些列既不在分组条件里,也没有通过聚合函数处理,在多数SQL数据库(比如关闭ONLY_FULL_GROUP_BY模式的MySQL)中,会随机选取同组内某一行的对应值,这就导致了"混合行"——日期是分组内最早的,但其他列却来自同组的其他记录。
第一个嵌套SELECT方案的问题
这个写法逻辑完全错误:
SELECT * FROM inventory WHERE ( SELECT MIN(date) FROM inventory ) AND start is null GROUP BY category, subcategory
子查询SELECT MIN(date) FROM inventory返回的是整个表的最小日期值,在WHERE条件里单独放这个值会被当作布尔值(非零即真),等价于只过滤start is null,再加上GROUP BY的原有问题,自然达不到需求。
第二个INNER JOIN方案的问题
子查询部分和初始查询犯了同样的错误:GROUP BY后选取的description、code、index是随机的,所以子查询得到的index大概率不是对应分组内MIN(date)那条记录的索引,JOIN后自然返回错误结果,甚至遗漏正确行。
正确解决方案
方案1:使用窗口函数(推荐)
用ROW_NUMBER()窗口函数给每个category+subcategory分组内的记录按date升序编号,取编号为1的记录(即日期最早的那条):
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY category, subcategory ORDER BY date ASC ) AS rn FROM inventory WHERE start IS NULL ) AS ranked WHERE rn = 1;
这个方法逻辑清晰,能确保返回每个分组内date最早的完整记录,不会出现混合行问题。如果同一分组内有多个记录日期完全相同,只会返回其中一条(若需返回所有相同日期的记录,可改用RANK()函数)。
方案2:关联子查询匹配分组最小日期
先找出每个category+subcategory分组内的最小date,再关联回原表找到对应完整记录:
SELECT inv.* FROM inventory inv WHERE inv.start IS NULL AND inv.date = ( SELECT MIN(date) FROM inventory WHERE category = inv.category AND subcategory = inv.subcategory AND start IS NULL );
这个方法不需要窗口函数,兼容不支持窗口函数的老版本数据库。如果同一分组内有多个记录日期相同,会返回所有这些记录。
方案3:JOIN分组最小日期表
先通过GROUP BY得到每个分组的最小date,再和原表JOIN匹配完整记录:
SELECT inv.* FROM inventory inv INNER JOIN ( SELECT category, subcategory, MIN(date) AS min_date FROM inventory WHERE start IS NULL GROUP BY category, subcategory ) AS group_min ON inv.category = group_min.category AND inv.subcategory = group_min.subcategory AND inv.date = group_min.min_date AND inv.start IS NULL;
这个方法和方案2逻辑类似,同样要注意同一分组内多记录日期相同的情况,会返回所有匹配记录。
内容的提问来源于stack exchange,提问作者Jefferson Paul Jones

