问题描述
就像任何零售业务一样,我们都有一个Orders表和一个库存表。我想做的是检查有足够库存可发送的订单。我需要考虑的几件事:
-
如果一个订单中的所有项目均可用,则认为该订单是“可交付的”
-
按
OrderID
(int
值)的顺序检查订单的可交付状态,即OrderID = 1
然后是2,依此类推。 -
如果我们的库存不足以容纳订单中的1个或多个项目,请完全忽略该订单,并且不要减少下一个要检查的订单的可用库存量。
在以下示例中:
-
Order = 100
可以完全交付,因为我们有足够的库存来容纳所有产品。 -
Order = 200
不能完全交付,因为PID 2需要数量5,但是在100被订单100消耗之后,我们只剩下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,3,1) --/,(200,5) ----> This order is not fully available,1) --/ because of PID 2 only 3 QTY left,(300,1); --/
预期输出:
OrderID Status
------------------------
100 Deliverable
200 NOT Deliverable
300 Deliverable
我的尝试:我知道这与实际解决方案相去甚远,但我仍想分享自己一直在尝试的方法:)
WITH OrderCTE AS
(
SELECT
DENSE_RANK() OVER (ORDER BY OrderID) AS OrderRN,OrderID,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.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
解决方法
以下方法无法产生正确的结果。当我将所有片段放在一起时,我得到了:
+---------+--------------------+
| OrderID | OrderIsDeliverable |
+---------+--------------------+
| 100 | 1 |
| 200 | 0 |
| 300 | 0 |
+---------+--------------------+
Order=300
被标记为不可交付,因为我的查询独立处理所有产品,这是不正确的。尽管该Order=200
不能整体交付(基于PID=3
以外的产品),但先前的Order=200
却占用了PID=3
的数量,并且不会影响以下订单。但这确实影响了以下顺序,这是不正确的。
我看不到如何在没有显式循环的情况下编写单个查询。
A。
您可以使用递归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.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等),确保您拥有适当的索引,测量它们在实际数据和硬件上的性能,并决定使用哪种方法。
,编辑: 因为我雄心勃勃,所以我现在也找到了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,DeliverableQty INT NOT NULL)
INSERT INTO @OrdersDeliverableQty
(OrderID,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,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,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))
INSERT INTO @Inventory
(PID,Qty)
VALUES (1,10),(2,6),(3,5)
INSERT INTO @Order
(OrderID,Qty)
VALUES (100,2),(100,2,3,(200,5),1),(300,0),(400,(500,(600,(700,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))
INSERT INTO @OrderQty
(OrderID,DeliverableQty INT NOT NULL)
INSERT INTO @OrdersDeliverableQty
(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.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.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,@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