无需curson或While循环即可通过减少库存数量来获取可以交付的订单状态
Posted
技术标签:
【中文标题】无需curson或While循环即可通过减少库存数量来获取可以交付的订单状态【英文标题】:Get Orders status that can be delivered with reducing quantity from stock without curson or While loop 【发布时间】:2020-09-03 16:04:02 【问题描述】:就像任何零售企业一样,我们有一个订单表和一个库存表。我正在尝试做的是检查我们有足够库存可供发货的订单。我需要考虑的几件事:
如果一个订单中的所有商品仅可用,则将此订单视为“可交付”
按照OrderID
(int
值)的顺序检查订单的可交付状态。即OrderID = 1
然后是2,依此类推。
在检查下一个订单的可交付性之前,减少下一个订单的可用库存(不更新 Inventory 表,只考虑上一个订单已经消耗的库存数量)。
如果我们没有足够的库存用于订单中的 1 件或多件商品,请完全忽略该订单,并且不要减少可用库存数量以供下一个要检查的订单使用。
在以下示例中:
Order = 100
完全可以交付,因为我们有足够的库存用于所有产品。
Order = 200
无法完全交付,因为 PID 2 需要数量 5,但在订单 100 消耗 2 后我们只剩下 3 个
最后,Order = 300
也完全可以交付,因为我们有足够的库存用于所有产品。
测试数据
INSERT INTO @Inventory (PID, Qty)
VALUES (1 , 10)
, (2 , 5)
, (3 , 2)
INSERT INTO @Order (OrderID, PID, Qty)
VALUES (100 , 1 , 2) --\
, (100 , 2 , 2) ----> This order is fully available
, (100 , 3 , 1) --/
, (200 , 1 , 2) --\
, (200 , 2 , 5) ----> This order is not fully available
, (200 , 3 , 1) --/ because of PID 2 only 3 QTY left
, (300 , 1 , 2) --\
, (300 , 2 , 2) ----> This order is fully available
, (300 , 3 , 1); --/
预期输出:
OrderID Status
------------------------
100 Deliverable
200 NOT Deliverable
300 Deliverable
我的尝试:我知道这与实际的解决方案相去甚远,但我仍然想分享我一直在尝试的方法:)
WITH OrderCTE AS
(
SELECT
DENSE_RANK() OVER (ORDER BY OrderID) AS OrderRN
, OrderID
, PID
, Qty
FROM
@Order
)
, CTE AS
(
SELECT
o.OrderID
, o.PID
, o.Qty
, i.Qty - o.Qty AS QtyAvailable
, o.OrderRN AS OrderRN
FROM
OrderCTE o
INNER JOIN
@Inventory i ON i.PID = o.PID
WHERE
o.OrderID IN (SELECT TOP 1 o.OrderID
FROM @Order o
WHERE NOT EXISTS (SELECT 1 FROM @Inventory i
WHERE i.PID = o.PID AND i.Qty < o.Qty)
ORDER BY o.OrderID)
UNION ALL
SELECT
o.OrderID
, o.PID
, o.Qty
, o.Qty - c.QtyAvailable
, c.OrderRN + 1
FROM
OrderCTE o
INNER JOIN
@Inventory i ON i.PID = o.PID
INNER JOIN
CTE c ON c.OrderRN + 1 = o.OrderRN AND c.PID = o.PID
WHERE
o.Qty <= c.QtyAvailable
)
SELECT *
FROM CTE
【问题讨论】:
至少有礼貌链接your duplicate post 对于逻辑生活来说,迭代解决方案是你所需要的,恐怕。 【参考方案1】:以下方法不会产生正确的结果。当我把所有部分放在一起时,我得到了:
+---------+--------------------+
| OrderID | OrderIsDeliverable |
+---------+--------------------+
| 100 | 1 |
| 200 | 0 |
| 300 | 0 |
+---------+--------------------+
Order=300
被标记为不可交付,因为我的查询独立处理所有产品,这是不正确的。之前的Order=200
占用了PID=3
的数量,尽管这个Order=200
总体上无法交付(基于PID=3
以外的产品)并且它不应该影响以下订单。但它确实影响了以下命令,这是不正确的。
我看不到如何在此处编写没有显式循环的单个查询。
唉。
您可以使用递归 CTE 模拟循环。
我将向您展示一个查询,它完成核心工作,其余的留给您,因为总体而言它变得太长了。
主要思想 - 您需要一个在达到阈值时重置的运行总计。关于这个话题有很多问题,我以this作为回答的基础。
在下面的查询中,我只查看您的数据片段,仅查看一个特定的PID = 2
。
CTE_RN
为我们提供了要迭代的行号。 CTE_Recursive
是检查运行总数是否超过限制的主循环。如果是,它会从该订单中丢弃 Qty
并设置 OrderIsDeliverable
标志。
查询
WITH
CTE_RN
AS
(
SELECT
O.OrderID
,O.PID
,O.Qty
,I.Qty AS LimitQty
,ROW_NUMBER() OVER (ORDER BY O.OrderID) AS rn
FROM
@Order AS O
INNER JOIN @Inventory AS I ON I.PID = O.PID
WHERE O.PID = 2 -- this would become a parameter
)
,CTE_Recursive
AS
(
SELECT
CTE_RN.OrderID
,CTE_RN.PID
,CTE_RN.Qty
,CTE_RN.LimitQty
,CTE_RN.rn
-- this would generate a simple running total
--,CTE_RN.Qty AS SumQty
-- the very first order may exceed the limit
,CASE WHEN CTE_RN.Qty > CTE_RN.LimitQty
THEN 0
ELSE CTE_RN.Qty
END AS SumQty
,CASE WHEN CTE_RN.Qty > CTE_RN.LimitQty
THEN 0
ELSE 1
END AS OrderIsDeliverable
FROM
CTE_RN
WHERE
CTE_RN.rn = 1
UNION ALL
SELECT
CTE_RN.OrderID
,CTE_RN.PID
,CTE_RN.Qty
,CTE_RN.LimitQty
,CTE_RN.rn
-- this would generate a simple running total
--,CTE_RN.Qty + CTE_Recursive.SumQty AS SumQty
-- check if running total exceeds the limit
,CASE WHEN CTE_RN.Qty + CTE_Recursive.SumQty > CTE_RN.LimitQty
THEN CTE_Recursive.SumQty -- don't increase the running total
ELSE CTE_RN.Qty + CTE_Recursive.SumQty
END AS SumQty
,CASE WHEN CTE_RN.Qty + CTE_Recursive.SumQty > CTE_RN.LimitQty
THEN 0
ELSE 1
END AS OrderIsDeliverable
FROM
CTE_RN
INNER JOIN CTE_Recursive ON CTE_Recursive.rn + 1 = CTE_RN.rn
)
SELECT * FROM CTE_Recursive
;
结果
+---------+-----+-----+----------+----+--------+--------------------+
| OrderID | PID | Qty | LimitQty | rn | SumQty | OrderIsDeliverable |
+---------+-----+-----+----------+----+--------+--------------------+
| 100 | 2 | 2 | 5 | 1 | 2 | 1 |
| 200 | 2 | 5 | 5 | 2 | 2 | 0 |
| 300 | 2 | 2 | 5 | 3 | 4 | 1 |
+---------+-----+-----+----------+----+--------+--------------------+
现在您需要为每个PID
运行此查询。我会将此查询包装到带有参数的table-valued function 中,并将PID
作为参数传递。也许你也可以在没有函数的情况下做到这一点。显然,要创建一个不能有表变量的函数,您需要在函数中引用实际的表,因此请相应地调整代码。
然后这样称呼它:
SELECT
...
FROM
@Inventory AS I
CROSS APPLY dbo.MyFunc(I.PID) AS A
这将返回与@Order
表中相同的行数。然后您需要按 OrderID 对其进行分组并查看OrderIsDeliverable
标志。如果一个订单的此标志至少为一次0
,则该订单不可交付。
类似这样的:
SELECT
A.OrderID
,MIN(OrderIsDeliverable) AS OrderIsDeliverable
FROM
@Inventory AS I
CROSS APPLY dbo.MyFunc(I.PID) AS A
GROUP BY
A.OrderID
;
理想情况下,您应该尝试各种方法(游标、递归 CTE 等),确保您有合适的索引,在您的真实数据和硬件上测量它们的性能,然后决定使用哪一种。
【讨论】:
我尝试了类似的方法,但遇到了问题。假设您对产品 1 进行计算,这会删除一些订单,然后为产品 2 运行,这会删除一些较早的订单。一些通过产品 1 删除的订单现在可能能够履行,因为列表中的先前订单已通过产品 2 删除)。这可能会改变产品 2 的可用性。我尝试通过递归 CTE 来实现(按订单进行,确定所有产品的标志,然后转到下一个)。但是在递归 CTE 中不允许聚合,例如 MIN(product_available_flag)。 @seanb,是的。您不仅需要一个循环,还需要两个嵌套循环。此外,仅能看到前一行是不够的,我们需要查看前一个Order
的所有行,并以原子方式“处理”Order
的所有行。换句话说,只看一个运行总数是不够的。我们需要一次维护多个运行总计。在您的示例中,有 3 个 PID,因此我们需要 3 个运行总计。毕竟,它看起来像带有显式循环的经典程序方法更适合这里。
感谢@Vladimir,这很好地总结了这个问题。我不是 OP,但我尝试将其作为一个有趣的问题来回答,并在递归 CTE 中进行练习——但在那一点上卡住了。我考虑一次在一个订单内对一种产品进行递归 CTE,但在您的评论结束时放弃并得出结论 - 只需循环一次,因为即使我想出了答案,它也太复杂了。但是当您发布表值函数可能会有所帮助时,我也很感兴趣。
@seanb,实际上,也许可以用 CTE 解决原来的问题。我们只需要一个方法来拥有一个值数组。我们需要存储的不是单个运行总计,而是与产品一样多的值。我们需要一系列运行总计。在 SQL Server 中实现它的一种方法是让varbinary
足够长,并在其中打包整数 4 字节值。总的来说,它并不复杂,只是丑陋而繁琐。【参考方案2】:
编辑: 因为我雄心勃勃,所以我现在也找到了 CTE 的解决方案。如果您发现任何错误或不正确的结果,请给我的反馈。我的旧光标解决方案如下。
带有 CTE 的新代码:
DECLARE @OrderQty TABLE
(OrderID INT NOT NULL,
PID INT NOT NULL,
CountOfOrder INT NOT NULL,
StockQty INT NOT NULL,
Qty INT NOT NULL,
DeliverableOrderQty INT NOT NULL,
PRIMARY KEY CLUSTERED(OrderID,PID))
INSERT INTO @OrderQty
(OrderID, PID, CountOfOrder, StockQty, Qty, DeliverableOrderQty)
SELECT o.OrderID,
o.PID,
foo.CountOfOrder,
foo.StockQty,
o.Qty,
foo.StockQty / IIF(o.Qty = 0,1,o.Qty) AS DeliverableOrderQty
FROM @Order AS o
INNER JOIN (SELECT o.PID,
COUNT(DISTINCT o.OrderID) AS CountOfOrder,
i.Qty AS StockQty,
SUM(o.Qty) AS TotalOrderOty
FROM @Order AS o
INNER JOIN @Inventory AS i ON o.PID = i.PID
GROUP BY o.PID,
i.Qty) AS foo ON o.PID = foo.PID
DECLARE @OrdersDeliverableQty TABLE
(OrderID INT NOT NULL PRIMARY KEY,
CountOfOrder INT NOT NULL,
DeliverableQty INT NOT NULL)
INSERT INTO @OrdersDeliverableQty
(OrderID, CountOfOrder, DeliverableQty)
SELECT oq.OrderID,
oq.CountOfOrder,
MIN(oq.DeliverableOrderQty) AS DeliverableQty
FROM @OrderQty AS oq
GROUP BY oq.OrderID,
oq.CountOfOrder
DECLARE @AllOrders TABLE
(OrderID INT NOT NULL PRIMARY KEY)
INSERT INTO @AllOrders
(OrderID)
SELECT o.OrderID
FROM @Order AS o
GROUP BY o.OrderID
DECLARE @DeliverableOrder TABLE
(OrderID INT NOT NULL PRIMARY KEY);
WITH CTE_1(RankID, OrderID, PID, StockQty, Qty)
AS (SELECT RANK() OVER(
ORDER BY oq.PID,
oq.DeliverableOrderQty DESC,
oq.Qty,
oq.OrderID) AS RankID,
oq.OrderID,
oq.PID,
oq.StockQty,
oq.Qty
FROM @OrderQty AS oq
INNER JOIN @OrdersDeliverableQty AS ohmttoq ON oq.OrderID = ohmttoq.OrderID
AND oq.DeliverableOrderQty = ohmttoq.DeliverableQty),
CTE_2(MinRankID, MaxRankID)
AS (SELECT MIN(c.RankID) AS MinRankID,
MAX(c.RankID) AS MaxRankID
FROM CTE_1 AS c),
CTE_3(NextRankID, MaxRankID, RankID, OrderID, PID, StockQty, RestQty, Qty)
AS (SELECT c2.MinRankID + 1 AS NextRankID,
c2.MaxRankID AS MaxRankID,
c.RankID,
c.OrderID,
c.PID,
c.StockQty,
c.StockQty - c.Qty AS RestQty,
c.Qty
FROM CTE_1 AS c
INNER JOIN CTE_2 AS c2 ON c.RankID = c2.MinRankID
UNION ALL
SELECT c3.NextRankID + 1 AS NextRankID,
c3.MaxRankID,
c3.NextRankID,
c1.OrderID,
c1.PID,
c1.StockQty,
CASE
WHEN c3.PID = C1.PID
THEN c3.RestQty
ELSE c1.StockQty
END - c1.Qty AS RestQty,
c1.Qty
FROM CTE_3 AS c3
INNER JOIN CTE_1 AS c1 ON c3.NextRankID = c1.RankID
WHERE c3.NextRankID <= c3.MaxRankID)
INSERT INTO @DeliverableOrder
(OrderID)
SELECT c.OrderID
FROM CTE_3 AS c
WHERE c.RestQty >= 0
SELECT ao.OrderID,
CASE
WHEN oo.OrderID IS NULL
THEN 'NOT Deliverable'
ELSE 'Deliverable'
END AS STATUS
FROM @AllOrders AS ao
LEFT JOIN @DeliverableOrder AS oo ON ao.OrderID = oo.OrderID
测试数据:
DECLARE @Inventory TABLE
(PID INT NOT NULL PRIMARY KEY,
Qty INT NOT NULL)
DECLARE @Order TABLE
(OrderID INT NOT NULL,
PID INT NOT NULL,
Qty INT NOT NULL,
PRIMARY KEY CLUSTERED(OrderID,PID))
INSERT INTO @Inventory
(PID, Qty)
VALUES (1,10),
(2,6),
(3,5)
INSERT INTO @Order
(OrderID, PID, Qty)
VALUES (100,1,2), (100,2,2), (100,3,2),
(200,1,2), (200,2,5), (200,3,1),
(300,1,2), (300,2,2), (300,3,0),
(400,1,2), (400,2,1), (400,3,2),
(500,1,5), (500,2,5), (500,3,5),
(600,1,1), (600,2,1), (600,3,1),
(700,1,0), (700,2,1), (700,3,1)
结果:
OrderID Status
100 Deliverable
200 NOT Deliverable
300 Deliverable
400 NOT Deliverable
500 NOT Deliverable
600 Deliverable
700 Deliverable
如果您需要更多信息或解释,请发表评论。
带光标的旧代码:
DECLARE @OrderQty TABLE
(OrderID INT NOT NULL,
PID INT NOT NULL,
CountOfOrder INT NOT NULL,
StockQty INT NOT NULL,
Qty INT NOT NULL,
DeliverableOrderQty INT NOT NULL,
PRIMARY KEY CLUSTERED(OrderID,PID))
INSERT INTO @OrderQty
(OrderID, PID, CountOfOrder, StockQty, Qty, DeliverableOrderQty)
SELECT o.OrderID,
o.PID,
foo.CountOfOrder,
foo.StockQty,
o.Qty,
foo.StockQty / IIF(o.Qty = 0,1,o.Qty) AS DeliverableOrderQty
FROM @Order AS o
INNER JOIN (SELECT o.PID,
COUNT(DISTINCT o.OrderID) AS CountOfOrder,
i.Qty AS StockQty,
SUM(o.Qty) AS TotalOrderOty
FROM @Order AS o
INNER JOIN @Inventory AS i ON o.PID = i.PID
GROUP BY o.PID,
i.Qty) AS foo ON o.PID = foo.PID
DECLARE @OrdersDeliverableQty TABLE
(OrderID INT NOT NULL PRIMARY KEY,
CountOfOrder INT NOT NULL,
DeliverableQty INT NOT NULL)
INSERT INTO @OrdersDeliverableQty
(OrderID, CountOfOrder, DeliverableQty)
SELECT oq.OrderID,
oq.CountOfOrder,
MIN(oq.DeliverableOrderQty) AS DeliverableQty
FROM @OrderQty AS oq
GROUP BY oq.OrderID,
oq.CountOfOrder
DECLARE @AllOrders TABLE
(OrderID INT NOT NULL PRIMARY KEY)
INSERT INTO @AllOrders
(OrderID)
SELECT o.OrderID
FROM @Order AS o
GROUP BY o.OrderID
DECLARE @DeliverableOrder TABLE
(OrderID INT NOT NULL PRIMARY KEY)
DECLARE @OrderID INT,
@PID INT,
@StockQty INT,
@Qty INT
DECLARE @LastPIDCursor INT
DECLARE @QtyRest INT
DECLARE order_qty_cursor CURSOR
FOR SELECT oq.OrderID,
oq.PID,
oq.StockQty,
oq.Qty
FROM @OrderQty AS oq
INNER JOIN @OrdersDeliverableQty AS ohmttoq ON oq.OrderID = ohmttoq.OrderID
AND oq.DeliverableOrderQty = ohmttoq.DeliverableQty
ORDER BY oq.PID,
oq.DeliverableOrderQty DESC,
oq.Qty
OPEN order_qty_cursor
FETCH NEXT FROM order_qty_cursor INTO @OrderID,
@PID,
@StockQty,
@Qty
WHILE @@Fetch_Status = 0
BEGIN
IF @LastPIDCursor IS NULL
OR @LastPIDCursor <> @PID
BEGIN
SET @QtyRest = @StockQty - @Qty
END
ELSE
BEGIN
SET @QtyRest = @QtyRest - @Qty
END
IF @QtyRest >= 0
AND NOT EXISTS (SELECT 1
FROM @DeliverableOrder
WHERE OrderID = @OrderID)
BEGIN
INSERT INTO @DeliverableOrder
(OrderID)
VALUES
(@OrderID)
END
SET @LastPIDCursor = @PID
FETCH NEXT FROM order_qty_cursor INTO @OrderID,
@PID,
@StockQty,
@Qty
END
CLOSE order_qty_cursor
DEALLOCATE order_qty_cursor
SELECT ao.OrderID,
CASE
WHEN oo.OrderID IS NULL
THEN 'NOT Deliverable'
ELSE 'Deliverable'
END AS STATUS
FROM @AllOrders AS ao
LEFT JOIN @DeliverableOrder AS oo ON ao.OrderID = oo.OrderID
【讨论】:
这是一个不幸的查询没有产生正确结果的例子。将Orders
表(200 , 2 , 5)
中的这一行更改为(200 , 2 , 3)
。它应该使OrderID=200
可交付,因为PID=2
现在有足够的库存用于OrderID=100
和OrderID=200
。 OrderID=300
应该变为不可交付,因为前两个订单已耗尽库存。您的查询不会产生此结果。
@VladimirBaranov 我重构了我的代码,现在我使用临时表来排列数据和一个光标来获取结果
嗯,这个问题的重点是在没有显式循环或光标的情况下进行操作。你的答案没有错,使用光标来完成这个任务似乎没问题,但是 OP 想找到一个没有它的解决方案。
@VladimirBaranov 你说得对,我的光标解决方案没有被问到,我现在插入了一个 CTE 解决方案。以上是关于无需curson或While循环即可通过减少库存数量来获取可以交付的订单状态的主要内容,如果未能解决你的问题,请参考以下文章
机械设备行业库存压力大怎么办?应用数商云供应链管理系统实现智能化库存管理