Composite Keys and Referential Integrity in T-SQL - sql

Is it possible, in T-SQL, to have a relationship table with a composite key composed of 1 column defining Table Type and another column defining the Id of a row from a table referenced in the Table Type column?
For a shared-email address example:Three different user tables (UserA, UserB, UserC)One UserType Table (UserType)One Email Table (EmailAddress)One Email-User Relationship Table (EmailRelationship)The EmailRelationship Table contains three columns, EmailId, UserTypeId and UserId
Can I have a relationship from each User table to the EmailRelationship table (or some other way?) to maintain referential integrity?
I've tried making all three columns in the EmailRelationship table into primary keys, I've tried making only UserTypeId and UserId primary.
CREATE TABLE [dbo].[UserType](
[Id] [int] IDENTITY(1,1) NOT NULL ,
[Type] [varchar](50) NOT NULL)
insert into [dbo].[UserType]
([Type])
values
('A'),('B'),('C')
CREATE TABLE [dbo].[UserA](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserTypeId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL)
insert into [dbo].[UserA]
(UserTypeId,Name)
values
(1,'UserA')
CREATE TABLE [dbo].[UserB](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserTypeId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL)
insert into [dbo].[UserB]
(UserTypeId,Name)
values
(2,'UserB')
CREATE TABLE [dbo].[UserC](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserTypeId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL)
insert into [dbo].[UserC]
(UserTypeId,Name)
values
(3,'UserC')
CREATE TABLE [dbo].[Email](
[Id] [int] IDENTITY(1,1) NOT NULL,
[EmailAddress] [varchar](50) NOT NULL)
insert into [dbo].[email]
(EmailAddress)
values
('SharedEmail#SharedEmail.com')
CREATE TABLE [dbo].[EmailRelationship](
[EmailId] [int] NOT NULL,
[UserTypeId] [int] NOT NULL,
[UserId] [int] NOT NULL)
insert into [dbo].[EmailRelationship]
(EmailId, UserTypeId, UserId)
values
(1,1,1),(1,2,1),(1,3,1)

