Why won't my master table column increment in my trigger? - sql

I need to create a trigger that will keep track of the number of times a movie is rented from a business like Blockbuster. I need a separate trigger for an insert, a delete and an update.
The column that tracks this number of times rented is called num_rentals and has a datatype of int. This column is part of the Movies table which has *movie_id (pk*), movie_title, release_year, movie_description, and num_rentals. The customer_rentals table has item_rental_id(pk), *movie_id(fk*), customer_id.
I searched and found a similar thread here for this and tried using the supplied answer, but to no avail. I executed the following string without any errors but I saw no change in the num_rentals column when I inserted data into either the Movie or the Customer_rentals table. What am I doing wrong?
CREATE TRIGGER dbo.tr_num_rented_insert ON dbo.customer_rentals
FOR INSERT
AS
BEGIN
UPDATE m
SET num_rentals = num_rentals + 1
FROM dbo.movies AS m
INNER JOIN inserted AS i ON m.movie_id = i.movie_id ;
END
I added the num_rentals field to the table later and I also need to know how to initialize the field value to zero for all records currently in the Movies table.
I want to understand this as much as I want the answer so assistance is greatly appreciated. I read that there may be more efficient ways to manage this type of data, but this is the way my instructor wants it. Thank you in advance!

I suspect your problem is null num_rental column. So you're adding null to 1, resulting in null.
Personally, I'd set the num_rentals column to non-nullable with a default of zero, but assuming you can't do that, use isnull().
Like so
alter TRIGGER dbo.tr_num_rented_insert ON dbo.customer_rentals
FOR INSERT
AS
BEGIN
UPDATE m
SET num_rentals = isnull(num_rentals,0) + 1
FROM dbo.movies AS m
INNER JOIN inserted AS i ON m.movie_id = i.movie_id ;
END
However, even though this works, there is a problem with it; if you add multiple rows to customer_rentals, it's only going to increment by one. Personally, I'd change the trigger to account for that.
Like this
alter TRIGGER dbo.tr_num_rented_insert ON dbo.customer_rentals
FOR INSERT
AS
BEGIN
UPDATE m
SET num_rentals = isnull(num_rentals,0) + (select COUNT(*) from inserted where movie_id = m.movie_id)
FROM dbo.movies AS m
where movie_id in (select movie_id from inserted)
END
As far as the duplicate information goes, Aaron is right that this is needlessly redundant and in my experience, this type of thing often gets out of sync. With such a simple database, the num_rentals column is overkill (to be generous), but your movie database is a contrived example to teach you a concept. Basically, sometimes you will want calculated values to be easily accessible or filtered on. Take SO rep for example, I assume they don't recalc that every time they display it.

Aaron's frequently upvoted comment suggests that you are doing something unwise. However, since you are doing what your instructor is specifying, I suggest that you look at a couple of things.
First, will your update query work for the initial rental? If the initial value is 0 it will. If the initial value is null it won't.
Second, you might be referencing the wrong table. The trigger is on customer_rentals but your trigger refers to inserted. That strikes me as odd.
Just so you know, I was one of those who upvoted Aaron's comment.

Related

How to Use Trigger to Log Changes in SQL Server?

