Related
We have DVD Rental company. In this particular scenario we consider only Member, Rental and Membership tables.
The task is to write a trigger that prevents a customer from being shipped a DVD
if they have reached their monthly limit for DVD rentals as per their membership contract using the function.
My trigger leads to infinite loop. It works without While loop, but then it does not work properly, if I consider multiple updates to the Rental table. Where I am wrong?
-- do not run, infinite loop
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE #MemberId INT
DECLARE #RentalId INT
SELECT * INTO #TempTable FROM inserted
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
IF UPDATE(RentalShippedDate)
BEGIN
IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
END;
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
END;
My function looks as follows:
CREATE OR ALTER FUNCTION dvd_numb_left(#member_id INT)
RETURNS #tab_dvd_numb_left TABLE(MemberId INT, Name VARCHAR(50), TotalDvdLeft INT, AtTimeDvdLeft INT)
AS
BEGIN
DECLARE #name VARCHAR(50)
DECLARE #dvd_total_left INT
DECLARE #dvd_at_time_left INT
DECLARE #dvd_limit INT
DECLARE #dvd_rented INT
DECLARE #dvd_at_time INT
DECLARE #dvd_on_rent INT
SET #dvd_limit = (SELECT Membership.MembershipLimitPerMonth FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = #member_id))
SET #dvd_rented = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE CONCAT(month(Rental.RentalShippedDate), '.', year(Rental.RentalShippedDate)) = CONCAT(month(GETDATE()), '.', year(GETDATE())) AND Rental.MemberId = #member_id)
SET #dvd_at_time = (SELECT Membership.DVDAtTime FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = #member_id))
SET #dvd_on_rent = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE Rental.MemberId = #member_id AND Rental.RentalReturnedDate IS NULL)
SET #name = (SELECT CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) FROM Member WHERE Member.MemberId = #member_id)
SET #dvd_total_left = #dvd_limit - #dvd_rented
SET #dvd_at_time_left = #dvd_at_time - #dvd_on_rent
IF #dvd_total_left < 0
BEGIN
SET #dvd_total_left = 0
SET #dvd_at_time_left = 0
INSERT INTO #tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(#member_id, #name, #dvd_total_left, #dvd_at_time_left)
RETURN;
END
INSERT INTO #tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(#member_id, #name, #dvd_total_left, #dvd_at_time_left)
RETURN;
END;
Will be glad for any advice.
Your main issue is that even though you populate #TempTable you never pull any values from it.
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE #MemberId INT, #RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 #RentalID = RentalId, #MemberId = MemberId FROM #TempTable;
-- Select our values to its working
-- SELECT #RentalID, #MemberId;
IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
END;
An easy way to debug simply triggers like this is to copy the T-SQL out and then create an #Inserted table variable e.g.
DECLARE #Inserted table (RentalId INT, MemberId INT);
INSERT INTO #Inserted (RentalId, MemberId)
VALUES (1, 1), (2, 2);
DECLARE #MemberId INT, #RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
-- IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM #inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 #RentalID = RentalId, #MemberId = MemberId FROM #TempTable;
-- Select our values to its working
SELECT #RentalID, #MemberId;
-- IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
-- BEGIN
-- ROLLBACK
-- RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
-- END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
Note: throw is the recommended way to throw an error instead of raiserror.
Another thing to consider is that you must try to transform your UDF into an inline TVF because of some side effects.
Like this one:
CREATE OR ALTER FUNCTION dvd_numb_left(#member_id INT)
RETURNS TABLE
AS
RETURN
(
WITH
TM AS
(SELECT Membership.MembershipLimitPerMonth AS dvd_limit,
Membership.DVDAtTime AS dvd_at_time,
CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) AS [name]
FROM Membership AS MS
JOIN Member AS M
ON MS.MembershipId = M.MembershipId
WHERE M.MemberId = #member_id
),
TR AS
(SELECT COUNT(Rental.MemberId) AS dvd_rented
FROM Rental
WHERE YEAR(Rental.RentalShippedDate ) = YEAR(GETDATE)
AND MONTH(Rental.RentalShippedDate ) = MONTH(GETDATE)
AND Rental.MemberId = #member_id
)
SELECT MemberId, [Name],
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_limit - dvd_rented END AS TotalDvdLeft,
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_at_time - dvd_on_rent END AS AtTimeDvdLeft
FROM TM CROSS JOIN TR
);
GO
Which will be much more efficient.
The absolute rule to have performances is: TRY TO STAY IN A "SET BASED" CODE instead of iterative code.
The above function can be optimized by the optimzer whilet yours cannot and will needs 4 access to the same tables.
I have a created a stored procedure (please ignore syntax errors)
alter proc usp_newServerDetails
(#appid int, #envid int, #serType varchar(20), #servName varchar(20))
as
declare #oTbl_sd table (ID int)
declare #outID1
declare #oTbl_cd table (ID int)
declare #outID2
begin Transaction
insert into server_details(envid, servertype, servername)
output inserted.serverid into #oTbl_sd(ID)
values(#envid, #serType, #servName)
select #outID1 = ID from #oTbl_sd
insert into configdetails(serverid, servertype, configpath, configtype)
output inserted.configid into #oTbl_cd(ID)
(select #outID1, cm.servertype, cm.configpath, cm.configtype
from configpthmaster cm
where cm.appid = #appid )
select #outID2 = ID from #oTbl_cd
insert into configkeydetails(confiid, keyname)
output inserted.Keyid into #oTbl_ckd(ID)
(select #outID2, cm.key
from configpthmaster cm
where cm.appid = #appid)
begin
commit
end
server_details table has an identity column ID with is auto-generated ie. #outID1 and first insert query inserts only 1 row.
configpthmaster table is not related to any other table directly and has 2 unique data rows, which I want to fetch to insert data into other tables, one by one during insertion.
The second insert query fetch data from configpthmaster table
and insert 2 rows in configdetails while generating (auto-generated) ID ie. #outID2.
It also has a FK mapped to server_details.
The problem is "#outID2" giving last inserted ID only (ie. if two id generated 100,101 i am getting 101) which eventually on 3rd insertion, inserting 2 rows with same id 101 only but i want the insertion should be linear. i.e one for 100 and other for 101.
If zero rows affected while insertion how to rollback the transaction?
How can I achieve these requirements? Please help.
Change your procedure like below,and try again.
ALTER PROCEDURE usp_newServerDetails(#appid int, #envid int,#serType varchar(20),#servName varchar(20))
AS
BEGIN
BEGIN TRY
DECLARE #Output TABLE (ID int,TableName VARCHAR(50),cmKey VARCHAR(50)) --table variable for keeping Inserted ID's
BEGIN TRAN
IF EXISTS ( SELECT 1 FROM configpthmaster cm WHERE cm.appid = #appid )
AND ( SELECT 1 FROM configkeydetails ck WHERE ck.appid = #appid ) --add a conditon to satisfy the valid insertions
BEGIN
INSERT INTO server_detials(envid,servertype,servername)
OUTPUT inserted.serverid,'server_detials',NULL INTO #Output(ID,TableName,cmKey )
VALUES(#envid ,#serType ,#servName)
INSERT INTO configdetails(serverid,servertype,configpath,configtype)
OUTPUT inserted.configid,'configdetails',cm.Key INTO #Output(ID,TableName,cmKey )
SELECT t.ID,cm.servertype,cm.configpath,cm.configtype
FROM configpthmaster cm
CROSS APPLY (SELECT ID FROM #Output WHERE TableName='server_detials')t
WHERE cm.appid = #appid
INSERT INTO configkeydetails(configId,keyname)
SELECT ID,cmKey FROM #Output
WHERE TableName='configdetails'
END
COMMIT TRAN
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK
END CATCH
END
Could you try this solution?
alter proc usp_newServerDetails(#appid int, #envid int,#serType varchar(20),#servName varchar(20))
as
declare #oTbl_sd table (ID int)
declare #outID1
declare #oTbl_cd table (ID int)
declare #outID2
begin Transaction
insert into server_detials(envid,servertype,servername)
output inserted.serverid into #oTbl_sd(ID)
values(#envid ,#serType ,#servName)
select #outID1 = ID from #oTbl_sd
insert into configdetails(serverid,servertype,configpath,configtype)
output inserted.configid into #oTbl_cd(ID)
(select #outID1 ,cm.servertype,cm.configpath,cm.configtype from configpthmaster cm where cm.appid = #appid )
select #outID2 = ID from #oTbl_cd
insert into configkeydetails(confiid,keyname)
output inserted.Keyid into #oTbl_ckd(ID)
(select isnull(replace(stuff((SELECT inserted.configid FOR xml path('')), 1, 1, ''), '&', '&'), '') ,cm.key, from configpthmaster cm where cm.appid = #appid )
begin
commit
end
I just added STUFF in your code.
The STUFF function inserts a string into another string.
Do take note that using STUFF drastically slows the processing time of the code.
for more information about STUFF
I have been trying inserting some rows into a temporary table which required identity from table A ...this A table is being inserted from a cursor..
everything is working fine here but only the problem i am getting is that i don't get identity of first inserted row in table A
suppose i insert 3 rows in table A ...i get last 2 rows in temporary table ..not first one..and if i send just one row in table A then temporary table is blank
here is my stored procedure ..table A is tblClients
Create PROCEDURE [dbo].[lsp1_propAdmClnt]
(
#usrprflId bigint,
#preClient tpClient readonly
)
as
declare #err int
CREATE TABLE #tblids(
clntid int,
imgname nvarchar(350)
)
declare #clntid as int
declare #clntname nvarchar(300)
declare #imgname nvarchar(300)
Begin Transaction
declare transfclntid cursor for select clntname, [imgname] from #preClient
open transfclntid
fetch next from transfclntid into #clntname, #imgname
while ##fetch_status=0
begin
insert into tblClients (usrprflId,Name,Img,cdate)
values(#usrprflId, #clntname, #imgname,GETDATE())
SET #clntid = (SELECT SCOPE_IDENTITY())
insert into #tblids (clntid, imgname) values (#clntid, #imgname)
fetch next from transfclntid into #clntname, #imgname
end
close transfclntid
deallocate transfclntid
select * from #tblids
select #err=##TOTAL_ERRORS
if(#err<>0)
Begin
Rollback Transaction
return 0
End
Commit transaction
First, you should replace that cursor with following INSERT SELECT statement:
declare #tblids table
(
clntid INT NOT NULL,
imgname NVARCHAR(350)
);
insert into tblClients (usrprflId, Name, Img, cdate)
output inserted.clntid, inserted.Img into #tblids (clntid, imgname)
select #usrprflId, clntname, [imgname], GETDATE()
from #preClient
Then if you need the first identity value you could use:
SELECT MIN(clntid)
FROM #tblids
In my sql code I have declared a table(A) and inserting some rows to that table from a database table(B). Then I have to take those inserted rows from A and put it into a CURSOR and after I do a FETCH NEXT still the ##FETCH_STATUS is -1. But the expected value for the ##FETCH_STATUS is 0.
I am putting a simplified code below the question.
Can I know what is the wrong with this code. Can I use declared tables to populate the CURSORs in SQL or cursor has to be populated from a created table in the database.
// This is a code that goes inside a Stored Procedure.
AS
DECLARE A TABLE (.........)// A table has same fields in table B
DECLARE s INT
WHILE EXISTS ( SELECT * FROM B WHERE ......)
BEGIN
BEGIN TRAN
INSERT INTO A SELECT TOP 10 (....)FROM B WITH (UPDLOCK, HOLDLOCK) WHERE ....
SELECT s = count(*) from A // this returns some value which means inserting is working
DECLARE dataSet CURSOR FOR (SELECT..... FROM A)
OPEN dataSet
FETCH NEXT FROM dataSet INTO ...
WHILE ##FETCH_STATUS = 0 // coming value for this is -1
BEGIN
//Code goes here
FETCH NEXT FROM dataSet INTO ...
END
CLOSE dataSet
DEALLOCATE dataSet
DELETE FROM A
COMMIT TRAN
END
Here's a runnable version of the OPs question - but it doesn't exhibit the issue. This isn't an answer, so CW, and I'll delete if/when the OP does post an actual example:
create table B (ID int not null,Val1 varchar(10) not null)
go
insert into B(ID,Val1) values (1,'abc'),(2,'ade')
go
create procedure DoStuff
AS
DECLARE #A TABLE (ID int not null,Val1 varchar(10) not null)
DECLARE #s INT
WHILE EXISTS ( SELECT * FROM B WHERE Val1 like 'a%')
BEGIN
BEGIN TRAN
INSERT INTO #A SELECT TOP 10 ID,Val1 FROM B WITH (UPDLOCK, HOLDLOCK) WHERE Val1 like 'a%'
SELECT #s = count(*) from #A
DECLARE dataSet CURSOR FOR (SELECT ID,Val1 FROM #A)
declare #ID int
declare #Val1 varchar(10)
OPEN dataSet
FETCH NEXT FROM dataSet INTO #ID,#Val1
WHILE ##FETCH_STATUS = 0
BEGIN
RAISERROR('%i: %s',10,1,#ID,#Val1) WITH NOWAIT
UPDATE B set Val1 = 'done' where ID = #ID
FETCH NEXT FROM dataSet INTO #ID,#Val1
END
CLOSE dataSet
DEALLOCATE dataSet
DELETE FROM #A
COMMIT TRAN
END
GO
EXEC DoStuff
GO
SELECT * from B
Output:
(2 row(s) affected)
(2 row(s) affected)
1: abc
(1 row(s) affected)
2: ade
(1 row(s) affected)
(2 row(s) affected)
(2 row(s) affected)
and table B:
ID Val1
1 done
2 done
Since its uncommited transaction on table A, table A might have been locked. So for your dataset cursor try-
select '' from A with (nolock) where ...
i think create temp table then declare it for cursor, not must be have an existing table
like:
DECLARE cursor_name CURSOR FOR SELECT id INTO temp_table FROM user_id
I'm using SQL Server 2005.
I am migrating data over from a current database (single table) to a new database (normalized - many tables). In the new database, I have a base table (let's call it "BaseTable"), and multiple other tables (let's call them "DependentA", and "DependentB"). Some of the data from the old database will go to BaseTable, and some will go to the other two. BaseTable has a one-to-one relationship with both DependentA and DependentB, using the Id of them as the foreign key.
So here's my question. How should I migrate the data over? Here is a query I've been trying, which is working except for one thing: the foreign keys in BaseTable for the other two are identical, instead or having a different one each.
Begin SQL:
BEGIN TRANSACTION
DECLARE #dep1Id int
DECLARE #dep2Id int
INSERT INTO DependentA (column1, column2)
SELECT c1, c2
FROM OldDatabase.OldTable
SELECT #dep1Id = Scope_Identity()
INSERT INTO DependentB (column3, column4)
SELECT c3, c4
FROM OldDatabase.OldTable
SELECT #dep2Id = Scope_Identity()
INSERT INTO BaseTable (column5, dependentTable1Id, dependentTablr2Id)
SELECT c5, #dep1Id, #dep2Id
FROM OldDatabase.OldTable
COMMIT
The problem is that #dep1Id and #dep1Id are scalar and are retaining the last value only from the two set based inserts.
Since it's a one off you should probably do it as a cursor
DECLARE CURSOR #curs FOR
SELECT c1,c2,c3,c4,c5 FROM OldDatebase
open #curs
fetch next from #curs into
#c1,#c2,#c3,#c4,#c5 --declare these!
while ##fetch_status <> 0
BEGIN
INSERT INTO DependentA (column1, column2) VALUES #c1, #c2
SELECT #dep1Id = Scope_Identity()
INSERT INTO DependentB (column3, column4) VALUES #c3, #c4
SELECT #dep2Id = Scope_Identity()
INSERT INTO BaseTable (column5, department1Id, department2Id) #c5, #dep1Id, #dep2Id
fetch next from #curs into
#c1,#c2,#c3,#c4,#c5
END
close #curs
deallocate #curs
My cursor syntax is probably riddled with errors, but you get the idea.
To avoid a cursor for large data sets, temporarily include the OldTable_id in the new tables.
BEGIN TRANSACTION
INSERT INTO DependentA (OldTable_id, column1, column2)
SELECT ot.id, ot.c1, ot.c2
FROM OldDatabase.OldTable ot
INSERT INTO BaseTable (OldTable_id, column5)
SELECT ot.id, ot.c5
FROM OldDatabase.OldTable ot
UPDATE BaseTable
SET BaseTable.dependentTable1_id = DependentA.id
FROM BaseTable
INNER JOIN DependentA on DependentA.OldTable_id = BaseTable.OldTable_id
COMMIT
Do the same for DependentB table and any other tables being normalized out of the OldTable.
Delete OldTable_id after the data migration.
[enter image description here][1]ZeorOne is the main table from which you want to get data and insert it into zero and one table respectively.
select idzero,namezero,idone,nameone from zeroone
insert into zero
select idzero,namezero from zeroone
insert into one
select idone,nameone from zeroone
or you want to use cursor to insert data with selected columns from Zeroone
into to two tables the query is here
Declare #idzero int
Declare #namezero varchar(50)
Declare #idone int
Declare #nameone varchar(50)
Declare Cur Cursor for
select idzero,namezero,idone,nameone from zeroone
open Cur
fetch Cur into #idzero,#namezero,#idone,#nameone
While ##fetch_status = 0
begin
insert into zero
select #idzero,#namezero
insert into one
select #idone,#nameone
fetch Cur into #idzero,#namezero,#idone,#nameone
end
close Cur
Deallocate Cur
DECLARE #Product_Name varchar(50),#Generic_Name varchar(50),#Category_Name varchar(50),#Manufacture_Name varchar(50),
#UOM_Name varchar(50),#ProductId int,#GenericId int,#CategoryId int,#ManufactureId int,#UOMId int
DECLARE MultiplTable CURSOR FOR
SELECT ProductName,GenericName,CategoryName,ManufacturerName,UOMName from Noor_ProductList
open MultiplTable
fetch next from MultiplTable into
#Product_Name,#Generic_Name,#Category_Name,#Manufacture_Name,#UOM_Name --declare these!
while ##fetch_status = 0
BEGIN
INSERT INTO Noor_GenericMaster(GenericName) VALUES (#Generic_Name)
SELECT #GenericId = Scope_Identity()
INSERT INTO Noor_CategoryMaster(CategoryName) VALUES (#Category_Name)
SELECT #CategoryId = Scope_Identity()
INSERT INTO Noor_ManufaturerMaster(ManufaturerName) VALUES (#Manufacture_Name)
SELECT #ManufactureId = Scope_Identity()
INSERT INTO Noor_UOMMaster(UOMName) VALUES (#UOM_Name)
SELECT #UOMId = Scope_Identity()
INSERT INTO Noor_ProductMaster (ProductName,GenericID,CategoryID,ManufaturerID,UOMID)
values (#Product_Name,#GenericId,#CategoryId,#ManufactureId,#UOMId)
SELECT #ProductId = Scope_Identity()
fetch next from MultiplTable into #Product_Name,#Generic_Name,#Category_Name,#Manufacture_Name,#UOM_Name
END
close MultiplTable
deallocate MultiplTable