No there isn't, a foreign key can refer to one table, and one table only, I can think of three ways you could approach this.
The first is to have 3 columns, one for each user table, each column with a foreign key, and a check constraint to check that at one, and only one of the values is not null
CREATE TABLE dbo.EmailRelationship
(
EmailId INT NOT NULL,
UserTypeId INT NOT NULL,
UserAId INT NULL,
UserBId INT NULL,
UserCId INT NULL,
CONSTRAINT FK_EmailRelationship__UserAID FOREIGN KEY (UserAId)
REFERENCES dbo.UserA (Id),
CONSTRAINT FK_EmailRelationship__UserBID FOREIGN KEY (UserBId)
REFERENCES dbo.UserB (Id),
CONSTRAINT FK_EmailRelationship__UserCID FOREIGN KEY (UserCId)
REFERENCES dbo.UserC (Id),
CONSTRAINT CK_EmailRelationship__ValidUserId CHECK
(CASE WHEN UserTypeID = 1 AND UserAId IS NOT NULL AND ISNULL(UserBId, UserCId) IS NULL THEN 1
WHEN UserTypeID = 2 AND UserBId IS NOT NULL AND ISNULL(UserAId, UserCId) IS NULL THEN 1
WHEN UserTypeID = 3 AND UserCId IS NOT NULL AND ISNULL(UserAId, UserBId) IS NULL THEN 1
ELSE 0
END = 1)
);
Then as a quick example trying to insert a UserAId with a user Type ID of 2 gives you an error:
INSERT EmailRelationship (EmailID, UserTypeID, UserAId)
VALUES (1, 1, 1);
The INSERT statement conflicted with the CHECK constraint "CK_EmailRelationship__ValidUserId".
The second approach is to just have a single user table, and store user type against it, along with any other common attributes
CREATE TABLE dbo.[User]
(
Id INT IDENTITY(1, 1) NOT NULL,
UserTypeID INT NOT NULL,
Name VARCHAR(50) NOT NULL,
CONSTRAINT PK_User__UserID PRIMARY KEY (Id),
CONSTRAINT FK_User__UserTypeID FOREIGN KEY (UserTypeID) REFERENCES dbo.UserType (UserTypeID),
CONSTRAINT UQ_User__Id_UserTypeID UNIQUE (Id, UserTypeID)
);
-- NOTE THE UNIQUE CONSTRAINT, THIS WILL BE USED LATER
Then you can just use a normal foreign key constraint on your email relationship table:
CREATE TABLE dbo.EmailRelationship
(
EmailId INT NOT NULL,
UserId INT NOT NULL,
CONSTRAINT PK_EmailRelationship PRIMARY KEY (EmailID),
CONSTRAINT FK_EmailRelationship__EmailId
FOREIGN KEY (EmailID) REFERENCES dbo.Email (Id),
CONSTRAINT FK_EmailRelationship__UserId
FOREIGN KEY (UserId) REFERENCES dbo.[User] (Id)
);
It is then no longer necessary to store UserTypeId against the email relationship because you can join back to User to get this.
Then, if for whatever reason you do need specific tables for different user types (this is not unheard of), you can create these tables, and enforce referential integrity to the user table:
CREATE TABLE dbo.UserA
(
UserID INT NOT NULL,
UserTypeID AS 1 PERSISTED,
SomeOtherCol VARCHAR(50),
CONSTRAINT PK_UserA__UserID PRIMARY KEY (UserID),
CONSTRAINT FK_UserA__UserID_UserTypeID FOREIGN KEY (UserID, UserTypeID)
REFERENCES dbo.[User] (Id, UserTypeID)
);
The foreign key from UserID and the computed column UserTypeID back to the User table, ensures that you can only enter users in this table where the UserTypeID is 1.
A third option is just to have a separate junction table for each User table:
CREATE TABLE dbo.UserAEmailRelationship
(
EmailId INT NOT NULL,
UserAId INT NOT NULL,
CONSTRAINT PK_UserAEmailRelationship PRIMARY KEY (EmailId, UserAId),
CONSTRAINT FK_UserAEmailRelationship__EmailId FOREIGN KEY (EmailId)
REFERENCES dbo.Email (Id),
CONSTRAINT FK_UserAEmailRelationship__UserAId FOREIGN KEY (UserAId)
REFERENCES dbo.UserA (Id)
);
CREATE TABLE dbo.UserBEmailRelationship
(
EmailId INT NOT NULL,
UserBId INT NOT NULL,
CONSTRAINT PK_UserBEmailRelationship PRIMARY KEY (EmailId, UserBId),
CONSTRAINT FK_UserBEmailRelationship__EmailId FOREIGN KEY (EmailId)
REFERENCES dbo.Email (Id),
CONSTRAINT FK_UserBEmailRelationship__UserBId FOREIGN KEY (UserBId)
REFERENCES dbo.UserB (Id)
);
Each approach has it's merits and drawbacks, so you would need to assess what is best for your scenario.

No it does not work that way. You cannot use a column value as a dynamic reference to different tables.
In general the data design is flawed.

