问题
I am try to perform a recursive join of my 2 tables (SQL Server 2012) like below:
Table: Purchase
szProductID nQty szSupplierCode
0001 5 A-101
0001 50 A-102
0001 2 A-103
0001 70 A-104
and
Table: Sales
szProductID nQty szSalesID
0001 10 S-101
0001 20 S-102
0001 20 S-103
0001 50 S-104
And I need my result like this :
szProductID nQtySales SupplierCode SalesID
0001 5 A-101 S-101
0001 5 A-102 S-101
0001 20 A-102 S-102
0001 20 A-102 S-103
0001 5 A-102 S-104
0001 2 A-103 S-104
0001 43 A-104 S-104
The goal is to find how many item sold by szSupplierCode. I've found plenty of examples for doing selects but i'm not sure if CTE can solve my problem.
If anyone can confirm this is possible with a CTE or cursor, I'd appreciate it.
Thanks!
回答1:
You can use a Recursive CTE:
;WITH PurchaseRN AS (
-- Add row number field to Purchase table
SELECT szProductID, nQty, szSupplierCode,
ROW_NUMBER() OVER (PARTITION BY szProductID
ORDER BY szSupplierCode) AS rn
FROM Purchase
), SalesRN AS (
-- Add row number field to Sales table
SELECT szProductID, nQty, szSalesID,
ROW_NUMBER() OVER (PARTITION BY szProductID
ORDER BY szSalesID) AS rn
FROM Sales
), ConsumePurchases AS (
-- Consume 1st Sales record using 1st Purchase record
SELECT p.szProductID,
IIF(p.nQty > s.nQty, s.nQty, p.nQty) AS nQtySales,
p.szSupplierCode AS SupplierCode,
s.szSalesID AS SalesID,
-- Propagate un-consumed Purchase/Sales quantities to next recursion level
IIF(p.nQty > s.nQty, p.nQty - s.nQty, 0) AS pResidue,
IIF(p.nQty > s.nQty, 0, s.nQty- p.nQty) AS sResidue,
-- Purchase row number processed by current recursion level
1 AS prn,
-- Sales row number processed by current recursion level
1 AS srn
FROM PurchaseRN AS p
INNER JOIN SalesRN AS s ON p.szProductID = s.szProductID
WHERE p.rn = 1 AND s.rn = 1
UNION ALL
SELECT p.szProductID,
-- Calculate Sales quantity consumed by current recursion level
-- If un-consumed Purchase/Sales quantities exist from previous level
-- then use this instead of nQty field.
IIF(c.pResidue > 0,
IIF(c.pResidue > s.nQty, s.nQty, c.pResidue),
IIF(c.sResidue > 0,
IIF(p.nQty > c.sResidue, c.sResidue, p.nQty),
IIF(p.nQty > s.nQty, s.nQty, p.nQty))) AS nQtySales,
p.szSupplierCode AS SupplierCode,
s.szSalesID AS SalesID,
x.pResidue,
x.sResidue,
x.prn AS prn,
x.srn AS srn
FROM PurchaseRN AS p
INNER JOIN SalesRN AS s ON p.szProductID = s.szProductID
INNER JOIN ConsumePurchases AS c ON c.szProductID = s.szProductID
CROSS APPLY (
SELECT -- if previous Purchare record is not fully consumed (c.pResidue > 0)
-- then stay at the same Purchase record (c.prn), else get next record.
CASE
WHEN c.pResidue > 0 THEN c.prn
ELSE c.prn + 1
END AS prn,
-- if previous Sales record is not fully consumed (c.sResidue > 0)
-- then stay at the same Sales record (c.srn), else get next record.
CASE
WHEN c.sResidue > 0 THEN c.srn
ELSE c.srn + 1
END AS srn,
-- calculate Sales quantity left un-cosumed (sResidue) after current record
-- has been processed
CASE
WHEN c.sResidue > 0 THEN IIF(c.sResidue - p.nQty > 0, c.sResidue - p.nQty, 0)
WHEN c.pResidue > 0 THEN IIF(c.pResidue > s.nQty, 0, s.nQty - c.pResidue)
ELSE IIF(p.nQty > s.nQty, p.nQty - s.nQty, 0)
END AS sResidue,
-- calculate Purchase quantity left un-cosumed (pResidue) after current record
-- has been processed
CASE
WHEN c.pResidue > 0 THEN IIF(c.pResidue - s.nQty > 0, c.pResidue - s.nQty, 0)
WHEN c.sResidue > 0 THEN IIF(p.nQty > c.sResidue, p.nQty - c.sResidue, 0)
ELSE IIF(p.nQty > s.nQty, p.nQty - s.nQty, 0)
END AS pResidue) AS x(prn, srn, sResidue, pResidue)
-- Continue until there are no more Purchase/Sales records to process
WHERE p.rn = x.prn AND s.rn = x.srn
)
SELECT szProductID, nQtySales, SupplierCode, SalesID
FROM ConsumePurchases
Demo here
回答2:
Thank you Giorgos...
I've been working in more than 3days for this and you did it in a day...
But i did it with different approach and with more than 200 lines...:)
first i did it by calculate all the purchase and sales (total) like below:
Table: PurchaseTmp
szProductID szSupplierCode nQtyPurchase nQtySold
0001 A-101 5 5
0001 A-102 50 50
0001 A-103 2 2
0001 A-104 70 43
After that i iterate row by row...and it makes more than 200 lines of code by using Fetch Next Statement..
Again..thank you Giorgos...
This question is answered.
回答3:
There's a high probability that cursors/recursion can be rewritten using Windowed Aggregate Functions. In your case it can be done by calculating cumulative sums of the purchased/sold quantities and then join on overlapping ranges:
with a as
(
SELECT id, szProductID, szSupplierCode, nQty,
--cumulative sum of quantities
SUM(nQty) OVER (PARTITION BY szProductID ORDER BY id ROWS UNBOUNDED PRECEDING) AS cumsum
FROM Purchase
)
, b as
(
SELECT id, szProductID, szSalesID, nQty,
--cumulative sum of quantities
SUM(nQty) OVER (PARTITION BY szProductID ORDER BY id ROWS UNBOUNDED PRECEDING) AS cumsum
FROM Sales
)
SELECT
a.szSupplierCode,b.szSalesID,
-- calculate the assigned quantity
CASE WHEN a.cumsum < b.cumsum THEN a.cumsum ELSE b.cumsum END
-CASE WHEN a.cumsum -a.nQty > b.cumsum - b.nQty THEN a.cumsum - a.nQty ELSE b.cumsum - b.nQty END
FROM a
JOIN b
ON a.szProductID = b.szProductID
AND a.cumsum > b.cumsum - b.nQty -- check for overlapping cumultive sums
AND a.cumsum - a.nQty < b.cumsum
id
is any column determining the sort order, e.g. a date or szSalesID
/szSupplierCode
.
SQL Server doesn't support LEAST
/GREATEST
, otherwise the quantity calculation would be easier:
LEAST(a.cumsum, b.cumsum) - GREATEST(a.cumsum -a.nQty, b.cumsum - b.nQty)
I highjacked @GiorgosBetsos fiddle :)
来源:https://stackoverflow.com/questions/33094828/sql-server-cte-with-2-tables-until-qty-consumed