I created the following two test tables with a trigger to log all the action (Insert, Delete and Update).
Set up tables and trigger:
-- drop table test; drop table testLog
create table test (id int identity primary key, x int);
create table testLog (idx int identity primary key, Action varchar(10), id int not null,
x_deleted int, x_inserted int, uid uniqueidentifier);
go
-- Trigger to log the changes
create trigger trigger_test on test
after insert, delete, update
as
declare #id uniqueidentifier = context_info();
print #id;
insert testLog (id, Action, x_deleted, x_inserted, uid)
select isnull(d.id, i.id) ,
case when i.id is not null and d.id is not null then 'Updated'
when d.id is not null then 'Deleted'
when i.id is not null then 'Inserted'
end ,
d.x ,
i.x ,
#id
from Deleted d
full outer join inserted i on i.id = d.id;
set context_info 0;
go
Now insert some sample data
set context_info 0
insert test (x) values (10), (20), (30), (40), (50);
SELECT * FROM test;
SELECT * FROM testLog
go
The following statements work fine. The correct context_info() is saved in the log table.
begin tran
declare #newid uniqueidentifier = newid()
--
set context_info #newid
print #newid
insert test(x) values (1)
set context_info #newid
update test set x = 2 where id = 1
SELECT * FROM dbo.testLog;
rollback
go
However, only insert part of the Merge got the value in context_info()?
begin tran
declare #newid uniqueidentifier = newid()
--
set context_info #newid
print #newid;
with v as (select * from (values (1, 11), (2, 22), (6, 66)) v (id, x))
merge test as t using v on t.id = v.id
when matched then update set x = v.x
when not matched by target then insert (x) values (x);
SELECT * FROM dbo.testLog;
rollback
go
The uid of the last two updates got zeros.
Don't set context_info to zero in the trigger. Why would you do that in the first place - it is not the trigger's responsibility to "clean up". The merge statement will cause the trigger to execute for inserts separately from updates. Did you not notice the multiple "prints" in the results pane? That should have been a big clue.
I have two tables; the first named PAYMENT and the second is a historical table named RecordPay.
I have two triggers, the first one is for insert in order to insert into the historical tables records from Payment table.
Here is the code:
ALTER TRIGGER [dbo].[INSERT_HIST]
ON [dbo].[PAYMENT]
FOR INSERT
AS
BEGIN
DECLARE #User_op varchar(50)
DECLARE #RGNO varchar(50)
DECLARE #PAYEUR varchar(50)
DECLARE #DATESYS SMALLDATETIME
DECLARE #RG_DATE SMALLDATETIME
DECLARE #RG_Montant varchar(50)
SELECT #User_op = cbUserName
FROM cbUserSession
WHERE cbSession = ##SPID
SELECT #PAYEUR = CT_NumPayeur FROM INSERTED
SELECT #DATESYS = GETDATE()
SELECT #RG_Montant = RG_Montant FROM INSERTED
SELECT #RG_DATE = RG_DATE FROM INSERTED
SELECT #RGNO = RG_No FROM INSERTED
INSERT INTO RecordPay (RG_NO, PAYEUR, CAISSIER, Montant, DATESYS, DATECAI)
VALUES (#RGNO, #PAYEUR, #user_op, #RG_Montant, #DATESYS, #RG_DATE)
This works well, my problem when I delete a row from PAYMENT, in RecordPay the record exists, and then when I insert another row in PAYMENT I had two RG_NO whith the same number.
For example I insert a row in PAYMENT with RG_NO=1 then I deleted, and I create another row with RG_NO=2, in the recordPay (historical table) i get two lines with RG_NO=1.
Here is the trigger for delete but it does not work
ALTER TRIGGER [dbo].[DEL_HIST]
ON [dbo].[PAYMENT]
AFTER DELETE
AS
BEGIN
DECLARE #User_op varchar(50)
DECLARE #RGNO varchar(50)
DECLARE #PAYEUR varchar(50)
DECLARE #DATESYS SMALLDATETIME
DECLARE #RG_DATE SMALLDATETIME
DECLARE #RG_Montant varchar(50)
SELECT #PAYEUR = CT_NumPayeur FROM DELETED
SELECT #RG_Montant = RG_Montant FROM DELETED
SELECT #RG_DATE = RG_DATE FROM DELETED
SELECT #RGNO = RG_No FROM DELETED
DELETE FROM RECORDPAY WHERE
RG_NO=#RGNO and PAYEUR= #PAYEUR and CAISSIER=#user_op and Montant=#RG_Montant
END
Your trigger will BREAK as soon as an INSERT statement inserts more than 1 row at a time - because in that case, your trigger gets called once for the INSERT statement, and Inserted will contain multiple rows.
Which one of those 10 rows are you selecting from here??
SELECT #PAYEUR = CT_NumPayeur FROM INSERTED
SELECT #RG_Montant = RG_Montant FROM INSERTED
SELECT #RG_DATE = RG_DATE FROM INSERTED
SELECT #RGNO = RG_No FROM INSERTED
It's arbitrary and non-deterministic - and you will simply ignore all other rows in Inserted.
You need to rewrite your trigger to take this into account:
ALTER TRIGGER [dbo].[INSERT_HIST]
ON [dbo].[PAYMENT]
FOR INSERT
AS
BEGIN
DECLARE #User_op varchar(50)
SELECT #User_op = cbUserName
FROM cbUserSession
WHERE cbSession = ##SPID
-- insert a record for ALL the rows that were inserted into
-- your history table in a single, elegant, set-based statement
INSERT INTO RecordPay (RG_NO, PAYEUR, CAISSIER, Montant, DATESYS, DATECAI)
SELECT
RG_No, CT_NumPayeur, #User_op, RG_Montant, SYSDATETIME(), RG_Date
FROM
Inserted
I have a requirement to insert multiple rows into table1 and at the same time insert a row into table2 with a pkID from table1 and a value that comes from a SP parameter.
I created a stored procedure that performs a batch insert with a table valued parameter which contains the rows to be inserted into table1. But I have a problem with inserting the row into table2 with the corresponding Id (identity) from table1, along with parameter value that I have passed.
Is there anyone who implemented this, or what is the good solution for this?
CREATE PROCEDURE [dbo].[oSP_TV_Insert]
#uID int
,#IsActive int
,#Type int -- i need to insert this in table 2
,#dTableGroup table1 READONLY -- this one is a table valued
AS
DECLARE #SQL varchar(2000)
DECLARE #table1Id int
BEGIN
INSERT INTO dbo.table1
(uID
,Name
,Contact
,Address
,City
,State
,Zip
,Phone
,Active)
SELECT
#uID
,Name
,Contact
,Address
,City
,State
,Zip
,Phone
,Active
,#G_Active
FROM #dTableGroup
--the above query will perform batch insert using the records from dTableGroup which is table valued
SET #table1ID = SCOPE_IDENTITY()
-- this below will perform inserting records to table2 with every Id inserted in table1.
Insert into table2(#table1ID , #type)
You need to temporarily store the inserted identity values and then create a second INSERT statement - using the OUTPUT clause.
Something like:
-- declare table variable to hold the ID's that are being inserted
DECLARE #InsertedIDs TABLE (ID INT)
-- insert values into table1 - output the inserted ID's into #InsertedIDs
INSERT INTO dbo.table1(ID, Name, Contact, Address, City, State, Zip, Phone, Active)
OUTPUT INSERTED.ID INTO #InsertedIDs
SELECT
#ID, Name, Contact, Address, City, State, Zip, Phone, Active, #G_Active
FROM #dTableGroup
and then you can have your second INSERT statement:
INSERT INTO dbo.table2(Table1ID, Type)
SELECT ID, #type FROM #InsertedIDs
See the MSDN docs on the OUTPUT clause for more details on what you can do with the OUTPUT clause - one of the most underused and most "unknown" features of SQL Server these days!
Another approach using OUTPUT clause and only one statement for inserting data in both destination tables:
--Parameters
DECLARE #TableGroup TABLE
(
Name NVARCHAR(100) NOT NULL
,Phone VARCHAR(10) NOT NULL
);
DECLARE #Type INT;
--End Of parameters
--Destination tables
DECLARE #FirstDestinationTable TABLE
(
FirstDestinationTableID INT IDENTITY(1,1) PRIMARY KEY
,Name NVARCHAR(100) NOT NULL
,Phone VARCHAR(10) NOT NULL
);
DECLARE #SecondDestinationTable TABLE
(
SecondDestinationTable INT IDENTITY(2,2) PRIMARY KEY
,FirstDestinationTableID INT NOT NULL
,[Type] INT NOT NULL
,CHECK([Type] > 0)
);
--End of destination tables
--Test1
--initialization
INSERT #TableGroup
VALUES ('Bogdan SAHLEAN', '0721200300')
,('Ion Ionescu', '0211002003')
,('Vasile Vasilescu', '0745600800');
SET #Type = 9;
--execution
INSERT #SecondDestinationTable (FirstDestinationTableID, [Type])
SELECT FirstINS.FirstDestinationTableID, #Type
FROM
(
INSERT #FirstDestinationTable (Name, Phone)
OUTPUT inserted.FirstDestinationTableID
SELECT tg.Name, tg.Phone
FROM #TableGroup tg
) FirstINS
--check records
SELECT *
FROM #FirstDestinationTable;
SELECT *
FROM #SecondDestinationTable;
--End of test1
--Test2
--initialization
DELETE #TableGroup;
DELETE #FirstDestinationTable;
DELETE #SecondDestinationTable;
INSERT #TableGroup
VALUES ('Ion Ionescu', '0210000000')
,('Vasile Vasilescu', '0745000000');
SET #Type = 0; --Wrong value
--execution
INSERT #SecondDestinationTable (FirstDestinationTableID, [Type])
SELECT FirstINS.FirstDestinationTableID, #Type
FROM
(
INSERT #FirstDestinationTable (Name, Phone)
OUTPUT inserted.FirstDestinationTableID
SELECT tg.Name, tg.Phone
FROM #TableGroup tg
) FirstINS
--check records
DECLARE #rc1 INT, #rc2 INT;
SELECT *
FROM #FirstDestinationTable;
SET #rc1 = ##ROWCOUNT;
SELECT *
FROM #SecondDestinationTable;
SET #rc2 = ##ROWCOUNT;
RAISERROR('[Test2 results] #FirstDestinationTable: %d rows; ##SecondDestinationTable: %d rows;',1,1,#rc1,#rc2);
--End of test1
Since you need all inserted identity values, look at the output clause of the insert statement: http://msdn.microsoft.com/en-us/library/ms177564.aspx
Let's say I have the following simple table variable:
declare #databases table
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
Is declaring and using a cursor my only option if I wanted to iterate through the rows? Is there another way?
First of all you should be absolutely sure you need to iterate through each row — set based operations will perform faster in every case I can think of and will normally use simpler code.
Depending on your data it may be possible to loop using just SELECT statements as shown below:
Declare #Id int
While (Select Count(*) From ATable Where Processed = 0) > 0
Begin
Select Top 1 #Id = Id From ATable Where Processed = 0
--Do some processing here
Update ATable Set Processed = 1 Where Id = #Id
End
Another alternative is to use a temporary table:
Select *
Into #Temp
From ATable
Declare #Id int
While (Select Count(*) From #Temp) > 0
Begin
Select Top 1 #Id = Id From #Temp
--Do some processing here
Delete #Temp Where Id = #Id
End
The option you should choose really depends on the structure and volume of your data.
Note: If you are using SQL Server you would be better served using:
WHILE EXISTS(SELECT * FROM #Temp)
Using COUNT will have to touch every single row in the table, the EXISTS only needs to touch the first one (see Josef's answer below).
Just a quick note, if you are using SQL Server (2008 and above), the examples that have:
While (Select Count(*) From #Temp) > 0
Would be better served with
While EXISTS(SELECT * From #Temp)
The Count will have to touch every single row in the table, the EXISTS only needs to touch the first one.
This is how I do it:
declare #RowNum int, #CustId nchar(5), #Name1 nchar(25)
select #CustId=MAX(USERID) FROM UserIDs --start with the highest ID
Select #RowNum = Count(*) From UserIDs --get total number of records
WHILE #RowNum > 0 --loop until no more records
BEGIN
select #Name1 = username1 from UserIDs where USERID= #CustID --get other info from that row
print cast(#RowNum as char(12)) + ' ' + #CustId + ' ' + #Name1 --do whatever
select top 1 #CustId=USERID from UserIDs where USERID < #CustID order by USERID desc--get the next one
set #RowNum = #RowNum - 1 --decrease count
END
No Cursors, no temporary tables, no extra columns.
The USERID column must be a unique integer, as most Primary Keys are.
Define your temp table like this -
declare #databases table
(
RowID int not null identity(1,1) primary key,
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
Then do this -
declare #i int
select #i = min(RowID) from #databases
declare #max int
select #max = max(RowID) from #databases
while #i <= #max begin
select DatabaseID, Name, Server from #database where RowID = #i --do some stuff
set #i = #i + 1
end
Here is how I would do it:
Select Identity(int, 1,1) AS PK, DatabaseID
Into #T
From #databases
Declare #maxPK int;Select #maxPK = MAX(PK) From #T
Declare #pk int;Set #pk = 1
While #pk <= #maxPK
Begin
-- Get one record
Select DatabaseID, Name, Server
From #databases
Where DatabaseID = (Select DatabaseID From #T Where PK = #pk)
--Do some processing here
--
Select #pk = #pk + 1
End
[Edit] Because I probably skipped the word "variable" when I first time read the question, here is an updated response...
declare #databases table
(
PK int IDENTITY(1,1),
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
--/*
INSERT INTO #databases (DatabaseID, Name, Server) SELECT 1,'MainDB', 'MyServer'
INSERT INTO #databases (DatabaseID, Name, Server) SELECT 1,'MyDB', 'MyServer2'
--*/
Declare #maxPK int;Select #maxPK = MAX(PK) From #databases
Declare #pk int;Set #pk = 1
While #pk <= #maxPK
Begin
/* Get one record (you can read the values into some variables) */
Select DatabaseID, Name, Server
From #databases
Where PK = #pk
/* Do some processing here */
/* ... */
Select #pk = #pk + 1
End
If you have no choice than to go row by row creating a FAST_FORWARD cursor. It will be as fast as building up a while loop and much easier to maintain over the long haul.
FAST_FORWARD
Specifies a FORWARD_ONLY, READ_ONLY cursor with performance optimizations enabled. FAST_FORWARD cannot be specified if SCROLL or FOR_UPDATE is also specified.
This will work in SQL SERVER 2012 version.
declare #Rowcount int
select #Rowcount=count(*) from AddressTable;
while( #Rowcount>0)
begin
select #Rowcount=#Rowcount-1;
SELECT * FROM AddressTable order by AddressId desc OFFSET #Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end
Another approach without having to change your schema or using temp tables:
DECLARE #rowCount int = 0
,#currentRow int = 1
,#databaseID int
,#name varchar(15)
,#server varchar(15);
SELECT #rowCount = COUNT(*)
FROM #databases;
WHILE (#currentRow <= #rowCount)
BEGIN
SELECT TOP 1
#databaseID = rt.[DatabaseID]
,#name = rt.[Name]
,#server = rt.[Server]
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY t.[DatabaseID], t.[Name], t.[Server]
) AS [RowNumber]
,t.[DatabaseID]
,t.[Name]
,t.[Server]
FROM #databases t
) rt
WHERE rt.[RowNumber] = #currentRow;
EXEC [your_stored_procedure] #databaseID, #name, #server;
SET #currentRow = #currentRow + 1;
END
You can use a while loop:
While (Select Count(*) From #TempTable) > 0
Begin
Insert Into #Databases...
Delete From #TempTable Where x = x
End
Lightweight, without having to make extra tables, if you have an integer ID on the table
Declare #id int = 0, #anything nvarchar(max)
WHILE(1=1) BEGIN
Select Top 1 #anything=[Anything],#id=#id+1 FROM Table WHERE ID>#id
if(##ROWCOUNT=0) break;
--Process #anything
END
I really do not see the point why you would need to resort to using dreaded cursor.
But here is another option if you are using SQL Server version 2005/2008
Use Recursion
declare #databases table
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
--; Insert records into #databases...
--; Recurse through #databases
;with DBs as (
select * from #databases where DatabaseID = 1
union all
select A.* from #databases A
inner join DBs B on A.DatabaseID = B.DatabaseID + 1
)
select * from DBs
-- [PO_RollBackOnReject] 'FININV10532'
alter procedure PO_RollBackOnReject
#CaseID nvarchar(100)
AS
Begin
SELECT *
INTO #tmpTable
FROM PO_InvoiceItems where CaseID = #CaseID
Declare #Id int
Declare #PO_No int
Declare #Current_Balance Money
While (Select ROW_NUMBER() OVER(ORDER BY PO_LineNo DESC) From #tmpTable) > 0
Begin
Select Top 1 #Id = PO_LineNo, #Current_Balance = Current_Balance,
#PO_No = PO_No
From #Temp
update PO_Details
Set Current_Balance = Current_Balance + #Current_Balance,
Previous_App_Amount= Previous_App_Amount + #Current_Balance,
Is_Processed = 0
Where PO_LineNumber = #Id
AND PO_No = #PO_No
update PO_InvoiceItems
Set IsVisible = 0,
Is_Processed= 0
,Is_InProgress = 0 ,
Is_Active = 0
Where PO_LineNo = #Id
AND PO_No = #PO_No
End
End
It's possible to use a cursor to do this:
create function [dbo].f_teste_loop
returns #tabela table
(
cod int,
nome varchar(10)
)
as
begin
insert into #tabela values (1, 'verde');
insert into #tabela values (2, 'amarelo');
insert into #tabela values (3, 'azul');
insert into #tabela values (4, 'branco');
return;
end
create procedure [dbo].[sp_teste_loop]
as
begin
DECLARE #cod int, #nome varchar(10);
DECLARE curLoop CURSOR STATIC LOCAL
FOR
SELECT
cod
,nome
FROM
dbo.f_teste_loop();
OPEN curLoop;
FETCH NEXT FROM curLoop
INTO #cod, #nome;
WHILE (##FETCH_STATUS = 0)
BEGIN
PRINT #nome;
FETCH NEXT FROM curLoop
INTO #cod, #nome;
END
CLOSE curLoop;
DEALLOCATE curLoop;
end
I'm going to provide the set-based solution.
insert #databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server
From ... (Use whatever query you would have used in the loop or cursor)
This is far faster than any looping techique and is easier to write and maintain.
I prefer using the Offset Fetch if you have a unique ID you can sort your table by:
DECLARE #TableVariable (ID int, Name varchar(50));
DECLARE #RecordCount int;
SELECT #RecordCount = COUNT(*) FROM #TableVariable;
WHILE #RecordCount > 0
BEGIN
SELECT ID, Name FROM #TableVariable ORDER BY ID OFFSET #RecordCount - 1 FETCH NEXT 1 ROW;
SET #RecordCount = #RecordCount - 1;
END
This way I don't need to add fields to the table or use a window function.
I agree with the previous post that set-based operations will typically perform better, but if you do need to iterate over the rows here's the approach I would take:
Add a new field to your table variable (Data Type Bit, default 0)
Insert your data
Select the Top 1 Row where fUsed = 0 (Note: fUsed is the name of the field in step 1)
Perform whatever processing you need to do
Update the record in your table variable by setting fUsed = 1 for the record
Select the next unused record from the table and repeat the process
DECLARE #databases TABLE
(
DatabaseID int,
Name varchar(15),
Server varchar(15),
fUsed BIT DEFAULT 0
)
-- insert a bunch rows into #databases
DECLARE #DBID INT
SELECT TOP 1 #DBID = DatabaseID from #databases where fUsed = 0
WHILE ##ROWCOUNT <> 0 and #DBID IS NOT NULL
BEGIN
-- Perform your processing here
--Update the record to "used"
UPDATE #databases SET fUsed = 1 WHERE DatabaseID = #DBID
--Get the next record
SELECT TOP 1 #DBID = DatabaseID from #databases where fUsed = 0
END
Step1: Below select statement creates a temp table with unique row number for each record.
select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp
Step2:Declare required variables
DECLARE #ROWNUMBER INT
DECLARE #ename varchar(100)
Step3: Take total rows count from temp table
SELECT #ROWNUMBER = COUNT(*) FROM #tmp_sri
declare #rno int
Step4: Loop temp table based on unique row number create in temp
while #rownumber>0
begin
set #rno=#rownumber
select #ename=ename from #tmp_sri where rno=#rno **// You can take columns data from here as many as you want**
set #rownumber=#rownumber-1
print #ename **// instead of printing, you can write insert, update, delete statements**
end
This approach only requires one variable and does not delete any rows from #databases. I know there are a lot of answers here, but I don't see one that uses MIN to get your next ID like this.
DECLARE #databases TABLE
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
DECLARE #CurrID INT
SELECT #CurrID = MIN(DatabaseID)
FROM #databases
WHILE #CurrID IS NOT NULL
BEGIN
-- Do stuff for #CurrID
SELECT #CurrID = MIN(DatabaseID)
FROM #databases
WHERE DatabaseID > #CurrID
END
Here's my solution, which makes use of an infinite loop, the BREAK statement, and the ##ROWCOUNT function. No cursors or temporary table are necessary, and I only need to write one query to get the next row in the #databases table:
declare #databases table
(
DatabaseID int,
[Name] varchar(15),
[Server] varchar(15)
);
-- Populate the [#databases] table with test data.
insert into #databases (DatabaseID, [Name], [Server])
select X.DatabaseID, X.[Name], X.[Server]
from (values
(1, 'Roger', 'ServerA'),
(5, 'Suzy', 'ServerB'),
(8675309, 'Jenny', 'TommyTutone')
) X (DatabaseID, [Name], [Server])
-- Create an infinite loop & ensure that a break condition is reached in the loop code.
declare #databaseId int;
while (1=1)
begin
-- Get the next database ID.
select top(1) #databaseId = DatabaseId
from #databases
where DatabaseId > isnull(#databaseId, 0);
-- If no rows were found by the preceding SQL query, you're done; exit the WHILE loop.
if (##ROWCOUNT = 0) break;
-- Otherwise, do whatever you need to do with the current [#databases] table row here.
print 'Processing #databaseId #' + cast(#databaseId as varchar(50));
end
This is the code that I am using 2008 R2. This code that I am using is to build indexes on key fields (SSNO & EMPR_NO) n all tales
if object_ID('tempdb..#a')is not NULL drop table #a
select 'IF EXISTS (SELECT name FROM sysindexes WHERE name ='+CHAR(39)+''+'IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+char(39)+')'
+' begin DROP INDEX [IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+'] ON '+table_schema+'.'+table_name+' END Create index IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+ ' on '+ table_schema+'.'+table_name+' ('+COLUMN_NAME+') ' 'Field'
,ROW_NUMBER() over (order by table_NAMe) as 'ROWNMBR'
into #a
from INFORMATION_SCHEMA.COLUMNS
where (COLUMN_NAME like '%_SSNO_%' or COLUMN_NAME like'%_EMPR_NO_')
and TABLE_SCHEMA='dbo'
declare #loopcntr int
declare #ROW int
declare #String nvarchar(1000)
set #loopcntr=(select count(*) from #a)
set #ROW=1
while (#ROW <= #loopcntr)
begin
select top 1 #String=a.Field
from #A a
where a.ROWNMBR = #ROW
execute sp_executesql #String
set #ROW = #ROW + 1
end
SELECT #pk = #pk + 1
would be better:
SET #pk += #pk
Avoid using SELECT if you are not referencing tables are are just assigning values.