I have a table in MS SQL Server where the EXPECTED result should look like this:
Prior to expected result/query execution, all FieldX values are NULL. When I run my query, FieldX is only updated from row 2 to 8.
I need to UPDATE FieldX using a set of rules, which I define as such:
WITH cte_previous_rows AS (
SELECT Date, Staff_Id, LAG(FieldX) OVER (partition by Staff_Id ORDER by [date]) as Prev_Row
FROM Sales
) UPDATE Sales
SET FieldX = (CASE
WHEN Staff_id_sales < 1500 AND ClosedSale = 0 THEN 0
WHEN Staff_id_sales = 1500 and ClosedSale = 0 THEN 5
WHEN Staff_id_sales <= 3000 and Staff_id_sales > 1500 and ClosedSale = 0 THEN 1
WHEN Staff_id_sales > 3000 and (c.Prev_Row = 1 OR c.Prev_Row = 0) THEN 2
WHEN Staff_id_sales > 3000 and (c.Prev_Row = 2 or c.Prev_Row = 3) THEN 3
ELSE FieldX
END)
FROM Sales
JOIN cte_previous_rows as c ON Sales.staff_id = c.staff_id AND Sales.Date = c.Date;
This query works just fine. But the problem lies in the last two WHEN statements. The reason for this, is of course that c.Prev_Row (previous row) is used in the rule set for these two last WHEN statements..
How can I edit my query so that the above rule set is applied on to all 50k rows in a SINGLE execution? Perhaps a new method is required..
A recursive CTE that works from the earliest row for each Staff_Id forward may be the ticket:
Note: This query was not run on an image of the data, so it might have some errors.
Related
In this question #GordonLinoff provided a solution (recursive common table expression) to my initial question. This is a follow-up question.
Initial question:
How can I loop through registrations until a certain amount (sum) of AmountPersons was reached and if the next AmountPersons was too high to be invited check the AmountPersons of the next row to see if it would fit?
Please check the initial question via the link above to get the full picture.
New situation:
First we have 20 available seats and we run through the data rows to fill these seats (initial question).
Then I sorted on Count_Invited and updated the order by from the row_number() function. So people who were invited the least should get priority.
Then I also added the Count_Registered column, because people who registered most, but got invited least, should also get priority.
New question:
How can I scramble the last two people who are invited from the below result if a third, forth, fifth.. user also has the same values (Count_Invited and Count_Registered and AmountPersons are 1)?
The top data is ordered correctly, but only for the last few rows it would need to randomize the invitee.
I know of this ORDER BY NEWID() functionality to randomize rows, but it can't be applied on all rows in my case. I don't know how to approach this... More info below.
The new T-SQL code:
WITH tn AS (
SELECT g.[Id],
g.[FirstName],
g.[LastName],
g.[E-mail],
g.[Count_Invited],
g.[Count_Registered],
r.[DateReservation],
r.[AmountPersons],
row_number() over(order by g.[Count_Invited], g.[Count_Registered] DESC) as seqnum
FROM USERTABLE g
INNER JOIN RESERVATION r ON r.[UserId] = g.[Id]
WHERE r.[PartyId] = 21
),
cte AS (
SELECT [Id], [FirstName], [LastName], [E-mail], [Count_Invited], [Count_Registered], [DateReservation],
[AmountPersons], [AmountPersons] as total, 1 as is_included, seqnum
FROM tn
WHERE seqnum = 1
UNION ALL
SELECT tn.[Id], tn.[FirstName], tn.[LastName], tn.[E-mail], tn.[Count_Invited], tn.[Count_Registered], tn.[DateReservation], tn.[AmountPersons],
(case when tn.[AmountPersons] +cte.total <= 20
then tn.[AmountPersons] +cte.total
else cte.total
end),
(case when tn.[AmountPersons] +cte.total <= 20
then 1
else 0
end) as is_included,
tn.seqnum
FROM cte join
tn
on tn.seqnum = cte.seqnum + 1
WHERE cte.total < 20
)
SELECT cte.Id AS userId,
cte.FirstName,
cte.LastName,
cte.[E-mail],
cte.Count_Invited,
cte.Count_Registered,
cte.AmountPersons,
cte.DateReservation
FROM cte
WHERE is_included = 1
This is the result I'm getting every time I execute the above code:
I hope this makes sense to someone. Thank you.
Output from suggested answer by #George Menoutis:
Edit: Extra clarification steps. This is what should happen:
-- declare amountSeats = 25
-- select 1st value of Count_Invited
-- if that value is 0
-- do sum of AmountPersons (multiple rows) where Count_Invited is 0
-- if that sum is lower than amountSeats, let's say it's 10 now
-- insert all rows with value 0 in temp table (not sure if this is the way to go...)
-- select 2nd value (not second row) of Count_Invited --> so where Count_Invited is not 0
-- if that value is 1
-- do sum of AmountPersons (multiple rows) where Count_Invited is 1
-- sum count of Count_Invited = 0 + Count_Invited = 1
-- if that sum is still lower then amountSeats, let's say it's 15 now
-- insert (add) all rows with Count_Invited 1 in temp table
-- select 3rd value (not 3rd row) of Count_Invited --> so where Count_Invited NOT IN (0, 1)
-- if that value is 5
-- do count of AmountPersons (multiple rows) where Count_Invited is 5
-- sum count of Count_Invited = 0 + Count_Invited = 1 + Count_Invited = 5
-- let's now say the count for AmountPersons is now 40
-- this means that not everyone with Count_Invited = 5 can be invited as there are only 10 open seats
-- a random selection needs to be made of these rows
-- select random rows where Count_Invited is 5 until the sum of these rows is 10
-- if 10 can't be matched, get as close as possible by looping through the leftover rows, but don't exceed 10
I actually think your newid() idea is best. It's just that the correct way to put it is in the definition of seqnum:
row_number() over(order by g.[Count_Invited], g.[Count_Registered] DESC, newid() asc) as seqnum
Addendum: After OP's comment, I made a new question here. So, it seems that it will work out but you will have to make tn a temp table first, else newid() is triggered multiple times by the following cte.
My query is as follows:
I have a table Named CALLS as given below:
CALL_NUMBER PART_NUMBER QTY ARRIVAL_DATE
A1 XXX1 5
B2 YYY2 25
C3 ZZZ3 120
D4 ZZZ3 80
E5 ZZZ3 25
And another table SHIPPING as given below:
PART_NUMBER QTY SHIP_DATE ARRIVAL_DATE
XXX1 100 26-Dec 28-Dec
YYY2 5 29-Dec 6-Jan
ZZZ3 200 29-Dec 18-Jan
Now my aim is to put in an arrival date in the calls table based on if the qty required is satisfied by the quantity shipped. If not then, no date should be given in the CALLS table.
I thought the update query below may work:
UPDATE CALLS
SET ARRIVAL_DATE = s.ARRIVAL_DATE
FROM
(
SELECT PART_NUMBER,QTY,ARRIVAL_DATE
FROM SHIPPING
)s
WHERE PART_NUMBER = s.PART_NUMBER
AND QTY < s.QTY
But then how do I subtract the assigned quantities from the remining ones in SHIPPING?
Kindly help me out with this one.
Correct me if wrong. You want to update ARRIVAL_DATE column from Calls table and if the ARRIVAL_DATE column updates then at the same time you want to reduce its QTY value from SHIPPING table.
If yes then this can only be done by looping through each rows in calls table and updating the shipping table after. I have used while loop instead of cursors as mentioned below :
declare #i int = (select count(*) from calls)
declare #j int = 0
declare #t table
(part_number varchar(10),qty int)
while #j <= #i
begin
update a
set a.arrival_date = b.arrival_date
output inserted.part_number,inserted.qty into #t
from
(select *,row_number() over (order by (select 1)) as rn
from calls
) as a
inner join shipping as b
on a.part_number = b.part_number
where a.qty < = b.qty and a.rn = (case when #j = 0 then 1 else #j end)
update a
set a.qty = a.qty - b.qty
from shipping as a
inner join #t as b
on a.part_number = b.part_number
delete from #t
set #j = #j + 1
end
Shouldn't you just have to subtract the QTY in CALLS from the QTY in SHIPPING?
(updated 12/16 with the following as the original update didn't lend itself well to what I'm proposing...take a look)
UPDATE CALLS
SET C.PART_NUMBER = S.PART_NUMBER,
C.QTY = SUM(C.QTY - S.QTY),
C.ARRIVAL_DATE = S.ARRIVAL_DATE
FROM SHIPPING S JOIN CALLS C ON C.PART_NUMBER = S.PART_NUMBER
WHERE C.QTY < S.QTY
Since you are only updating the ones with enough quantities in CALLS then all that satisfy updating the ARRIVAL_DATE field - unless I'm misunderstanding something.
Please, as always with SQL.. test this first somewhere before using on live data. :)
update outbound.notification
set statuscode = 1
select * from outbound.Notification
where statuscode = 4 --not in (3,6)
and convert(varchar,createdtimestamp,102) > '2015.01.10'
and OutboundID <> 647
Executed the above query. Meant to have the select commented out, so that I could alternate between selecting queries in that status and updating them. What did I do to what rows?
You updated all rows of outbound.Notification. Your query is generally two separate queries.
First, this updates all rows of outbound.Notification:
update outbound.notification set statuscode = 1
Second, this is just a SELECT statement.
select * from outbound.Notification
where statuscode = 4 --not in (3,6)
and convert(varchar,createdtimestamp,102) > '2015.01.10'
and OutboundID <> 647
What could be wrong with this query:
SELECT
SUM(CASE
WHEN (SELECT TOP 1 ISNULL(StartDate,'01-01-1900')
FROM TestingTable
ORDER BY StartDate Asc) <> '01-01-1900' THEN 1 ELSE 0 END) AS Testingvalue.
The get the error:
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
As koppinjo stated what your current (broken) query is doing is checking if you have a NULL-value (or StartDate = '01-01-1900') in your table, return either a 1 or a 0 depending on which, and then attempting to SUM that single value.
There are 2 different logical things you want.
Either getting the amount of rows that has a StartDate or checking if any row is missing StartDate.
SELECT --Checking if there is a NULL-value in table
(
CASE WHEN
(SELECT TOP 1 ISNULL(StartDate,'01-01-1900')
FROM TestingTable
ORDER BY StartDate Asc) <> '01-01-1900' THEN 1
ELSE 0
END
) AS TestingValue
SELECT SUM(TestingValue) TestingValue --Give the count of how many non-NULLs there is
FROM
(
SELECT
CASE WHEN
ISNULL(StartDate,'01-01-1900') <> '01-01-1900' THEN 1
ELSE 0
END AS TestingValue
FROM TestingTable
) T
Here is a SQL Fiddle showing both outputs side by side.
Hard to say, but you probably want something like this:
SELECT
SUM(TestingValue)
FROM
(SELECT
CASE
WHEN ISNULL(StartDate,'01-01-1900') <> '01-01-1900'
THEN 1
ELSE 0
END AS TestingValue
FROM TestingTable) t
As your original query is written now, your subquery will return 1 value overall, so your sum would be 1 or 0 always, not to mention it is illegal. To get around that, this SQL will apply the case statement to every row in the TestingTable and insert the result into a derived table (t), then the 'outer' select will sum the results. Hope this helps!
I have a simple table that stores stock levels. ie.
ID int PK
LocationID int
StockLevel real
There could be multiple rows in this table for each location ie:
ID | LocationID | StockLevel
----------------------------
1 | 1 | 100
2 | 1 | 124
3 | 2 | 300
In this example its trivial to see that 224 units exist at location 1.
When I come to decrement the stock level at location 1 I am using
a cursor to iterate over all rows at where LocationID is 1 and using some simple
logic decide whether the stock available at the current row will satisfy the passed in
decrement value. If the row has sufficient quantity to satisfy the requirement I decrement the rows value and break out of the cursor, and end the procedure, however if the row doesnt have sufficient quantity available I decrement its value to zero and move to the next row and try again (with the reduced quantity)
Its quite simple and works ok, but the inevitable question is: Is there a way of performing
this RBAR operation without a cursor?? I have attempted to search for alternatives but even wording
the search criteria for such an operation is painful!
Thanks in advance
Nick
ps. I am storing data in this format because each row also contains other columns that are unique, and hence cant simply be aggregated into one row for each location.
pps. Cursor Logic as requested (where '#DecrementStockQuantityBy' is the quantity that we need
to reduce the stock level by at the specified location):
WHILE ##FETCH_STATUS = 0
BEGIN
IF CurrentRowStockStockLevel >= #DecrementStockQuantityBy
BEGIN
--This row has enough stock to satisfy decrement request
--Update Quantity on the Current Row by #DecrementStockQuantityBy
--End Procedure
BREAK
END
IF CurrentRowStockStockLevel < #DecrementStockQuantityBy
BEGIN
--Update CurrentRowStockStockLevel to Zero
--Reduce #DecrementStockQuantityBy by CurrentRowStockStockLevel
--Repeat until #DecrementStockQuantityBy is zero or end of rows reached
END
FETCH NEXT FROM Cursor
END
Hope this is clear enough? Let me know if further/better explanation is required.
Thanks
You are correct sir a simple update statement can help you in this scenario I'm still trying to find a legitimate use for a cursor or while that I can't solve with CTE or set based.
After looking a little deeper into your question I will also propose an alternate solution:
Declare #LocationValue int = 1,#decimentvalue int = 20
with temp (id,StockLevel,remaining) as (
select top 1 id, Case when StockLevel - #decimentvalue >0 then
StockLevel = StockLevel - #decimentvalue
else
StockLevel = 0
end, #decimentvalue - StockLevel
from simpleTable st
where st.LocationID = #LocationValue
union all
select top 1 id, Case when StockLevel - t.remaining >0 then
StockLevel = StockLevel -t.remaining
else
StockLevel = 0
end, t.remaining - StockLevel
from simpleTable st
where st.LocationID = #LocationValue
and exists (select remaining from temp t
where st.id <> t.id
group by t.id
having min(remaining ) >0) )
update st
set st.StockLevel = t.StockLevel
from simpleTable st
inner join temp t on t.id = st.id