Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is multi-row uniqueness possible in SQL Server?

Tags:

sql-server

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).

like image 207
mortysporty Avatar asked Feb 23 '26 20:02

mortysporty


1 Answers

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.

like image 83
Edmond Quinton Avatar answered Feb 27 '26 01:02

Edmond Quinton



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!