Users table:
LoginLog table:
How can I log Name, Password, LastLogonTime to LoginLog table when Users table LastLogonTime column is updated and insert a row?
You need a fairly simple trigger on the update of the Users table. The trickier part is being aware of the fact that triggers are fired only once for each statement - and such a statement could potentially update multiple rows which would then be in your Inserted and Deleted pseudo tables in the trigger.
You need to write your trigger to be aware of this set-based manner and handle it correctly. In order to be properly able to link the old and new values, your table Users must have a proper primary key (you didn't mention anything about that) - something like a UserId or the like.
Try something like this:
CREATE TRIGGER dbo.trg_LogUserLogon
ON dbo.Users
FOR UPDATE
AS
-- inspect the Inserted (new values, after UPDATE) and Deleted (old values, before UPDATE)
-- pseudo tables to find out which rows have had an update in the LastLogonTime column
INSERT INTO dbo.LoginLog (Name, Password, LastLogonTime)
SELECT
i.Name, i.Password, i.LastLogonTime
FROM
Inserted i
INNER JOIN
-- join the two sets of data on the primary key (which you didn't specify)
-- could be i.UserId = d.UserId or something similar
Deleted d on i.PrimaryKey = d.PrimaryKey
WHERE
-- only select those rows that have had an update in the LastLogonTime column
i.LastLogonTime <> d.LastLogonTime
But please also by all means take #Larnu's comments about not EVER storing passwords in plain text into account! This is really a horribly bad thing to do and needs to be avoided at all costs.

How to create a sql trigger to set a table column equal to a value after insert?

new to oracle and sql but trying to learn triggers. I think I'm having some syntax errors here, but let me explain what I am trying to do.
I have two tables: 1. group_membership with the columns
user_internal_id | group_internal_id (FK) | joined_time
and 2. group_details with the columns
group_internal_id (PK) | group_name | group_owner | created_time | movie_cnt | member_cnt|
(PK and FK stand for Primary Key and Foreign Key that relates to that Primary Key respectively.)
What I want to do:
After a new row is inserted into the group_membership table, I want to
update the value of member_cnt in the group_details table with the amount of times a particular group_internal_id appears in the group_membership table.
--
Now, my DBA for the app we are working on has created a trigger that simply updates the member_cnt of a particular group by reading the group_internal_id of the row inserted to group_membership, then adding 1 to the member_cnt. Which works better probably, but I want to figure out how come my trigger is having errors. Here is the code below
CREATE OR REPLACE TRIGGER set_group_size
AFTER INSERT ON group_membership
FOR EACH ROW
DECLARE g_count NUMBER;
BEGIN
SELECT COUNT(group_internal_id)
INTO g_count
FROM group_membership
GROUP BY group_internal_id;
UPDATE group_details
SET member_cnt = g_count
WHERE group_details.group_internal_id = group_membership.group_internal_id;
END;
The errors I'm receiving are:
Error(7,5): PL/SQL: SQL Statement ignored
Error(9,45): PL/SQL: ORA-00904: "GROUP_MEMBERSHIP"."GROUP_INTERNAL_ID": invalid identifier
I came here because my efforts have bene futile in troubleshooting. Hope to hear some feedback. Thanks!
The immeidate issue with your code is the update query of your trigger:
UPDATE group_details
SET member_cnt = g_count
WHERE group_details.group_internal_id = group_membership.group_internal_id;
group_membership is not defined in that scope. To refer to the value on the rows that is being inserted, use pseudo-table :new instead.
WHERE group_details.group_internal_id = :new.group_internal_id;
Another problem is the select query, that might return multiple rows. It would need a where clause that filters on the newly inserted group_internal_id:
SELECT COUNT(*)
INTO g_count
FROM group_membership
WHERE group_internal_id = :new.group_internal_id;
But these obvious fixes are not sufficient. Oracle won't let you select from the table that the trigger fired upon. On execution, you would meet error:
ORA-04091: table GROUP_MEMBERSHIP is mutating, trigger/function may not see it
There is no easy way around this. Let me suggest that this whole design is broken; the count of members per group is derived information, that can easily be computed on the fly whenever needed. Instead of trying to store it, you could, for example, use a view:
create view view_group_details as
select group_internal_id, group_name,
(
select count(*)
from group_membership gm
where gm.group_internal_id = gd.group_internal_id
) member_cnt
from group_details gd
Agree with #GMB that your design is fundamentally flawed, but if you insist on keeping a running count there is a easy solution to mutating they point out. The entire process is predicated on maintaining the count in group_details.member_count column. Therefore since that column has the previous count you do not need to count them - so eliminate the select. Your trigger becomes:
create or replace trigger set_group_size
after insert on group_membership
for each row
begin
update group_details
set member_cnt = member_cnt + 1
where group_details.group_internal_id = :new.group_internal_id;
end;
Of course then you need to handle group_membership Deletes and Updates of group_internal_id. Also, what happens when 2 users process the same group_membership simultaneously? Maintaining a running total for a derivable column is just not worth the effort. Best option just create the view as GMB suggested.

Sql trigger for update in specific column after insert

I made a trigger that is supposed to update another value in the same table a after I make an insert to the table. I do get the result I am looking for, but when I ask my teacher if it is correct he responded that this trigger updates "all" tables(?) and thus incorrect. He would not explain more than that (he is that kind of teacher...). Can anyone understand what he means? Not looking for the right code, just an explanation of what I might have misunderstood.
CREATE TRIGGER setDate
ON Loans
AFTER INSERT
AS
BEGIN
UPDATE Loans
set date = GETDATE()
END;
Your teacher intends to say that the query updates all rows in the table -- perhaps you misunderstood her or him.
The best way to do what you want is to use a default value:
alter table loans alter column date datetime default getdate();
That is, a trigger is not needed. If you did use a trigger, I'll give you two hints:
An instead of trigger.
inserted should be somewhere in the trigger.
Hello there. I upvoted your question because that might be the question of many beginners in SQL Server.
As I see you defined the trigger right! It is a correct way although it's not the best.
By the way we are not discussing all ways you could choose or not, I'm gonna correct your code and explain what your teacher meant.
Look at this UPDATE you wrote:
UPDATE Loans
SET date = GETDATE()
If you write a SELECT without a WHERE clause, what does it do? It would result all the rows in the table which was selected. right?
SELECT * FROM dbo.loans
So without a WHERE clause, your UPDATE will update all of the rows in the table.
OK now what to do to just update the only row ( or rows) which were recently inserted?
When you are writing a trigger, you are allowed to use this table: Inserted
which it has the rows were recently inserted. kinda these rows first come to this table and then go to the final destination table. So you can update them before they go the dbo.loans
it would be like this:
Update dbo.loans
SET date = GETDATE()
WHERE Id IN (SELECT * FROM Inserted)

can't make a trigger update only the relevant rows

I'm working on a school project and my trigger gives me a hard time.
Its' purpose is to update the Rating field of an updated Product, but it updates all rows in Products instead.
CREATE TRIGGER Update_Rating
ON dbo.Reviews
FOR Insert
as
Update dbo.Products
set Rating=(Select [avarage_rating]=avg(r.Rating)
From dbo.Reviews as r join inserted on r.ItemNumber = inserted.ItemNumber
where r.ItemNumber = Inserted.ItemNumber)
Your help is much appreciated
What you are trying to do is calculate the average of a column, which is a number. There will not be a unique average per row. Hence the Rating column in your desired table gets updated with this value. Try changing your update condition and it should work.

Equivalent of C# 'readonly' for an MS SQL column?

Imagine there is a Price column in Products table, and the price may change.
I'm fine with it changing but I want to store the original Price value in another column.
Is there any automatic way MS SQL server may do this?
Can I do this using Default Value field?
Do I have to declare a trigger?
Update
I tried to use Price to simplify the question but it looks like this provoked "use separate table" type of answers.
I'm sorry for the confusion I caused.
In the real world, I need to store a foreign key ID and I'm 100% I only need current and original values.
Update 2
I got a little confused by the different approaches suggested so please let me explain the situation again.
Imaginary Products table has three fields: ID, Price and OriginalPrice.
I want to setOriginalPrice to Price value on any insert.
Sometimes it is a single product that gets created from code. Sometimes there are thousands of products created by a single insert from a stored procedure so I want to handle these properly as well.
Once OriginalPrice has been set, I never intend to update it.
Hope my question is clearer now.
Thanks for your effort.
Final Update
I want to thank everyone, particularly #gbn, for their help.
Although I posted my own answer, it is largely based on #gbn's answer and his further suggestions. His answer is also more complete, therefore I mark it as correct.
After your update, let's assume you have only old and new values.
Let's ignore if the same update happens in quick succession because of a client-code bug and that you aren't interested in history (other answers)
You can use a trigger or a stored procedure.
Personally, I'd use a stored proc to provide a basic bit of control. And then no direct UPDATE permissions are needed, which means you have read only unless via your code.
CREATE PROC etc
...
UPDATE
MyTable
SET
OldPrice = Price,
Price = #NewPrice,
UpdatedBy = (variable or default)
UpdatedWhen = DEFAULT --you have a DEFAULT right?
WHERE
PKCol = #SomeID
AND --provide some modicum of logic to trap useless updates
Price <> #NewPrice;
A trigger would be similar but you need to have a JOIN with the INSERTED and DELETED tables
What if someone updates OldPrice directly?
UPDATE
T
SET
OldPrice = D.Price
FROM
Mytable T
JOIN
INSERTED I ON T.PKCol = I.PKCol
JOIN
DELETED D ON T.PKCol = D.PKCol
WHERE
T.Price <> I.Price;
Now do you see why you got jumped on...?
After question edit, for INSERT only
UPDATE
T
SET
OriginalPrice = I.Price
FROM
Mytable T
JOIN
INSERTED I ON T.PKCol = I.PKCol
But if all INSERTs happen via stored procedure I'd set it there though....
There is no readonly attribute for a SQL Server table column. BUT you could implement the functionality you describe using a trigger (and restricting permissions)
Except, it is not the best way to solve the problem. Instead treat the price as Type 2 'slowly changing dimension'. This involves having a 'ValidTo' column (os 'StartDate' and 'EndDate' columns), and closing off a record:
Supplier_Key Supplier_Code Supplier_Name Supplier_State Start_Date End_Date
123 ABC Acme Supply Co CA 01-Jan-2000 21-Dec-2004
124 ABC Acme Supply Co IL 22-Dec-2004
If you do go the route of a trigger (I suggest you use SCD type 2), make sure it can handle multiple rows: Multirow Considerations for DML Triggers
I would recommend storing your price in a seperate table called Prices, with the columns Price and Date.
Then whenever the price is updated, INSERT a new record into the Prices table. Then when you need to know the current price, you can pull from there.
However, if you wish to update an OriginalPrice column automatically, you could add a TRIGGER to the table to do this:
http://msdn.microsoft.com/en-us/library/aa258254%28v=sql.80%29.aspx
This is what I ended up with, with a heavy help from #gbn, #Mitch and #Curt:
create trigger TRG_Products_Price_I --common-ish naming style
on dbo.Products after insert as
begin
set nocount on
update m
set OriginalPrice = i.Price
from Products p
join inserted i on p.ID = i.ID
end
I tried to follow this article as well.
Thanks to everyone!