Remove row from table (cascade) that have 2 foreign keys - sql

I have next table structure :
RoomId and HouseId are nullable, one field is always null. I got next exception while attempt to delete row from Houses table.
The DELETE statement conflicted with the REFERENCE constraint
"FK_dbo.Images_dbo.Rooms_RoomId"
Why the cascade delete fails? I have used EF code first.
EDIT
Working trigger based on #Juan Carlos example:
BEGIN TRANSACTION
delete from images
where houseId in ( select id from deleted )
delete from images
where roomId in (
select rooms.Id
from rooms
where rooms.HouseId in (select id from deleted)
)
delete from Rooms
where houseId in ( select id from deleted )
delete from houses
where ID in ( select ID from deleted )
COMMIT

The problem is the cascade order. You have follow this sequence:
Delete rooms
Delete images
Delete houses
You need create a trigger to solve that. Now the problem is I dont know how create that using Code First.
create trigger house_cascade
on Houses
instead of delete
as
set nocount on
delete from rooms
where room.id in (select i.ID
from images i
inner join deleted d
on i.house_id = d.id)
delete from images
where house_id in ( select id from deleted )
delete from houses
where ID in ( select ID from deleted )
More info here
Especially this comment
But SQL Server doesn't support this. It's supper annoying, no other serious DB engine has this problem, people complained about it in 2005, Microsfot agreed that it was a "desirable feature" in 2008, but still here in 2014 they don't have it. – Shavais Aug 5 '14 at 21:28

Related

SQL delete records in order

