I have 3 tables:
Category(CategoryId, Name)
Product(ProductId, Name, Description, CategoryId)
OrderItem(OrderId, OrdinalNumber, ProductId, CategoryId)
I want to create an AFTER UPDATE trigger that changes CategoryId (based on new ProductId) in OrderItem after update of ProductId in OrderItem.
Can somebody help with this trigger?
Duplicating the category ID in the order line isn't something you'd usually want to do, but if you're set on that, you need a 'before' trigger, not an 'after' one - since you need to change a value in the row being updated:
create or replace trigger orderitem_cat_trig
before insert or update on orderitem
for each row
begin
select categoryid
into :new.categoryid
from product
where productid = :new.productid;
end;
/
I've made it both insert and update on the assumption you'll want to set the value for new order items too.
Unless you like database deadlocks, general performance issues, data corruption and unpredictable results, this type of updates is not advisable. If your performance is a problem, check indexes and queries. Do not replicate your columns in tables, especially not when they're part of an foreign key. I'm not the dogmatic type, but in this case I will not budge ;-)
Related
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.
Hi everyone I am still sort of new to SQL, I have a slight problem and maybe someone can help.
I have researched all about triggers and what I read make sense, but I can't get the answer I need from it for some reason so I will explain what I need to do
I have 2 tables Products, LabelPrint
In products there are 5 columns upc, itemcode, description, price, labelprint
In LabelPrint there are the same columns
What I need is a trigger for when a new item is created or a old item is edited and the column LabelPrint is modified to =1 for yes
I need the item in question to be copied over to labelprint table
The label print table is automatically cleaned of this items after a certain period of time, I just hope someone can give me some help in understanding how I can make this trigger work the way I hope it will
thanks brandon
I would try something like this:
CREATE TRIGGER InsertProducts
ON dbo.Products
FOR INSERT
AS
INSERT INTO dbo.LabelPrint(upc, itemcode, description, price, labelprint)
SELECT
upc, itemcode, description, price, labelprint
FROM Inserted
WHERE labelprint = 1
This first trigger will fire whenever you insert data into the table dbo.Products, and if the labelprint column is set to 1, then those rows are inserted into dbo.LabelPrint as well.
The UPDATE is a bit trickier:
CREATE TRIGGER UpdateProducts
ON dbo.Products
FOR UPDATE
AS
INSERT INTO dbo.LabelPrint(upc, itemcode, description, price, labelprint)
SELECT
i.upc, i.itemcode, i.description, i.price, i.labelprint
FROM Inserted i
INNER JOIN Deleted d ON i.itemcode = d.itemcode
WHERE i.labelprint = 1 AND d.labelprint <> 1
Here, I check to see that the row has been updated (not 100% sure if itemcode is the best column to use to link the old and new values - adapt as needed if it's not), and I make sure the old value (from the Deleted pseudo-table) is not 1 while the new value (from Inserted) is 1 - in that case, the column LabelPrint has been updated to 1 and that row will be copied into the LabelPrint table.
One word of caution: you need to be aware that SQL Server does not fire the trigger for each row that is being inserted and/or updated. The trigger will fire once for each statement - and that one statement could very well update 50 rows - in that case, the trigger fires once, and the pseudo-tables inside the trigger (Inserted, Deleted) will contain 50 rows each. Just don't ever assume that Inserted only ever contains a single row - that assumption would be totally false.
I have three tables:
Table1: Customers (CustomerId, Name, CustomerAddress)
Table2: AccountManagers(ManagerId, Name)
Table3: CustomerAccountManagers (CustomerId, ManagerId, Remarks)
Table4: CustomerHistory(CustomerAddress, ManagerId, Date)
The CustomerHistory table is used to store any changes made to "CustomerAddress" OR "ManagerId" for example CustomerAddress is updated from "Address1" to "Address2" OR The CustomerAccountManager changes from "Manager1" to "Manager2".
I need to store the changes in CustomerHistory table through SQL Trigger. The issue is that the on which table should i have my trigger? Please note that the changes are made at the same time to both the tables "Customers" & "CustomerAccountManagers".
Thanks
First of all, the CustomerHistory table should probably contain CustomerId as well, so that a history record can be tracked back to the proper customer.
You'll need two triggers: one on CustomerAccountManagers and one on Customers. If you can guarantee the order in which they are executed it's fine: the first trigger will insert, the second update the history record.
If you cannot guarantee the order, things get complicated, as each trigger would have to: 1) try to insert a new record, and failing that 2) update the existing. You'd have to protect yourself from the intermittent insertion by another trigger and this likely means running within a transaction with serializable isolation level (locking the whole table). This is deadlock-prone, so it's really better to use two history tables, as others already suggested.
You should have different historic tables for each normal table you have.
Then you can place a trigger in each normal table.
Finally, if you need, you can create a view CustomerHistory joining the different historic tables.
You need to add your trigger to the table where data changes will "trigger" a need to do something.
So a trigger on the Customers table to track CustomerAddress changes and a trigger on CustomerAccountManagers when the ManagerId is changed.
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!
Given that I have two tables
Customer (id int, username varchar)
Order (customer_id int, order_date datetime)
Now I want to insert into Order table based on customer information which is available in Customer table.
There are a couple of ways I can approch this problem.
First - I can query the customer information into a variable and then use it in an INSERT statement.
DECLARE #Customer_ID int
SELECT #Customer_ID = id FROM Customer where username = 'john.smith'
INSERT INTO Orders (customer_id, order_date) VALUES (#Customer_ID, GETDATE())
Second Approach is to use a combination of INSERT and SELECT query.
INSERT INTO Orders (customer_id, order_date)
SELECT id, GETDATE() FROM Customers
WHERE username = 'john.smith'
So my question is that which is a better way to proceed in terms of speed and overhead and why ? I know if we have a lot of information getting queried from Customer table then the second approach is much better.
p.s. I was asked this question in one of the technical interviews.
The second approach is better.
The first approach will fail if the customer is not found. No check is being done to make sure the customer id has been returned.
The second approach will do nothing if the customer is not found.
From an overhead approach why create variables if they are not needed. Set based sql is usually the better approach.
In a typical real-world order-entry system, the user has already looked the Customer up via a Search interface, or has chosen the customer from a list of customers displayed alphabetically; so your client program, when it goes to insert an order for that customer, already knows the CustomerID.
Furthermore, the order date is typically defaulted to getdate() as part of the ORDERS table definition, and your query can usually ignore that column.
But to handle multiple line items on an order, your insert into ORDER_HEADER needs to return the order header id so that it can be inserted into the ORDER DETAIL line item(s) child rows.
I don't recommend either approach. Why do you have the customer name and not the id in the first place? Don't you have a user interface that maintains a reference to the current customer by holding the ID in its state? Doing the lookup by name exposes you to potentially selecting the wrong customer.
If you must do this for reasons unknown to me, the 2nd approach is certainly more efficient because it only contains one statement.
Make the customer id in order table a foreign key which refers to customer table.