MS SQL How to avoid loops when allocating amounts - sql

I'm building a system that needs to automatically allocate amounts to a Charge_Detail table.
I have the following tables:
-- a list of charges that are outstanding
DECLARE Charge_Master TABLE
( ID INT NOT NULL,
CompanyID VARCHAR(6) NOT NULL,
EntryDate SMALLDATETIME,
Ref VARCHAR(30) NOT NULL,
Amount DECIMAL(12,2) NOT NULL
)
-- directly linked to master table to represent charges paid in detail
DECLARE Charge_Detail TABLE
( ID INT NOT NULL,
Charge_MasterID INT NOT NULL, --Foreign Key
EntryDate SMALLDATETIME,
Ref VARCHAR(30) NOT NULL,
Amount DECIMAL(12,2) NOT NULL
)
INSERT Charge_Master
VALUES ('ABC123', '01/01/2018', 'INV-111', 25),
('ABC123', '21/03/2018', 'INV-222', 30),
('ABC123', '11/05/2018', 'INV-333', 15)
The objective is to have a query take the following parameters:
CompanyId e.g. 'ABC123'
Amount e.g. 45
Ref e.g. 'REF-142'
Based on the parameters it should work out the records that need to be INSERTED into the Charge_Detail table and the associated Charge_MasterID.
Example:
If the total amount to allocate is 45 against CompanyId: ABC123
then this is the Expected output inserted into the Charge_Detail table.
/* Charge_Master Table
*
* ID CompanyID EntryDate Ref Amount
* 1 ABC123 01/01/2018 INV-111 25
* 2 ABC123 21/03/2018 INV-222 30
* 3 ABC123 11/05/2018 INV-333 15
*/
/* Charge_Detail Table
*
* ID Charge_MasterID EntryDate Ref Amount
* 1 1 12/08/2018 REF-142 25
* 2 2 12/08/2018 REF-142 20 -- cannot fully allocate therefore 10 still remaining to be allocated for next time
*/
What's the best approach with this? Would a CTE help? I'm not too familiar with them but I have tried a SELECT query with subqueries and case statement but I can't get it to decrement the amount remaining without using a loop.
Any advice would be much appreciated!

