Update count column from data in another table - sql-server

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

Related

SQL Server- Trigger. Update a column on one table with a count of columns from another table

Trying to learn triggers and can't seem to wrap my head around whether this is the best way to go about this. There is a total of 'tickets' that I would like to count and put into a column for an 'order'.
CREATE TRIGGER DAILYTICKETTOTALS
ON db.ORDER
AFTER UPDATE
AS
BEGIN
INSERT INTO db.ORDER (
--Finding the order they made for the day:
SELECT * FROM db.ORDER
WHERE DESCRIPTION = 'specific description' AND
CAST(CAST(CAST(GETDATE()AS float) AS int) AS datetime) =
CAST(CAST(CAST(db.ORDER.STARTDATE AS float) AS int) AS datetime)
AND db.ORDER.CREATEDBY = 'Last, First'
)
SET db.ORDER.COLUMNTOTALS VALUES(
--Finding the total of tickets input for the day
SELECT COUNT(*)
FROM db.ticket
WHERE CAST(CAST(CAST(db.ticket.DATE AS float) AS int) AS datetime) =
CAST(CAST(CAST(GETDATE()AS float) AS int) AS datetime)
AND db.ticket.INPUTBY = 'Last, First');
Thanks for any help, and I appreciate you reviewing this.
Do some research on the "Special" tables available inside a trigger, such as "INSERTED" as that will give you the details of the rows that have been updated, and you can join that to anything else (including the actual table being updated) that you need to. Remember that you should not assume there is only 1 row updated, as the trigger will fire once per batch, not once per row.
This is a version of your stored proc that assumes that each order has a unique ID named OrderID, and that both the ticket and order table have a personID rather than looking for them by name. If not, they should, and if you cant change it then use your existing messy joins.
CREATE TRIGGER DAILYTICKETTOTALS
ON db.ORDER
AFTER UPDATE
AS
BEGIN
update o
set columntotals=TicketNum
from inserted
join db.[order] o on o.orderid=inserted.orderid
join (
select convert(date,ticket,[date]) as ticketdate, PersonID, count(*) as TicketNum
from db.ticket
group by convert(date,ticket,[date]) as ticketdate, PersonID
) t on t.PersonID=o.PersonID and t.ticketdate=convert(date,getdate())
This solved my problem, in case any of you were curious. I don't know if its ideal, but it works.
CREATE TRIGGER DAILYTICKETTOTALS
ON db.ORDER
AFTER INSERT, UPDATE
AS
BEGIN
DECLARE #INS NVARCHAR(50)
SELECT #INS = NAMECOLUMN FROM INSERTED
WHERE DESCRIPTION = 'specific description' AND
CAST(CAST(CAST(STARTDATE AS float) AS int) AS datetime)=
CAST(CAST(CAST(GETDATE() AS float) AS int) AS datetime)
END
IF #INS = ‘Last, First’
BEGIN
UPDATE db.ORDER
SET COLUMNTOTALS = EXAMPLENAME.CT
FROM (SELECT COUNT(*) AS CT FROM ticket WHERE INPUTBY = ‘Last, First’) EXAMPLENAME
WHERE DESCRIPTION = 'specific description' AND INPUTBY = ‘Last, First’
END
BEGIN

How to UPDATE TOP(n) with ORDER BY giving a predictable result?

I'm trying to read the top 100 items of a database table that is being used like a queue. As I do this I'm trying to mark the items as done like this:
UPDATE TOP(#qty)
QueueTable WITH (READPAST)
SET
IsDone = 1
OUTPUT
inserted.Id,
inserted.Etc
FROM
QueueTable
WHERE
IsDone = 0
ORDER BY
CreatedDate ASC;
The only problem is, according to UPDATE (Transact-SQL) on MSDN, the ORDER BY is not valid in an UPDATE and:
The rows referenced in the TOP expression used with INSERT, UPDATE, or
DELETE are not arranged in any order.
How can I achieve what I need which is to update the items at the top of the queue while also selecting them?
SQL Server allows you to update a derived table, CTE or view:
UPDATE x
SET
IsDone = 1
OUTPUT
inserted.Id,
inserted.Etc
FROM (
select TOP (N) *
FROM
QueueTable
WHERE
IsDone = 0
ORDER BY
CreatedDate ASC;
) x
No need to compute a set of IDs first. This is faster and usually has more desirable locking behavior.
Tested in SSMS, it works fine. You may need to do some modification accordingly.
--create table structure
create table #temp1 (
id int identity(1,1),
value int
)
go
--insert sample data
insert #temp1 values (1)
go 20
--below is solution
declare #qty int = 10
declare #cmd nvarchar(2000) =
N'update #temp1
set value= 100
output inserted.value
where id in
(
select top '+ cast(#qty as nvarchar(5)) +' id from #temp1
order by id
)';
execute sp_executesql #cmd
You can use ranking function (for example row_number).
update top (100) q
set IsDone = 1
output
inserted.Id,
inserted.Etc
from (
select *, row_number() over(order by CreatedDate asc, (select 0)) rn
from QueueTable) q
where rn <= 100

