I have a table where I try to define a covariate gouping like so
ID Rank Covariate
1 1 Age
1 2 Gender
1 3 YearOfBirth
2 1 Gender
The ID captures which covariates belong together in the same group. So covariate group 1 (ID = 1) is composed of age, gender and year of birth, whereas group 2 is Gender only.
Now, inserting a new covariate group consisting of, say gender only, should be illegal as this group already exists, however inserting a new group consisting of Age and Gender should be allowed (it is a subset of group 1 but not an exact match).
Also the rank matters so
ID Rank Covariate
2 Age
1 Gender
3 YearOfBirth
Should not be considered equal to group 1.
Is there a way to enforce this in sql-server?
Ideally the ID column would autoincrement on a legal insert (but thats a different issue).
I don’t know of any means to enforce the Covariant group uniqueness criteria via standard uniqueness constraints or check constraints or any other elegant solution for that matter. However, you can enforce your constraints by only allowing access to the table via a stored procedure or alternatively a view with a “INSTEAD OF INSERT” trigger defined.
Method 1 - Stored Procedure
The following example shows the stored procedure method. First we create a table value type so that we can pass our covariant group as a read-only parameter to our stored procedure.
CREATE TYPE CovariateGroupEntry AS TABLE
(
[Rank] INT NOT NULL
,[Covariate] NVARCHAR(50)
PRIMARY KEY([Rank], [Covariate])
)
Next we create our base table that will contain our covariant groups:
CREATE TABLE CovariateGroups
(
[ID] INT NOT NULL
,[Rank] INT NOT NULL
,[Covariate] NVARCHAR(50)
PRIMARY KEY([ID], [Rank], [Covariate])
)
Next step we create a dummy table that will be used to auto generate our ID:
CREATE TABLE CovariateGroupIDs
(
[GroupID] INT PRIMARY KEY IDENTITY
,[CreatedDateTime] DATETIME NOT NULL
)
Final step we create our procedure:
CREATE PROCEDURE CovariateGroup_Add
(
@covariateGroupEntry dbo.CovariateGroupEntry READONLY
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @groupID INT;
DECLARE @groupSize INT;
DECLARE @groupMatchCount INT;
DECLARE @minRank INT;
DECLARE @maxRankDelta INT;
DECLARE @minRankDelta INT;
-- Get the size of the new group which user will attempt to add.
SELECT @groupSize = COUNT([Rank])
FROM @covariateGroupEntry
-- Validate that the new group rank starts at 1 and increments by 1 step value only.
SELECT @minRank = ISNULL(MIN([Rank]), 0)
,@maxRankDelta = ISNULL(MAX(Delta), 0)
,@minRankDelta = ISNULL(MIN(Delta), 0)
FROM (
SELECT [Rank]
,[Rank] - (LAG([Rank], 1, 0) OVER (ORDER BY [Rank])) AS Delta
FROM @covariateGroupEntry
) RankValidation
IF ( (@minRank > 1) OR (@maxRankDelta > 1) OR (@minRankDelta < 1) )
BEGIN
-- Raise an error if our input data sets rank column does not start at 1 or does not increment by 1 as expected.
RAISERROR (N'Attempting to add covariant group with invalid rank order.', -- Message text.
15, -- Severity,
1 -- State
); -- Second argument.
END
ELSE
BEGIN
-- Generate a new group ID
INSERT INTO [dbo].[CovariateGroupIDs]
(
[CreatedDateTime]
)
SELECT GETDATE() AS [CreatedDateTime]
SET @groupID = SCOPE_IDENTITY();
WITH CTE_GroupsCompareSize
AS
(
-- Compare the size of the new group with all of the existing groups. If the size is different we can
-- safely assume that the group is either a sub set or super set of the compared group. These groups
-- can be excluded from further consideration.
SELECT [CovariateGroups].[ID]
,[CovariateGroups].[Rank]
,[CovariateGroups].[Covariate]
,COUNT([CovariateGroups].[Rank]) OVER (PARTITION BY [CovariateGroups].[ID]) GroupSize
,@groupSize AS NewGroupSize
FROM [CovariateGroups]
)
,CTE_GroupsCompareRank
AS
(
-- For groups of the same size left outer join the new group on the original groups on both rank and covariant entry.
-- If the MIN() OVER window function return a value of 0 then there is at least on entry in the compared groups that does
-- not match and is therefore deemed different.
SELECT [OrginalGroup].[ID]
,[OrginalGroup].[Rank]
,[OrginalGroup].[Covariate]
,MIN(
CASE
WHEN [NewGroup].[Covariate] IS NULL THEN 0
ELSE 1
END
) OVER (PARTITION BY [OrginalGroup].[ID]) AS EntireGroupRankMatch
FROM CTE_GroupsCompareSize [OrginalGroup]
LEFT OUTER JOIN @covariateGroupEntry [NewGroup] ON ([OrginalGroup].[Rank] = [NewGroup].[Rank] AND [OrginalGroup].[Covariate] = [NewGroup].[Covariate])
WHERE GroupSize = NewGroupSize
)
SELECT @groupMatchCount = COUNT(EntireGroupRankMatch)
FROM CTE_GroupsCompareRank
WHERE EntireGroupRankMatch = 1
IF ISNULL(@groupMatchCount, 0) = 0
BEGIN
INSERT INTO [CovariateGroups]
(
[ID]
,[Rank]
,[Covariate]
)
SELECT @groupID AS [ID]
,[Rank]
,[Covariate]
FROM @covariateGroupEntry
END
ELSE
BEGIN
-- Raise an error if our uniqueness constraints are not met.
RAISERROR (N'Uniqueness contain violation, the covariant set is not unique with table "CovariateGroups".', -- Message text.
15, -- Severity,
1 -- State
); -- Second argument.
END
END
END
Method 2 - View with trigger
The second method involves using a views and creating an instead of insert trigger on the view.
First we create the view as follow:
CREATE VIEW CovariateGroupsView
AS
SELECT [ID]
,[Rank]
,[Covariate]
FROM CovariateGroups
Then we create the trigger:
ALTER TRIGGER CovariateGroupsViewInsteadOfInsert on CovariateGroupsView
INSTEAD OF INSERT
AS
BEGIN
DECLARE @groupID INT;
DECLARE @groupSize INT;
DECLARE @groupMatchCount INT;
DECLARE @minRank INT;
DECLARE @maxRankDelta INT;
DECLARE @minRankDelta INT;
-- Get the size of the new group which user will attempt to add.
SELECT @groupSize = COUNT([Rank])
FROM inserted
-- Validate that the new group rank starts at 1 and increments by 1 step value only.
SELECT @minRank = ISNULL(MIN([Rank]), 0)
,@maxRankDelta = ISNULL(MAX(Delta), 0)
,@minRankDelta = ISNULL(MIN(Delta), 0)
FROM (
SELECT [Rank]
,[Rank] - (LAG([Rank], 1, 0) OVER (ORDER BY [Rank])) AS Delta
FROM inserted
) RankValidation
IF ( (@minRank > 1) OR (@maxRankDelta > 1) OR (@minRankDelta < 1) )
BEGIN
RAISERROR (N'Attempting to add covariant group with invalid rank order.', -- Message text.
15, -- Severity,
1 -- State
); -- Second argument.
END
ELSE
BEGIN
-- Generate a new group ID
INSERT INTO [dbo].[CovariateGroupIDs]
(
[CreatedDateTime]
)
SELECT GETDATE() AS [CreatedDateTime]
SET @groupID = SCOPE_IDENTITY();
WITH CTE_GroupsCompareSize
AS
(
-- Compare the size of the new group with all of the existing groups. If the size is different we can
-- safely assume that the group is either a sub set or super set of the compared group. These groups
-- can be excluded from further consideration.
SELECT [CovariateGroups].[ID]
,[CovariateGroups].[Rank]
,[CovariateGroups].[Covariate]
,COUNT([CovariateGroups].[Rank]) OVER (PARTITION BY [CovariateGroups].[ID]) GroupSize
,@groupSize AS NewGroupSize
FROM [CovariateGroups]
)
,CTE_GroupsCompareRank
AS
(
-- For groups of the same size left outer join the new group on the original groups on both rank and covariant entry.
-- If the MIN() OVER window function return a value of 0 then there is at least on entry in the compared groups that does
-- not match and is therefore deemed different.
SELECT [OrginalGroup].[ID]
,[OrginalGroup].[Rank]
,[OrginalGroup].[Covariate]
,MIN(
CASE
WHEN [NewGroup].[Covariate] IS NULL THEN 0
ELSE 1
END
) OVER (PARTITION BY [OrginalGroup].[ID]) AS EntireGroupRankMatch
FROM CTE_GroupsCompareSize [OrginalGroup]
LEFT OUTER JOIN inserted [NewGroup] ON ([OrginalGroup].[Rank] = [NewGroup].[Rank] AND [OrginalGroup].[Covariate] = [NewGroup].[Covariate])
WHERE GroupSize = NewGroupSize
)
SELECT @groupMatchCount = COUNT(EntireGroupRankMatch)
FROM CTE_GroupsCompareRank
WHERE EntireGroupRankMatch = 1
IF ISNULL(@groupMatchCount, 0) = 0
BEGIN
INSERT INTO [CovariateGroups]
(
[ID]
,[Rank]
,[Covariate]
)
SELECT @groupID AS [ID]
,[Rank]
,[Covariate]
FROM inserted
END
ELSE
BEGIN
RAISERROR (N'Uniqueness contain violation, the covariant set is not unique with table "CovariateGroups".', -- Message text.
15, -- Severity,
1 -- State
); -- Second argument.
END
END
END;
The following example show how the stored procedure should be executed:
DECLARE @covariateGroupEntry AS dbo.CovariateGroupEntry
-- INSERT GROUP 1 -------------------
INSERT INTO @covariateGroupEntry
(
[Rank]
,[Covariate]
)
SELECT 1 ,'Age' UNION ALL
SELECT 2 ,'Gender' UNION ALL
SELECT 3 ,'YearOfBirth'
EXEC CovariateGroup_Add @covariateGroupEntry
Following example shows how to insert a group using the view:
DECLARE @covariateGroupEntry AS dbo.CovariateGroupEntry
-- INSERT GROUP 1 -------------------
INSERT INTO @covariateGroupEntry
(
[Rank]
,[Covariate]
)
SELECT 1 ,'Age' UNION ALL
SELECT 2 ,'Gender' UNION ALL
SELECT 3 ,'YearOfBirth'
INSERT INTO [dbo].[CovariateGroupsView]
(
[Rank]
,[Covariate]
)
SELECT [Rank]
,[Covariate]
FROM @covariateGroupEntry
DELETE @covariateGroupEntry -- Delete our memory table if we intend to use it again.
In general I would avoid using the view method since it will be susceptible to more edge cases than the stored procedure and can have some unexpected behaviors. Example the following call:
INSERT INTO [dbo].[CovariateGroupsView]
(
[Rank]
,[Covariate]
)
SELECT 1 ,'Age' UNION ALL
SELECT 2 ,'Gender' UNION ALL
SELECT 3 ,'YearOfBirth'
Will not work as expected since the trigger on the view will treat every row as a separate data set / group. As a result the validation checks will fail.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With