Use cursor for with 2 tables to insert rows - sql-server

I have 2 tables: items and cost.
In table items, I have old item code and new item code
In table cost, I have the item code and the cost
I want to create new rows in table cost for the new item code according to the cost that already exist for the old item code.
For example:
I expect to see this result after I run the cursor in the cost table:
I try to run this, but it's been running forever and duplicates every row 100000 times:
DECLARE #item_code_old nvarchar (50)
DECLARE #item_code_new nvarchar (50)
DECLARE CostCURSOR CURSOR FOR
SELECT item_code_old, item_code_new
FROM item
WHERE company = 'AEW' AND item_code_new IS NOT NULL
OPEN CostCURSOR
FETCH NEXT FROM CostCURSOR INTO #item_code_old, #item_code_new
WHILE (##FETCH_STATUS = 0)
BEGIN
SELECT #item_code_old = item_code_old
FROM cost
WHERE company = 'AEW' AND year = 2021
INSERT INTO cost
SELECT
company,
year,
month,
#item_code_new,
unit_cost
FROM
cost
WHERE
company = 'AEW' AND year = 2021
FETCH NEXT FROM CostCURSOR INTO #item_code_old, #item_code_new
END
CLOSE CostCURSOR
DEALLOCATE CostCURSOR
What am I missing?

What you are doing with your variables doesn't really make sense, you fetch your next value into #item_code_old, but then at the start of each loop you then assign it a value pretty much at random:
SELECT #item_code_old = item_code_old
FROM cost
WHERE company = 'AEW'
AND year = 2021;
Then when it comes to inserting the new cost, you don't use this variable at all:
INSERT INTO cost
SELECT company, year, month, #item_code_new, unit_cost
FROM cost
WHERE company = 'AEW'
AND year = 2021;
I'd guess that you don't need this first step that reassigns #item_code_old, and you do need to apply the filter to the insert query.
With that being said, a cursor is completely unnecessary here (which you'll find is often the case with cursors), you can do this with a single insert, using something like this:
INSERT Cost (Company, Year, Month, item_code_old, unit_cost)
SELECT c.company, c.year, c.month, i.item_code_new, c.unit_cost
FROM cost AS c
INNER JOIN Item AS i
ON c.item_code_old = i.item_code_old
WHERE c.company = 'AEW'
AND c.year = 2021
AND i.item_code_new IS NOT NULL;
For the sake of completeness, your cursor solution would be something the following, but I would stress this is not a good approach. Cursors should be a last resort, and are probably something it is not even worth practising as it is not something you will find very useful. Instead of practicing cursors, instead practice trying to find the set-based approach, this will be significantly better use of your time. Anyway, rant over:
DECLARE #item_code_old NVARCHAR(50);
DECLARE #item_code_new NVARCHAR(50);
DECLARE CostCURSOR CURSOR LOCAL FAST_FORWARD FOR
SELECT item_code_old, item_code_new
FROM item
WHERE company = 'AEW'
AND item_code_new IS NOT NULL;
OPEN CostCURSOR;
FETCH NEXT FROM CostCURSOR INTO #item_code_old, #item_code_new;
WHILE(##FETCH_STATUS = 0)
BEGIN
INSERT INTO cost (Company, Year, Month, Item_Code_Old, unit_cost)
SELECT company, year, month, #item_code_new, unit_cost
FROM cost
WHERE company = 'AEW'
AND year = 2021
AND item_code_old = #item_code_old;
FETCH NEXT FROM CostCURSOR INTO #item_code_old, #item_code_new;
END;
CLOSE CostCURSOR;
DEALLOCATE CostCURSOR;

Related

SQL Server Computed Column with recursion

MS SQL Server 2014
Table schema (part of one) like following:
where Coinh1d_Close has type float.
For analytic purposes i need another column, more precisely computed column, Coinh1d_EMA12 based on Coinh1d_Close and previous value itself. First value is always known and calculates based on Coinh1d_Close only. The following values must to be calculated based on both Coinh1d_Close and previous value of the same column Coinh1d_EMA12, like in Excel:
From Calculating value using previous value of a row in T-SQL i wrote some T-SQL expression
;with cteCalculation as (
select t.Coinh1d_Id, t.Coinh1d_Time, t.Coinh1d_Close, t.Coinh1d_Close as Column2
from Coinsh1d t
where t.Coinh1d_Id in (1)
union all
select t.Coinh1d_Id, t.Coinh1d_Time, t.Coinh1d_Close, cast(t.Coinh1d_Close as float)*cast(2 as float)/(cast(12 as float)+cast(1 as float))+cast(c.Column2 as float)*(cast(1 as float)-cast(2 as float)/(cast(12 as float)+cast(1 as float))) as Column2
from Coinsh1d t
inner join cteCalculation c
on t.Coinh1d_Id-1 = c.Coinh1d_Id
where t.Coinh1d_Name='BTC'
) select c.Coinh1d_Id, c.Coinh1d_Time, c.Coinh1d_Close, c.Column2
from cteCalculation c option (maxrecursion 0)
It gives exactly what i need
But my question is it possible to use previous value of Computed Column for the next value (i am using UDF function for Computed Column). I need like from this question Calculating value using previous value of a row in T-SQL but for Computed Column.
UPDATED
More information for clarification: this table consist of data for 1000 coins (BTC, ETH, etc.) and information for every coin starts with specific time Coinh1d_Time (Unix Timestamp) = 1346976000. At the begin we load to this table all info from this time to current time (about 2 000 000 records) and then every hour t-sql script updates this table (add 1000 row - new data per hour).
Also this table has many computed column (including one thet depends from Coinh1d_EMA12).
If it does not succeed to create Computed Column for Coinh1d_EMA12 i see solution: first of all create ordynary column Coinh1d_EMA12. Then one time update all table
ALTER PROCEDURE [dbo].[UpdateEMA12h1d_notused]
-- Add the parameters for the stored procedure here
#Coin varchar(50)
AS
BEGIN
SET NOCOUNT ON;
print #coin
;with cte as (
select
t.Coinh1d_Id, t.Coinh1d_Close, t.Coinh1d_Close as Column2
from
Coinsh1d t
where
t.Coinh1d_Id in (select MIN(Coinh1d_Id) from Coinsh1d where Coinh1d_Name=#Coin)
union all
select
t.Coinh1d_Id, t.Coinh1d_Close, cast(t.Coinh1d_Close as float)*cast(2 as float)/(cast(12 as float)+cast(1 as float))+cast(c.Column2 as float)*(cast(1 as float)-cast(2 as float)/(cast(12 as float)+cast(1 as float))) as Column2
from
Coinsh1d t
inner join
cte c
on
t.Coinh1d_Id-1 = c.Coinh1d_Id
where
t.Coinh1d_Name=#Coin
)
select
c.Coinh1d_Id, c.Column2
into
#tempEMA12
from
cte c
option
(maxrecursion 0)
update
t1
set
t1.Coinh1d_Ema12 = t2.Column2
from
Coinsh1d as t1
inner join
#tempEMA12 as t2
on
t1.Coinh1d_Id = t2.Coinh1d_Id
drop table #tempEMA12
END
Call for every coin:
DECLARE #MyCursor CURSOR;
DECLARE #MyField varchar(50);
BEGIN
SET #MyCursor = CURSOR FOR
select Coin_Symbol from Coins
OPEN #MyCursor
FETCH NEXT FROM #MyCursor
INTO #MyField
WHILE ##FETCH_STATUS = 0
BEGIN
exec UpdateEMA12h1d_notused #MyField
FETCH NEXT FROM #MyCursor INTO #MyField
END;
CLOSE #MyCursor ;
DEALLOCATE #MyCursor;
END;
It takes about 30 minutes. And then create trigger for this column with above CTE code.
Starting with the data from your function, you can use the LAG() (if on SQL Server 2012 or higher) window function to place a previous value in the same row. The code would follow this schema:
SELECT
C.CoinID,
C.CoinTime,
C.CoinClose,
C.FunctionComputedColumn,
PreviousRowFunctionComputedColumn =
LAG(
C.FunctionComputedColumn, -- The column you need from the previous row
1, -- How many rows back you need to go
0) -- Which value should it take if there is no previous row
OVER (
PARTITION BY
CoinID -- Row ordering resets with each different CoinID
ORDER BY
CoinTime ASC) -- Since it's ascending, the previous one is older
FROM
CoinData AS C
ORDER BY
C.CoinID,
C.CoinTIme
You can then use the PreviousRowFunctionComputedColumn in any expression you need.

I need to update age in SQL Server 2008 according to DOB

I have a table in SQL Server 2008, which has DOB (e.g. 1992-03-15) and in same table, I have an Age column, which is right now Null. I need to update the Age according to the DOB. I have both columns (AGE and DOB) in the same table. I need script which does my job to update Age according to DOB
And other one is in same table, I have Arrival Month (e.g. 8) and Arrival year (e.g. 2011), according to that I need to update another column (Time in country). Say let's say according to example (08(MM), 2011(YYYY)), should update (TimeInCountry) - 4.2 something like that. Which should deduct from current date and time has mentioned into month and year
Do let me know if you need anything else.
Not sure what is the data type your age column is
You can do something like below
Update TableName
Set Age = DATEDIFF(yy, DOB, getdate())
if you using decimal
Age = DATEDIFF(hour,DOB,GETDATE())/8766.0
I believe creating a trigger bill be use full, if you adding new rows in future
For yopur first Problem,
UPDATE TABLE_NAME SET AGE=DATEDIFF(hour,DOB_COLUMN,GETDATE())/8766.0
If you want in Round ,
UPDATE TABLE_NAME SET
AGE= CONVERT(int,ROUND(DATEDIFF(hour,DOB_COLUMN,GETDATE())/8766.0,0))
And Not Sure what you really want to do in the Second Problem,but my guess you can try something like..
Update Table_Name set
TimeInCountry=cast(len(Arrival_year) as varchar(4))+'.'+cast(len(Arrival_Month) as varchar(2))
I have implemented User Defined function
Here it is which may help to someone.
ALTER FUNCTION [dbo].[TimeInCountry]
(
#ArrivalMonth varchar(10),
#ArrivalYear Varchar(10)
) RETURNS VARCHAR(10)
BEGIN
Declare #Ageyear int
Declare #Agemonth int
Declare #Final varchar(10)
Declare #CurrentMonth int
Declare #Currentyear int
Set #CurrentMonth = (Select DatePart(mm, GetDate()))
Set #Currentyear = (Select DatePart(yyyy,GetDate()))
Select #AgeYear = #Currentyear - #ArrivalYear
Select #AgeMonth = #CurrentMonth - #ArrivalMonth
if (#AgeMonth < 0)
BEGIN
Set #AgeYear = #AgeYear - 1
Set #AgeMonth = #AgeMonth + 12
END
Select #Final = (Select Cast(#AgeYear as Varchar(max)) +'.'+ Cast(#AgeMonth as varchar(max)))
Return #Final
---And finally call this function where to update.
--To Check
Select [DBName].TimeInCountry (8,2013)
--- and Finally updating.
Update [DBName].[dbo].[TableName] Set TimeInCountry = dbo.TimeInCountry (ArrivalMonth,ArrivalYear) from [DBName].[dbo].[TableName]
Thanks again everyone.

Problems with MSSQL IF statement

I'm having trouble converting a query from MySQL to MSSQL. Most of my errors are coming from an if statement that's supposed to see how many manhours we have left on a given day. It checks to see if the person hasn't worked yet or is currently working and adds either the amount of time they're scheduled for or the amount of time they have left to work.
Round(Sum(IF(mon_endtime > Curtime(),IF(mon_starttime > Curtime(),mon_duration, (mon_endtime - Curtime()) / 10000), 0)),1) AS hours
Where mon_ information is stored in the employee table. I'm aware that MSSQL doesn't have curtime() and I have a variable in place to hold it, but I'm having problems getting the correct amount with the following code:
declare #cur_time time;
declare #starttime time;
declare #endtime time;
set #cur_time = CONVERT(time, CURRENT_TIMESTAMP);
select #starttime = tue_starttime from employee;
select #endtime = tue_endtime from employee;
if (#endtime > #cur_time)
begin
if (#starttime > #cur_time)
begin
select sum(tue_duration) as hours from employee
end
else begin
select sum(datediff(hh,#cur_time,tue_endtime)) as hours from employee
end
end
else begin
select 0 as hours from employee
end
Any help would be greatly appreciated
Try using this CASE statement instead:
ROUND(
SUM(
CASE WHEN (mon_endtime > Curtime())
THEN
CASE WHEN (mon_starttime > Curtime())
THEN mon_duration
ELSE ((mon_endtime - Curtime()) / 10000)
END
ELSE 0
END)
,1) AS Hours
(formatted so I could understand the statement a little better!)

Incorrect syntax near case in trigger

I have a Reservations table with the following columns
Reservation_ID
Res_TotalAmount - money
Res_StartDate - datetime
IsDeleted - bit column with default value - false
So when a user tries to delete his reservation I've created a trigger that instead of delete - he just updates the value of column IsDelete to true;
So far so good - but this tourist may owe some compensation to the firm, for example when he has cancelled his reservation 30 days to 20 days from the start_date of the reservation - he owes 30% of the Res_TotalAmount and so on
And here is my trigger
Create Trigger tr_TotalAMountUpdateAfterInsert on RESERVATIONS after Delete
As
Begin
Declare #period int
Declare #oldResAmount int
Declare #newAmount money
Declare #resID int
Select #resID = Reservation_ID from deleted
select #oldResAmount = Res_TotalAmount from deleted
Select #period= datediff (day,Res_StartDate,GETDATE()) from deleted
case
#period is between 30 and 20 then #newAmount=30%*#oldResAmount
#period is between 20 and 10 then #newAmount=50%*#oldResAmount
end
exec sp_NewReservationTotalAmount #newAmount #resID
End
GO
As I have to use both triggers and stored procedure you see that I call at the end of the trigger one stored procedure which just updates Res_TotalAmount column
Create proc sp_NewReservationTotalAmount(#newTotalAmount money, #resID)
As
declare #resID int
Update RESERVATIONS set Res_TotalAmount=#newTotalAmount where Reservation_ID=resID
GO
So my first problem is that it gives me incorrect syntax near case
And my second - I would appreciate suggestions how to make both the trigger and stored procedure better.
Your fundamental flaw is that you seem to expect the trigger to be fired once per row - this is NOT the case in SQL Server. Instead, the trigger fires once per statement, and the pseudo table Deleted might contain multiple rows.
Given that that table might contain multiple rows - which one do you expect will be selected here??
Select #resID = Reservation_ID from deleted
select #oldResAmount = Res_TotalAmount from deleted
Select #period= datediff (day,Res_StartDate,GETDATE()) from deleted
It's undefined - you might get the values from arbitrary rows in Deleted.
You need to rewrite your entire trigger with the knowledge the Deleted WILL contain multiple rows! You need to work with set-based operations - don't expect just a single row in Deleted !
Also: the CASE statement in T-SQL is just intended to return an atomic value - it's not a flow control statement like in other languages, and it cannot be used to execute code. So your CASE statement in your trigger is totally "lost" - it needs to be used in an assignment or something like that ....
1) Here is the correct syntax for the CASE statement. Note that:
I changed the order of your comparisons with the CASE statement; the smaller value has to come first.
I have included an "ELSE" case so you don't wind up with an undefined value when #period is not within your given ranges
SELECT #newAmount =
CASE
WHEN #period between 10 and 20 then 0.5 * #oldResAmount
WHEN #period between 20 and 30 THEN 0.3 * #oldResAmount
ELSE #oldResAmount
END
2) You are going to have an issue with this trigger if ever a delete statement affects more than one row. Your statements like "SELECT #resID = Reservation_ID from deleted;" will simply assign one value from the deleted table at random.
EDIT
Here is an example of a set-based approach to your problem that will still work when multiple rows are "deleted" within the transaction (example code only; not tested):
Create Trigger tr_TotalAMountUpdateAfterInsert on RESERVATIONS after Delete
As
Begin
UPDATE RESERVATIONS
SET Res_TotalAmount =
d.Res_TotalAmount * dbo.ufn_GetCancellationFactor(d.Res_StartDate)
FROM RESERVATIONS r
INNER JOIN deleted d ON r.Reservation_ID = d.Reservation_ID
End
GO
CREATE FUNCTION dbo.ufn_GetCancellationFactor (#scheduledDate DATETIME)
RETURNS FLOAT AS
BEGIN
DECLARE #cancellationFactor FLOAT;
DECLARE #period INT = DATEDIFF (DAY, #scheduledDate, GETDATE());
SELECT #cancellationFactor =
CASE
WHEN #period <= 10 THEN 1.0 -- they owe the full amount (100%)
WHEN #period BETWEEN 11 AND 20 THEN 0.5 -- they owe 50%
WHEN #period BETWEEN 21 AND 30 THEN 0.3 -- they owe 30%
ELSE 0 -- they owe nothing
END
RETURN #cancellationFactor;
END;
GO
About the case:
The syntax is wrong. Even if it worked (see below) you'd be missing the WHENs:
case
WHEN #period is between 30 and 20 then #newAmount=30%*#oldResAmount
WHEN #period is between 20 and 10 then #newAmount=50%*#oldResAmount
end
Yet, the case statement can not be used this way. In the context where you want it, you need to use if. It's not like the switch statement in C++/C# for example. You can only use it in queries like
SELECT
case
WHEN #period is between 30 and 20 then value1
WHEN #period is between 20 and 10 then value2
end
Having said the above: I didn't actually read all your code. But now that I've read some of it, it is really important to understand how triggers work in SQL Server, as mark_s says.

last occurence of date

I need to find the last occurence of a date (in my case a static date 1st of may)
I made this which works but i know this can be done in a much smarter way
declare #lastmay date
set #lastmay = DATEADD(YY,YEAR(GETDATE())-2000,'20000501')
IF #lastmay <= GETDATE()
BEGIN
SET #lastmay = DATEADD(YY,-1,#lastmay)
END
When you are working with dates in SQL it can be a real help to have a Dates utility table on your database that you can then compare against.
This article discusses it well: http://www.techrepublic.com/blog/datacenter/simplify-sql-server-2005-queries-with-a-dates-table/326
If you implemented that your queries could become quite simple such as (using columns from article)
Declare #lastmay date
Select #lastmay = DateFull
from DateLookup
where MonthNumber = 5
and MonthDay = 1
and Datefull < getdate()

Resources