SQL unique contraint if a column is set to x - mysql

is there a way in SQL to create the constraint that a column has to be unique, if a specific column has a specific value?
Example: the rows are not really deleted, but marked as 'deleted' in the database. And within the 'not-deleted' rows, ValueA has to be unique:
ID ValueA ValueB Deleted
-----------------------------------------------------
1 'foo' 10 0
2 'bar' 20 0
3 'bar' 30 1
4 'bar' 40 1
5 'foo' 50 0 --NOT ALLOWED
I thought of something like a CHECK constraint, however I don't know how to do this.

with SQL92 this is not possible, may be you could implement something with a trigger

Can you change the design a little bit?
It seems to me that you have a list of "thingies". For each ValueA, there's a single active "thingy" at any one time. This can best be modeled as follows:
Remove ValueA and Deleted from your main Thingies table.
Create a new table ActiveThingies with columns ValueA and ID. Protect this table by making ValueA a unique or primary key. (You may also need to make ID unique as well depending on whether a single ID can represent more than 1 ValueA).
Now, use the ActiveThingies table to control which record is current at any time. To change the active (non-deleted) record for "foo", update it's ID column in ActiveThingies.
To get your list of non-deleted items join the two tables.
With this design, however, you will lose the ability to remember the ValueA for "deleted" "thingies". If you need to remember those values, you will also need to include the ValueA column in Thingies.

There is workaround this problem - create another column deleted_on
deleted_on timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
and make unique key on both ValueA and deleted_on
UNIQUE KEY not_deleted (ValueA, deleted_on)
When soft deleting a record insert NOW() for value of deleted_on

MySQL ignores CHECK constraints, so you cannot do this in MySQL as you might in another database.
Here is a hack. Unique constraint on valueA + deleted. When deleting rows you cannot use just 1, they must be 1, 2, 3...
This at least lets you do it server-side in MySQL, but introduces a step. When marking a row for deletion, you have to first go find the max(deleted), add 1, and plug that value in when marking for deletion.

