Is there any "reverse" ON DELETE CASCADE option? - sql

Let's say I have the following database in SQL Server:
CREATE TABLE [Order]
(
ID BIGINT IDENTITY(1,1)
CONSTRAINT PK_Order PRIMARY KEY CLUSTERED (ID)
);
CREATE TABLE OrderItem
(
ID BIGINT IDENTITY(1,1),
ORDER_ID BIGINT NOT NULL,
PRICE_ID BIGINT NOT NULL,
DISCOUNTED_PRICE_ID BIGINT NULL,
CONSTRAINT PK_OrderItem PRIMARY KEY CLUSTERED (ID)
);
CREATE TABLE Price
(
ID BIGINT IDENTITY(1,1),
AMOUNT FLOAT NOT NULL,
CURRENCY VARCHAR(3) NOT NULL,
CONSTRAINT PK_Price PRIMARY KEY CLUSTERED (ID)
);
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_Order
FOREIGN KEY (ORDER_ID) REFERENCES [Order](ID) ON DELETE CASCADE;
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_Price
FOREIGN KEY (PRICE_ID) REFERENCES Price(ID);
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_DiscountedPrice
FOREIGN KEY (DISCOUNTED_PRICE_ID) REFERENCES Price(ID);
If I delete an order, all order items will be deleted (because of ON DELETE CASCADE on FK_OrderItem_Order constraint), but corresponding prices (normal and discounted) will remain in the database forever.
Is there any option in SQL Server (or generic SQL) to delete corresponding prices from Price table?
I can think of a trigger which is a perfect match, but it is too much hassle for such simple (and common) task. I would prefer to specify something on my constraints (FK_OrderItem_Price and FK_OrderItem_DiscountedPrice) that basically say "this is one-to-one relationship", delete parent (Price is a parent table in this case) if a child was deleted.

In a nutshell: no. Cascading works only from parent to child1, not the other way around.
It could be argued that some parents should be removed when they lose the last of their children, but that's simply not how current DBMSes are implemented.
You'll have to use a trigger for such "special" referential action, or a batch job it it doesn't have to happen immediately. Or hide the operations behind some sort of API (stored procedure, middle-tier method) that does that explicitly.
See also: order stability.
1 In you case, Order and Price both act as parents to OrderItem.

Add the OrderItemID to Price and set up a cascade delete relationship. This column is of course redundant but it allows you to have a cascade delete in the right direction.
Consider inlining the Price table two times into OrderItems. As this is a 1:1 relationship you can do that. It is a matter of taste whether you like this solution or not.

you can create a trigger which acts as a reverse cascade:
DELIMITER $$
CREATE TRIGGER reverse_cascade_OrderItem_Price
AFTER DELETE ON `OrderItem`
FOR EACH ROW
BEGIN
DELETE FROM `Price` WHERE ID = old.PRICE_ID;
END$$

Related

Foreign key constraint cycles or multiple cascade paths

