31

My database contains a table of users. Every active user has a unique username. I'd like to be able to deactivate a user and free up the username they're using, but keep them in the same table.

Is there a way to only conditionally enforce the uniqueness constraint?

icktoofay
  • 126,289
  • 21
  • 250
  • 231
user2692862
  • 311
  • 1
  • 3
  • 3
  • Make what possible? Have the UI display deleted users as "USER DELETED" or having that column in that table contain the same value multiple times? –  Aug 17 '13 at 21:37
  • @delnan: Given the title, I'd assume the latter. – icktoofay Aug 17 '13 at 21:37
  • Just create field - something like status (on - off). Or dtime (time of deleting lol). Or... – Cthulhu Aug 17 '13 at 21:38
  • @Cthulhu You mean a field? – Nabil Kadimi Aug 17 '13 at 21:39
  • @NabilKadimi Yes right. – Cthulhu Aug 17 '13 at 21:39
  • @Cthulhu: That's one part of the solution, but that won't stop MySQL from enforcing the uniqueness constraint on the username column. – icktoofay Aug 17 '13 at 21:39
  • Call me crazy, but if the user is deleted, why does he/she still have a record in that table *in the first place* ? More direct to the question, the only duplicate value I'm aware of that can be repeated in a uniquely indexed column is NULL, and even that requires the table setup allow it in the first place. – WhozCraig Aug 17 '13 at 21:42
  • @WhozCraig: It's helpful to be able to undelete them later if, say, the deletion was erroneous, and keeping them in the table keeps all foreign key relationships valid, which may be desirable. – icktoofay Aug 17 '13 at 21:45
  • 1
    @WhozCraig: One possiblity is that there may be activity records, transaction records, messages, et al. that need to be retained for historical purposes. Perhaps "deleted" really means something like "disabled and archived". (Which goes back to Cthulhu's idea of a separate column for a flag or datetime. – spencer7593 Aug 17 '13 at 21:46
  • @icktoofay I can buy that, certainly. – WhozCraig Aug 17 '13 at 21:46

7 Answers7

70

Add another column called something like isactive. The create a unique constraint on (username, isactive).

Then you can have both an active and inactive user name at the same time. You will not be able to have two active user names.

If you want multiple inactive names, use NULL for the value of isactive. NULL values can be repeated in a unique index.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • 4
    If an answer is ever selected, it should be this one, for actually answering the question that was asked. – Dan Hunsaker Dec 20 '14 at 00:42
  • Is using NULL instead of ‘false’ to get around the constraint considered bad practice? – theyuv Aug 19 '18 at 19:08
  • 1
    @theyuv . . . How `null` behaves in unique constraints depends on the database. For that reason, I would not prefer that solution. – Gordon Linoff Aug 19 '18 at 19:12
  • Any suggestion for a better solution? I have a column `IsEnabled` I would like only one row to be enabled given an additional set of constrains (eg: userid) but there can be several disabled rows for the same user. This answer (https://stackoverflow.com/questions/4617914/how-to-create-a-unique-constraint-on-a-boolean-mysql-column/4618525#answer-4618525) suggests an alternative which I didn't understand. – theyuv Aug 20 '18 at 09:08
  • @theyuv . . . You might want to ask a question about your specific problem. – Gordon Linoff Aug 20 '18 at 10:20
  • 3
    The trouble with this plan is that if the second user of a username becomes inactive, it would violate the uniqueness constraint. – MiguelMunoz Jan 05 '21 at 20:25
9

No, a UNIQUE constraint can't be "conditional".

One option is to set the username column to NULL. The UNIQUE constraint will allow multiple rows with NULL value.

You could translate that to any string you wanted for display. either in the application, or in the SQL

SELECT IFNULL(t.username,'USER DELETED') AS username
  FROM mytable t

If you are retaining these rows for historical/archive purposes, you probably do NOT want to update the username column. (If you change the value of the username column, then a subsequent statement will be allowed to insert a row with the same value as the previous username.)

You could instead add an additional column to your table, to represent the "user deleted" condition. For example:

user_deleted TINYINT(1) UNSIGNED DEFAULT 0 COMMENT 'boolean' 

You could check this column and return the 'USER DELETED' constant in place of the username column whenever the user_deleted boolean is set:

SELECT IF(u.user_deleted,'USER DELETED',u.username) AS username

(Use a value of 1 to indicated a logical "user deleted" condition.)

The big advantage to this approach is that the username column does NOT have to be modified, the username value, and the UNIQUE constraint will prevent a new row with a duplicate username from being inserted.

spencer7593
  • 106,611
  • 15
  • 112
  • 140
  • 1
    +1 for the only repeatable value allowed in a unique column (and I was only speculating in general-comment above. I've never tried it in MySQL; only SQL Server, but good to know it is possible the same way). – WhozCraig Aug 17 '13 at 21:45
2

Different way to achieve the same result. May not be really required for the question asked. But just for information.

  1. Create a trigger on insert / update
  2. Check if there is duplicate records found with current (NEW) records values.
    a. This can be checked by counting dupicates or checking of OTHER records exists with the same values, but different primary key
  3. If found raise a Signal to throw an error

This is best suited if your condition is complex to decide uniqueness. Also consider the performance cost.

Sample

DELIMITER $$

CREATE TRIGGER `my_trigger` BEFORE INSERT/UPDATE
    ON `usertable`
    FOR EACH ROW BEGIN

    IF EXISTS (SELECT 1 FROM usertable WHERE userid <> NEW.userid AND username = NEW.username AND isactive = 1) THEN 
        SELECT CONCAT(NEW.username, ' exists !') INTO @error_text; 



     SIGNAL SQLSTATE '45000' SET message_text = @error_text; 
    END IF;
    END$$

DELIMITER ;
Akhil
  • 2,602
  • 23
  • 36
  • Does this even work? I keep getting a mysql syntax error? – DUDANF Aug 02 '19 at 08:51
  • That's a very rude response. – DUDANF Aug 05 '19 at 09:31
  • I am not sure what is a rude response. Please try and understand the strategy and find equivalent in the newer versions of mysql. This answer was posted on October 2017. I am sure mysql has changed after that. – Akhil Aug 22 '19 at 06:58
1

I would just create another (non-unique) field called FORMER_NAME and move the original name to that field when a user goes inactive. No need for a special uniqueness constraint that's not possible.

MiguelMunoz
  • 4,548
  • 3
  • 34
  • 51
0

Nope, if there is a unique index (hence the name) you can not have duplicates. Either add a extra column to make each record unique. Or change the value so its unique.

Not recommended but for example you could add a timestamp "USER DELETED 2013/08/17:233805"

Roger
  • 7,535
  • 5
  • 41
  • 63
0

This is my solution when I met a similar problem:

add a column inactive, so the unique key as: (username,inactive)

for inactive, inactive = 0 means a user is active, inactive > 0 means a user is active

when deactivate a user, just set inactive = user_id, not 1 as we usually did!

now it allows duplicated usernames for inactive users, but only unique usernames for active users.

leungxd
  • 486
  • 4
  • 6
0

I expanded on @gordon-linoff answer by adding a generated column which provides the nullable functionality. I would rather have a true not null active column that has a definitive true and false value I can use to read and write that is not confusing and won't get messed up by accidentally forgetting about this null behavior later on when writing code. So I compute a column with a specialized name and then use that value in the constraint, so I get the nullable unique active behavior but can use the active column as I wish.

isactive BOOL NOT NULL,
_isactive_constraint_key_ BOOL AS (CASE WHEN isactive IS true THEN true END),
CONSTRAINT active_user UNIQUE(username, _isactive_constraint_key)
Galen Howlett
  • 530
  • 4
  • 12