Split your table into two tables: One which has a UNIQUE constraint on ValueA and one that doesn't. Use a view+triggers to combine the two tables. Something like:
CREATE TABLE _Active (
ID INTEGER,
ValueA VARCHAR(255) UNIQUE,
ValueB INTEGER
);
CREATE TABLE _Deleted (
ID INTEGER,
ValueA VARCHAR(255), /* NOT unique! */
ValueB INTEGER
);
CREATE VIEW Thingies AS
SELECT ID, ValueA, ValueB, 0 AS Deleted FROM _Active
UNION ALL
SELECT ID, ValueA, ValueB, 1 AS Deleted FROM _Deleted;
CREATE TRIGGER _trg_ii_Thingies_Active
INSTEAD OF INSERT ON Thingies
FOR EACH ROW WHEN NOT NEW.Deleted
BEGIN
INSERT INTO _Active(ID, ValueA, ValueB)
VALUES (NEW.ID, NEW.ValueA, NEW.ValueB);
END;
CREATE TRIGGER _trg_ii_Thingies_Deleted
INSTEAD OF INSERT ON Thingies
FOR EACH ROW WHEN NEW.Deleted
BEGIN
INSERT INTO _Deleted(ID, ValueA, ValueB)
VALUES (NEW.ID, NEW.ValueA, NEW.ValueB);
END;
/* Add triggers for DELETE and UPDATE as appropriate */
(I'm not sure about the CREATE TRIGGER syntax, but you know what I mean.)

Related

Unique constraint on tables for particular values of a field

I have an invoice table. It has many fields, but the problem is around 2 main fields
InvoiceNo - alphanumeric values
Deleted - boolean 1 or 0 ,to represent a record is deleted or not.
Our business requires InvoiceNo to be unique. However if a row is deleted we can re-use the InvoiceNo
InvoiceNo Deleted
123Er 1
123Er 0
Above is a valid use case. But i don't want to have another record with 123Er & 0.
Is it possible to create unique key on combination of 2 fields for certain values Unique (InvoiceNo, Deleted=0) or should we go for stored procedure or triggers ?
It is quite easy to achieve in other RDBMS systems with help of a function based index
As for now MySql doesn't have such a feature, but starting from version 5.7 it can be simulated with use of a virtual (or generated) column.
Simple working example: http://rextester.com/HGY68688
CREATE TABLE IF NOT EXISTS mytable1234(
InvoiceNo varchar(10),
Deleted int,
xxx varchar(10) generated always as (case when deleted = 0 then InvoiceNo end) virtual
);
create unique index myindex12 on mytable1234( xxx );
INSERT INTO mytable1234( InvoiceNo, Deleted) VALUES ('aaa1', 0 );
INSERT INTO mytable1234( InvoiceNo, Deleted) VALUES ('aaa1', 1 );
INSERT INTO mytable1234( InvoiceNo, Deleted) VALUES ('aaa1', 1 );
-- INSERT INTO mytable1234( InvoiceNo, Deleted) VALUES ('aaa1', 0 );
If you uncomment the last INSERT in this snippet, and then try to run this snippet, then you will get: Duplicate entry 'aaa1' for key 'myindex12' error.
In this way, there may be multiple records in the table with the same InvoiceNo value for deleted = 1, but only one value for deleted = 0, because MySql will not allow this.
You could rename the field something like DeletedIfNull (or IsActive).
Then, the field would take the value of "1" or "true" or whatever if the column is active. It would be NULL for any other values. Then you can create:
create unique index unq_t_invoiceno_isactive on t(invoiceno, isactive);
MySQL (although not all databases) allows repeats when a unique index is defined. Hence, this will solve your immediate problem.
You can just create a unique key (InvoiceNo, Deleted), and its done. I dont understand why you need to make (InvoiceNo, Deleted=0), which is not supported. Having a unique key of those 2 columns provide you what you need.
UPDATE:
I got your problem that you might have 2 deleted rows of same values. In this case, i would suggest you prepend something to InvoiceNo column of deleted values so that they won't be unique.
For example:
You have 123er and then delete it, so it becomes 123er-1. When you delete another one, it becomes 123er-2. You can even do it basically in an update(before) trigger.

How to update id set from 1?

I have an id i.e primary key and auto increment. Is there any query to update my existing id and make my id start from 1 and next id 2 and so on..
For example
id name
3 ABC
5 XYZ
9 PQR
NOTE: id is already primary and auto increment and I don't want truncate my id.
if possible i want to get
id name
1 ABC
2 XYZ
3 PQR
ALTER TABLE table AUTO_INCREMENT = 1; is not my solution.
Thanks
Of course there is a way:
set #counter = 0;
update table_name
set id = (#counter := #counter + 1);
EDIT
To avoid problem with duplicate keys you can run something like this before to temporary change current ids to negative equivalents:
update table_name
set id = 0 - id;
Is there any query to update my existing id and make my id start from 1 and next id 2 and so on
What you can do is transfer the content of your table to another table. Reset the auto increment counter, insert your data back into the original table but let MySQL assign the primary key.
Assuming your table name is mytable You do it like this:
CREATE TABLE mytable_tmp select * from mytable;
TRUNCATE TABLE mytable;
ALTER TABLE mytable AUTO_INCREMENT = 1;
INSERT INTO mytable(name) SELECT name FROM mytable_tmp ORDER BY id;
DROP TABLE mytable_tmp;
In my opinion you shouldn't mess with auto_increment columns at all. Let them be as they are. Their only job is to identify a row uniquely. If you want a nice serial number use another column (make it unique if you wish)!
You will always run into trouble and there will always happen things, that mess with your nice 1, 2, 3, ... sequence. A transaction gets rolled back? Boom, your sequence is 1, 2, 3, 5, ... instead of your intended 1, 2, 3, 4, ...
This can also be a very heavy operation. An auto_increment column is always also a primary key. Every other index on this table includes the primary key. Every time you reset your auto_increments, every index on this table is rewritten.
So my advice is, don't mess with auto_increments.
This query will work for your scenario:
ALTER TABLE tablename DROP id
ALTER TABLE tablename ADD id INT NOT NULL AUTO_INCREMENT FIRST, ADD PRIMARY KEY (id), AUTO_INCREMENT=1

Get all available UIDs between two UIDs from a table

I have a table with UID is the primary key. In the old system, it wasn't the primary key. So, people can insert data to that field, but we don't want to do it anymore.
In this table, I have a gap between UID 2000 and 2005 (2003 is taken). How do I get the list integers inside that gap?
UPDATED
I actually don't want the list of consecutive numbers between 2 uids. Assuming that I may have some UIDs existing between 2 numbers but I don't know that. I just want to get the list of available UIDs between 2 UIDs
I want this list to return:
MISSING
2001
2002
2004
See Generating a range of numbers in MySQL for how to create a table that lists all the numbers in a range. Then do:
set #start = 2000;
set #end = 2005;
SELECT n AS missing
FROM number_table AS nt
LEFT JOIN your_table AS t ON nt.n = t.uid
WHERE n BETWEEN #start AND #end
AND t.uid IS NULL
Summary of sections seen below.
Section 1 mimicks a table that has gaps in id like your question
Section 2 shows a fast way to put a 4 million row table together with incrementing pk.
completely not used for this but perhaps useful.
if it seems left like a half-thought it is because it was not useful entirely
Section 3 creates a table inspired by section 2 to leave you with a table of
it is just a worktable where ordering in important, both to insert into it and processing it
The new id to use (the pk)
Your current id (the one that is gap-prone)
and a column that says whether or not has been processed so you can do them in batches
section 3 is where the action is
xxxxxxxxxxxxxxxxxxxxxxxxxx
Section 1:
create table tbl1
( // this mimicks your current table. naturally you have one already
id bigint not null auto_increment primary key,
thing varchar(100) -- whatever columns you have
)engine=MyISAM;
insert tbl1(thing) values('a'),('a'),('b');
show table status from test; -- auto_increment=4, max is 3
alter table tbl1 auto_increment=2000;
insert tbl1(thing) values('a'),('a'),('b');
alter table tbl1 auto_increment=100000; -- 100k
insert tbl1(thing) values('a'),('a'),('b');
alter table tbl1 auto_increment=110000; -- 110k
insert tbl1(thing) values('a'),('a'),('b');
alter table tbl1 auto_increment=2000000; -- 2m
insert tbl1(thing) values('a'),('a'),('b');
show table status from test; -- auto_increment=2000003, max is 2000002
select count(*) from tbl1 -- 15 rows
xxxxxxxxxxxxxxxxxxxxxxxxxx
Section 2:
create table idFix
( colIs bigint auto_increment primary key, -- Is your Key
colShouldBe bigint null -- Should be your new Key
processedYet tinyint null -- 1 if processed
)engine=myisam;
insert into idFix(colIs) values(null); -- prime it with 1 row
-- this is pretty fast, don't laugh
-- run the following line 22 times
insert into idFix(colIs) select null from idFix;
-- you now have 4.2m rows in tbl2 (4,194,304)
select count(*) from idFix
select count(*) from idFix
where colIs not in (select id from tbl1)
-- 4,194,289
xxxxxxxxxxxxxxxxxxxxxxxxxx
Section 3:
Backup data first. Then perform tests in a scratch database of the following
create table idFix2
( yourIdShouldBe bigint auto_increment primary key, -- This becomes what your new key should be
yourIdIs bigint null, -- This is what your gap-prone id is right now
processedYet tinyint null -- 1 if processed, null otherwise
)engine=myisam;
-- the order by is important
insert into idFix2(yourIdIs,processedYet)
select id,null from tbl1 order by id
-- again order by in above stmt is important
Now you have a table that what your key should be, what your key is, and processedYet is null.
Do them in batches in a stored proc or front end code (say java/c#, whatever)
It is important to do them top to bottom. any other way will screw up your data
Did i mention it is important to do it top to bottom?
I will leave my thought out of it about getting everyone out of system and requiring a table lock
only you know your system not us.
select *
from idFix2
where processedYet is null and yourIdShouldBe<>yourIdIs
order by yourIdShouldBe -- order is important
limit 2 -- you might want to choose a bigger number :>
Did i mention it is important to do it top to bottom ??
Here is the flow using result set from above select stmt
(a) get next row in result set
(b) insert new parent record using data from tbl1 data back into tbl1 using
data from row yourIdIs but the insert will be pk=yourIdShouldBe
The insert will guarantee you won't have foreign key constraints in children tweaked below
(c) update children that use the old yourIdIs to hang under the new yourIdShouldBe
in their tables (there can be scads of these tables). the children's foreign key constraints
will be honored because the new parent row is in place already from step(b)
(d) delete the parent row from tbl1 where pk is yourIdIs. fear not that this will cause even more
gaps because those will be filled based on looping thru (a) which will slot fill them
(e) update idFix2 set processedYet=1 for the row your are processing from step (a) result set
(f) GoTo (a)
When you have no more processedYet=null you are almost done
Set new auto_increment value to what it should be (1 more than max(id) in tbl1, let's call that number nnnn)
alter table tbl1 auto_increment=nnnn;
xxxxxxxxxxxxxxxxxxxxxxxxxx
Note the following
show table status from test where name like 'tbl1%'; -- auto_increment=2000003
I have nothing in slot4, 2000 will become slot 4
insert tbl1(id,thing) values(4,'stuff from record2000 you get the drift');
show table status from test where name like 'tbl1%'; -- auto_increment=2000003 is left as is
So you are free to fill the gaps without screwing with auto_increment until the end
There it is and your gaps go away. If it fails, consider taking a vacation day.
Oh, I forgot, you were testing this first in a scratch database anyway.
Good luck!

Update unique or primary keys in MySQL

I'm building a database management tool for a client to use on his own, and I'm having some problem dealing with the possibility of the update of primary/unique keys. So, given that the data for the update is passed by a PHP script on a per row basis, here's what I've come up with (from "immediatly" to "after some time"):
DELETE/INSERT instead of UPDATE (awful, I now...):
DELETE FROM table WHERE unique_key=x;
DELETE FROM table WHERE unique_key=y;
INSERT INTO table VALUES (unique_key=y, field=record1), (unique_key=x, field=record2);
Alter my primary/unique key and then substitute them with the modified value:
UPDATE table SET unique_key=x* WHERE unique_key=x;
UPDATE table SET unique_key=y* WHERE unique_key=y;
UPDATE table SET unique_key=y WHERE unique_key=x*;
UPDATE table SET unique_key=x WHERE unique_key=y*;
Add a not modifiable auto_increment field "id" to all my tables, which act as a surrogate primary key
As now, I'm on the route of adding an "id" field to everything. Other options?
Updating a primary key isn't a problem; all values in SQL (and in the relational model) are supposed to be updatable.
The problem seems to be swapping primary keys, which
doesn't make sense to me if you use surrogate keys (because they're meaningless, so updates aren't necessary) and which
doesn't make sense to me if you use natural keys, because that's like swapping my StackOverflow userid with yours.
Adding an "ID" column to every table won't help you. The "unique_key" column still has to be declared unique. Adding an "ID" column doesn't change that business requirement.
You could swap primary key values if MySQL supported deferred constraints. (Deferred constraints are a feature in standard SQL.) But MySQL doesn't support that feature. In PostgreSQL, for example, you could do this.
create table test (
unique_key char(1) primary key deferrable initially immediate,
other_column varchar(15) not null
);
insert into test values
('x', 'record2'),
('y', 'record1');
begin;
set constraints test_pkey deferred;
update test set unique_key = 'y' where other_column = 'record2';
update test set unique_key = 'x' where other_column = 'record1';
commit;
select * from test;
unique_key other_column
--
y record2
x record1
You should be able to use a CASE expression to do this kind of update. For example:
UPDATE tbl SET col =
CASE WHEN col = 1 THEN 2
WHEN col = 2 THEN 1
END
WHERE col IN (1,2);
(untested code)

MySQL, two columns both set to the primary key

For the sake of simplicity lets say I have a table with 3 columns; id, parent_id and name. In this table id is my auto-incrementing primary key. I want to group multiple names together in this table, to do this all names in a group will share the same parent_id. If I am inserting the first name in the group I want the id=parent_id, if i am inserting another name I want to specify a specific parent_id to place that name into a specific group. It would be nice if I could define a default for that column to be the same as the id, if I specify a value for parent_id in the insert query then I would like it to use that value. I know you can set a default to be a specific static value, but can you specify the default to be the same as that row's auto-incrementing primary key? Perhaps this is a job for a trigger or stored procedure?
(I know I could obtain the primary key generated by the last insert and then update the table, but that's 2 quires I'd rather not burn.)
Thanks!
This is a job of a trigger!
CREATE TRIGGER NAME1 AFTER INSERT ON TABLE1
BEGIN
UPDATE TABLE1 SET parent_id = id WHERE (parent_id IS NULL OR parent_id = '');
END;
INSERT INTO TABLE1 (id,parent_id) VALUES (null,null); -- parent_id will be equal to id
INSERT INTO TABLE1 (id,parent_id) VALUES (null,'1'); -- parent_id will be 1
INSERT INTO TABLE1 (id,parent_id) VALUES (null,'2'); -- parent_id will be 2