I have a database design like below. I have 3 tables Compartment, CompartmentRelation and CompartmentRelationType . CompartmentRelation table keeps the other compartments around the selected compartment (below,above,behind,infront,etc). CompartmentRelationType keeps the position. Think that i have compartments in the Compartment table named comp-1, comp-2, comp-3, comp-4 and insert the the compartments above comp-1 as comp-2,comp-3 in CompartmentRelation as below. Problem is that setting delete action as cascade for the column RelatedCompId in CompartmentRelation table throw the excaption as
Unable to create relationship 'FK_CompartmentRelation_Compartment1'.
Introducing FOREIGN KEY constraint 'FK_CompartmentRelation_Compartment1' on table 'CompartmentRelation' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
Which way should i follow ?
Compartment
comp-1
comp-2
comp-3
comp-4
Compartment Relation
comp-1 -> comp-2
comp-1 -> comp-3
CREATE TABLE [dbo].[Compartment] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (500) NOT NULL,
CONSTRAINT [PK_Compartment] PRIMARY KEY CLUSTERED ([Id] ASC),
CREATE TABLE [dbo].[CompartmentRelation] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[CompId] INT NOT NULL,
[RelationTypeId] INT NOT NULL,
[RelatedCompId] INT NOT NULL,
CONSTRAINT [PK_CompartmentRelation] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_CompartmentRelation_CompartmentRelationType] FOREIGN KEY ([RelationTypeId]) REFERENCES [dbo].[CompartmentRelationType] ([Id]) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT [FK_CompartmentRelation_Compartment1] FOREIGN KEY ([RelatedCompId]) REFERENCES [dbo].[Compartment] ([Id]),
CONSTRAINT [FK_CompartmentRelation_Compartment] FOREIGN KEY ([CompId]) REFERENCES [dbo].[Compartment] ([Id]) ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE [dbo].[CompartmentRelationType] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (200) NOT NULL,
[NameLan1] NVARCHAR (200) NOT NULL,
[NameLan2] NVARCHAR (200) NULL,
CONSTRAINT [PK_CompartmentRelationType] PRIMARY KEY CLUSTERED ([Id] ASC)
);
Problem is that setting delete action as cascade for the column
RelatedCompId in CompartmentRelation table throw the excaption as
Unable to create relationship
'FK_CompartmentRelation_Compartment1'. Introducing FOREIGN KEY
constraint 'FK_CompartmentRelation_Compartment1' on table
'CompartmentRelation' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other
FOREIGN KEY constraints. Could not create constraint or index.
See previous errors.
The basic issue appears to be that deletes from table Compartment (and updates to its PKs) can cascade to CompartmentRelation through two different foreign keys. If you intend to support Compartments being related to themselves, then that's end-of-story for cascading from Compartment to CompartmentRelation -- you can't do it.* If you intend to forbid self relationships then you could try adding a check constraint to CompartmentRelation to enforce that prohibition, though I'm not at all sure that SQL Server will take that into account.
If SQL Server won't accept the cascading deletes then you have at least three options:
Make it an application responsibility to clean up compartment relationships before deleting compartments. (And don't cascade.)
Create triggers to handle relationship deletion when compartments are deleted. (And don't cascade.)
Create a stored procedure for deleting compartments, and make it handle the needed relationship deletions. (And don't cascade.)
Which way should i follow ?
Whichever of those makes the most sense for your application. All have advantages and disadvantages.
Additionally,
Do not cascade updates of surrogate key columns, especially when the key values are machine generated, as all yours are. Those keys should never be updated in the first place, and if an attempt were ever made to update one then it would be better for the DB to reject it, for whatever reason, than to accept it.
You probably don't want to cascade deletions of CompartmentRelationType to ComponentRelation. Including such cascading allows for deleting all the relations of a given type by deleting the type itself, but such a cascade is more likely to be performed mistakenly than intentionally, and if it were performed mistakenly then the resulting data loss would be significant. It's probably better to make the application delete all those relations explicitly if that's what it really means to do, and otherwise to reject deletion of types that are in use by existing relations.
*Technically, you could do it by cascading from only one of the two FKs with Compartment, but it seems unlikely that such a half-measure would serve your purposes.

PSQL ON UPDATE interaction with multi-column foreign key

So, I have been tasked with setting up a new DB to handle server patching. I'm still in the design phase and I've been unable to find something to answer this question so I'm reaching out for help.
I have so far:
CREATE TABLE servers (
name varchar(50) PRIMARY KEY,
patch_id varchar(10) REFERENCES patch_info(id) NOT NULL,
env varchar(5),
...
)
CREATE TABLE patch_info (
id varchar(10) PRIMARY KEY,
patch_time timestamptz NOT NULL,
reboot_time timestamptz NOT NULL,
)
CREATE TABLE patch_data (
id big serial PRIMARY KEY,
name varchar(50),
patch_id varchar(10),
env varchar(5),
FOREIGN KEY (name, patch_id, env) REFERENCES servers (name, patch_id, env) NOT NULL ON UPDATE CASCADE ON DELETE CASCADE
)
My question is what is the behavior if I update the patch_id in the servers table. the patch_info table basically just contains a full list of patch/reboot time identifiers referenced by the id value in other tables. The actual patch_info table itself will only change once a month when the ids are updated to reflect time/dates in the current month, but individual servers frequently change their times on a month to month basis, so when the patch_id field changes in the servers table that change needs to cascade down to the patch_data table.
The other question was about the ON DELETE option. I want the patch_data table to cascade a delete if the actual server.name field is deleted, but not if the patch_id is null or removed. Is this the current behavior of my ON CASCADE ON DELETE statement or can I use the patch_data.name column in two separate foreign keys?
Any help is appreciated, thanks.
First, you will need a unique or primary key constraint on servers (name, patch_id, env), since every foreign key must reference either a primary or a unique key.
ON UPDATE CASCADE will update the patch_id column (there are no "fields" in tables) in patch_data if you update patch_id in servers.
ON DELETE CASCADE will delete rows in patch_data if you delete the referenced row from servers.
You cannot "delete a field". Perhaps you mean "update the column to NULL". If the constraint on servers is a primary key constraint, then you cannot do that, because primary key columns are not nullable. If it is a unique constraint, and you update patch_id to NULL in servers, the referencing rows in patch_data will also have patch_id set to NULL. The consequence of that is that the foreign key relationship is no longer effective, because foreign keys are only enforced for columns that are not NULL.
If that sounds confusing, right you are. Consequently, make sure that all involved columns are defined NOT NULL, then nothing weird can happen.

Efficiently enforcing a 1:1 relationship between two rows with foreign key constraints without creating redundant unique indexes

I have two PostgreSQL tables designed in the following way:
create type content_owner as enum (
'document',
'task'
);
create table content (
id serial not null primary key,
owner content_owner not null,
owner_document_id int references document(id) deferrable initially deferred,
owner_task_id int references task(id) deferrable initially deferred,
-- ...
constraint collab_content_owner_document
check (owner_document_id is null or (owner = 'document' and owner_document_id is not null)),
constraint collab_content_owner_task
check (owner_task_id is null or (owner = 'task' and owner_task_id is not null))
);
create table document (
id serial not null primary key,
content_id int not null references content(id),
-- ...
);
create table task (
id serial not null primary key,
content_id int not null references content(id),
-- ...
);
I want to enforce a 1:1 relationship at the database level for the document<->content relationship and the task<->content relationship.
Adding the following constraints accomplishes that:
alter table collab_content add foreign key (owner_document_id, id) references document (id, content_id) deferrable initially deferred;
alter table collab_content add foreign key (owner_task_id, id) references task (id, content_id) deferrable initially deferred;
alter table document add foreign key (content_id, id) references collab_content (id, owner_document_id);
alter table task add foreign key (content_id, id) references collab_content (id, owner_task_id);
Since I’m saying the ID pair should reference the same ID pair in the other table for both directions. However, this also requires me to create the following indexes:
alter table document add unique (id, content_id);
alter table task add unique (id, content_id);
alter table collab_content add unique (id, owner_document_id);
alter table collab_content add unique (id, owner_task_id);
These indexes feel pretty redundant given that there’s already a primary key on the id columns for these tables. It feels like PostgreSQL should be smart enough to be able to use the existing primary key constraint to make sure the foreign key constraints are met. Ideally I wouldn’t create a second, redundant, index on these tables for the purpose of these foreign key constraints.
Is there a way for me to avoid creating new unique indexes and instead tell PostgreSQL to only lookup the unique ID when resolving the foreign key?
Will PostgreSQL detect that these unique indexes are redundant (because the first column is the primary key) and not materialize a new index on disk for their purpose?
Is there a better way to enforce this constraint?
Two-way linking like this is a recipe for headaches. I recommend avoiding reference cycles if you can. In your case, the simplest way to store this information is to relax the constraint that there cannot be a content without a document or a task. Ask yourself, how might such a situation occur, how else could it be avoided, and what damage might it cause if it happens?
If we can remove that constraint, then we can have a very simple structure where document and task each have a content_id foreign key, and a unique index on it to ensure that no two documents have the same content.
If we can't remove that constraint, then the answers to your questions are:
There is no way to avoid creating those new unique indexes for the foreign keys. Foreign keys must have matching unique indexes.
Postgres will not detect that these indexes are redundant, and they will indeed be materialized and take up space.

Referential integrity over table portion

I'm designing a database and can't figure out how to model referential integrity.
I have the following tables
CREATE TABLE Groups
(
GroupId INT PRIMARY KEY,
GroupName VARCHAR(50)
)
CREATE TABLE GroupMembers
(
GroupId INT NOT NULL,
MemberId INT NOT NULL,
MemberName VARCHAR(50),
CONSTRAINT pk_GroupMember PRIMARY KEY (GroupId, MemberId)
)
CREATE TABLE Missions
(
MissionId INT PRIMARY KEY,
GroupId INT NOT NULL,
MissionName VARCHAR(50)
)
CREATE TABLE MissionRollAssignments
(
MissionId INT NOT NULL,
MemberId INT NOT NULL,
MemberRoll VARCHAR(50) --This will probably become RollId and move details to another table
)
Every mission will have assignments for some/all members of the corresponding group. There will be several missions associated with each group, but only one mission per group is active at a given time.
My question is:
Is it possible to enforce referenciay integrity for roll assignments such that only members
of the corresponding group (given by the MissionId) are selected? I know I can filter this from the GUI, but I'd feel more comfortable if I could create a FK constraint from MissionRollAssignments to GroupMembers while considering the GroupId indicated in the Mission.
A second question would be if you guys think this is a good way to model my domain, or maybe I should try a different approach.
Thanks in advance for any help on this.
Best regards,
Awer
You could put GroupId into MissionRollAssignments and then add two constraints as follows:
ALTER TABLE MissionRollAssignments
ADD CONSTRAINT fk1 FOREIGN KEY (GroupId, Memberid)
REFERENCES GroupMembers (GroupId, Memberid);
ALTER TABLE MissionRollAssignments
ADD CONSTRAINT fk2 FOREIGN KEY (GroupId, MissionId)
REFERENCES Missions (GroupId, MissionId);
To achieve this SQL Server first requires a (redundant) UNIQUE constraint on (GroupId, MissionId) in the Missions table. Other DBMSs are not so strict but SQL Server requires a FOREIGN KEY constraint to match exactly the columns of a uniqueness constraint.
You should use Foreign Keys to reinforce this, eg Mission.GroupId should refer to Group.GroupId.
Is it possible to enforce referenciay integrity for roll assignments such that only members of the corresponding group (given by the MissionId) are selected?
Yes. You need to use identifying relationships to propagate the GroupId all the way down to the bottom of this "diamond-shaped" dependency, similar to this:
Note FK1 and FK2 in front of MissionRollAssignment.GroupId, indicating that foreign keys exist up the both "sides" of this "diamond-shaped" dependency.
As single active mission can be modeled as a foreign key in the opposite direction, in this case as Group {GroupId, ActiveMissionNo} that references the Mission primary key.
Such circular foreign key presents a "chicken-and-egg" problem on a DBMS that doesn't support deferred constraints (which SQL Server doesn't). However, you can just leave Group.ActiveMissionNo NULL-able, so a DBMS that enforces foreign keys in a MATCH SIMPLE fashion (which SQL Server does) will ignore the whole composite foreign key if just one of its fields is NULL. This will allow you to temporarily "disable" the foreign key and break the "chicken-and-egg" cycle when inserting new data.

SQL: Can I write a CHECK constraint that validates data in a foreign table using a foreign key?

I am designing a test database in SQL Server 2008 R2 and/or SQL Azure. (All of my code will run on both, so far.)
I have a table with a foreign key, and I need to add a constraint that references a field in the foreign table.
Normally I would have the foreign table manage it's own validation checks, but there are cases where that is impossible (or illogical). I've provided some sample-code that displays what I am trying to accomplish.
CREATE TABLE CustomerOrder
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Customer(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
ProductID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Product(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
Quantity INT NOT NULL DEFAULT 1,
IsPaid BIT NOT NULL DEFAULT 0
)
GO
CREATE TABLE RMA
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerOrderID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES CustomerOrder(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
-- Add constraint to prevent RMAs from being
-- created for orders that have not been paid.
-- This could be a column constraint, or a table constraint.
CHECK ( CustomerOrderID.IsPaid = 1 )
-- ERROR: 'The multi-part identifier "CustomerOrderID.IsPaid"
-- could not be bound.'
)
GO
In this example, it doesn't make sense to put the CHECK constraint in the CustomerOrder table, because a row in the CustomerOrder table is perfectly happy being unpaid as long as there are no RMAs for the order. Furthermore, a constraint in the CustomerOrder table would still need to reference the RMA table to confirm whether there is an RMA, so the same issue remains.
I've also tried:
CHECK (EXISTS(SELECT co.ID FROM CustomerOrder co
WHERE co.ID=CustomerOrderID AND
co.IsPaid=1))
-- ERROR: 'Subqueries are not allowed in this context.
-- Only scalar expressions are allowed.'
Since this is a static constraint for basic data validation, and will never be referenced by any other object, I'd like to avoid making this into a scalar function or stored procedure.
However, in order to avoid using a scalar function, I will need to define the constraint within SQL (preferrably at the same time my database is deployed and the table is created).
What SQL syntax could I use here to define this type of constraint?
Another option (since you asked for a solution without adding triggers or enlarging the foreign key), is removing the IsPaid column and adding another table for paid orders:
CREATE TABLE CustomerOrder
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Customer(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
ProductID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Product(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
Quantity INT NOT NULL DEFAULT 1
)
GO
CREATE TABLE CustomerOrderPaid
(
ID INT NOT NULL PRIMARY KEY
FOREIGN KEY REFERENCES CustomerOrder(ID)
ON DELETE NO ACTION ON UPDATE CASCADE
)
GO
CREATE TABLE RMA
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerOrderID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES CustomerOrderPaid(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
)
GO