Recursive in SQL Server 2008 - sql

I've a scenario(table) like this:
This is table(Folder) structure. I've only records for user_id = 1 in this table. Now I need to insert the same folder structure for another user.
Sorry, I've updated the question...
yes, folder_id is identity column (but folder_id can be meshed up for a specific userID). Considering I don't know how many child folder can exists.
Folder_Names are unique for an user and Folder structures are not same for all user. Suppose user3 needs the same folder structure of user1, and user4 needs same folder structure of user2.
and I'll be provided only source UserID and destination UserID(assume destination userID doesn't have any folder structure).
How can i achieve this?

You can do the following:
SET IDENTITY_INSERT dbo.Folder ON
go
declare #maxFolderID int
select #maxFolderID = max(Folder_ID) from Folder
insert into Folder
select #maxFolderID + FolderID, #maxFolderID + Parent_Folder_ID, Folder_Name, 2
from Folder
where User_ID = 1
SET IDENTITY_INSERT dbo.Folder OFF
go
EDIT:
SET IDENTITY_INSERT dbo.Folder ON
GO
;
WITH m AS ( SELECT MAX(Folder_ID) AS mid FROM Folder ),
r AS ( SELECT * ,
ROW_NUMBER() OVER ( ORDER BY Folder_ID ) + m.mid AS rn
FROM Folder
CROSS JOIN m
WHERE User_ID = 1
)
INSERT INTO Folder
SELECT r1.rn ,
r2.rn ,
r1.Folder_Name ,
2
FROM r r1
LEFT JOIN r r2 ON r2.Folder_ID = r1.Parent_Folder_ID
SET IDENTITY_INSERT dbo.Folder OFF
GO

