Do triggers cause other triggers to execute? - tsql

I have a question regarding triggers in TSQL.
Let's say I have 3 tables:
Companies
---------
Id
Employees
---------
Id
CompanyId
Tasks
-----
EmployeeId
Now I create 2 triggers for the top 2 tables Companies and Employees:
CREATE TRIGGER DeleteCompany
ON [dbo].[Companies]
INSTEAD OF DELETE
AS
DELETE FROM [dbo].[Employees]
WHERE CompanyId IN (SELECT Id FROM deleted)
GO
CREATE TRIGGER DeleteEmployee
ON [dbo].[Employees]
INSTEAD OF DELETE
AS
DELETE FROM [dbo].[Tasks]
WHERE EmployeeId IN (SELECT Id FROM deleted)
GO
So far so good. Now, if I delete a company, the trigger DeleteCompany will be executed and the mapped Employees will be removed. My question is, will this cause the trigger DeleteEmployee to execute? Because I am trying to basically do this but only the first trigger (DeleteCompany) is executed.

OK I have found the problem. I was a little confused about what INSTEAD OF actually does. It turns out, it completely replaces the DELETE so I had to delete the actual record after I deleted it's references like so:
CREATE TRIGGER DeleteCompany
ON [dbo].[Companies]
INSTEAD OF DELETE
AS
DELETE FROM [dbo].[Employees]
WHERE CompanyId IN (SELECT Id FROM deleted)
DELETE [dbo].[Companies] WHERE [dbo].[Companies].Id IN (SELECT Id FROM deleted)
GO
CREATE TRIGGER DeleteEmployee
ON [dbo].[Employees]
INSTEAD OF DELETE
AS
DELETE FROM [dbo].[Tasks]
WHERE EmployeeId IN (SELECT Id FROM deleted)
DELETE [dbo].[Employees] WHERE [dbo].[Employees].Id IN (SELECT Id FROM deleted)
GO
This does the trick and the desired cascading delete effect is achieved.
This is the post that led me to this realization: Link

Related

Recursive DELETE statement to remove all posts within topic category and all subtopics

I have a challenge I am solving that requires me to do the following:
"Delete all published posts in the “Customer Success” topic and its subtopics. A subtopic is a child or descendant topic (similar to folders vs subfolders) and can include many levels. For example, there might be a topic “Company” with a subtopic “Engineering” with a subtopic “Backend” with a subtopic “Elixir” so the hierarchy is Company > Engineering > Backend > Elixir. Here Elixir is also a subtopic of Company."
The tables I have that may be included in this statement are the: public.topics, public.posts_topics, and public.posts
I am new to PostgreSQL and have never done anything like a recursive deletion of child elements before. I know the posts_topics table has a foreign key to both the posts and topics tables.
Does anyone have any advice for how this statement should be written?
When you want to create an appropriate table structure you should think about FOREIGN KEYS. The FK connects a record in one table with a record of the same or another table. You can tell the FK that the related record must be deleted if the referenced record is deleted. This is done in the table definition using
FOREIGN KEY (column) REFERENCES another_table(primary_key_column) ON DELETE CASCADE
demo:db<>fiddle
CREATE TABLE topics (
id int PRIMARY KEY,
name text,
-- references another id in same table (build the topic hierarchy)
parent int REFERENCES topics(id) ON DELETE CASCADE
);
CREATE TABLE posts (
id int PRIMARY KEY,
post_text text,
-- connects the owner topic to the post
owner_id int REFERENCES topics(id) ON DELETE CASCADE
);
-- in fact, because we are using the owner_id in table "posts",
-- this table is not really required anymore but you requested it
CREATE TABLE posts_topics (
p_id int REFERENCES posts(id) ON DELETE CASCADE,
t_id int REFERENCES topics(id) ON DELETE CASCADE
);
However, if you really wanted to do this with a recursive query, the query could look like this:
demo:db<>fiddle
WITH RECURSIVE trace_tree AS (
SELECT id -- 1
FROM topics
WHERE id = 2
UNION
SELECT t.id
FROM topics t
JOIN trace_tree tt ON tt.id = t.parent
), del_posts_topics AS ( -- 2
DELETE FROM posts_topics WHERE t_id = ANY (
SELECT * FROM trace_tree
)
RETURNING p_id
), del_posts AS ( -- 3
DELETE FROM posts WHERE id = ANY (
SELECT * FROM del_posts_topics
)
)
DELETE FROM topics WHERE id = ANY ( -- 4
SELECT * FROM trace_tree
);
This is the recursion (WITH RECURSIVE). A recursive CTE contains two parts, combined by the UNION clause. First is the recursion initialization. Second is the recursion which joins the previous run on the current table. Finally this returns a list of ids which represents the ancestors of the topic given in the initialization.
Next CTE: Delete all entry in the posts_topics join table with all records which contains a topic t_id from the list queried above. The DELETE statement returns the post ids (p_id) which were deleted in this step using RETURNING.
The previously returned (and deleted) posts' p_ids are used in this DELETE statement for deleting the real posts.
Finally delete all the topics queried in (1)

