two updates using the same CTE - sql-server

I have to "unremove" folders in an arborescence (thay have been flagged as removed, I change the value of the flag). Each folder can contain files or folders (which are stored in different tables). I have a CTE that defines all the folders that need to be updated.
WITH arbre(id) AS(
SELECT idDossier
FROM portail_managers_dossier
WHERE idDossier = #id
UNION ALL
SELECT d.idDossier
FROM portail_managers_dossier AS d
INNER JOIN arbre AS a
ON a.id = d.idParent)
Then I have two UPDATE request, one for each table
UPDATE portail_managers_dossier
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT id FROM arbre);
UPDATE portail_managers_document
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT id FROM arbre);
My problem is : I don't know how to merge two UPDATE requests on different tables. The CTE only exists until the end of the request, so I have to define it twice. Is there any way to write all of the above code in a single request ?

As you have discovered, CTE's will lose scope after the first update. But, instead of using a CTE, why not write the results of the query within the CTE to a temp table, and do your updates based on the contents temp table?

You can save the list of idDossier into variable from cte before execute the updates.
-- declare table variable
declare #listOfIDs table (idDossier int);
-- select ids of two tables
;WITH arbre(id) AS
(
SELECT idDossier
FROM portail_managers_dossier
WHERE idDossier = #id
UNION ALL
SELECT d.idDossier
FROM portail_managers_dossier AS d
INNER JOIN arbre AS a
ON a.id = d.idParent
)
-- saving ids
insert #listOfIDs(idDossier) select idDossier from arbre
-- execute twice updates
UPDATE portail_managers_dossier
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT idDossier FROM #listOfIDs);
UPDATE portail_managers_document
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT idDossier FROM #listOfIDs);

Related

TSQL Where clause based on temp table data

