库存盘点几乎是每个有仓储业务系统都绕不开的功能。当出入库流水表膨胀到几十万甚至上千万行时,如果还用程序循环逐条累加,不仅代码臃肿,性能也会被拖垮。其实数据库本身就是为了这类聚合计算而生的,用GROUP BY配合聚合函数,一条SQL语句就能完成按SKU、按仓库、按批次的分类汇总,效率远超应用层手工计算。这篇文章就来系统地讲讲如何用GROUP BY做好库存盘点。

一、GROUP BY的分组原理与执行顺序
要写对分组统计的SQL,先得理解GROUP BY到底做了什么。简单说,GROUP BY会把结果集按照指定字段拆分成若干个小组,每个小组内的行在分组字段上取值相同。随后聚合函数(如SUM、COUNT、AVG、MAX、MIN)会在每个小组内部独立计算,最终每个小组输出一行结果。这就是为什么用了GROUP BY之后,SELECT后面的字段要么是分组字段本身,要么必须被聚合函数包裹——因为每个小组的多行数据被压缩成了一行,没有被分组也没有被聚合的列,数据库根本不知道该取哪一行的值。
还需要掌握SQL的 Logical 执行顺序:FROM → WHERE → GROUP BY → 聚合计算 → HAVING → SELECT → ORDER BY。这个顺序解释了很多常见报错。比如有人想在WHERE里直接写SUM(qty) > 0做过滤,数据库会报错,因为WHERE在分组之前执行,此时聚合结果还不存在。对分组结果的过滤必须放到HAVING里,这是盘点SQL中最常见的误区之一。
二、典型盘点场景:出入库流水汇总
假设有一张出入库流水表stock_log,结构大致如下:SKU编码(sku)、仓库编号(warehouse)、单据类型(io_type,IN表示入库、OUT表示出库)、数量(qty)、发生时间(created_at)。要盘点每个SKU的当前结存,思路是把入库记为正数、出库记为负数,再按SKU分组求和。
SELECT
sku,
warehouse,
SUM(CASE WHEN io_type = 'IN' THEN qty ELSE -qty END) AS balance
FROM stock_log
WHERE created_at <= '2024-06-30 23:59:59'
GROUP BY sku, warehouse
HAVING SUM(CASE WHEN io_type = 'IN' THEN qty ELSE -qty END) <= 0
ORDER BY balance ASC;这条语句里有几个值得注意的细节。WHERE限定截止日期,确保只统计盘点时点之前的流水,这是做静态盘点快照的关键;GROUP BY同时按SKU和仓库分组,得到的是每个仓每个SKU的明细结存,而不是全局总数;HAVING筛出结存小于等于零的记录,这些往往就是库存异常(超发、漏记入库)的线索,盘点时需要重点关注。如果去掉HAVING子句,就是一份完整的分仓分SKU库存余额表。
有时盘点还需要附带库存的价值信息,比如按移动平均法估算库存均价。这时可以在SELECT里追加AVG函数计算加权单价,或者先算出结存量再除以总入库金额,取决于业务对成本口径的定义。聚合函数可以并列写多个,互不干扰,这正是GROUP BY统计的灵活性所在。
三、多维度盘点:分布、滞销与周转分析
单纯的结存数量只是盘点的第一步,管理者更关心库存结构。比如想看每个仓库存了多少个SKU、总量是多少,只需按仓库分组:
SELECT
warehouse,
COUNT(DISTINCT sku) AS sku_count,
SUM(CASE WHEN io_type = 'IN' THEN qty ELSE -qty END) AS total_balance
FROM stock_log
GROUP BY warehouse
ORDER BY total_balance DESC;滞销库存筛查则是另一种典型用法。利用MAX函数取每个SKU最后一次出入库时间,再与当前日期比较,凡是超过比如90天没有动过的SKU,就是滞销候选。这里MAX配合GROUP BY的组合非常实用:
SELECT
sku,
MAX(created_at) AS last_move_time,
TIMESTAMPDIFF(DAY, MAX(created_at), NOW()) AS idle_days
FROM stock_log
GROUP BY sku
HAVING idle_days > 90
ORDER BY idle_days DESC;需要注意的是,MySQL允许在HAVING中引用SELECT里定义的别名,但SQL Server、Oracle等数据库不一定支持,跨库场景下更稳妥的写法是把表达式完整重复一遍,或者改用子查询包一层。另外TIMESTAMPDIFF是MySQL的函数,其他数据库需要换成对应的日期差函数,如SQL Server的DATEDIFF,逻辑完全一致。
四、盘点SQL的常见坑与性能建议
第一个坑是分组粒度过细。如果GROUP BY的字段里包含了类似流水号、时间戳这类几乎不重复的列,每个小组可能只有一行,统计结果退化成明细数据,毫无汇总意义。分组字段应该是真正的维度字段:SKU、仓库、品类、月份等。
第二个坑是NULL值的处理。分组字段为NULL的行会被归到同一个NULL组,容易被忽略。盘点前最好用WHERE sku IS NOT NULL过滤掉脏数据,或者在结果里专门检查NULL组,避免账实不符。
性能方面,GROUP BY在数据量大时会触发排序或哈希聚合,建议给分组字段和WHERE条件字段建复合索引,例如idx_warehouse_sku (warehouse, sku),让索引顺序与分组顺序一致,可以避免额外的排序开销。对于千万级流水表,如果盘点是高频操作,更合理的做法是用触发器或定时任务维护一张库存余额汇总表,把实时聚合变成增量更新,查询时直接读汇总表,GROUP BY只用于对账校验。这样既保证了盘点速度,也保留了一条SQL随时核对的能力,两种手段配合使用才是库存统计的最佳实践。