It should be something like this:
DECLARE #CompanyID VARCHAR(6) = 'ABC123',
#Ref VARCHAR(30) = 'REF-142',
#Amount DECIMAL(12,2) = 45
;WITH cte as (
SELECT *, SUM (Amount) OVER (ORDER BY Id) AS RunningAmount
FROM Charge_Master
), Limits as (
SELECT TopRow = (SELECT MIN(ID) FROM CTE WHERE RunningAmount > #Amount),
LastRow = (SELECT MAX(ID) FROM CTE WHERE RunningAmount < #Amount))
SELECT c.ID, c.EntryDate, #Ref, c.Amount
FROM Limits as l INNER JOIN cte as c ON c.ID < l.TopRow
UNION ALL
SELECT c2.ID, c2.EntryDate, #Ref, #Amount-c1.RunningAmount
FROM Limits as l
INNER JOIN cte as c1 ON c1.ID = l.LastRow
INNER JOIN cte as c2 ON c2.ID = l.TopRow
ORDER BY 1

Related

SQL Server Stored Procedure for Menu Performance Report

I have four tables in my SQL database i.e MenuItems, Categories, Invoices and InvoiceDetails. Now what I want is to show the menu performance report for a certain date i.e total Qty and total Amount for
each menu item for a specific date. It shows the desired result without the date in the where clause but excludes menu items with null values.
Here is my stored procedure:
CREATE PROCEDURE spGetMenuPerformanceByDay
#Date date,
#Terminal int
AS
BEGIN
SELECT
M.Name,
ISNULL(SUM(D.Amount), 0) AS Amount,
ISNULL(SUM(D.Qty), 0) AS Qty
FROM
MenuItems AS M
JOIN
Categories AS C ON C.Id = M.CategoryId
LEFT JOIN
InvoiceDetails AS D ON M.Id = D.ItemId
LEFT JOIN
Invoices I ON I.Id = d.InvoiceId
WHERE
#Terminal IN (I.TerminalId, C.TerminalId)
AND CONVERT(date, I.Time) = #Date
OR NULL IN (Amount, Qty)
GROUP BY
M.Name, M.Id, D.ItemId
ORDER BY
(Qty) DESC
END
The result this stored procedure returns on adding Date in where clause:
Item
Amount
Qty
KOFTA ANDA
1950
3
HOT N SOUR SOUP
550
1
CHICKEN CHOWMEIN
250
1
CHICKEN KORMA
850
1
And the result I want is but don't get it on adding Date in where clause :
Item
Amount
Qty
KOFTA ANDA
1950
3
HOT N SOUR SOUP
550
1
CHICKEN CHOWMEIN
250
1
CHICKEN KORMA
850
1
CRISPY CHICKEN
0
0
MEXICAN BURGER
0
0
What if you don't put criteria for Invoices in the WHERE clause?
Sample data
create table Categories (
Id int primary key,
Name varchar(30) not null,
TerminalId int not null
);
create table MenuItems (
Id int identity(21,1) primary key,
Name varchar(30) not null,
CategoryId int not null,
foreign key (CategoryId) references Categories(Id)
);
create table Invoices (
Id int identity(31,1) primary key,
TerminalId int not null,
ItemId int not null,
Time datetime,
foreign key (ItemId) references MenuItems(Id)
);
create table InvoiceDetails (
InvoiceDetailId int identity(41,1) primary key,
InvoiceId int,
Amount decimal(10,2),
Qty int,
foreign key (InvoiceId) references Invoices(Id)
);
insert into Categories (Id, Name, TerminalId) values
(1,'KOFTA', 1),
(2,'SOUP', 1),
(3,'CHICKEN', 1),
(4,'BURGER', 1);
insert into MenuItems (CategoryId, Name) values
(1,'KOFTA ANDA'),
(2,'HOT N SOUR SOUP'),
(3,'CHICKEN CHOWMEIN'),
(3,'CHICKEN KORMA'),
(3,'CRISPY CHICKEN'),
(4,'MEXICAN BURGER');
insert into Invoices (ItemId, TerminalId, Time)
select itm.Id, cat.TerminalId, GetDate() as Time
from MenuItems itm
join Categories cat on cat.Id = itm.CategoryId
where itm.Name in (
'KOFTA ANDA',
'HOT N SOUR SOUP',
'CHICKEN CHOWMEIN',
'CHICKEN KORMA'
);
insert into InvoiceDetails (InvoiceId, Amount, Qty) values
(31, 1950, 3),
(32, 550, 1),
(33, 250, 1),
(34, 850, 1);
Query
DECLARE #TerminalId INT = 1;
DECLARE #Date DATE = GetDate();
SELECT
V.[Date],
C.Name AS Category,
M.Name AS MenuItemName,
ISNULL(SUM(D.Amount), 0) AS Amount,
ISNULL(SUM(D.Qty), 0) AS Qty
FROM Categories AS C
CROSS JOIN (SELECT #Date AS [Date], #TerminalId AS TerminalId) V
JOIN MenuItems AS M
ON M.CategoryId = C.Id
LEFT JOIN Invoices I
ON I.ItemId = M.Id
AND I.TerminalId = V.TerminalId
AND CAST(I.Time AS DATE) = V.[Date]
LEFT JOIN InvoiceDetails AS D
ON D.InvoiceId = I.Id
WHERE C.TerminalId = V.TerminalId
GROUP BY V.[Date], C.Id, M.Id, C.Name, M.Name
ORDER BY SUM(D.Qty) DESC
Date
Category
MenuItemName
Amount
Qty
2021-12-18
KOFTA
KOFTA ANDA
1950.00
3
2021-12-18
SOUP
HOT N SOUR SOUP
550.00
1
2021-12-18
CHICKEN
CHICKEN CHOWMEIN
250.00
1
2021-12-18
CHICKEN
CHICKEN KORMA
850.00
1
2021-12-18
CHICKEN
CRISPY CHICKEN
0.00
0
2021-12-18
BURGER
MEXICAN BURGER
0.00
0
Demo on db<>fiddle here
Here's my crack at your goal. Notice the changes. I found the reference to TerminalId in Category table highly suspicious - so much that I suspect it is a model flaw. Along those lines I note that TerminalId should likely have a foreign key to a missing table for Terminals. So I ignore that.
With that out, references to Category are now irrelevant. So that was removed as well. I also changed the procedure name since I find the reference to "day" misleading. It is highly likely "menu performance" would be evaluated on a "day" basis since retail (especially food service) sales vary by day of week consistently. So let's not mislead anyone thinking that is what this procedure does.
For simplicity and clarity, I removed the ISNULL usage. Add it back if desired but such things are usually better handled by the consumer of the resultset. I left the ORDER BY clause as a stub for you to re-evaluate (and you need to).
So how does this work? Simply calculate the sums directly in the CTE and then outer join from the menu items to the CTE sums to get all menu items along with the relevant performance information for the date specified.
CREATE PROCEDURE dbo.GetMenuPerformanceByDate
#Date date,
#Terminal int
AS
BEGIN
with sales as (
select det.ItemId, SUM(det.Amount) as amt, SUM(det.Qty) as qty
from dbo.Invoices as inv
inner join dbo.InvoiceDetails as det
on inv.Id = det.InvoiceId
where cast(inv.Time as date) = #Date
and inv.TerminalId = #Terminal
group by det.ItemId
)
select menu.name, sales.amt, sales.qty
from dbo.MenuItems as menu
left join sales
on menu.Id = sles.ItemId
order by ...
;
END;
One last note. This filter:
cast(inv.Time as date) = #Date
is generally not a good method of filtering a datetime column. Far better to use inclusive lower and exclusive upper boundaries like:
inv.Time >= #date and inv.Time < dateadd(day, 1, #date)
for this reason.
My last note - there is a potential flaw regarding MenuItems. Presumably "name" is unique. It is highly unlikely that multiple rows would have the same name, but "unlikely" is not a restriction. If you generate rows based on name and name turns out to NOT be unique, your results are difficult to interpret.

Get Orders status that can be delivered with reducing quantity from stock without curson or While loop

Just like any retail business we have an Orders table and an Inventory table. What I am trying to do is to check Orders for which we have enough stock available to dispatch. A few things I need to consider:
If all the items in an order are available only then consider this order to be “Deliverable”
Check Order's deliverable status in the order of OrderID (int value) .i.e OrderID = 1 then 2 and so on.
Before checking for deliverability of next order, reduce the available of stock for the next order (not update the Inventory table but just take into account the stock quantity that has been already consumed by previous orders).
If we do not have enough stock for 1 or more items in the order, completely ignore the order and do not reduce available stock quantity for the next order to be checked.
In the following example:
Order = 100 is fully deliverable because we have enough stock for all products.
Order = 200 is not fully deliverable because PID 2 requires Qty 5 but we only have 3 left after 2 being consumed by the Order 100
Finally, Order = 300 is also fully deliverable because we have enough stock for all products.
Test data
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); --/
Expected output:
OrderID Status
------------------------
100 Deliverable
200 NOT Deliverable
300 Deliverable
My attempt: I know that it is far from the actual solution but I still wanted to share what I have been trying :)
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
The method below doesn't produce correct results. When I put all pieces together I got:
+---------+--------------------+
| OrderID | OrderIsDeliverable |
+---------+--------------------+
| 100 | 1 |
| 200 | 0 |
| 300 | 0 |
+---------+--------------------+
Order=300 was marked as non deliverable, because my query processes all Products independently and this is not correct. The previous Order=200 hogged the quantity for PID=3, even though this Order=200 was not deliverable overall (based on Products other than PID=3) and it should not affect the following orders. But it did affect the following orders, which is not correct.
I don't see how to write a single query without explicit loop(s) here.
Alas.
You can simulate a loop using recursive CTE.
I will show you a query that does the core thing and leave the rest to you, because overall it becomes too long.
The main idea - you need a running total that resets when it reaches a threshold. There are many questions on this topic, I used this as a basis for my answer.
In the query below I'm looking only at a slice of your data, only at one specific PID = 2.
CTE_RN gives us row numbers to iterate upon. CTE_Recursive is the main loop which checks if the running total exceeds the limit. If it does, it discards the Qty from that Order and sets the OrderIsDeliverable flag.
Query
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
;
Result
+---------+-----+-----+----------+----+--------+--------------------+
| 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 |
+---------+-----+-----+----------+----+--------+--------------------+
Now you need to run this query for each PID. I would wrap this query into a table-valued function with parameter and pass PID as parameter. Maybe you can do it without a function as well. Obviously, to create a function you can't have table variables, you need actual tables to reference in your function, so adjust the code accordingly.
Then call it something like this:
SELECT
...
FROM
#Inventory AS I
CROSS APPLY dbo.MyFunc(I.PID) AS A
This would return the same number of rows as in the #Order table. Then you need to group this by OrderID and look at the OrderIsDeliverable flag. If this flag is 0 at least once for an Order, this Order is not deliverable.
Something like this:
SELECT
A.OrderID
,MIN(OrderIsDeliverable) AS OrderIsDeliverable
FROM
#Inventory AS I
CROSS APPLY dbo.MyFunc(I.PID) AS A
GROUP BY
A.OrderID
;
Ideally, you should try various approaches (cursor, recursive CTE, etc.), make sure that you have appropriate indexes, measure their performance on your real data and hardware and decide which one to use.
EDIT:
Because I'm ambitious, I've now also found a solution with CTE. Please give my feedback if you find any bug or incorrect results. My old cursor solution is below.
New code with 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
Test Data:
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)
Result:
OrderID Status
100 Deliverable
200 NOT Deliverable
300 Deliverable
400 NOT Deliverable
500 NOT Deliverable
600 Deliverable
700 Deliverable
If you need more information or explanations, leave a comment.
Old code with cursor:
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

MSSQL: How to get multiple values from another table

For a given "CustomerId" I need to get 4 related values from a column ("CompanySales") in another table.
I have joined the two tables and, with the query below, manage to get 2 "CompanySales" values from the column in the other table.
How do I do this do get 4 values (I need CompanySales for "WeekNumber" = 1,2,3 and 4)
This is the SQL query I have to secure "CompanySales" for "Weeknumber" = 1 and 2:
Declare #TempTable1 table
(
CustomerID INT,
CustomerName Varchar (50),
CompanySales DEC (8,2),
WeekNumber INT
)
INSERT INTO #TempTable1 ("CustomerID","CustomerName", "WeekNumber")
SELECT Customer.CustomerID, Customer.CustomerName, Company.WeekNumber, company.Sales
FROM Customer INNER JOIN
Company ON Customer.CustomerID = Company.CustomerID;
With tblDifference as
(
Select Row_Number() OVER (Order by WeekNumber) as RowNumber,CustomerID,CustomerName, companysales, WeekNumber from #TempTable1
)
Select Top (50) cur.CustomerID, Cur.CustomerName, Cur.WeekNumber as CurrentWeek, Prv.WeekNumber as PreviousWeek, Cur.CompanySales as CurrentSales, Prv.CompanySales as PreviousSales, CAST(((Cur.CompanySales-Prv.CompanySales)/Prv.CompanySales)*100 As Decimal(8,2)) as PercentChange from
tblDifference Cur Left Outer Join tblDifference Prv
On Cur.CustomerID=Prv.CustomerID
Where cur.WeekNumber = 1 AND prv.WeekNumber = 2
Order BY PercentChange ASC
How about adding between there cur.WeekNumber Between 1 and 4?

Query matching stock trading buyers/sellers based on conditions

I'm trying to create a stored procedure that matches buyers and sellers for a fake stock market program. I eventually I'd like to use this to populate a transaction table to finalize peoples orders. This would also be in a SQL Server Agent Job. Here's the table I have:
Id UserId Type QtyRemaining Price CreatedOn
1 3 BUY 50 1.00 2012-09-09 05:25:48.4470000
2 6 BUY 50 1.00 2012-09-09 19:25:34.4300000
3 5 SELL 30 1.00 2012-09-09 19:22:59.5900000
4 3 SELL 50 0.90 2012-09-09 06:39:34.9100000
5 2 SELLALL 50 1.00 2012-09-09 04:10:01.8400000
There are several conditions that need to be satisfied to make these matches:
A buyer must look for a seller that has >= amount of shares that the buyer wants if its a 'SELL' order. If its a 'SELLALL' order then the Qty must be equal for the buyer to buy the stock. E.g. The seller must be selling 50 shares and the buyer MUST be buying 50 shares at the same price or lower.
A buyer wants the minimum price for the stock.
If there are multiple sellers with the conditions above a buyer takes the oldest selling stock first after the minimum price.
So the pairs of traders would be #1 and #4, #2 and #5. Therefore #3 would still be pending.
Here's the code I was playing around with but I can't get it to match up properly with the minimum price and the oldest first:
select o.*, oa.*, r.* from [Order] o
join OrderActivity oa on o.Id = oa.OrderId
join (
select o2.Id, o2.VideoId, o2.UserId, oa2.Price, oa2.QtyRemaining, o2.[Type] from [Order] o2
join OrderActivity oa2 on o2.Id = oa2.OrderId
where o2.Type = 'buy' and oa2.Status = 'open'
) as r on (o.VideoId = r.VideoId and oa.Price <= r.Price and r.QtyRemaining = oa.QtyRemaining and o.UserId != r.UserId)
where (o.Type = 'sell' or o.Type = 'sellall') and oa.Status = 'open'
try this
Briefly ,
1.Rank buyer based on the createddate(named as stocks in the below query) 2.Rank seller based on the price,createddate(named as lowNoldstock)3.Get the matching rank
select stocks.*,lowNoldStock.*
from (select *,row_number() over(order by createdon) as buyerrank
from stocktable(nolock) where c='buy' ) stocks
inner join
(select *,row_number() over(order by price,createdon) as sellerrank
from stocktable(nolock) where [type]='sell' or [type]='sellall' ) lowNoldstock
on (stocks.qty<=lowNoldStock.qty and lowNoldStock.type='sell')
or (lowNoldStock.type='sellall' and stocks.qty=lowNoldStock.qty and stocks.price>=lowNoldStock.price)
where lowNoldStock.sellerrank=stocks.buyerrank
test script in sql fiddle ,for some reason it is showing partial result in sql fiddle
This works in my local database
You are welcome to play with this:
declare #Transactions as Table
( Id Int, UserId Int, Type VarChar(8), QtyRemaining Int, Price Money, CreatedOn DateTime )
insert into #Transactions ( Id, UserId, Type, QtyRemaining, Price, CreatedOn ) values
( 1, 3, 'BUY', 50, 1.00, '2012-09-09 05:25:48.447' ),
( 2, 6, 'BUY', 50, 1.00, '2012-09-09 19:25:34.430' ),
( 3, 5, 'SELL', 30, 1.00, '2012-09-09 19:22:59.590' ),
( 4, 3, 'SELL', 50, 0.90, '2012-09-09 06:39:34.910' ),
( 5, 2, 'SELLALL', 50, 1.00, '2012-09-09 04:10:01.840' )
-- Split the transactions into temporary working tables.
declare #Buyers as Table
( Id Int, UserId Int, Type VarChar(8), QtyRemaining Int, Price Money, CreatedOn DateTime )
declare #Sellers as Table
( Id Int, UserId Int, Type VarChar(8), QtyRemaining Int, Price Money, CreatedOn DateTime )
insert into #Buyers
select Id, UserId, Type, QtyRemaining, Price, CreatedOn
from #Transactions
where Type = 'BUY'
insert into #Sellers
select Id, UserId, Type, QtyRemaining, Price, CreatedOn
from #Transactions
where Type like 'SELL%'
-- Process the buy orders in the order in which they were created.
declare #BuyerId as Int = ( select top 1 Id from #Buyers order by CreatedOn )
declare #SellerId as Int
declare #Balance as Int
while #BuyerId is not NULL
begin
-- Pair a seller, if possible, with the current buyer.
; with Willard as (
select Row_Number() over ( order by S.Price, S.CreatedOn ) as Priority,
S.Id as S_Id, S.QtyRemaining as S_QtyRemaining,
B.QtyRemaining as B_QtyRemaining
from #Sellers as S inner join
#Buyers as B on B.Id = #BuyerId and
case
when S.Type = 'SELL' and B.QtyRemaining <= S.QtyRemaining then 1
when S.Type = 'SELLALL' and B.QtyRemaining = S.QtyRemaining then 1
else 0
end = 1
)
select #SellerId = S_Id, #Balance = S_QtyRemaining - B_QtyRemaining
from Willard
where Priority = 1
-- Display the result.
select #BuyerId as BuyerId, #SellerId as SellerId, #Balance as RemainingShares
-- Update the working tables.
if #Balance = 0
delete from #Sellers
where Id = #SellerId
else
update #Sellers
set QtyRemaining = #Balance
where Id = #SellerId
delete from #Buyers
where Id = #BuyerId
-- Find the next buy order.
set #BuyerId = ( select top 1 Id from #Buyers order by CreatedOn )
end
-- Display any unfilled orders.
select 'Unmatched Buy', *
from #Buyers
select 'Unmatched Sell', *
from #Sellers

Database Join Query

I am having a problem with a database join query and I am looking for someone to help me out.
Basically I've got two tables, Invoices and Receipts.
Invoices
Invoice ID
Amount
Date_Added
Receipts
ReceiptID
InvoiceID
Amount
Date_Added
The thing is that I need to produce a table like below, but I have multiple records in Receipts and I am pretty sure that the data is stored in a good way, just not exactly sure what the query would be.
InvoiceID RecieptID Amount Balance Date_Added
1 0 100.00 100.00 01.05.2012
1 1 100.00 0.00 02.05.2012
2 0 250.00 250.00 03.05.2012
3 0 100.00 350.00 04.05.2012
2 2 100.00 250.00 05.05.2012
Does this make sense? So it should be in date order. So effectively I can see line by line what is going on each date.
Setup:
USE tempdb;
GO
CREATE TABLE dbo.Invoices
(
InvoiceID INT,
Amount DECIMAL(12,2),
DateAdded SMALLDATETIME
);
CREATE TABLE dbo.Receipts
(
ReceiptID INT,
InvoiceID INT,
Amount DECIMAL(12,2),
DateAdded SMALLDATETIME
);
SET NOCOUNT ON;
INSERT dbo.Invoices SELECT 1, 100, '20120501'
UNION ALL SELECT 2, 250, '20120503'
UNION ALL SELECT 3, 100, '20120504';
INSERT dbo.Receipts SELECT 1, 1, 100, '20120502'
UNION ALL SELECT 2, 2, 100, '20120505';
Query:
;WITH x AS
(
SELECT InvoiceID, ReceiptID, Amount, DateAdded,
rn = ROW_NUMBER() OVER (ORDER BY DateAdded)
FROM
(
SELECT InvoiceID, ReceiptID = 0, Amount, DateAdded
FROM dbo.Invoices -- where clause?
UNION ALL
SELECT InvoiceID, ReceiptID, Amount, DateAdded
FROM dbo.Receipts -- where clause?
) AS y
),
z AS
(
SELECT xrn = x.rn, x.InvoiceID, x.ReceiptID, x.Amount, x.DateAdded,
PlusMinus = CASE WHEN x.ReceiptID > 0 THEN -x.Amount ELSE x.Amount END
FROM x LEFT OUTER JOIN x AS x2
ON x.rn = x2.rn + 1
)
SELECT InvoiceID, ReceiptID, Balance = (
SELECT SUM(COALESCE(PlusMinus, Amount))
FROM z AS z2
WHERE z2.xrn <= z.xrn
), Amount, DateAdded
FROM z
ORDER BY DateAdded;
Cleanup:
DROP TABLE dbo.Invoices, dbo.Receipts;
you can make a view that select data from the 2 tables like if the DB is oracle :
CREATE VIEW INVOICE_RECPT_VIEW as
select a.InvoiceID, b.RecieptID, b.Amount, b.Date_Added
from invoices a, receipts b
where a.InvoiceID = b.InvoiceID
order by Date_Added