MS SQL Server cross table constraint
You can create an indexed view based on the Relation
and Screen
tables and apply a unique constraint there.
create view DRI_UniqueScreens
with SCHEMABINDING
as
select r.AppId,s.Name
from
[Schema].Relation r
inner join
[Schema].Screen s
on
r.ScreenId = s.ScreenId
GO
CREATE UNIQUE CLUSTERED INDEX IX_DRI_UniqueScreens
on DRI_UniqueScreens (AppId,Name)
Cross Table Dependency/Constraint in SQL Database
TL;DR
But the dependency is in a separate table.
You mean there is a dependency (in the everyday sense) on another table. We say there is a constraint on the two tables. (They depend on each other.) In addition to the FK (foreign key) constraint that every students
classes
value is a classes
class
value.
What is this dependency called?
We can reasonably categorize the constraint as "inter-table". It is that classes
equals SELECT class, SUM(student) AS total FROM classes LEFT JOIN students USING (class) GROUP BY class
.
And can we say this is violating 3NF?
The constraint doesn't involve violating a NF. Moreover normalization applies only to a single table and its FDs (functional dependencies).
(A straightforward design is to have base students
, base classes1
that is the original classes
without total
, and VIEW classes AS SELECT class, SUM(student) AS total FROM classes1 LEFT JOIN students USING (class) GROUP BY class
.)
If I had a column in
classes
that stored the total number of students a class has, this feels like it should violate 3NF.
Whether a table is in a given NF (normal form) has nothing to do with any other tables. (We say a database is in a given NF when all its tables are.) Whether your design is nevertheless bad is another matter.
Since a class has just one total number of students, there is a FD (functional dependency) of total
on class
in classes
, ie class
functionally determines total
.
We say that a set of columns functionally determines another set in a table when each subrow for the first always appears with the same subrow for the second. Normalization to higher NFs replaces a table by projections of it that join back ot it, per the FDs & JDs (join dependencies) that hold in it. There is redundancy in a database when two tables say the same thing about the business/application situation; but not all redundancy is bad. Learn proper information modeling & database design.
It may or may not violate a NF to have your class student count as a column in classes
. What FDs violate a NF depends on all the FDs present and the NF. (And it only make sense to talk about a particular FD in a particular table violating a particular NF if you are talking about a particular part of a particular definition of that NF.)
(If a DBMS-calculated/computed/generated column violates a NF that would hold without it then that is not a problem, because it is controlled by the DBMS. You can think of the table as view of the table without the column.)
But the dependency is in a separate table.
When a sequence of database states cannot hold all the values possible per the the columns of tables we say constraints hold or the database is constrained. FDs (functional dependencies), MVDs (multi-valued dependencies), JDs (join dependencies), INDs (inclusion dependencies), EQDs (equality dependencies) and other "dependencies" (which technically are expressions given a context) are each associated with certain constraints. CKs (candidate keys), PKs (primary keys), superkeys (SQL PK
& UNIQUE NOT NULL
), FKs (foreign keys) (which technically are all column sets) & other notions are also each associated with certain constraints. But arbitrary conditions can hold on a sequence of database states.
SQL has a distinct but related notion of a constraint
characterized by a name and an expression/condition (constraint in the above sense), declared by appropriate syntax. A state is constrained by column typing, PK
, UNIQUE
, NOT NULL
& CHECK
constraints. ASSERTION
gives an arbitrary condition on a state but it is not supported by most DBMSs. CASCADES
supports some inter-state inter-table constraints. SQL TRIGGER
s enforce arbitrary constraints. Indexes also enforce constraints in a DBMS-specific way.
Because in some sense it has all the problems of a 3NF violation.
Your edits improved your question. Using the wrong words or using words in the wrong way at best states something that is not what we mean. But when what we write doesn't make sense it suggests that our problem, whatever else it involves, involves not knowing what the words mean. Forcing ourselves to use words correctly allows others to know what we really mean. Eg here maybe "... in the join of tables ... there would be a 3NF-violating FD ...". Even by explicitly saying that we are unsure we can communicate some of our vague groping without saying something that we don't mean. Eg your "this feels like ...". But it also leads us to clearly organize what we are faced with. This helps not only the problem we are working on but improves our problem solving.
How to enforce a CHECK constraint across multiple tables
You can create triggers on Breedings table to check this rule. Trigger is a special stored procedure which executed automatically on INSERT\UPDATE\DELETE on some table. So you can write a trigger that checks all new rows inserted in Breedings and if there is a row where Date is less then appropriate BirthDate, throw error. Same for UPDATE, if Date column is altered, check appropriate animal's BirthDate and throw error accordingly. DELETEs are safe in this matter.
CHECKs are not that good for rules that involve other tables. General suggestion is to use them only for basic checks inside one table.
LATER EDIT
Try this trigger body
...
BEGIN
if exists
(
SELECT 1
FROM inserted i
join Animals.Females f
on i.DataID = f.DataID
join Animals.Master m
on f.DataID = m.DataID
WHERE
m.BirthDate > i.Date
)
RAISERROR("Trigger iCheckBreedingDateAfterBirthDate - Breedings.Date is wrong", 18, 0)
END
GO
Difficult Temporal Cross-Table Database Constraint
Don't complicate it, I'd go with triggers. There is no shame in using them, this is what they are there for.
To avoid lots of logic in the triggers, I add an "Editable" bit column into the header table, then basically use a divide with Editable to either work or cause a divide by zero error, which I CATCH and convert to a Invoice is not editable, no changes permitted
message. There are no EXISTS used to eliminate extra overhead. Try this:
CREATE TABLE testInvoices
(
InvoiceID INT not null IDENTITY(1,1) PRIMARY KEY
,Editable bit not null default (1) --1=can edit, 0=can not edit
,yourData char(2) not null default ('xx')
)
go
CREATE TABLE TestFees
(
FeeID INT IDENTITY(1,1) PRIMARY KEY
,InvoiceID INT REFERENCES testInvoices(InvoiceID)
,Amount MONEY
)
go
CREATE TRIGGER trigger_testInvoices_instead_update
ON testInvoices
INSTEAD OF UPDATE
AS
BEGIN TRY
--cause failure on updates when the invoice is not editable
UPDATE t
SET Editable =i.Editable
,yourData =i.yourData
FROM testInvoices t
INNER JOIN INSERTED i ON t.InvoiceID=i.InvoiceID
WHERE 1=CONVERT(int,t.Editable)/t.Editable --div by zero when not editable
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_testInvoices_instead_delete
ON testInvoices
INSTEAD OF DELETE
AS
BEGIN TRY
--cause failure on deletes when the invoice is not editable
DELETE t
FROM testInvoices t
INNER JOIN DELETED d ON t.InvoiceID=d.InvoiceID
WHERE 1=CONVERT(int,t.Editable)/t.Editable --div by zero when not editable
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_TestFees_instead_insert
ON TestFees
INSTEAD OF INSERT
AS
BEGIN TRY
--cause failure on inserts when the invoice is not editable
INSERT INTO TestFees
(InvoiceID,Amount)
SELECT
f.InvoiceID,f.Amount/i.Editable --div by zero when invoice is not editable
FROM INSERTED f
INNER JOIN testInvoices i ON f.InvoiceID=i.invoiceID
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_TestFees_instead_update
ON TestFees
INSTEAD OF UPDATE
AS
BEGIN TRY
--cause failure on updates when the invoice is not editable
UPDATE f
SET InvoiceID =ff.InvoiceID
,Amount =ff.Amount/i.Editable --div by zero when invoice is not editable
FROM TestFees f
INNER JOIN INSERTED ff ON f.FeeID=ff.FeeID
INNER JOIN testInvoices i ON f.InvoiceID=i.invoiceID
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_TestFees_instead_delete
ON TestFees
INSTEAD OF DELETE
AS
BEGIN TRY
--cause failure on deletes when the invoice is not editable
DELETE f
FROM TestFees f
INNER JOIN DELETED ff ON f.FeeID=ff.FeeID
INNER JOIN testInvoices i ON f.InvoiceID=i.invoiceID AND 1=CONVERT(int,i.Editable)/i.Editable --div by zero when invoice is not editable
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
here is a simple test script to test out the different combinations:
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (1,111) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (1,1111) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,22) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,222) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,2222) --works
update testInvoices set Editable=0 where invoiceid=3 --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (3,333) --error<<<<<<<
UPDATE TestFees SET Amount=1 where feeID=1 --works
UPDATE testInvoices set Editable=0 where invoiceid=1 --works
UPDATE TestFees SET Amount=11111 where feeID=1 --error<<<<<<<
UPDATE testInvoices set Editable=1 where invoiceid=1 --error<<<<<<<
UPDATE testInvoices set Editable=0 where invoiceid=2 --works
DELETE TestFees WHERE invoiceid=2 --error<<<<<
DELETE FROM testInvoices where invoiceid=2 --error<<<<<
UPDATE testInvoices SET Editable='A' where invoiceid=1 --error<<<<<<< Msg 245, Level 16, State 1, Line 1 Conversion failed when converting the varchar value 'A' to data type bit.
Using unique index for cross table constraint
No, you can't use a unique index to enforce this sort of rule.
You could try to enforce this sort of rule via triggers but that tends to get rather difficult in a multiuser environment. You'd need to do things like lock the department
row to ensure that only one session can be modifying employee
information for a particular department
at a time which tends to introduce significant scalability issues. It also tends to involve quite a bit of code to handle all the potential cases.
Cross table constraints in PostgreSQL
Clarifications
The formulation of this requirement leaves room for interpretation:
where UserRole.role_name
contains an employee role name.
My interpretation:
with an entry in UserRole
that has role_name = 'employee'
.
Your naming convention is was problematic (updated now). User
is a reserved word in standard SQL and Postgres. It's illegal as identifier unless double-quoted - which would be ill-advised. User legal names so you don't have to double-quote.
I am using trouble-free identifiers in my implementation.
The problem
FOREIGN KEY
and CHECK
constraint are the proven, air-tight tools to enforce relational integrity. Triggers are powerful, useful and versatile features but more sophisticated, less strict and with more room for design errors and corner cases.
Your case is difficult because a FK constraint seems impossible at first: it requires a PRIMARY KEY
or UNIQUE
constraint to reference - neither allows NULL values. There are no partial FK constraints, the only escape from strict referential integrity are NULL values in the referencing columns due to the default MATCH SIMPLE
behavior of FK constraints. Per documentation:
MATCH SIMPLE
allows any of the foreign key columns to be null; if any
of them are null, the row is not required to have a match in the referenced table.
Related answer on dba.SE with more:
- Two-column foreign key constraint only when third column is NOT NULL
The workaround is to introduce a boolean flag is_employee
to mark employees on both sides, defined NOT NULL
in users
, but allowed to be NULL
in user_role
:
Solution
This enforces your requirements exactly, while keeping noise and overhead to a minimum:
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
That's all.
These triggers are optional but recommended for convenience to set the added tags is_employee
automatically and you don't have to do anything extra:
-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
Again, no-nonsense, optimized and only called when needed.
SQL Fiddle demo for Postgres 9.3. Should work with Postgres 9.1+.
Major points
Now, if we want to set
user_role.role_name = 'employee'
, then there has to be a matchinguser.employee_nr
first.You can still add an
employee_nr
to any user, and you can (then) still tag anyuser_role
withis_employee
, irregardless of the actualrole_name
. Easy to disallow if you need to, but this implementation does not introduce any more restrictions than required.users.is_employee
can only betrue
orfalse
and is forced to reflect the existence of anemployee_nr
by theCHECK
constraint. The trigger keeps the column in sync automatically. You could allowfalse
additionally for other purposes with only minor updates to the design.The rules for
user_role.is_employee
are slightly different: it must be true ifrole_name = 'employee'
. Enforced by aCHECK
constraint and set automatically by the trigger again. But it's allowed to changerole_name
to something else and still keepis_employee
. Nobody said a user with anemployee_nr
is required to have an according entry inuser_role
, just the other way round! Again, easy to enforce additionally if needed.If there are other triggers that could interfere, consider this:
How To Avoid Looping Trigger Calls In PostgreSQL 9.2.1
But we need not worry that rules might be violated because the above triggers are only for convenience. The rules per se are enforce withCHECK
and FK constraints, which allow no exceptions.Aside: I put the column
is_employee
first in the constraintUNIQUE (is_employee, users_id)
for a reason.users_id
is already covered in the PK, so it can take second place here:
DB associative entities and indexing
SQL Unique constraint across multiple tables
Add a 4th table specifically for these values you want to be unique then link these keys from this table into the others using a one to many relationship.
For example you will have the unique table with an ID, AppName and ItemName to make up its 3 columns. Then have this table link to the others.
For how to do this here is a good example
Create a one to many relationship using SQL Server
EDIT: This is what I would do but considering your server needs you can change what is needed:
CREATE TABLE AllItems(
[id] [int] IDENTITY(1,1) NOT NULL,
[itemType] [int] NOT NULL,
[AppName] [nvarchar](20) NOT NULL,
[ItemName] [nvarchar](32) NOT NULL,
CONSTRAINT [pk_AllItems] PRIMARY KEY CLUSTERED ( [id] ASC )
) ON [PRIMARY]
CREATE TABLE Analog(
[itemId] [int] NOT NULL,
[Value] [float] NOT NULL
)
CREATE TABLE Discrete(
[itemId] [int] NOT NULL,
[Value] [bit] NOT NULL
)
CREATE TABLE Message(
[itemId] [bigint] NOT NULL,
[Value] [nvarchar](256) NOT NULL
)
ALTER TABLE [Analog] WITH CHECK
ADD CONSTRAINT [FK_Analog_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Analog] CHECK CONSTRAINT [FK_Analog_AllItems]
GO
ALTER TABLE [Discrete] WITH CHECK
ADD CONSTRAINT [FK_Discrete_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Discrete] CHECK CONSTRAINT [FK_Discrete_AllItems]
GO
ALTER TABLE [Message] WITH CHECK
ADD CONSTRAINT [FK_Message_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Message] CHECK CONSTRAINT [FK_Message_AllItems]
GO
From what I can tell your syntax is fine, I simply changed it to this way simply because I am more familiar with it but either should work.
Related Topics
Create SQL Server Job Automatically
Caculate Point 50 Miles Away (North, 45% Ne, 45% Sw)
There Is Already an Object Named '#Result' in the Database
Copy Multiple Records with a Master-Details Relationship
How to Make This SQL More Efficient
See SQL from Entityframework with Collection-Like Queries
Oracle: Is There Any Logical Reason Not to Use Parallel Execution with Subqueries in the Select List
Splitting Variable Length Delimited String Across Multiple Rows (Sql)
Generate a Sequential Number (Per Group) When Adding a Row to an Access Table
Where Clause Using Values That Could Be Null
Tsql Datediff to Return Number of Days with 2 Decimal Places
Postgres Drop Table Syntax Error
Find Top 10 Latest Record for Each Buyer_Id for Yesterday's Date
Convert a String to a Date in Access
Insert Record to SQL Table with Identity Column
Differencebetween Prepared Statements and SQL or Pl/Pgsql Functions, in Terms of Their Purpose
Any Performance Impact in Oracle for Using Like 'String' VS = 'String'