MySQL先进先出(FIFO)库存扣减:日期优先扣减库存
2025-03-07 17:23:06
MySQL 中如何根据日期扣减库存数量(先进先出)
遇到一个数据库库存管理问题:需要根据生产日期(或入库日期),按照先进先出(FIFO)的原则,从表中扣减指定数量的库存。换句话说,要优先扣减日期较早的库存记录。
原始的提问中已经指出了这是个重复问题, 指向了另一个问题的链接: How to Manage FIFO rule for reducing stock in a Point of Sale 但没有很好地解决我的具体情景. 我将在这里更系统, 更完善地讲解我的做法和各种可能.
表结构和示例数据
表结构如下:
CREATE TABLE ARTICOLISCADENZA(
CODARTICOLO int NOT NULL,
QUANTITA INT,
DATA DATE,
LOTTO VARCHAR(200),
FOREIGN KEY(CODARTICOLO) REFERENCES ARTICOLIDETT(CODARTICOLO)
ON DELETE CASCADE
ON UPDATE CASCADE
);
示例数据:
CODARTICOLO | QUANTITA | DATA | LOTTO |
---|---|---|---|
1503 | 4 | 2025-01-10 | ... |
1503 | 2 | 2025-02-10 | ... |
问题分析
我们需要编写一个或一组查询, 实现以下逻辑:对于给定的商品(CODARTICOLO
)和要扣减的数量,从最早日期的库存记录开始扣减,直到扣减完为止。如果一条记录的库存不足以扣减,则继续扣减下一条(日期稍晚)记录。
例如,要从 CODARTICOLO = 1503
的商品中扣减 5 个数量,应该先扣减 DATA = '2025-01-10'
的 4 个,再扣减 DATA = '2025-02-10'
的 1 个。
解决方案
下面提供几种不同的解决方案,各有优缺点。
方案一:使用循环和临时表(存储过程)
最直观的方法是使用存储过程,通过循环逐条处理库存记录。
-
原理:
- 创建一个临时表,按日期升序存储需要扣减库存的记录。
- 循环遍历临时表,逐条更新
ARTICOLISCADENZA
表中的库存数量。 - 如果当前记录的库存足够扣减,则直接更新数量并结束循环。
- 如果当前记录的库存不足以扣减,则将该记录数量更新为 0,并更新剩余要扣减的数量,继续循环处理下一条记录。
-
代码示例:
DELIMITER //
CREATE PROCEDURE SubtractStock(IN articleCode INT, IN quantityToSubtract INT)
BEGIN
DECLARE remainingQuantity INT DEFAULT quantityToSubtract;
DECLARE currentDate DATE;
DECLARE currentQuantity INT;
DECLARE done INT DEFAULT FALSE;
-- 创建临时表
CREATE TEMPORARY TABLE IF NOT EXISTS TempStock (
ID INT AUTO_INCREMENT PRIMARY KEY,
DATA DATE,
QUANTITA INT
);
-- 禁用外键检查
SET foreign_key_checks = 0;
-- 清空临时表
TRUNCATE TABLE TempStock;
-- 将需要处理的数据插入临时表,按日期排序
INSERT INTO TempStock (DATA, QUANTITA)
SELECT DATA, QUANTITA
FROM ARTICOLISCADENZA
WHERE CODARTICOLO = articleCode
ORDER BY DATA ASC;
-- 声明游标
DECLARE cur CURSOR FOR SELECT DATA, QUANTITA FROM TempStock;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 开启事务
START TRANSACTION;
OPEN cur;
read_loop: LOOP
FETCH cur INTO currentDate, currentQuantity;
IF done THEN
LEAVE read_loop;
END IF;
IF remainingQuantity > 0 THEN
IF currentQuantity >= remainingQuantity THEN
-- 库存足够,直接扣减
UPDATE ARTICOLISCADENZA
SET QUANTITA = QUANTITA - remainingQuantity
WHERE CODARTICOLO = articleCode AND DATA = currentDate;
SET remainingQuantity = 0;
LEAVE read_loop;
ELSE
-- 库存不足,扣减当前记录并继续
UPDATE ARTICOLISCADENZA
SET QUANTITA = 0
WHERE CODARTICOLO = articleCode AND DATA = currentDate;
SET remainingQuantity = remainingQuantity - currentQuantity;
END IF;
END IF;
END LOOP;
CLOSE cur;
-- 提交事务
COMMIT;
-- 删除临时表
DROP TEMPORARY TABLE IF EXISTS TempStock;
-- 启用外键检查
SET foreign_key_checks = 1;
END //
DELIMITER ;
-- 调用存储过程
CALL SubtractStock(1503, 5);
-
安全建议:
- 在存储过程中使用事务(
START TRANSACTION
和COMMIT
),确保数据的一致性。 - 禁用再重新启用外键检查.
- 在存储过程中使用事务(
-
进阶技巧
- 可以在更新
ARTICOLISCADENZA
数量的UPDATE
语句后加入ROW_COUNT()
判断更新了多少行. 进而判断是否真的更新成功.
- 可以在更新
方案二:使用变量和单条 UPDATE 语句(较为复杂)
这种方法尝试只用一条 UPDATE 语句完成所有扣减操作,避免了循环,但逻辑比较复杂。
-
原理:
- 使用用户定义的变量来跟踪剩余要扣减的数量。
- 通过
CASE
表达式判断当前记录的库存是否足够扣减。 - 使用
ORDER BY
子句确保按日期升序处理。 @remaining
这个用户变量,必须和UPDATE的表在同一个连接里, 如果不是同一个连接, UPDATE执行时获取不到这个变量。
-
代码示例:
SET @remaining := 5; -- 要扣减的总数量
UPDATE ARTICOLISCADENZA
SET QUANTITA =
CASE
WHEN @remaining <= 0 THEN QUANTITA -- 已经扣减完毕,数量不变
WHEN QUANTITA >= @remaining THEN QUANTITA - @remaining -- 库存足够,直接扣减
ELSE 0 -- 库存不足,当前记录清零
END,
@remaining := GREATEST(0, @remaining - QUANTITA) -- 更新剩余要扣减的数量
WHERE CODARTICOLO = 1503
ORDER BY DATA;
- 安全建议:
- 复杂的单条UPDATE较难调试,容易出错,出错后排查较为困难.
- 由于使用了变量,需要注意并发情况下的数据一致性问题。在较高并发的场景下,这种方法可能不适用,推荐使用方案一(存储过程)。
- 进阶使用技巧:
- 这个
UPDATE
语句可以和SELECT
组合使用,以在执行更新操作之后立即查看库存结果:
SET @remaining := 5; -- 要扣减的总数量
UPDATE ARTICOLISCADENZA
SET QUANTITA =
CASE
WHEN @remaining <= 0 THEN QUANTITA -- 已经扣减完毕,数量不变
WHEN QUANTITA >= @remaining THEN QUANTITA - @remaining -- 库存足够,直接扣减
ELSE 0 -- 库存不足,当前记录清零
END,
@remaining := GREATEST(0, @remaining - QUANTITA) -- 更新剩余要扣减的数量
WHERE CODARTICOLO = 1503
ORDER BY DATA;
SELECT * FROM ARTICOLISCADENZA WHERE CODARTICOLO = 1503;
方案三: 使用窗口函数(MySQL 8.0 及以上版本)
如果你的 MySQL 版本是 8.0 或更高版本,可以使用窗口函数来简化操作。
-
原理:
- 使用
SUM() OVER (ORDER BY DATA)
计算累积库存量 - 利用
CASE
算出扣除后的剩余量。 GREATEST()
函数确保剩余数量不会小于零.
- 使用
-
代码示例:
WITH CalculatedStock AS (
SELECT
CODARTICOLO,
QUANTITA,
DATA,
LOTTO,
SUM(QUANTITA) OVER (PARTITION BY CODARTICOLO ORDER BY DATA) AS CumulativeQuantity
FROM
ARTICOLISCADENZA
WHERE CODARTICOLO = 1503
),
Remaining as(
SELECT
*,
GREATEST(0, CumulativeQuantity - 5) AS RemainingQuantity -- 5为要减去的库存
FROM
CalculatedStock
)
UPDATE ARTICOLISCADENZA AS t1
INNER JOIN Remaining AS t2
ON t1.CODARTICOLO = t2.CODARTICOLO AND t1.DATA = t2.DATA
SET t1.QUANTITA = CASE
WHEN t2.QUANTITA > t2.RemainingQuantity THEN t2.RemainingQuantity
ELSE t1.QUANTITA
END;
-
进阶使用技巧:
可以根据需要调整窗口函数的
PARTITION BY
和ORDER BY
子句,进行更加复杂的库存计算.
例如不同商品分组, 不同仓库等等.
总结
以上三种方法都可以实现根据日期扣减库存的需求。
- 方案一(存储过程)最为直观和易于理解,适合处理复杂的逻辑和高并发场景。
- 方案二(单条 UPDATE)性能可能稍好,但逻辑复杂,容易出错,且在并发情况下可能存在问题。
- 方案三(窗口函数)需要 MySQL 8.0 或更高版本,代码简洁,但对于不熟悉窗口函数的人来说可能不太容易理解。
- 方案一最安全可靠,推荐使用.
根据实际情况和数据库版本选择合适的解决方案。无论使用哪种方案,记得充分测试,确保业务数据正确性.