I have a straight forward SQL query that I am working with and trying to figure out the best way to approach the where clause.
Essentially, there are two temp tables created and if there is data in the XML string passed to the stored procedure, those tables are populated.
My where clause needs to check these temp tables for data, and if there is no data, it ignores them like they are not there and fetches all data.
-- Create temp tables to hold our XML filter criteria
DECLARE #users AS TABLE (QID VARCHAR(10))
DECLARE #dls AS TABLE (dlName VARCHAR(50))
-- Insert our XML filters
IF #xml.exist('/root/data/users') > 0
BEGIN
INSERT INTO #users( QID )
SELECT ParamValues.x1.value('QID[1]', 'varchar(10)')
FROM #xml.nodes('/root/data/users/user') AS ParamValues(x1)
END
IF #xml.exist('/root/data/dls') > 0
BEGIN
INSERT INTO #dls( dlName )
SELECT ParamValues.x1.value('dlName[1]', 'varchar(50)')
FROM #xml.nodes('/root/data/dld/dl') AS ParamValues(x1)
END
-- Fetch our document details based on the XML provided
SELECT d.documentID ,
d.sopID ,
d.documentName ,
d.folderLocation ,
d.userGroup ,
d.notes
FROM dbo.Documents AS d
LEFT JOIN dbo.DocumentContacts AS dc
ON dc.documentID = d.documentID
LEFT JOIN dbo.DocumentContactsDLs AS dl
ON dl.documentID = d.documentID
-- How can I make these two logic checks work only if there is data, otherwise, include everything.
WHERE dc.QID IN (SELECT QID FROM #users)
AND dl.DL IN (SELECT dlName FROM #dls)
FOR XML PATH ('data'), ELEMENTS, TYPE, ROOT('root');
In the query above, I am trying to used the data in the temp tables only if there is data in them, otherwise, it needs to act like that where statement isn't there for that specific value and include records regardless.
Example: If only #users had data, it would ignore AND dl.DL IN (SELECT dlName FROM #dls) and get everything, regardless of what was in the DL column on those joined records.
Use NOT EXISTS to check the existence of any record in variable table. Here is one way
WHERE ( dc.QID IN (SELECT QID FROM #users)
OR NOT EXISTS (SELECT 1 FROM #users) )
AND ( dl.DL IN (SELECT dlName FROM #dls)
OR NOT EXISTS (SELECT 1 FROM #dls) )
Try this. But please note that I did not get a chance to test it properly and I believe that you want to check the values in #users first and if there is no record existing in that table, then you want to check with the entries in #dls. Also if there are no entries in both of these tables, then you want to skip both the tables.
DECLARE #fl bit = 0
SELECT #fl = CASE WHEN EXISTS (SELECT 1 FROM #users) THEN
1
WHEN EXISTS (SELECT 1 FROM #dls) THEN
2
ELSE
0
END
WHERE ( (dc.QID IN (SELECT QID FROM #users) AND #fl = 1)
OR
(dl.DL IN (SELECT dlName FROM #dls) AND #fl = 2)
OR (1=1 AND #fl = 0)
)

Retrieve values for multiple ids given in the order

I am having a stored procedure which uses CSV to accept multiple IDs. I want to retrieve the values from the table in the order of the Ids given. The stored procedure I am currently using is
CREATE procedure [dbo].[Proc1]
#Userid VARCHAR(MAX)=NULL
as
begin
set nocount on;
DECLARE #Useridord TABLE (Userid VARCHAR(MAX),Position int identity(1,1));
INSERT INTO #Useridord
SELECT item
FROM [dbo].[Split] (#Userid, ',')
select * from User where User.UserId IN (select TOP 100 Percent Userid from #Useridord ORDER BY Position)
end
GO
What I am trying to do is from csv I am inserting the values into temporary table adding the order value as position. As I use Orderby Position, it is only implemented in the inner select. But the Output is given as the order in the table. I know to use an orderby in the outer select statement but I don't know the correct syntax to execute it. Can anyone help me?
How about using INNER JOIN?
SELECT
u.*
FROM User u
INNER JOIN #Useridord uo
ON uo.Userid = u.UserId
ORDER BY uo.Position
I believe that you should guaranty position inside function like:
create function fnSplit(#users varchar(100), #del char(10))
returns #t table(userid int, pos int) as
begin
insert into #t values(3,1),(2,2),(1,3)
return
end
Then you can join directly on function result like:
Select u.* from users u
join fnSplit('','') s on u.id = s.userid
order by s.pos
Here is fiddle http://sqlfiddle.com/#!6/a8acc/4

Update count column from data in another table

In my DB I have two tables Items(Id, ..., ToatlViews int) and ItemViews (id, ItemId, Timestamp)
In ItemViews table I store all views of an item as they come to the site. From time to time I want to call a stored procedure to update Items.ToatlViews field. I tried to do this SP using a cursor ... but the update statement is wrong. Can you help me to correct it? Can I do this without cursor?
CREATE PROCEDURE UpdateItemsViews
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #currentItemId int
DECLARE #currentItemCursor CURSOR
SET #currentItemCursor = CURSOR FOR SELECT Id FROM dbo.Items
OPEN #currentItemCursor
FETCH NEXT FROM #currentItemCursor INTO #currentItemId
WHILE ##FETCH_STATUS = 0
BEGIN
Update dbo.Items set TotalViews = count(*)
from dbo.ItemViews where ItemId=#currentItemId
FETCH NEXT FROM #currentItemCursor INTO #currentItemId
END
END
GO
You can use a direct UPDATE statement
update Items set TotalViews =
(select COUNT(id) from ItemViews where ItemViews.ItemId = Items.Id)
You might want to test performance for the various ways to do this, if that's important.
You could use update ... from instead of a cursor:
update i
set TotalViews = iv.cnt
from dbo.Item i
join (
select ItemId
, count(*) as cnt
from dbo.ItemViews
group by
ItemId
) iv
on i.Id = iv.ItemId
;WITH x AS
(
SELECT ItemID, c = COUNT(*)
FROM dbo.ItemViews
GROUP BY ItemID
)
UPDATE i
SET TotalViews = x.c
FROM dbo.Items AS i
INNER JOIN x
ON x.ItemID = i.ItemID;
But why do you want to store this value, when you can always get the count at runtime? You're going to have to run this update statement every time you touch the ItemViews table in any way, otherwise the count stored with Items is going to be incorrect.
What you may consider doing instead is setting up an indexed view:
CREATE VIEW dbo.ItemViewCount
WITH SCHEMABINDING
AS
SELECT ItemID, ItemCount = COUNT_BIG(*)
FROM dbo.ItemViews
GROUP BY ItemID;
GO
CREATE UNIQUE CLUSTERED INDEX x ON dbo.ItemViewCount(ItemID);
Now you can join to the view in your queries and know that the count is always up to date (without paying the penalty of scanning for the count of each item). The downside to the indexed view is that you pay that cost incrementally when there are inserts/updates/deletes to the ItemViews table.
I found this question / answer a year after it was written and answered. the answer was okay, but I was after something a bit more automatic. I ended up writing a trigger to automatically recalculate the column when a relevant row in the other table was inserted, deleted or updated.
I think it's a better solution than running something manually to do the recalculation as there isn't any possibility of someone forgetting to run the code:
CREATE TRIGGER [dbo].[TriggerItemTotalViews]
ON [dbo].[ItemViews]
AFTER INSERT, DELETE, UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE [Items]
SET [TotalViews] =
(
SELECT COUNT(id)
FROM [ItemViews]
WHERE [ItemViews].[ItemId] = [Items].[ItemId]
)
WHERE [Items].[ItemId] IN
(
SELECT [ItemId] FROM [INSERTED]
UNION
SELECT [ItemId] FROM [DELETED]
)
END
Same but different:
declare #productId int = 24;
declare #classificationTypeId int = 86;
update s
set CounterByProductAndClassificationType = row_num
from Samples s
join
(
select row_number() over (order by (select Id)) row_num, Id
from Samples
where
ProductId = #productId and
ClassificationTypeId = #classificationTypeId
) s_row on s.Id = s_row.Id
For who need to include zero count too
UPDATE Items as i,
(SELECT
i.Id as Id, COUNT(iv.ItemId) AS c
FROM
Items AS i
LEFT JOIN ItemViews AS iv ON i.Id = iv.ItemId
GROUP BY i.Id) AS ic
SET
i.TotalViews = ic.c
WHERE
i.Id = ic.Id

What comparison method is better?

I have a trigger in a table with a good number of columns (perhaps around 100) and quite a lot of updates (for some definition of "a lot of").
If any of some fields have changed, the trigger inserts some data in another table.
For obvious reasons, I want this trigger to run as fast as possible. What's the best method to do the comparison?
For now I have those:
IF NOT EXISTS (SELECT * FROM Inserted i, Deleted d WHERE
i.Fld1 = d.Fld1 AND i.Fld2 = d.Fld2 AND
i.Fld3 = d.Fld3 AND i.Fld4 = d.Fld4 AND
i.Fld5 = d.Fld5 AND i.Fld6 = d.Fld6 AND
i.Fld7 = d.Fld7)
THEN ...
IF ((SELECT Fld1 FROM Inserted) <> (SELECT Fld1 FROM Deleted) OR
(SELECT Fld2 FROM Inserted) <> (SELECT Fld2 FROM Deleted) OR
(SELECT Fld3 FROM Inserted) <> (SELECT Fld3 FROM Deleted) OR
(SELECT Fld4 FROM Inserted) <> (SELECT Fld4 FROM Deleted) OR
(SELECT Fld5 FROM Inserted) <> (SELECT Fld5 FROM Deleted) OR
(SELECT Fld6 FROM Inserted) <> (SELECT Fld6 FROM Deleted) OR
(SELECT Fld7 FROM Inserted) <> (SELECT Fld7 FROM Deleted))
THEN...
I would usually prefer the first method, as it's more compact and seems more idiomatic. However, when speed is an issue, how should I do it?
The second version is completely broken for multi-row UPDATES, so for that reason alone, I'd do a variant of the first:
INSERT INTO ANotherTable (Column1, COlumn2, /* Etc */)
SELECT i.Column1,d.Column1, /* Other COlumns */
FROM
inserted i
inner join
deleted d
on
i.Fld1 = d.Fld1 and /* For each column in PK */
i.Fld2 <> d.Fld2 /* For each non-PK column */
Assuming the PK is stable and unchanging
Why don't you test your changing columns by using IF UPDATE(Column1,Column2,...) That will let you know whether any of the columns have changed that you're interested in. See http://msdn.microsoft.com/en-us/library/ms187326.aspx for details on the UPDATE() function.
You'll also be able to use it if the PK has been modified, whereas comparing between the inserted and deleted the way you're trying to go about it will miss out on changes to the PK.
The solution involving testing the inequality of all of the fields will fail unless you SET ANSI_NULLS OFF before comparing.
For example:
create table table1 ( a varchar(4), b varchar(4) null)
create table table2 ( a varchar(4), b varchar(4) null)
go
insert into table1 ( a, b ) select 'asdf', null
insert into table2 ( a, b ) select 'asdf', 'zzzz'
--Expect no results
select *
from table1
inner join table2
on a.a = b.a
where a.b <> b.b
set ansi_nulls off
--Expect 1 result
select *
from table1
inner join table2
on a.a = b.a
where a.b <> b.b
Of course, you could put in additional tests rather than going with the ansi_nulls option, but that gets particularly crazy if you're doing this with many fields. Also: you must set ansi_nulls off prior to creating the trigger - you can't turn it on and off inside the trigger, so the whole thing must have the same ansi_nulls setting.

Can I pass multiple rows from a stored procedure query into another query?

In my previous question I asked about storing the result of a query in a variable... now I realize the query can return multiple rows.
I currently have this:
SELECT #UserId = UserId FROM aspnet_Users WHERE UserName = #username
And I want to do something like this:
DELETE FROM some_table WHERE UserId IN ( *the ID list* )
DELETE FROM some_table_2 WHERE UserId IN ( *the ID list* )
My first instinct is to use "GROUP_CONCAT" but apparently that's a MySQL-only feature. There are some ways to make equivalent functionality, but I'm wondering if there is a better way to structure the queries?
SELECT * FROM dbo.aspnet_UsersInRoles
WHERE UserId IN (
SELECT UserId FROM aspnet_Users
WHERE UserName = #username
)
this should do it ..
SELECT
*
FROM
dbo.aspnet_UsersInRoles
WHERE
UserId IN (
SELECT
UserId
FROM
aspnet_Users
WHERE
UserName = #username
)
delete from st
from some_table st
inner join aspnet_Users au
on st.UserId = au.UserId
where /* add additional criteria here to produce "* the ID list *" */
If you want to avoid to repeat subquery you can put its result into a temp table or a table variable. For instance:
/*declare and fill a table variable containing all user ids you need to manipulate*/
declare #t table(userid int)
insert into #t(userid) select UserId from aspnet_Users where UserName=#username
/*delete from some table by using the temp variable*/
delete from st
from some_table st
inner join #t t
on st.userid = t.userid
/*repeat this for some other table*/
delete from st
from some_other_table st
inner join #t t
on st.userid = t.userid
If you want to avoid multiple delete statements and if presence of user ids in some_other_table doesn't make sense if this doesn't exist in the some_table then you can create trigger on some_table:
create trigger x on some_table for delete
as
begin
delete from some_other_table
where userid in (select userid from deleted)
end

Resources