two updates using the same CTE

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);

Checking if something has changed in a trigger

I have a need to monitor a subset of fields on a table and perform a task when one of them changes.
I am using a trigger on the table update which and then am looking at the changes as follows:
-- join the deleted and inserted to get a full list of rows
select * into #tmp from (select * from inserted union select * from deleted) un
-- select a count of differing rows, > 1 means something is different
select distinct count(*) from #tmp
This is fine and a count of 2 or more means something is different on single line updates. Issue is if I am doing a multiple line update then this breaks down.
Is there a way I can get this to work for a multi line update or do I need to try a different approach completely.
You could do something like this (syntax completely untested)
IF NOT UPDATE(col)
RETURN
SELECT inserted.key, inserted.col as i_col, deleted.col as d_col
INTO #interestingrows
FROM inserted JOIN deleted on inserted.key = deleted.key
and inserted.col <> deleted.col /*If col is nullable cater for that as well*/
IF ##ROWCOUNT=0
RETURN
/*Process contents of #interestingrows*/
I ended up with a fairly simple solution. I wrote an additional loop around the check that did the check per line in inserted.
-- get a list of updated line id's
select field1 as id into #loop from inserted
-- loop through all the id's and do a compare
while (select count(*) from #loop) > 0 begin
select top 1 #id = id from #loop
select * into #tmp from (select * from inserted where field1 = #id union
select * from deleted where field1 = #id) un
-- do a select ditinct to count the differing lines.
if (select distinct count(*) from #tmp) > 1 begin
-- the 2 lines don't match, so mark for update
update test1 set flag = 1 where field1 = #id
end
drop table #tmp
delete #loop where id = #id
end

SQL Server: UPDATE a table by using ORDER BY

I would like to know if there is a way to use an order by clause when updating a table. I am updating a table and setting a consecutive number, that's why the order of the update is important. Using the following sql statement, I was able to solve it without using a cursor:
DECLARE #Number INT = 0
UPDATE Test
SET #Number = Number = #Number +1
now what I'd like to to do is an order by clause like so:
DECLARE #Number INT = 0
UPDATE Test
SET #Number = Number = #Number +1
ORDER BY Test.Id DESC
I've read: How to update and order by using ms sql The solutions to this question do not solve the ordering problem - they just filter the items on which the update is applied.
Take care,
Martin
No.
Not a documented 100% supported way. There is an approach sometimes used for calculating running totals called "quirky update" that suggests that it might update in order of clustered index if certain conditions are met but as far as I know this relies completely on empirical observation rather than any guarantee.
But what version of SQL Server are you on? If SQL2005+ you might be able to do something with row_number and a CTE (You can update the CTE)
With cte As
(
SELECT id,Number,
ROW_NUMBER() OVER (ORDER BY id DESC) AS RN
FROM Test
)
UPDATE cte SET Number=RN
You can not use ORDER BY as part of the UPDATE statement (you can use in sub-selects that are part of the update).
UPDATE Test
SET Number = rowNumber
FROM Test
INNER JOIN
(SELECT ID, row_number() OVER (ORDER BY ID DESC) as rowNumber
FROM Test) drRowNumbers ON drRowNumbers.ID = Test.ID
Edit
Following solution could have problems with clustered indexes involved as mentioned here. Thanks to Martin for pointing this out.
The answer is kept to educate those (like me) who don't know all side-effects or ins and outs of SQL Server.
Expanding on the answer gaven by Quassnoi in your link, following works
DECLARE #Test TABLE (Number INTEGER, AText VARCHAR(2), ID INTEGER)
DECLARE #Number INT
INSERT INTO #Test VALUES (1, 'A', 1)
INSERT INTO #Test VALUES (2, 'B', 2)
INSERT INTO #Test VALUES (1, 'E', 5)
INSERT INTO #Test VALUES (3, 'C', 3)
INSERT INTO #Test VALUES (2, 'D', 4)
SET #Number = 0
;WITH q AS (
SELECT TOP 1000000 *
FROM #Test
ORDER BY
ID
)
UPDATE q
SET #Number = Number = #Number + 1
The row_number() function would be the best approach to this problem.
UPDATE T
SET T.Number = R.rowNum
FROM Test T
JOIN (
SELECT T2.id,row_number() over (order by T2.Id desc) rowNum from Test T2
) R on T.id=R.id
update based on Ordering by the order of values in a SQL IN() clause
Solution:
DECLARE #counter int
SET #counter = 0
;WITH q AS
(
select * from Products WHERE ID in (SELECT TOP (10) ID FROM Products WHERE ID IN( 3,2,1)
ORDER BY ID DESC)
)
update q set Display= #counter, #counter = #counter + 1
This updates based on descending 3,2,1
Hope helps someone.
I had a similar problem and solved it using ROW_NUMBER() in combination with the OVER keyword. The task was to retrospectively populate a new TicketNo (integer) field in a simple table based on the original CreatedDate, and grouped by ModuleId - so that ticket numbers started at 1 within each Module group and incremented by date. The table already had a TicketID primary key (a GUID).
Here's the SQL:
UPDATE Tickets SET TicketNo=T2.RowNo
FROM Tickets
INNER JOIN
(select TicketID, TicketNo,
ROW_NUMBER() OVER (PARTITION BY ModuleId ORDER BY DateCreated) AS RowNo from Tickets)
AS T2 ON T2.TicketID = Tickets.TicketID
Worked a treat!
I ran into the same problem and was able to resolve it in very powerful way that allows unlimited sorting possibilities.
I created a View using (saving) 2 sort orders (*explanation on how to do so below).
After that I simply applied the update queries to the View created and it worked great.
Here are the 2 queries I used on the view:
1st Query:
Update MyView
Set SortID=0
2nd Query:
DECLARE #sortID int
SET #sortID = 0
UPDATE MyView
SET #sortID = sortID = #sortID + 1
*To be able to save the sorting on the View I put TOP into the SELECT statement. This very useful workaround allows the View results to be returned sorted as set when the View was created when the View is opened. In my case it looked like:
(NOTE: Using this workaround will place an big load on the server if using a large table and it is therefore recommended to include as few fields as possible in the view if working with large tables)
SELECT TOP (600000)
dbo.Items.ID, dbo.Items.Code, dbo.Items.SortID, dbo.Supplier.Date,
dbo.Supplier.Code AS Expr1
FROM dbo.Items INNER JOIN
dbo.Supplier ON dbo.Items.SupplierCode = dbo.Supplier.Code
ORDER BY dbo.Supplier.Date, dbo.Items.ID DESC
Running: SQL Server 2005 on a Windows Server 2003
Additional Keywords: How to Update a SQL column with Ascending or Descending Numbers - Numeric Values / how to set order in SQL update statement / how to save order by in sql view / increment sql update / auto autoincrement sql update / create sql field with ascending numbers
SET #pos := 0;
UPDATE TABLE_NAME SET Roll_No = ( SELECT #pos := #pos + 1 ) ORDER BY First_Name ASC;
In the above example query simply update the student Roll_No column depending on the student Frist_Name column. From 1 to No_of_records in the table. I hope it's clear now.
IF OBJECT_ID('tempdb..#TAB') IS NOT NULL
BEGIN
DROP TABLE #TAB
END
CREATE TABLE #TAB(CH1 INT,CH2 INT,CH3 INT)
DECLARE #CH2 INT = NULL , #CH3 INT=NULL,#SPID INT=NULL,#SQL NVARCHAR(4000)='', #ParmDefinition NVARCHAR(50)= '',
#RET_MESSAGE AS VARCHAR(8000)='',#RET_ERROR INT=0
SET #ParmDefinition='#SPID INT,#CH2 INT OUTPUT,#CH3 INT OUTPUT'
SET #SQL='UPDATE T
SET CH1=#SPID,#CH2= T.CH2,#CH3= T.CH3
FROM #TAB T WITH(ROWLOCK)
INNER JOIN (
SELECT TOP(1) CH1,CH2,CH3
FROM
#TAB WITH(NOLOCK)
WHERE CH1 IS NULL
ORDER BY CH2 DESC) V ON T.CH2= V.CH2 AND T.CH3= V.CH3'
INSERT INTO #TAB
(CH2 ,CH3 )
SELECT 1,2 UNION ALL
SELECT 2,3 UNION ALL
SELECT 3,4
BEGIN TRY
WHILE EXISTS(SELECT TOP 1 1 FROM #TAB WHERE CH1 IS NULL)
BEGIN
EXECUTE #RET_ERROR = sp_executesql #SQL, #ParmDefinition,#SPID =##SPID, #CH2=#CH2 OUTPUT,#CH3=#CH3 OUTPUT;
SELECT * FROM #TAB
SELECT #CH2,#CH3
END
END TRY
BEGIN CATCH
SET #RET_ERROR=ERROR_NUMBER()
SET #RET_MESSAGE = '#ERROR_NUMBER : ' + CAST(ERROR_NUMBER() AS VARCHAR(255)) + '#ERROR_SEVERITY :' + CAST( ERROR_SEVERITY() AS VARCHAR(255))
+ '#ERROR_STATE :' + CAST(ERROR_STATE() AS VARCHAR(255)) + '#ERROR_LINE :' + CAST( ERROR_LINE() AS VARCHAR(255))
+ '#ERROR_MESSAGE :' + ERROR_MESSAGE() ;
SELECT #RET_ERROR,#RET_MESSAGE;
END CATCH

Resources