Thanks to #GarethD I created a CHECK constraint that called a scalar-function that would enforce referential integrity (only upon insert, refer to caveat below):
Using my above example:
alter FUNCTION [dbo].[UserTableConstraint](#Id int, #UserTypeId int)
RETURNS int
AS
BEGIN
IF EXISTS (SELECT Id From [dbo].[UserA] WHERE Id = #Id and UserTypeId = #UserTypeId)
return 1
ELSE IF EXISTS (SELECT Id From [dbo].[UserB] WHERE Id = #Id and UserTypeId = #UserTypeId)
return 1
ELSE IF EXISTS (SELECT Id From [dbo].[UserC] WHERE Id = #Id and UserTypeId = #UserTypeId)
return 1
return 0
end;
alter table [dbo].[emailrelationship]
--drop constraint CK_UserType
with CHECK add constraint CK_UserType
CHECK([dbo].[UserTableConstraint](UserId,UserTypeId) = 1)
I am sure there is a not insignificant overhead to a Scalar-function call from within a CONSTRAINT. If the above becomes prohibitive I will report back here, though the tables in question will not have to deal with a large volume of INSERTs.
If there are any other reasons to not do the above, I would like to hear them. Thanks!
Update:
I've tested INSERT and UPDATE with 100k rows (SQL Server 2014, 2.1ghz quadcore w/ 8gb ram):
INSERT takes 2 seconds with out the CONSTRAINT
and 3 seconds with the CHECK CONSTRAINT
Turning on IO and TIME STATISTICS causes the INSERT tests to run in:
1.7 seconds with out the CONSTRAINT
and 10 seconds with the CHECK CONSTRAINT
I left the STATISTICS on for the UPDATE 100k rows test:
just over 1sec with out the CONSTRAINT
and 1.5sec with the CHECK CONSTRAINT
My referenced tables (UserA, UserB, UserC from my example) only contain around 10k rows each, so anybody else looking to implement the above may want to run some additional testing, especially if your referenced tables contain millions of rows.
Caveat:
The above solution may not be suitable for most uses, as the only time referential integrity is checked is during the CHECK CONSTRAINT upon INSERT. Any other operations or modifications of the data needs to take that into account. For example, using the above, if an Email is deleted any related EmailRelationship entries will be pointing to invalid data.

Related

How to use constraints to force two child items be from the same parent?

I have a Jobs table that holds jobs.
I have a Tasks table that holds tasks that belong to a job (1:many).
I have a Task_Relationships table that holds the data about which tasks depend on other tasks within a job.
I have 2 jobs, each job has 3 tasks and within the jobs the tasks are related as in the diagram. The Task_Relationships table is to represent that tasks within a job have dependencies between them.
How to ensure that when I add an entry to the Task_Relationships table say (1,2) representing the fact that task 1 is related to task 2, that tasks 1 and 2 are in the same job? I'm trying to enforce this through keys and not through code.
drop table if exists dbo.test_jobs
create table dbo.test_jobs (
[Id] int identity(1,1) primary key not null,
[Name] varchar(128) not null
)
drop table if exists dbo.test_tasks
create table dbo.test_tasks (
[Id] int identity(1,1) primary key not null,
[Job_Id] int not null,
[Name] varchar(128) not null
constraint fk_jobs foreign key ([Id]) references dbo.test_jobs(Id)
)
drop table if exists dbo.test_task_relationships
create table dbo.test_task_relationships (
[Id] int identity(1,1) not null,
[From_Task] int not null,
[To_Task] int not null
constraint fk_tasks_from foreign key ([From_Task]) references dbo.test_tasks(Id),
constraint fk_tasks_to foreign key ([To_Task]) references dbo.test_tasks(Id)
)
A reliance on identity columns as primary keys is not helping you here. And it is a logic fault to use an identity column in the relationship table IMO. Surely you do not intend to allow multiple rows to exist in that table with the same values for <from_task, to_task>.
Imagine the child table defined as:
create table dbo.test_tasks (
Job_Id int not null,
Task_Id tinyint not null,
Name varchar(128) not null,
constraint pk_tasks primary key clustered (Job_Id, Task_Id),
constraint fk_jobs foreign key ([Job_Id]) references dbo.test_jobs(Id)
);
Now your relationship table can be transformed into:
create table dbo.test_task_relationships (
From_Job int not null,
From_Task tinyint not null,
To_Job int not null,
To_Task tinyint not null
);
I'll leave it to you to complete the DDL but that should make your goal trivial.
You can declare a superkey in the Task table that includes the Job_Id column as well as columns from an existing key.
create table dbo.test_tasks (
[Id] int identity(1,1) primary key not null,
[Job_Id] int not null,
[Name] varchar(128) not null
constraint fk_jobs foreign key ([Id]) references dbo.test_jobs(Id),
constraint UQ_Tasks_WithJob UNIQUE (Id, Job_Id)
)
You can then add the Job_Id column to the relationships table and include it in both foreign key constraints:
create table dbo.test_task_relationships (
[Id] int identity(1,1) not null,
[From_Task] int not null,
Job_Id int not null,
[To_Task] int not null
constraint fk_tasks_from foreign key ([From_Task], Job_Id) references dbo.test_tasks(Id, Job_Id),
constraint fk_tasks_to foreign key ([To_Task], Job_Id) references dbo.test_tasks(Id, Job_Id)
)
There is now no way for the table to contain mismatched tasks. If necessary, wrap this table in a view/trigger if you don't want to expose the presence of the job_id column to applications and to automatically populate it during insert.

Using SQL, would it be possible to retrieve a field from a different table (using a stored procedure) using a foreign key referencing that table?

I have a table Film with the primary key FilmID, and a column called CoverImage. In a separate table History, I inserted FilmID as a foreign key. I wrote a stored procedure select_CoverImage_By_FilmID to retrieve an image by its FilmID. What would FROM need to reference in the stored procedure?
CREATE TABLE [dbo].[Film]
(
[FilmID] [INT] IDENTITY(1000000, 1) NOT NULL,
[CoverImage] [NVARCHAR](200) NOT NULL,
CONSTRAINT [pk_FilmID] PRIMARY KEY([FilmID] ASC)
)
GO
INSERT INTO [dbo].[Film] ([CoverImage], [FilmID])
VALUES ('\Notflix\Notflix_DB\CoverImages\SOTD-C.jpg', 1)
GO
CREATE TABLE [dbo].[ViewingHistory]
(
[UserAccountID] [INT] NOT NULL,
[FilmID] [INT] NOT NULL,
CONSTRAINT [pk_UserAccountID_] PRIMARY KEY ([UserAccountID] ASC),
CONSTRAINT [fk_Film#_FilmID]
FOREIGN KEY([FilmID]) REFERENCES [Film](FilmID)
ON UPDATE CASCADE
)
GO
INSERT INTO [dbo].[ViewingHistory] ([FilmID])
VALUES (1)
GO
CREATE PROCEDURE [select_CoverImage_By_FilmID]
(#FilmID [INT])
AS
BEGIN
SELECT
[CoverImage],
[FilmID]
FROM
[Film]
WHERE
[FilmID] = #FilmID
END;

CONSTRAINT to foreign keys to one table - causes error

I am starting to build something like system up to the just cooperate with suggestions coming into the database called ForslagOpslag.
Every time I try to update table with likes, you will see it with this one error:
Update cannot proceed due to validation errors. Please correct the following errors and try again.
SQL71516 :: The referenced table '[dbo].[ForslagOpslag]' contains no
primary or candidate keys that match the referencing column list in
the foreign key. If the referenced column is a computed column, it
should be persisted.
Here is how I built my ForslagOpslagLikes table:
CREATE TABLE [dbo].[ForslagOpslagLikes]
(
[fk_brugerid] INT NOT NULL,
[opretdato] DATETIME NOT NULL,
[getid] INT NOT NULL,
CONSTRAINT [PK_ForslagOpslagLikes]
PRIMARY KEY CLUSTERED ([fk_brugerid], [getid]),
CONSTRAINT [FK_ForslagOpslagLikes_ToGetid]
FOREIGN KEY ([getid])
REFERENCES [dbo].[ForslagOpslag]([Id]),
CONSTRAINT [FK_ForslagOpslagLikes_ToForslagBrugerid]
FOREIGN KEY ([fk_brugerid])
REFERENCES [dbo].[ForslagOpslag]([fk_brugerid])
);
Reason I have both fk_brugerid and getid is for sure me that the user can not vote / like more once!
The way I have built my ForslagOpslag table:
CREATE TABLE [dbo].[ForslagOpslag]
(
[Id] INT IDENTITY (1, 1) NOT NULL,
[text] NVARCHAR (MAX) NOT NULL,
[fk_brugerid] INT NOT NULL,
[opretdato] DATETIME NOT NULL,
PRIMARY KEY CLUSTERED ([Id] ASC)
);
like this to be my like system do:
ForslagOpslagLikes -> fk_brugerid to ForslagOpslag -> fk_brugerid
ForslagOpslagLikes -> getid to ForslagOpslag -> id
Well - the error seems pretty clear: you're trying to estabslish a foreign key relationship to ForslagOpslag.fk_brugerid here:
CONSTRAINT [FK_ForslagOpslagLikes_ToForslagBrugerid]
FOREIGN KEY ([fk_brugerid])
REFERENCES [dbo].[ForslagOpslag]([fk_brugerid])
but that column is NOT the primary key of that other table - and it's not a UNIQUE constraint either - so you cannot reference that column in a foreign key relationship.
But the column(s) that a foreign key references must be the primary key of that other table - or in SQL Server, it's good enough if there's a UNIQUE constraint on that column. You must ensure that the values you reference in FroslagOpslag only match a single column in that table - otherwise, you cannot establish a foreign key relationship
Try to remove the foreing key in your first table like this
CREATE TABLE [dbo].[ForslagOpslagLikes]
(
[fk_brugerid] INT NOT NULL,
[opretdato] DATETIME NOT NULL,
[getid] INT NOT NULL,
CONSTRAINT [PK_ForslagOpslagLikes]
PRIMARY KEY CLUSTERED ([fk_brugerid], [getid]),
CONSTRAINT [FK_ForslagOpslagLikes_ToGetid]
FOREIGN KEY ([getid])
REFERENCES [dbo].[ForslagOpslag]([Id]),
);
Then you need to add the foreign key in the second table with the primary key with the first table
CREATE TABLE [dbo].[ForslagOpslag]
(
[Id] INT IDENTITY (1, 1) NOT NULL,
[text] NVARCHAR (MAX) NOT NULL,
[fk_brugerid] INT NOT NULL,
[opretdato] DATETIME NOT NULL,
PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_ForslagOpslagLikes_ToForslagBrugerid]
FOREIGN KEY ([fk_brugerid])
REFERENCES [dbo].[ForslagOpslagLikes]([fk_brugerid])
);
You sound scandinavian, and Bruger means User (for all the non-scandinavians here).
What you appear to want is a Bruger (User) table, where fk_brugerid in ForslagOpslag is the user who created the record with opretdato being the creation date, and ForslagOpslagLikes is an association table of users who likes the ForslagOpslag with opretdato being the date they clicked on "Like".
CREATE TABLE [dbo].[Bruger]
(
[brugerid] INT IDENTITY (1, 1) NOT NULL,
...,
CONSTRAINT [PK_Bruger]
PRIMARY KEY CLUSTERED ([brugerid])
);
CREATE TABLE [dbo].[ForslagOpslag]
(
[Id] INT IDENTITY (1, 1) NOT NULL,
[text] NVARCHAR(MAX) NOT NULL,
[fk_brugerid] INT NOT NULL,
[opretdato] DATETIME NOT NULL,
CONSTRAINT [PK_ForslagOpslag]
PRIMARY KEY CLUSTERED ([Id]),
CONSTRAINT [FK_ForslagOpslag_Bruger]
FOREIGN KEY ([fk_brugerid])
REFERENCES [dbo].[Bruger] ([brugerid])
);
CREATE TABLE [dbo].[ForslagOpslagLikes]
(
[fk_brugerid] INT NOT NULL,
[opretdato] DATETIME NOT NULL,
[getid] INT NOT NULL,
CONSTRAINT [PK_ForslagOpslagLikes]
PRIMARY KEY CLUSTERED ([fk_brugerid], [getid]),
CONSTRAINT [FK_ForslagOpslagLikes_Bruger]
FOREIGN KEY ([fk_brugerid])
REFERENCES [dbo].[Bruger] ([brugerid]),
CONSTRAINT [FK_ForslagOpslagLikes_ForslagOpslag]
FOREIGN KEY ([getid])
REFERENCES [dbo].[ForslagOpslag]([Id])
);

Use a common table with many to many relationship

I have two SQL tables: Job and Employee. I need to compare Job Languages Proficiencies and Employee Languages Proficiencies. A Language Proficiency is composed by a Language and a Language Level.
create table dbo.EmployeeLanguageProficiency (
EmployeeId int not null,
LanguageProficiencyId int not null,
constraint PK_ELP primary key clustered (EmployeeId, LanguageProficiencyId)
)
create table dbo.JobLanguageProficiency (
JobId int not null,
LanguageProficiencyId int not null,
constraint PK_JLP primary key clustered (JobId, LanguageProficiencyId)
)
create table dbo.LanguageProficiency (
Id int identity not null
constraint PK_LanguageProficiency_Id primary key clustered (Id),
LanguageCode nvarchar (4) not null,
LanguageLevelId int not null,
constraint UQ_LP unique (LanguageCode, LanguageLevelId)
)
create table dbo.LanguageLevel (
Id int identity not null
constraint PK_LanguageLevel_Id primary key clustered (Id),
Name nvarchar (80) not null
constraint UQ_LanguageLevel_Name unique (Name)
)
create table dbo.[Language]
(
Code nvarchar (4) not null
constraint PK_Language_Code primary key clustered (Code),
Name nvarchar (80) not null
)
My question is about LanguageProficiency table. I added an Id has PK but I am not sure this is the best option.
What do you think about this scheme?
Your constraint of EmployeeId, LanguageProficiencyId allows an employee to have more than one proficiency per language. This sounds counterintuitive.
This would be cleaner, as it allows only one entry per language:
create table dbo.EmployeeLanguageProficiency (
EmployeeId int not null,
LanguageId int not null,
LanguageLevelId int not null,
constraint PK_ELP primary key clustered (EmployeeId, LanguageId)
)
I don't see the point of table LanguageProficiency at the moment.
Same applies to the Job of course. Unless you would like to allow a "range" of proficiencies. But assuming that "too high proficiency" does not hurt, it can easilly be defined through a >= statement in our queries.
Rgds

Error Defining Foreign Key

I have the following two database tables defined:
CREATE TABLE [dbo].[Classrooms] (
[ID] INT IDENTITY (1, 1) NOT NULL,
[SystemAccount_ID] INT NOT NULL,
[ClassroomName] VARCHAR (30) NOT NULL,
CONSTRAINT [PK_Table] PRIMARY KEY CLUSTERED ([ID]),
CONSTRAINT [FK_Classrooms_SystemAccount] FOREIGN KEY ([SystemAccount_ID]) REFERENCES [dbo].[SystemAccounts] ([ID])
);
CREATE TABLE [dbo].[Students] (
[ID] INT IDENTITY (1, 1) NOT NULL,
[SystemAccount_ID] INT NOT NULL,
[Classroom_ID] INT NULL,
[PhotoID] INT NULL,
[FirstName] VARCHAR (20) NOT NULL,
[LastName] VARCHAR (40) NOT NULL,
[NewsTemplate] TINYINT NOT NULL,
CONSTRAINT [PK_Students] PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [FK_Students_Classrooms] FOREIGN KEY ([Classroom_ID]) REFERENCES [dbo].[Classrooms] ([ID]),
CONSTRAINT [FK_Students_SystemAccounts] FOREIGN KEY ([SystemAccount_ID]) REFERENCES [dbo].[SystemAccounts] ([ID])
);
Data model details:
Students belong to zero or one classroom via Classroom_ID FK
Students belong to one System Account via SystemAccount_ID FK
Classrooms belong to one System Account via SystemAccount_ID FK (implying a system account can have zero or more Classrooms)
What I'm attempting to do is enforce when students are added to a classroom (by setting the Classroom_ID key in the Students table) that the classroom belongs to the same system account as the student. I could easily enforce this at the business logic layer but then I'd be requiring every programmer to remember to do this. So ideally, I'd be able to do this at the data layer as a constraint.
I tried adding a FK constraint to the Students table:
CONSTRAINT [FK_Students_ToTable] FOREIGN KEY ([SystemAccount_ID]) REFERENCES [Classrooms]([SystemAccount_ID])
Which results in the following error compliments of SQL Server:
Update cannot proceed due to validation errors.
Please correct the following errors and try again.
SQL71516 :: The referenced table '[dbo].[Classrooms]' contains no primary or candidate keys that match the referencing column list in the foreign key. If the referenced column is a computed column, it should be persisted.
I've tried a few different things but my SQL mojo isn't powerful enough to hack past this one. Any help would be greatly appreciated.
Add a UNIQUE constraint on the combination of the two columns in Classrooms:
CREATE TABLE [dbo].[Classrooms] (
[ID] INT IDENTITY (1, 1) NOT NULL,
[SystemAccount_ID] INT NOT NULL,
[ClassroomName] VARCHAR (30) NOT NULL,
CONSTRAINT [PK_Table]
PRIMARY KEY CLUSTERED ([ID]),
CONSTRAINT [FK_Classrooms_SystemAccount]
FOREIGN KEY ([SystemAccount_ID])
REFERENCES [dbo].[SystemAccounts] ([ID]),
CONSTRAINT [UQ_Classrooms_ID_SystemAccount_ID]
UNIQUE ([SystemAccount_ID], [ID])
);
Then, in the Students table, combine the two FOREIGN KEY constraints into one, or in your case (because Classroom_ID isnullable), change the FK to Classroom to use the combination of the two columns:
CREATE TABLE [dbo].[Students] (
[ID] INT IDENTITY (1, 1) NOT NULL,
[SystemAccount_ID] INT NOT NULL,
[Classroom_ID] INT NULL,
[PhotoID] INT NULL,
[FirstName] VARCHAR (20) NOT NULL,
[LastName] VARCHAR (40) NOT NULL,
[NewsTemplate] TINYINT NOT NULL,
CONSTRAINT [PK_Students]
PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [FK_Students_Classrooms]
FOREIGN KEY ([SystemAccount_ID], [Classroom_ID])
REFERENCES [dbo].[Classrooms] ([SystemAccount_ID], [ID]),
CONSTRAINT [FK_Students_SystemAccounts] -- this wouldn't be needed if
FOREIGN KEY ([SystemAccount_ID]) -- Classrooms_ID was NOT NULL
REFERENCES [dbo].[SystemAccounts] ([ID])
);