This is as close to set-based as I can make it. The issue is that we cannot know what new identity values will be assigned until the rows are actually in the table. As such, there's no way to insert all rows in one go, with correct parent values.
I'm using MERGE below so that I can access both the source and inserted tables in the OUTPUT clause, which isn't allowed for INSERT statements:
declare #FromUserID int
declare #ToUserID int
declare #ToCopy table (OldParentID int,NewParentID int)
declare #ToCopy2 table (OldParentID int,NewParentID int)
select #FromUserID = 1,#ToUserID = 2
merge into T1 t
using (select Folder_ID,Parent_Folder_ID,Folder_Name
from T1 where User_ID = #FromUserID and Parent_Folder_ID is null) s
on 1 = 0
when not matched then insert (Parent_Folder_ID,Folder_Name,User_ID)
values (NULL,s.Folder_Name,#ToUserID)
output s.Folder_ID,inserted.Folder_ID into #ToCopy (OldParentID,NewParentID);
while exists (select * from #ToCopy)
begin
merge into T1 t
using (select Folder_ID,p2.NewParentID,Folder_Name from T1
inner join #ToCopy p2 on p2.OldParentID = T1.Parent_Folder_ID) s
on 1 = 0
when not matched then insert (Parent_Folder_ID,Folder_Name,User_ID)
values (NewParentID,Folder_Name,#ToUserID)
output s.Folder_ID,inserted.Folder_ID into #ToCopy2 (OldParentID,NewParentID);
--This would be much simpler if you could assign table variables,
-- #ToCopy = #ToCopy2
-- #ToCopy2 = null
delete from #ToCopy;
insert into #ToCopy(OldParentID,NewParentID)
select OldParentID,NewParentID from #ToCopy2;
delete from #ToCopy2;
end
(I've also written this on the assumption that we don't ever want to have rows in the table with wrong or missing parent values)
In case the logic isn't clear - we first find rows for the old user which have no parent - these we can clearly copy for the new user immediately. On the basis of this insert, we track what new identity values have been assigned against which old identity value.
We then continue to use this information to identify the next set of rows to copy (in #ToCopy) - as the rows whose parents were just copied are the next set eligible to copy. We loop around until we produce an empty set, meaning all rows have been copied.
This doesn't cope with parent/child cycles, but hopefully you do not have any of those.

Assuming Folder.Folder_ID is an identity column, you would be best off doing this in two steps, the first step is to insert the folders you need, the next is to update the parent folder ID.
DECLARE #ExistingUserID INT = 1,
#NewUserID INT = 2;
BEGIN TRAN;
-- INSERT REQUIRED FOLDERS
INSERT Folder (Folder_Name, User_ID)
SELECT Folder_Name, User_ID = #NewUserID
FROM Folder
WHERE User_ID = #ExistingUserID;
-- UPDATE PARENT FOLDER
UPDATE f1
SET Parent_Folder_ID = p2.Folder_ID
FROM Folder AS f1
INNER JOIN Folder AS f2
ON f2.Folder_Name = f1.Folder_Name
AND f2.user_id = #ExistingUserID
INNER JOIN Folder AS p1
ON p1.Folder_ID = f2.Parent_Folder_ID
INNER JOIN Folder AS p2
ON p2.Folder_Name = p1.Folder_Name
AND p2.user_id = #NewUserID
WHERE f1.user_id = #NewUserID;
COMMIT TRAN;
Solution 2
DECLARE #Output TABLE (OldFolderID INT, NewFolderID INT, OldParentID INT);
DECLARE #ExistingUserID INT = 1,
#NewUserID INT = 2;
BEGIN TRAN;
MERGE Folder AS t
USING
( SELECT *
FROM Folder
WHERE user_ID = #ExistingUserID
) AS s
ON 1 = 0 -- WILL NEVER BE TRUE SO ALWAYS GOES TO MATCHED CLAUSE
WHEN NOT MATCHED THEN
INSERT (Folder_Name, User_ID)
VALUES (s.Folder_Name, #NewUserID)
OUTPUT s.Folder_ID, inserted.Folder_ID, s.Parent_Folder_ID
INTO #Output (OldFolderID, NewFolderID, OldParentID);
-- UPDATE PARENT FOLDER
UPDATE f
SET Parent_Folder_ID = p.NewFolderID
FROM Folder AS f
INNER JOIN #Output AS o
ON o.NewFolderID = f.Folder_ID
INNER JOIN #Output AS p
ON p.OldFolderID = o.OldParentID;
COMMIT TRAN;

Related

SQL - After insert Same Table

So I understand recursive triggers. Got to be careful of deadlocks etc. However this is only after an insert not after insert and update. Also, I have an audit trigger table that I am updating to make sure all is well. And querying after to double check. All looks fine but no update happens.
if exists (select 'a' from sys.triggers where name = 'invoicememologic')
begin
drop trigger invoicememologic
end
go
create trigger invoicememologic
on transactiontable
after insert
as
begin
declare #inum varchar(1000)
select #inum = min(transactioninvnum)
from
(select transactioninvnum
from inserted i
inner join project p on left(i.projectid, charindex(':', i.projectid)) = p.projectid
where right(i.projectid, 1) <> ':'
and abs(p.UseProjectMemoOnInv) = 1
group by transactioninvnum) b
while #inum is not null
begin
declare #rCount int
select #rCount = count(*)
from transactiontable
where TransactionInvNum = #inum
if #rCount = 1
begin
declare #tid varchar(100)
select #tid = transactionid
from transactiontable
where TransactionInvNum = #inum
declare #pmemo varchar(MAX)
select #pmemo = p.projectMemo
from transactiontable tt
inner join project p on left(tt.projectid, charindex(':', tt.projectid)) = p.projectid
where transactionInvNum = #inum
insert into audittrigger
values (#pmemo, #tid)
update transactiontable
set transactionmemo2 = #pmemo
where ltrim(rtrim(transactionid)) = ltrim(rtrim(#tid))
end
select #inum = min(transactioninvnum)
from
(select transactioninvnum
from inserted i
inner join project p on left(i.projectid, charindex(':', i.projectid)) = p.projectid
where abs(transactionjointinv) = 1
and right(i.projectid, 1) <> ':'
and abs(p.UseProjectMemoOnInv) = 1
group by transactioninvnum ) a
where transactioninvnum > #inum
end
end
Reason for trigger. 1 Invoice can be multiple rows in the database. 3 rows. So it only should update any one of the 3 rows. Doesn't matter. And it must grab the memo from the parent project of the phases that are being inserted into the database. hence the inner join on the left with charindex.
So I check the audit table. All looks well there. MEMO is there and the transactionid is there. I query after the trigger fires. TransactionID exists in the transactiontable but the memo2 is not being updated.
TransactionMemo2 is type of ntext. I thought it might be varchar with a direct update command will fail. I tried to update manually through setting a var as the text string and call the update manually with the transactionid being required. all worked fine. I am lost

A nested INSERT, UPDATE, DELETE, or MERGE statement must have an OUTPUT clause in UPDATE

I'm trying to update some values based on every Id in the list. The logic I have seems to be what I want.
I want to populate a temporary table of Ids. Then for every ID I want to apply this query and output the deleted date and the ID into a new table I've created.
I keep getting the error:
Msg 10716, Level 15, State 1, Line 25
A nested INSERT, UPDATE, DELETE, or MERGE statement must have an OUTPUT clause.
What does this mean? I thought I am OUTPUTTING into the new table I've created.
USE datatemp
GO
DECLARE #idlist TABLE (id INT)
INSERT INTO #idlist (id) VALUES (3009099)
DECLARE #EndDate DATETIME
SET #EndDate = '2099-12-12'
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TEMP_TABLE')
BEGIN
CREATE TABLE [TEMP_TABLE] (
[id] INT,
[thedatetoend] DATETIME);
END
BEGIN TRY
SELECT *
FROM #idlist AS idlist
OUTER APPLY(
UPDATE [custprofile]
SET thedatetoend = #EndDate
OUTPUT idlist.id, DELETED.thedatetoend
INTO [TEMP_TABLE]
FROM [custprofile] as bc
INNER JOIN [custinformation] as cc
ON cc.custengageid = bc.custengageid
WHERE cc.id = idlist.id
AND bc.modifierid = 2
AND bc.thedatetoend > GETDATE()
AND cc.type = 1) o
I think you may have more success by using a CTE and avoiding the outer apply approach you are currently using. Updates made to the CTE cascade to the source table. It might look something like the following but as some columns don't reference the table aliases don't expect this to work "as is" (i.e. I'm not sure if you are outputting ccid or bcid and I don't know which table thedatetoend belongs to.)
WITH
CTE AS (
SELECT
cc.id AS ccid, bcid AS bcid, thedatetoend
FROM [custprofile] AS bc
INNER JOIN [custinformation] AS cc ON cc.custengageid = bc.custengageid
INNER JOIN #idlist AS idlist ON cc.id = idlist.id
WHERE bc.modifierid = 2
AND bc.thedatetoend > GETDATE()
AND cc.type = 1
)
UPDATE CTE
SET thedatetoend = #EndDate
OUTPUT ccid, DELETED.thedatetoend
INTO [TEMP_TABLE]

SQL Server - Stored Procedure Return in Case Result Expression

I'd like to use a return from a procedure in a CASE statement. It can't be a function because that procedure returns an inserted key.
UPDATE TIM
SET CD_LINHA_EVENTO =
CASE WHEN
TIM.CD_SUBESTRUTURA_PARAMETRO = (SELECT TOP 1 SPZ.CD_SUBESTRUTURA_PARAMETRO FROM SUBESTRUTURA_PARAMETRO SPZ WITH (NOLOCK) WHERE SPZ.CD_SUBESTRUTURA = TIM.CD_SUBESTRUTURA
AND SPZ.FL_SELECAO = 1 ORDER BY SPZ.NR_ORDEM)
THEN
**EXEC [dbo].[SPRTO_NumeracaoEventos]**
ELSE
(SELECT MAX(TIM2.CD_LINHA_EVENTO) FROM #TB_INSERTED_MODIFIED TIM2 WITH(NOLOCK))
END
FROM #TB_INSERTED_MODIFIED TIM WITH (NOLOCK)
stored procedure [SPRTO_NumeracaoEventos]:
INSERT INTO TB_NUMERACAO_EVENTOS (VALOR) VALUES ('')
RETURN SCOPE_IDENTITY()
Thank you!
I wasn't able to figure out your table schema well enough to provide a direct answer, but I put together a way to use inserted identity values within an update trigger. Hopefully you can adapt this to fit your specific use case.
Have a look at this SQL Fiddle
As noted in the comments in the fiddle, this strategy is based on using the OUTPUT clause to capture identity values into a table variable. See http://msdn.microsoft.com/en-us/library/ms177564.aspx for more on the OUTPUT clause.
For rows where the case is true, we insert one row per true case into the secondary table and capture the identities to a table variable, then we update the underlying table with a row number join between the inserted virtual table and the identity table variable. Then we run another update for the rows where the case is false. Here's the relevant trigger code from my fiddle example.
-- Create a table variable to hold identity ID values from OUTPUT clause
DECLARE #table_2_ids TABLE (id INT NOT NULL)
-- Insert to our secondary table where the condition is true and capture the identity values to table variable
INSERT table_2 (textval)
OUTPUT inserted.id INTO #table_2_ids
SELECT 'from trigger'
FROM inserted
WHERE flag = 1
-- Use row number to match up rows from inserted where the condition is true to the identity value table variable
-- Update matched identity id to underlying table
UPDATE t
SET table_2_id = r.id, textval = textval + ' and trigger inserted to table_2'
FROM table_1 t
JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY id) rn FROM inserted WHERE flag = 1) i ON i.id = t.id
JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY id) rn FROM #table_2_ids) r ON r.rn = i.rn
-- and now update where condition is false
UPDATE t
SET textval = textval + ' and trigger did not insert to table_2'
FROM table_1 t
WHERE flag = 0

Efficient SQL Server stored procedure

I am using SQL Server 2008 and running the following stored procedure that needs to "clean" a 70 mill table from about 50 mill rows to another table, the id_col is integer (primary identity key)
According to the last running I made it is working good but it is expected to last for about 200 days:
SET NOCOUNT ON
-- define the last ID handled
DECLARE #LastID integer
SET #LastID = 0
declare #tempDate datetime
set #tempDate = dateadd(dd,-20,getdate())
-- define the ID to be handled now
DECLARE #IDToHandle integer
DECLARE #iCounter integer
DECLARE #watch1 nvarchar(50)
DECLARE #watch2 nvarchar(50)
set #iCounter = 0
-- select the next to handle
SELECT TOP 1 #IDToHandle = id_col
FROM MAIN_TABLE
WHERE id_col> #LastID and DATEDIFF(DD,someDateCol,otherDateCol) < 1
and datediff(dd,someDateCol,#tempDate) > 0 and (some_other_int_col = 1745 or some_other_int_col = 1548 or some_other_int_col = 4785)
ORDER BY id_col
-- as long as we have s......
WHILE #IDToHandle IS NOT NULL
BEGIN
IF ((select count(1) from SOME_OTHER_TABLE_THAT_CONTAINS_20k_ROWS where some_int_col = #IDToHandle) = 0 and (select count(1) from A_70k_rows_table where some_int_col =#IDToHandle )=0)
BEGIN
INSERT INTO SECONDERY_TABLE
SELECT col1,col2,col3.....
FROM MAIN_TABLE WHERE id_col = #IDToHandle
EXEC [dbo].[DeleteByID] #ID = #IDToHandle --deletes the row from 2 other tables that is related to the MAIN_TABLE and than from the MAIN_TABLE
set #iCounter = #iCounter +1
END
IF (#iCounter % 1000 = 0)
begin
set #watch1 = 'iCounter - ' + CAST(#iCounter AS VARCHAR)
set #watch2 = 'IDToHandle - '+ CAST(#IDToHandle AS VARCHAR)
raiserror ( #watch1, 10,1) with nowait
raiserror (#watch2, 10,1) with nowait
end
-- set the last handled to the one we just handled
SET #LastID = #IDToHandle
SET #IDToHandle = NULL
-- select the next to handle
SELECT TOP 1 #IDToHandle = id_col
FROM MAIN_TABLE
WHERE id_col> #LastID and DATEDIFF(DD,someDateCol,otherDateCol) < 1
and datediff(dd,someDateCol,#tempDate) > 0 and (some_other_int_col = 1745 or some_other_int_col = 1548 or some_other_int_col = 4785)
ORDER BY id_col
END
Any ideas or directions to improve this procedure run-time will be welcomed
Yes, try this:
Declare #Ids Table (id int Primary Key not Null)
Insert #Ids(id)
Select id_col
From MAIN_TABLE m
Where someDateCol >= otherDateCol
And someDateCol < #tempDate -- If there are times in these datetime fields,
-- then you may need to modify this condition.
And some_other_int_col In (1745, 1548, 4785)
And Not exists (Select * from SOME_OTHER_TABLE_THAT_CONTAINS_20k_ROWS
Where some_int_col = m.id_col)
And Not Exists (Select * From A_70k_rows_table
Where some_int_col = m.id_col)
Select id from #Ids -- this to confirm above code generates the correct list of Ids
return -- this line to stop (Not do insert/deletes) until you have verified #Ids is correct
-- Once you have verified that above #Ids is correctly populated,
-- then delete or comment out the select and return lines above so insert runs.
Begin Transaction
Delete OT -- eliminate row-by-row call to second stored proc
From OtherTable ot
Join MAIN_TABLE m On m.id_col = ot.FKCol
Join #Ids i On i.Id = m.id_col
Insert SECONDERY_TABLE(col1, col2, etc.)
Select col1,col2,col3.....
FROM MAIN_TABLE m Join #Ids i On i.Id = m.id_col
Delete m -- eliminate row-by-row call to second stored proc
FROM MAIN_TABLE m
Join #Ids i On i.Id = m.id_col
Commit Transaction
Explaanation.
You had numerous filtering conditions that were not SARGable, i.e., they would force a complete table scan for every iteration of your loop, instead of being able to use any existing index. Always try to avoid filter conditions that apply processing logic to a table column value before comparing it to some other value. This eliminates the opportunity for the query optimizer to use an index.
You were executing the inserts one at a time... Way better to generate a list of PK Ids that need to be processed (all at once) and then do all the inserts at once, in one statement.

Update or insert data depending on whether row exists

I have a collection of rows that I get from a web service. Some of these rows are to be inserted, some are updates to existing rows. There is no way of telling unless I do a query for the ID in the table. If I find it, then update. If I don't, then insert.
Select #ID from tbl1 where ID = #ID
IF ##ROWCOUNT = 0
BEGIN
Insert into tbl1
values(1, 'AAAA', 'BBBB', 'CCCC', 'DDD')
END
ELSE
BEGIN
UPDATE tbl1
SET
A = #AAA,
B = #BBB,
C = #CCC,
D = #DDD
WHERE ID = #ID
END
I am trying to figure out the most effient way to update/insert these rows into the table without passing them into a stored procedure one at a time.
UPDATE 1
I should have mentioned I am using SQL Server 2005. Also if I have 300 records I don't want to make 300 stored procedure calls.
the most efficient way will be first try to update the table if it returns 0 row updated then only do insertion. for ex.
UPDATE tbl1
SET
A = #AAA,
B = #BBB,
C = #CCC,
D = #DDD
WHERE ID = #ID
IF ##ROWCOUNT = 0
BEGIN
Insert into tbl1
values(1, 'AAAA', 'BBBB', 'CCCC', 'DDD')
END
ELSE
BEGIN
END
Instead of paying for a seek first and then updating using another seek, just go ahead and try to update. If the update doesn't find any rows, you've still only paid for one seek, and didn't have to raise an exception, but you know that you can insert.
UPDATE dbo.tbl1 SET
A = #AAA,
B = #BBB,
C = #CCC,
D = #DDD
WHERE ID = #ID;
IF ##ROWCOUNT = 0
BEGIN
INSERT dbo.tbl1(ID,A,B,C,D)
VALUES(#ID,#AAA,#BBB,#CCC,#DDD);
END
You can also look at MERGE but I shy away from this because (a) the syntax is daunting and (b) there have been many bugs and several of them are still unresolved.
And of course instead of doing this one #ID at a time, you should use a table-valued parameter.
CREATE TYPE dbo.tbl1_type AS TABLE
(
ID INT UNIQUE,
A <datatype>,
B <datatype>,
C <datatype>,
D <datatype>
);
Now your stored procedure can look like this:
CREATE PROCEDURE dbo.tbl1_Update
#List AS dbo.tbl1_type READONLY
AS
BEGIN
SET NOCOUNT ON;
UPDATE t
SET A = i.A, B = i.B, C = i.C, D = i.D
FROM dbo.tbl1 AS t
INNER JOIN #List AS i
ON t.ID = i.ID;
INSERT dbo.tbl1
SELECT ID, A, B, C, D
FROM #List AS i
WHERE NOT EXISTS
(
SELECT 1
FROM dbo.tbl1 WHERE ID = i.ID
);
END
GO
Now you can just pass your DataTable or other collection from C# directly into the procedure as a single parameter.
From the collection of rows you get from the server find out which ones are already there:
select #id from tbl1 where id in (....)
Then you have a list of ids that are in the table and one that there are not in the table.
You will have then 2 batch operations: one for update, the other for insert.
what i understand is this :
at the front end u issue a single sql statement
ArrayofIDsforInsert = select ID from tbl1 where ID not in ( array of ids at the front end)
ArrayofIDsforUpdate = (IntialArrayofids at frontend) - (ArrayofIdsforInsert)
one insert into table and one update table...
now call the insert into table with ArrayofIds for insert
call the update table with ArrayofIds for update..