Given the table structure:
Comment
-------------
ID (PK)
ParentCommentID (FK)
I want to run DELETE FROM Comments to remove all records.
However, the relationship with the parent comment record creates a FK conflict if the parent comment is deleted before the child comments.
To solve this, deleting in reverse ID order would work. How do I delete all records in a table in reverse ID order?
The following will delete all rows that are not themselves parents. If the table is big and there's no index on ParentCommentID, it might take a while to run...
DELETE Comment
from Comment co
where not exists (-- Correlated subquery
select 1
from Comment
where ParentCommentID = co.ID)
If the table is truly large, a big delete can do bad things to your system, such as locking the table and bloating the transaction log file. The following will limit just how many rows will be deleted:
DELETE top (1000) Comment -- (1000 is not very many)
from Comment co
where not exists (-- Correlated subquery
select 1
from Comment
where ParentCommentID = co.ID)
As deleting some but not all might not be so useful, here's a looping structure that will keep going until everything's gone:
DECLARE #Done int = 1
--BEGIN TRANSACTION
WHILE #Done > 0
BEGIN
-- Loop until nothing left to delete
DELETE top (1000) Comment
from Comment co
where not exists (-- Correlated subquery
select 1
from Comment
where ParentCommentID = co.ID)
SET #Done = ##Rowcount
END
--ROLLBACK
This last, of course, is dangerous (note the begin/end transaction used for testing!) You'll want WHERE clauses to limit what gets deleted, and something or to ensure you don't somehow hit an infinite loop--all details that depend on your data and circumstances.
With separate Parent and Child tables, ON DELETE CASCADE would ensure that deleting the parent also deletes the children. Does it work when both sets of data are within the same table? Maybe, and I'd love to find out!
How do I use cascade delete with SQL server.
this works (you can try replacing the subquery with top...)
create table #a1 (i1 int identity, b1 char(5))
insert into #a1 values('abc')
go 5
while ( (select count(*) from #a1 ) > 0)
begin
delete from #a1 where i1=(select top 1 i1 from #a1 order by i1 desc)
end

Logging rows that are deleted in a table sql server

i have made a topic before, but i think that i have not been really clear, i am going to try to explain it in as much detail as i possibly can.
i have a table called List, this has 32000 rows, 2000 for each year(1999-2014) which has columns: SongID,Top2000Year,Position,
What has to happen:
if someone deletes for example the year 2000, all 2000 songs of the year 2000 will be deleted, theres no problem with that
Delete from List WHERE top2000Year = 2000
But, the problem i am facing is: i need this to be logged, i created a log table called LogTable with the columns: Title,Artist,Year,Position
Title will be the song title, year is the year the song came out and position is the position in the list
those are from a different table, i join them like this
select title, name[artist],song.Year[SongYear], position from List
left join Song on Song.songid = List.songid
left join Artist on Artist.artistid = Song.artistid
where top2000Year = 2013
order by position asc
this shows a query like this http://puu.sh/i8f2u/8702ee7010.png (Censored the server, it's my teachers so i figured i'd censor it)
i am trying to fill my LogTable exactly like that, if the year 2013 is deleted, those 2000 rows should be inserted into the LogTable using a trigger
Edit:
i used this https://dbalink.wordpress.com/2008/06/20/how-to-sql-server-trigger-101/ and it worked, then i tried it with my database, and it didn't work.
Code:
USE [TOP2000DATABASE]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TRGDeleteList]
ON list
FOR DELETE
AS
INSERT INTO List (songid,top2000Year,position)
SELECT songid,top2000Year,position FROM Inserted
INSERT INTO [dbo].[LogTabel] (title,artist,Year,position)
SELECT title,artist,year,position FROM Deleted
Just create a trigger on your table for delete action:
CREATE TRIGGER TriggerName
ON List
FOR DELETE
AS
INSERT INTO LogTable (Title, Artist, Year, Position)
SELECT s.Title, a.Name, s.Year, s.Position
FROM DELETED d
LEFT JOIN Song s ON s.songid = d.songid
LEFT JOIN Artist a ON a.artistid = s.artistid
GO
You need to join on DELETED virtual table to get all deleted rows.

SQL Server - Cascading DELETE with Recursive Foreign Keys

I've spent a good amount of time trying to figure out how to implement a CASCADE ON DELETE for recursive primary keys on SQL Server for some time now. I've read about triggers, creating temporary tables, etc but have yet to find an answer that will work with my database design.
Here is a Boss/Employee database example that will work for demonstration purposes:
TABLE employee
id|name |boss_id
--|---------|-------
1 |John |1
2 |Hillary |1
3 |Hamilton |1
4 |Scott |2
5 |Susan |2
6 |Seth |2
7 |Rick |5
8 |Rachael |5
As you can see, each employee has a boss that is also an employee. So, there is a PK/FK relationship on id/boss_id.
Here is an (abbreviated) table with their information:
TABLE information
emp_id|street |phone
------|-----------|-----
2 |blah blah |blah
6 |blah blah |blah
7 |blah blah |blah
There is a PK/FK on employee.id/information.emp_id with a CASCADE ON DELETE.
For example, if Rick was fired, we would do this:
DELETE FROM employee WHERE id=7
This should delete Rick's rows from both employee and information. Yay cascade!
Now, say we've hit hard times and we need to lay of Hamilton and his entire department. This means that we would need to remove
Hamilton
Scott
Susan
Seth
Rick
Rachael
From both the employee and information tables when we run:
DELETE FROM employee WHERE id=3
I tried a simple CASCADE ON DELETE for id/emp_id, but SQL Server wasn't having it:
Introducing FOREIGN KEY constraint 'fk_boss_employee' on table 'employee' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
I was able to use CASCADE ON DELETE on a test database in Access, and it behaved exactly as I wanted it to. Again, I want every possible child, grandchild, great-grandchild, etc of a parent to be deleted if their parent, grandparent, great-grandparent, etc is deleted.
When I tried using triggers, I couldn't seem to get it to trigger itself (eg. when you try to delete Hamilton's employee Susan, first see if Susan has any employees, etc) let alone going down N-number of employees.
So! I think I've provided every detail I can think of. If something still isn't clear, I'll try to improve this description.
Necromancing.
There's 2 simple solutions.
You can either read Microsoft's sorry-excuse(s) of why they didn't
implement this (because it is difficult and time-consuming - and time is money), and explanation of why you don't/shouldn't need it (although you do), and implement the delete-function with a cursor in a stored procedure
because you don't really need delete cascade, because you always have the time to change ALL your and ALL of OTHER people's code (like interfaces to other systems) everywhere, anytime, that deletes an employee (or employees, note: plural) (including all superordinate and subordinate objects [including when a or several new ones are added]) in this database (and any other copies of this database for other customers, especially in production when you don't have access to the database [oh, and on the test system, and the integration system, and local copies of production, test, and integration]
or
you can use a proper DBMS that actually supports recursive cascaded deletes, like PostGreSQL (as long as the graph is directed, and non-cyclic; else ERROR on delete).
PS: That's sarcasm.
Note:
As long as your delete does not stem from a cascade, and you just want to perform a delete on a self-referencing table, you can delete any entry, as long as you remove all subordinate objects as well in the in-clause.
So to delete such an object, do the following:
;WITH CTE AS
(
SELECT id, boss_id, [name] FROM employee
-- WHERE boss_id IS NULL
WHERE id = 2 -- <== this here is the id you want to delete !
UNION ALL
SELECT employee.id, employee.boss_id, employee.[name] FROM employee
INNER JOIN CTE ON CTE.id = employee.boss_id
)
DELETE FROM employee
WHERE employee.id IN (SELECT id FROM CTE)
Assuming you have the following table structure:
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'dbo.employee') AND type in (N'U'))
BEGIN
CREATE TABLE dbo.employee
(
id int NOT NULL,
boss_id int NULL,
[name] varchar(50) NULL,
CONSTRAINT PK_employee PRIMARY KEY ( id )
);
END
GO
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'dbo.FK_employee_employee') AND boss_id_object_id = OBJECT_ID(N'dbo.employee'))
ALTER TABLE dbo.employee WITH CHECK ADD CONSTRAINT FK_employee_employee FOREIGN KEY(boss_id)
REFERENCES dbo.employee (id)
GO
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'dbo.FK_employee_employee') AND boss_id_object_id = OBJECT_ID(N'dbo.employee'))
ALTER TABLE dbo.employee CHECK CONSTRAINT FK_employee_employee
GO
The below might work for you (I haven't tested it so it may require some tweaking). Seems like all you have to do is delete the employees from the bottom of the hierarchy before you delete the ones higher-up. Use a CTE to build the delete hierarchy recursively and order the CTE output descending by the hierarchy level of the employee. Then delete in order.
CREATE PROC usp_DeleteEmployeeAndSubordinates (#empId INT)
AS
;WITH employeesToDelete AS (
SELECT id, CAST(1 AS INT) AS empLevel
FROM employee
WHERE id = #empId
UNION ALL
SELECT e.id, etd.empLevel + 1
FROM employee e
JOIN employeesToDelete etd ON e.boss_id = etd.id AND e.boss_id != e.id
)
SELECT id, ROW_NUMBER() OVER (ORDER BY empLevel DESC) Ord
INTO #employeesToDelete
FROM employeesToDelete;
DECLARE #current INT = 1, #max INT = ##ROWCOUNT;
WHILE #current <= #max
BEGIN
DELETE employee WHERE id = (SELECT id FROM #employeesToDelete WHERE Ord = #current);
SET #current = #current + 1;
END;
GO
This may sound extreme, but I don't think there is a simple baked in option for what you are looking to do. I would suggest creating a proc that would do the following:
Disable FK constraints
get a list of employees to be deleted using a recursive CTE (save this in a temp table)
Delete the rows from the parent / child table
Delete rows from the employee information table
Enable FK Constraints
Wrap the whole thing in a transaction to maintain consistency

SQL Queries instead of Cursors

I'm creating a database for a hypothetical video rental store.
All I need to do is a procedure that check the availabilty of a specific movie (obviously the movie can have several copies). So I have to check if there is a copy available for the rent, and take the number of the copy (because it'll affect other trigger later..).
I already did everything with the cursors and it works very well actually, but I need (i.e. "must") to do it without using cursors but just using "pure sql" (i.e. queries).
I'll explain briefly the scheme of my DB:
The tables that this procedure is going to use are 3: 'Copia Film' (Movie Copy) , 'Include' (Includes) , 'Noleggio' (Rent).
Copia Film Table has this attributes:
idCopia
Genere (FK references to Film)
Titolo (FK references to Film)
dataUscita (FK references to Film)
Include Table:
idNoleggio (FK references to Noleggio. Means idRent)
idCopia (FK references to Copia film. Means idCopy)
Noleggio Table:
idNoleggio (PK)
dataNoleggio (dateOfRent)
dataRestituzione (dateReturn)
dateRestituito (dateReturned)
CF (FK to Person)
Prezzo (price)
Every movie can have more than one copy.
Every copy can be available in two cases:
The copy ID is not present in the Include Table (that means that the specific copy has ever been rented)
The copy ID is present in the Include Table and the dataRestituito (dateReturned) is not null (that means that the specific copy has been rented but has already returned)
The query I've tried to do is the following and is not working at all:
SELECT COUNT(*)
FROM NOLEGGIO
WHERE dataNoleggio IS NOT NULL AND dataRestituito IS NOT NULL AND idNoleggio IN (
SELECT N.idNoleggio
FROM NOLEGGIO N JOIN INCLUDE I ON N.idNoleggio=I.idNoleggio
WHERE idCopia IN (
SELECT idCopia
FROM COPIA_FILM
WHERE titolo='Pulp Fiction')) -- Of course the title is just an example
Well, from the query above I can't figure if a copy of the movie selected is available or not AND I can't take the copy ID if a copy of the movie were available.
(If you want, I can paste the cursors lines that work properly)
------ USING THE 'WITH SOLUTION' ----
I modified a little bit your code to this
WITH film
as
(
SELECT idCopia,titolo
FROM COPIA_FILM
WHERE titolo = 'Pulp Fiction'
),
copy_info as
(
SELECT N.idNoleggio, N.dataNoleggio, N.dataRestituito, I.idCopia
FROM NOLEGGIO N JOIN INCLUDE I ON N.idNoleggio = I.idNoleggio
),
avl as
(
SELECT film.titolo, copy_info.idNoleggio, copy_info.dataNoleggio,
copy_film.dataRestituito,film.idCopia
FROM film LEFT OUTER JOIN copy_info
ON film.idCopia = copy_info.idCopia
)
SELECT COUNT(*),idCopia FROM avl
WHERE(dataRestituito IS NOT NULL OR idNoleggio IS NULL)
GROUP BY idCopia
As I said in the comment, this code works properly if I use it just in a query, but once I try to make a procedure from this, I got errors.
The problem is the final SELECT:
SELECT COUNT(*), idCopia INTO CNT,COPYFILM
FROM avl
WHERE (dataRestituito IS NOT NULL OR idNoleggio IS NULL)
GROUP BY idCopia
The error is:
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "VIDEO.PR_AVAILABILITY", line 9.
So it seems the Into clause is wrong because obviously the query returns more rows. What can I do ? I need to take the Copy ID (even just the first one on the list of rows) without using cursors.
You can try this -
WITH film
as
(
SELECT idCopia, titolo
FROM COPIA_FILM
WHERE titolo='Pulp Fiction'
),
copy_info as
(
select N.idNoleggio, I.dataNoleggio , I.dataRestituito , I.idCopia
FROM NOLEGGIO N JOIN INCLUDE I ON N.idNoleggio=I.idNoleggio
),
avl as
(
select film.titolo, copy_info.idNoleggio, copy_info.dataNoleggio,
copy_info.dataRestituito
from film LEFT OUTER JOIN copy_info
ON film.idCopia = copy_info.idCopia
)
select * from avl
where (dataRestituito IS NOT NULL OR idNoleggio IS NULL);
You should think in terms of sets, rather than records.
If you find the set of all the films that are out, you can exclude them from your stock, and the rest is rentable.
select copiafilm.* from #f copiafilm
left join
(
select idCopia from #r Noleggio
inner join #i include on Noleggio.idNoleggio = include.idNoleggio
where dateRestituito is null
) out
on copiafilm.idCopia = out.idCopia
where out.idCopia is null
I solved the problem editing the last query into this one:
SELECT COUNT(*),idCopia INTO CNT,idCopiaFilm
FROM avl
WHERE (dataRestituito IS NOT NULL OR idNoleggio IS NULL) AND rownum = 1
GROUP BY idCopia;
IF CNT > 0 THEN
-- FOUND AVAILABLE COPY
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- NOT FOUND AVAILABLE COPY
Thank you #Aditya Kakirde ! Your suggestion almost solved the problem.

how to delete duplicates from a database table based on a certain field

i have a table that somehow got duplicated. i basically want to delete all records that are duplicates, which is defined by a field in my table called SourceId. There should only be one record for each source ID.
is there any SQL that i can write that will delete every duplicate so i only have one record per Sourceid ?
Assuming you have a column ID that can tie-break the duplicate sourceid's, you can use this. Using min(id) causes it to keep just the min(id) per sourceid batch.
delete from tbl
where id NOT in
(
select min(id)
from tbl
group by sourceid
)
delete from table
where pk in (
select i2.pk
from table i1
inner join table i2
on i1.SourceId = i2.SourceId
)
good practice is to start with
select * from … and only later replace to delete from …