Loading a big table, referenced by others, efficiently

My use case is the following:
I have big table users (~200 millions rows) of users with user_id as the primary key. users is referenced by several other tables using foreign key with ON DELETE CASCADE.
Every day I have to replace the whole content of users using a lot of csv files. (Please don't ask why I have to do that, I just have to...)
My idea was to set the primary key and all foreign keys as DEFERRED, then, in the same transaction, DELETE the whole table and copying all the csvs using the COPY command. The expected result was that all check and index calculation would happen at the end of the transaction.
But actually the insert process is super slow (4hours, against 10min if I insert and the put the primary key) AND no foreign key can refer to a deferrable primary.
I can't remove the primary key during the insertion because of the foreign keys. I don't want get rid of the foreign key either because I would have to simulate the behavior of ON DELETE CASCADE manually.
So basically I am looking for a way to tell postgres to not care about primary key index or foreign key check until the very end of the transaction.
PS1: I made up the users table, I am actually working with very different kind of data but it's not really relevant to the problem.
PS2: As a rough estimation I would say that every day, on my 200+ millions records, I have 10 records removed, 1million updated and 1million added.
A full delete + a full insert will cause a flood of cascading FK,
which will have to be postponed by DEFERRED,
which will cause an avalanche of aftermath for the DBMS at commit time.
Instead, dont {delete+create} keys, but keep them right where they are.
Also, dont touch records that dont need to be touched.
-- staging table
CREATE TABLE tmp_users AS SELECT * FROM big_users WHERE 1=0;
COPY TABLE tmp_users (...) FROM '...' WITH CSV;
-- ... and more copying ...
-- ... from more files ...
-- If this fails, you have a problem!
ALTER TABLE tmp_users
ADD PRIMARY KEY (id);
-- [EDIT]
-- I added this later, because the user_comments table
-- was not present in the original question.
DELETE FROM user_comments c
WHERE NOT EXISTS (
SELECT * FROM tmp_users u WHERE u.id = c.user_id
);
-- These deletes are allowed to cascade
-- [we assume that the mport of the CSV files was complete, here ...]
DELETE FROM big_users b
WHERE NOT EXISTS (
SELECT *
FROM tmp_users t
WHERE t.id = b.id
);
-- Only update the records that actually **change**
-- [ updates are expensive in terms of I/O, because they create row-versions
-- , and the need to delete the old row-versions, afterwards ]
-- Note that the key (id) does not change, so there will be no cascading.
-- ------------------------------------------------------------
UPDATE big_users b
SET name_1 = t.name_1
, name_2 = t.name_2
, address = t.address
-- , ... ALL THE COLUMNS here, except the key(s)
FROM tmp_users t
WHERE t.id = b.id
AND (t.name_1, t.name_2, t.address, ...) -- ALL THE COLUMNS, except the key(s)
IS DISTINCT FROM
(b.name_1, b.name_2, b.address, ...)
;
-- Maybe there were some new records in the CSV files. Add them.
INSERT INTO big_users (id,name_1,name_2,address, ...)
SELECT id,name_1,name_2,address, ...
FROM tmp_users t
WHERE NOT EXISTS (
SELECT *
FROM big_users x
WHERE x.id = t.id
);
I found a hacky solution :
update pg_index set indisvalid = false, indisready=false where indexrelid= 'users_pkey'::regclass;
DELETE FROM users;
COPY TABLE users FROM 'file.csv';
REINDEX INDEX users_pkey;
DELETE FROM user_comments c WHERE NOT EXISTS (SELECT * FROM users u WHERE u.id = c.user_id )
commit;
The magic dirty hack is to disable the primary key index in the postgres catalog and at the end to force the reindexing (which will override what we changed). I can't use foreign key with ON DELETE CASCADE because for some reason it makes the constraint being executed immediatly... So instead my foreign keys are ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED and I have to do the delete myself.
This works well in my case because only a few users are being referred in other tables.
I wish there was a cleaner solution though...

T-SQL delete cascade (or trigger) with multiple references to child table

The problem is larger, but this is it in a nutshell. I have two tables Invoice and Address.
The Invoice table has two columns:
BillingAddressId int, FK to Address(AddressId)
ShippingAddressId int, FK to Address(AddressId)
I would like to declare both of these relationships with ON DELETE CASCADE, but I can't because this causes the "multiple cascade paths" error. I need to be able to delete the Invoice record and have it delete both Address records.
So instead, I create the trigger:
CREATE TRIGGER [dbo].[Trigger_Invoice_DeleteAddress]
ON [dbo].[Invoice]
FOR DELETE
AS
BEGIN
SET NoCount ON
DELETE FROM Address
WHERE AddressId IN (SELECT BillingAddressId FROM deleted);
DELETE FROM Address
WHERE AddressId IN (SELECT InvoiceAddressId FROM deleted);
END
This flat out doesn't work, as now I get:
The DELETE statement conflicted with the REFERENCE constraint "FK_Invoice_BillingAddress".
The conflict occurred in database "Example", table "dbo.Invoice", column 'BillingAddressId'.
Also tried this suggestion:
CREATE TRIGGER [dbo].[Trigger_Invoice_DeleteAddress]
ON [dbo].[Invoice]
INSTEAD OF DELETE
AS
BEGIN
SET NoCount ON
DELETE FROM Address
WHERE AddressId IN (SELECT BillingAddressId FROM deleted);
DELETE FROM Address
WHERE AddressId IN (SELECT InvoiceAddressId FROM deleted);
DELETE FROM Invoice
Where InvoiceId in (SELECT InvoiceID from deleted);
END
This has the same error.
What have I missed?
Your problem is because you are trying to delete the parent automatically when you delete the child. This may not possible because the parent could be linked to another invoice. In any event it is not generally a good idea. You really need to revisit this requirement and see if it truly makes sense to do. I have never seen a system (and I have worked with hundreds) where the deletion of a child automatically triggered the deletion of a parent record.
In this case it could be especially bad. If I were a customer and a I cancelled an invoice and went to buy a few days later and had to re-enter the address information I gave you previously, I would be annoyed and far less likely to want to continue to do business with a company foolish enough to do such a thing. This is the type of requirement that drives business away. Why on earth woudl you want to delete the address with the invoice? Why on earth would you be deleting invoices at all? Most accounting systems never ever delete invoices. If they need to cancel them, then they back them out with an offsetting charge. This whole idea could open up your system to fraud as no one person should ever be allowed to do such a thing, so it is really a bad idea. You really need to do some reading on accounting standards and especially on internal controls.
You can't delete the parent record until the child record is gone, this is why the triggers are not working for you.
I would suggest that you do all the deleting in a stored procedure or alternatively, have the trigger put the address ID records in a staging table and then delete them from a job that runs every five minutes. In this job, you would make sure to check to see if there are any other invoices attached to that address and only delete when there are none left. But still I think deleting either the addresses or the invoices is a truly bad idea.

Cascade new IDs to a second, related table

I have two tables, Contacts and Contacts_Detail. I am importing records into the Contacts table and need to run a SP to create a record in the Contacts_Detail table for each new record in the Contacts. There is an ID in the Contacts table and a matching ID_D in the Contacts_Detail table.
I'm using this to insert the record into Contacts_Detail but get the 'Subquery returned more than 1 value.' error and I can't figure out why. There are multiple records in Contacts that need have matching records in Contacts_Detail.
Insert into Contacts_Detail (ID_D)
select id from Contacts c
left join Contacts_Detail cd
on c.id = cd.id_d
where id_d is null
I'm open to a better way...
thanks.
It sounds like you're inserting blank child-records into your Contacts_Detail table -- so the first question I'd ask is: Why?
As for why your specific SQL isn't working...
A few things you can check:
Contacts table -- do you have any records there WHERE id is null?
(delete them -- then make the id field a primary key)
Contacts_Detail
table -- do you have any records there WHEERE id_d is null?
(delete them -- then go into your designer and create a relationship
/ enforce referential integrity.)
Verify that c.id is the primary
key, and cd.id_d is the correct foreign key to relate the tables.
Hope that helps
Why not just have a trigger? This seems a little simpler than having to determine for all time which rows are missing - that seems more like something you would do periodically to correct for some anomalies, not something you should have to do after every insert. Something like this should work:
CREATE TRIGGER dbo.NewContacts
ON dbo.Contacts
FOR INSERT
AS
BEGIN
INSERT dbo.Contacts_Detail(ID_D) SELECT ID FROM inserted;
END
GO
But I suspect you have a trigger on the Contacts_Detail table that is not written to correctly handle multi-row inserts, and that's where your subquery error is coming from. Can you show the trigger on Contacts_Detail?

SQL Server Trigger to DELETE one record from multiple tables

I know this can be done with foreign keys but I cannot add them or something weird happens when I am inserting new records. There are a lot of stored procedures in this database and I don't know what they do since I know nothing about stored procedures. I was hoping someone could help me figure out a trigger that will delete a specific ProductID when I delete it from the Product table. It is also located in tables called CompanyLink, TargetLink, and CategoryLink.
As of right now, when I delete the ProductID from the Product table, I have to manually delete it from the other 3 tables it was inserted into.
You can do it through a trigger like this:
CREATE TRIGGER [dbo].[ProductDeleted]
ON [dbo].[Product]
AFTER DELETE
AS
BEGIN
DELETE FROM CompanyLink WHERE ProductID = (SELECT TOP 1 ProductID FROM DELETED)
DELETE FROM TargetLink WHERE ProductID = (SELECT TOP 1 ProductID FROM DELETED)
END
Obviously the syntax might not be perfect, but this is close to what you need.