Enforce Unique Constraint Across Two Tables

  • I have an interesting problem.  An application allows users to "Tag" events with a label.  Each user can put a single tag on an event.  There is a list of default tags that users can choose from, but users can also define their own "custom" tags.  The tags must be unique, and we don't want the users to create a custom tag that matches one of the default tags.  Here is the current schema

    CREATE TABLE CustomTag
    ( UserCode  INT                    NOT NULL
     ,TagName   VARCHAR(50)  NOT NULL
    ,PRIMARY KEY ( UserCode, TagName )
    );

    CREATE TABLE StockTag
    ( TagName  VARCHAR(50)  PRIMARY KEY );

    Here is the constraint I would like to enforce:

    CREATE VIEW AllTags
    WITH SCHEMABINDING
    AS
       SELECT DISTINCT TagName
       FROM    CustomTag
    UNION ALL
       SELECT TagName
       FROM     StockTag;
    GO

    CREATE UNIQUE CLUSTERED INDEX UX_AllTags
      ON AllTags ( TagName );
    GO

    Of course, this is not allowed.  You can't use UNION in an indexed view, nor can you use the DISTINCT clause.  Is there any way to enforce this constraint without using a Trigger or a Check constraint that references a Scalar UDF? 

    Thank you for your help. 🙂

  • One way would be to change your table design to use just one table called Tags, and have a column that indicates TagType (Custom or Stock - perhaps simply C or S), and NOT use that as part of the unique key.   That solves the unique problem.   As long as you insist on two different tables, you have no alternative to a trigger on both tables.

    EDIT: realized right after I posted that you would not want to use the TagType in the uniqueness of the table.  Oops!

    Steve (aka sgmunson) 🙂 🙂 🙂
    Rent Servers for Income (picks and shovels strategy)

  • David Moutray - Tuesday, August 21, 2018 12:17 PM

    I have an interesting problem.  An application allows users to "Tag" events with a label.  Each user can put a single tag on an event.  There is a list of default tags that users can choose from, but users can also define their own "custom" tags.  The tags must be unique, and we don't want the users to create a custom tag that matches one of the default tags.  Here is the current schema

    CREATE TABLE CustomTag
    ( UserCode  INT                    NOT NULL
     ,TagName   VARCHAR(50)  NOT NULL
    ,PRIMARY KEY ( UserCode, TagName )
    );

    CREATE TABLE StockTag
    ( TagName  VARCHAR(50)  PRIMARY KEY );

    Here is the constraint I would like to enforce:

    CREATE VIEW AllTags
    WITH SCHEMABINDING
    AS
       SELECT DISTINCT TagName
       FROM    CustomTag
    UNION ALL
       SELECT TagName
       FROM     StockTag;
    GO

    CREATE UNIQUE CLUSTERED INDEX UX_AllTags
      ON AllTags ( TagName );
    GO

    Of course, this is not allowed.  You can't use UNION in an indexed view, nor can you use the DISTINCT clause.  Is there any way to enforce this constraint without using a Trigger or a Check constraint that references a Scalar UDF? 

    Thank you for your help. 🙂

    Are custom tags and stock tags really two different entities? Seems like it may be one entity with a property of custom or stock/default. In that case, there wouldn't be a problem. I just don't know why there is the UserID in one table and how that is used since it doesn't sound like the tags are unique by user and tag name and your only looking for tag names that are unique (as opposed to tag name and userid).

    Sue

  • Looks to me that your choices are exactly those you have said you don't want: a trigger or a scalar function in a constraint.

  • Why did i notice the "not a trigger" after writing this... >_< At least Lynn addresses the problem at hand above (in that the OP has ruled out all 3 options that would fix this; using 1 table, a Scalar Function in a Constraint, or a Trigger). 🙂 Personally, however, I would prefer a trigger over a Scalar function, but 1 table would be my overall preferred option.

    Rather than ensuring uniqueness via a CONSTRAINT/INDEX you could use a TRIGGER to check if the value exists in the StockTag table. If it there is a duplication, you can handle it there. One way would be to use ROLLBACK the transaction in the event that one of the tags exists:

    CREATE TRIGGER dbo.NotStockTag On dbo.CustomTag
    AFTER INSERT, UPDATE
    AS
    BEGIN
      IF EXISTS (SELECT 1
         FROM StockTag ST
         WHERE EXISTS(SELECT 1
              FROM inserted i
               WHERE i.TagName = ST.TagName)) BEGIN
     
      RAISERROR ('Cannot have a Custom tag that is a Stock Tag' ,15,1);
       ROLLBACK TRANSACTION;
      END
    END

    The problem with this, however, is that if you INSERT/UPDATE multiple rows, all of the rows will be rolled back if even one is a duplicate. Therefore, as this is a simple table design, you could instead use an INSTEAD OF:

    CREATE TRIGGER dbo.NotStockTag_Ins On dbo.CustomTag
    INSTEAD OF INSERT
    AS
    BEGIN
      INSERT INTO CustomTag(UserCode, TagName)
      SELECT i.UserCode, i.TagName
      FROM inserted i
      WHERE NOT EXISTS (SELECT 1
             FROM StockTag ST
            WHERE ST.TagName = i.TagName);
    END
    GO
    CREATE TRIGGER dbo.NotStockTag_Upd On dbo.CustomTag
    INSTEAD OF UPDATE
    AS
    BEGIN
      UPDATE CT
      SET TagName = i.TagName
      FROM CustomTag CT
       JOIN inserted i ON CT.UserCode = i.UserCode
            AND CT.TagName = i.TagName
      WHERE NOT EXISTS (SELECT 1
             FROM StockTag ST
            WHERE ST.TagName = i.TagName);
    END
    GO

    The latter won't produce an error, however, if there are any duplicates, it'll simply "ignore" those rows.

    Thom~

    Excuse my typos and sometimes awful grammar. My fingers work faster than my brain does.
    Larnu.uk

  • I think you all are right - I need to change the design.  I really did not want a single table, since the tags are just free-form text and they are not shared between users (except for the pre-defined tags).  However, I think that is the best option.  As usual, weird problems like this are best resolved by a design change.

    Thank you all for your input. 😀

  • Whether you use a single Tag table or not, a trigger would still be much easier than a constraint.  Why do you not want to even consider a trigger?

    SQL DBA,SQL Server MVP(07, 08, 09) "It's a dog-eat-dog world, and I'm wearing Milk-Bone underwear." "Norm", on "Cheers". Also from "Cheers", from "Carla": "You need to know 3 things about Tortelli men: Tortelli men draw women like flies; Tortelli men treat women like flies; Tortelli men's brains are in their flies".

  • ScottPletcher - Tuesday, August 21, 2018 2:26 PM

    Whether you use a single Tag table or not, a trigger would still be much easier than a constraint.  Why do you not want to even consider a trigger?

    I try to avoid triggers for a few reasons:
    1. They aren't "foolproof". They are easily disabled and there are circumstances when they won't fire.
    2. They can be performance hogs.
    3. They add an extra layer of complexity to your code.

    Also, in general I try to solve as many problems as I can in the data structure rather than code.  Constraints are part of the data structure, while triggers are code.  Constraints help define your data - both to other developers and to the Query Optimizer.  The Optimizer can use constraints to make assumptions about your data that allow it to more easily choose an optimum execution plan.

    Finally, as I mentioned in an earlier post, I've found that whenever you run into weird little problems like this, it is almost always a sign of a design problem.  I think of them as "Design Smells".  And design problems are a constant source of future bugs.  Get the design right and your code can be almost maintenance free. 😀

  • I tend to agree on triggers, I find they cause as many problems as they solve and have a habit of making reasoning about a piece of code unnecessarily difficult.

    You can solve this problem with a constraint, but it requires the introduction of a helper table to eliminate the need for a union:

    -- Helper table
    Drop Table If Exists IndexedViewDoubler
    Create Table IndexedViewDoubler(value int)
    Insert Into IndexedViewDoubler Values (0),(1)
    Go
    CREATE or ALTER VIEW AllTags
    WITH SCHEMABINDING
    AS
    Select
    CT.UserCode,
    CT.TagName
    From dbo.CustomTag CT
    Join dbo.StockTag ST On CT.TagName = ST.TagName
    Cross Join dbo.IndexedViewDoubler -- Double up rows if a custom tag matches a stock tag
    GO
    CREATE UNIQUE CLUSTERED INDEX UX_AllTags
    ON AllTags ( UserCode, TagName );


    That will allow users to have their own tags, as long as they don't match a stock tag and will allow different users to use the same tag.
  • andycadley - Tuesday, August 21, 2018 9:08 PM

    I tend to agree on triggers, I find they cause as many problems as they solve and have a habit of making reasoning about a piece of code unnecessarily difficult.

    You can solve this problem with a constraint, but it requires the introduction of a helper table to eliminate the need for a union:

    -- Helper table
    Drop Table If Exists IndexedViewDoubler
    Create Table IndexedViewDoubler(value int)
    Insert Into IndexedViewDoubler Values (0),(1)
    Go
    CREATE or ALTER VIEW AllTags
    WITH SCHEMABINDING
    AS
    Select
    CT.UserCode,
    CT.TagName
    From dbo.CustomTag CT
    Join dbo.StockTag ST On CT.TagName = ST.TagName
    Cross Join dbo.IndexedViewDoubler -- Double up rows if a custom tag matches a stock tag
    GO
    CREATE UNIQUE CLUSTERED INDEX UX_AllTags
    ON AllTags ( UserCode, TagName );


    That will allow users to have their own tags, as long as they don't match a stock tag and will allow different users to use the same tag.

    OK!  That is very clever!  That is exactly what I wanted, but I couldn't figure out how to make it work.  I already changed the table design (which is probably for the best), but I will have to keep this in mind for the future.

    Thank you! 😀

  • It feels to me like this should be enforced by the application or business layer rather than in the database. Probably the simplest way to do it would be to only insert records via a stored procedure and you could get the sproc to carry out the validation logic.

    As you say you have modified the schema, could you share your solution so that others with a similar problem can use it as a reference.

  • aaron.reese - Wednesday, August 22, 2018 6:33 AM

    It feels to me like this should be enforced by the application or business layer rather than in the database. Probably the simplest way to do it would be to only insert records via a stored procedure and you could get the sproc to carry out the validation logic.

    As you say you have modified the schema, could you share your solution so that others with a similar problem can use it as a reference.

    Well, if I had seen Andy Cadley's solution first, I would have used that rather than change the schema.  It is exactly what I wanted.  I will definitely keep it in mind when I have a No Intersection rule I want to enforce.  (He should write it up into a short article.  I think it would make a nice "SQL Spackle" sort of article. 🙂 )

    That said, what I actually did was to combine the two tag tables into one and add an "isDefault" flag, like so:

    CREATE TABLE ActionReminderTag
    ( TagId      INT          PRIMARY KEY
     ,TagName    VARCHAR(50)  UNIQUE
     ,isDefault  BIT          NOT NULL
     );


    I then added another table to hold the users:

    CREATE TABLE UserReminderTag
    ( UserCode  INT  NOT NULL
     ,TagID     INT  NOT NULL
     ,PRIMARY KEY ( UserCode, TagID )
     );

    Regarding your initial point, about enforcing this in code or in the business layer, my response is, "Yes, you should do that as well."  I believe that, as much as possible, the data layer should enforce basic business logic and constraints.  I have a few reasons for this.
    1. Communication:  Constraints in the Data Layer are obvious to anyone looking at the data layer.  This gives other developers information about the data structure that they might not see in the code.
    2. Other applications might use these tables, and the developers might not know (or forget) to add the constraint
    3. Business Layer constraints don't prevent your DBA from adding bad data via ad hoc SQL in SSMS.  I know they would never do that, but.... 😉
    4. As I said in an earlier post, Constraints also communicate with the SQL Query Optimizer - it uses constraints to make assumptions about your data that allow it to eliminate certain possibilities from the query "Solution Space".  Remember, the optimizer has to create a query plan that will always return correct data.  There might be no overlaps in the data between the StockTag table and the CustomTag table, but that does not guarantee that overlaps could never occur. Constraints do guarantee just that. So, the Optimizer can take that into account when formulating a plan.
    5. Finally, as much as possible, your data structure should match your business process.  Someone looking at an ERD of your database (or whatever subset applies to a given process), should be able to understand the process completely.

    I hope this helps! 😀

  • I can't claim to have invented it, I'm fairly sure I picked it up from this site somewhere. It may even be an article already...

  • So how does that constraint prevent a custom tag from being the same as a standard tag?

    SQL DBA,SQL Server MVP(07, 08, 09) "It's a dog-eat-dog world, and I'm wearing Milk-Bone underwear." "Norm", on "Cheers". Also from "Cheers", from "Carla": "You need to know 3 things about Tortelli men: Tortelli men draw women like flies; Tortelli men treat women like flies; Tortelli men's brains are in their flies".

  • I'm not sure why you wouldn't want to do this with a check constrint. It's quite easy:
    DROP TABLE CustomTag
    GO
    DROP TABLE StockTag
    GO
    DROP FUNCTION dbo.CheckNotOnStockTag
    GO
    CREATE TABLE StockTag
    ( TagName VARCHAR(50) PRIMARY KEY );

    --Here is the constraint I would like to enforce:
    GO
    CREATE TABLE CustomTag
    ( UserCode INT NOT NULL
    ,TagName VARCHAR(50) NOT NULL
    ,PRIMARY KEY ( UserCode, TagName )
    );
    GO
    CREATE FUNCTION dbo.CheckNotOnStockTag(@TagName varchar(50))
    RETURNS int AS
    BEGIN
      RETURN (SELECT COUNT(*) FROM StockTag WHERE TagName=@TagName)
    END
    GO
    ALTER TABLE CustomTag
    ADD CONSTRAINT chk_NotOnStckTag
    CHECK (dbo.CheckNotOnStockTag(TagName)=0)
    GO

    INSERT INTO StockTag(TagName) VALUES ('werwer')
    INSERT INTO StockTag(TagName) VALUES ('werwer2')

    INSERT INTO CustomTag(UserCode, TagName) VALUES (1, 'asdf')
    INSERT INTO CustomTag(UserCode, TagName) VALUES (2, 'azxcv')
    INSERT INTO CustomTag(UserCode, TagName) VALUES (3, 'werwer') -- Raises error

    SELECT * FROM StockTag
    SELECT * FROM CustomTag

Viewing 15 posts - 1 through 15 (of 25 total)

You must be logged in to reply to this topic. Login to reply