MySQL 8 - Trigger on INSERT - duplicate AUTO_INCREMENT id for VCS - mysql

Trying to
create trigger that is called on INSERT & sets originId = id (AUTO_INCREMENT),
I've used SQL suggested here in 1st block:
CREATE TRIGGER insert_example
BEFORE INSERT ON notes
FOR EACH ROW
SET NEW.originId = (
SELECT AUTO_INCREMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'notes'
);
Due to information_schema caching I have also set
information_schema_stats_expiry = 0
in my.cnf file. Now information gets updated almost instantly on every INSERT, as I've noticed..
But, performing "direct" INSERTs via console with ~2min intervals, I keep getting not updated AUTO_INCREMENT values in originId.
(They shoud be equal to id fields)
While explicit queries, fetching AUTO_) result in updated correct values.
Thus I suspect that the result of SELECT AUTO_INCREMENT... subquery gets somehow.. what? cached?
How can one get around this?
Thank you.
Edit 1
I intended to implement sort of VCS this way:
User creates new Note, app marks it as 'new' and performs an INSERT in MySQL table. It is the "origin" note.
Then user might edit this Note (completely) in UI, app will mark is as 'update' and INSERT it in MySQL table as a new row, again. But this time originId should be filled with an id of "origin" Note (by app logics). And so on.
This allows PARTITIONing by originId on SELECT, fetching only latest versions to UI.
initial Problem:
If originId of "origin" Note is NULL, MySQL 8 window function(s) in default (and only?) RESPECT_NULL mode perform(s) framing not as expected ("well, duh, it's all about your NULLs in grouping-by column").
supposed Solution:
Set originId of "origin" Notes to id on their initial and only INSERT, expecting 2 benefits:
Easily fetch "origin" Notes via originId = id,
perform correct PARTITION by originId.
resulting Problem:
id is AUTO_INCREMENT, so there's no way (known to me) of getting its new value (for the new row) on INSERT via backend (namely, PHP).
supposed Solution:
So, I was hoping to find some MySQL mechanism to solve this (avoiding manipulations with id field) and TRIGGERs seemed a right way...
Edit 2
I believed automated duplicating id AUTO_INCREMENT field (or any field) within MySQL to be extra fast & super easy, but it totally doesn't appear so now..
So, possibly, better way is to have vcsGroupId UNSIGNED INT field, responsible for "relating" Note's versions:
On create and "origin" INSERT - fill it with MAX(vcsGroupId) + 1,
On edit and "version" INSERT - fill it with "sibling"/"origin" vcsGroupId value (fetched with CTE),
On view and "normal" SELECT - perform framing with Window Function by PARTITION BY vcsGroupId, ORDER BY id or timestamp DESC, then just using 1st (or ascending order by & using last) row,
On view and "origin" SELECT - almost the same, but reversed..
It seems easier, doesn't it?

What you are doing is playing with fire. I don't know exactly what can go wrong with your trigger (beside that it doesn't work for you already), but I have a strong feeling that many things can and will go wrong. For example: What if you insert multiple rows in a single statement? I don't think, that the engine will update the information_schema for each row. And it's going to be even worse if you run an INSERT ... SELECT statement. So using the information_schema for this task is a very bad idea.
However - The first question is: Why do you need it at all? If you need to save the "origin ID", then you probably plan to update the id column. That is already a bad idea. And assuming you will find a way to solve your problem - What guarantees you, that the originId will not be changed outside the trigger?
However - the alternative is to keep the originId column blank on insert, and update it in an UPDATE trigger instead.
Assuming this is your table:
create table vcs_test(
id int auto_increment,
origin_id int null default null,
primary key (id)
);
Use the UPDATE trigger to save the origin ID, when it is changed for the first time:
delimiter //
create trigger vcs_test_before_update before update on vcs_test for each row begin
if new.id <> old.id then
set new.origin_id = coalesce(old.origin_id, old.id);
end if;
end;
delimiter //
Your SELECT query would then be something like this:
select *, coalesce(origin_id, id) as origin_id from vcs_test;
See demo on db-fiddle
You can even save the full id history with the following schema:
create table vcs_test(
id int auto_increment,
id_history text null default null,
primary key (id)
);
delimiter //
create trigger vcs_test_before_update before update on vcs_test for each row begin
if new.id <> old.id then
set new.id_history = concat_ws(',', old.id_history, old.id);
end if;
end;
delimiter //
The following test
insert into vcs_test (id) values (null), (null), (null);
update vcs_test set id = 5 where id = 2;
update vcs_test set id = 4 where id = 5;
select *, concat_ws(',', id_history, id) as full_id_history
from vcs_test;
will return
| id | id_history | full_id_history |
| --- | ---------- | --------------- |
| 1 | | 1 |
| 3 | | 3 |
| 4 | 2,5 | 2,5,4 |
View on DB Fiddle

Related

How to append an auto-incrementing value to a duplicate value?

I have access to a reporting dataset (that I don't control) that we retrieve daily from a cloud service and store in a mysql db to run advanced reporting and report combining locally with 3rd party data visualization software.
The data often has duplicate values on an id field that create problems when joining with other tables for data analysis.
For example:
+-------------+----------+------------+----------+
| workfile_id | zip_code | date | total |
+-------------+----------+------------+----------+
| 78002 | 90210 | 2016-11-11 | 2010.023 |
| 78002 | 90210 | 2016-12-22 | 427.132 |
+-------------+----------+------------+----------+
Workfile_id is duplicated because this is the same job, but additional work on the job was performed in a different month than the original work. Instead of the software creating another workfile id for the job, the same is used.
Doing joins with other tables on workfile_id is problematic when more than one of the same id is present, so I was wondering if it is possible to do one of two things:
Make duplicate workfile_id's unique. Have sql append a number to the workfile id when a duplicate is found. The first duplicate (or second occurrence of the same workfile id) would need to get a .01 appended to the end of the workfile id. Then later, if another duplicate is inserted, it would need to auto increment the appended number, say .02, and so on with any subsequent duplicate workfile_id. This method would work best with our data but I'm curious how difficult this would be for the server from a performance perspective. If I could schedule the alteration to take place after the data is inserted to speed up the initial data insert, that would be ideal.
Sum total columns and remove duplicate workfile_id row. Have a task that identifies duplicate workfile_ids and sums the financial columns of the duplicates, replacing the original total with new sum and deleting the 'new row' after the columns have been added together.
This is more messy from a data preservation perspective, but is acceptable if the first solution isn't possible.
My assumption is that there will be significant overhead to have the server compare new workfile_id values to all existing worlfile_id values each time data is inserted, but our dataset is small and new data is only inserted once daily, at 1:30am, and it also should be feasible to keep the duplicate workfile_id searching to rows inserted within the last 6 mo.
Is finding duplicates in a column (workfile_id) and appending an auto-incrementing value onto the workfile_id possible?
EDIT:
I'm having trouble getting my trigger to work based on sdsc81's answer below.
Any ideas?
DELIMITER //
CREATE TRIGGER append_subID_to_workfile_ID_salesjournal
AFTER INSERT
ON salesjournal FOR EACH ROW
BEGIN
SET #COUNTER = ( SELECT (COUNT(*)-1) FROM salesjournal WHERE workfile_id = NEW.workfile_id );
IF #COUNTER > 1 THEN
UPDATE salesjournal SET workfile_id = CONCAT(workfile_id, #COUNTER) WHERE id = NEW.id;
END IF;
END;//
DELIMITER ;
It's hard to know if the trigger isn't working at all, or if just the code in the trigger isn't working. I get no errors on insert. Is there any way to debug trigger errors?
Well, everything is posible ;)
You dont control the dataset but you can modifify the database, right?
Then you could use a trigger after every insert of a new value, and update it, if its duplicate. Something like:
SET #COUNTER = ( SELECT (COUNT(*)-1) FROM *your_table* WHERE workfile_id = NEW.workfile_id );
IF #COUNTER > 1 THEN
UPDATE *your_table* SET workfile_id = CONCAT(workfile_id, #COUNTER) WHERE some_unique_id = NEW.some_unique_id;
END IF;
If there are only one insert a day, and there is defined an index over the workfile_id value, then it shouldn't be any problem for your server at all.
Also, you could implement the second solution, doing:
DELIMITER //
CREATE TRIGGER append_subID_to_workfile_ID_salesjournal
AFTER INSERT ON salesjournal FOR EACH ROW
BEGIN
SET #COUNTER = ( SELECT (COUNT(*)-1) FROM salesjournal WHERE workfile_id = NEW.workfile_id );
IF #COUNTER > 1 THEN
UPDATE salesjournal SET total = total + NEW.total WHERE workfile_id = NEW.workfile_id AND id <> NEW.id;
DELETE FROM salesjournal WHERE id = NEW.id;
END IF;
END;//
DELIMITER ;
Hope this helps.

Iterate through a table and replace values in another table using a stored procedure or function

Hopelessly stuck at the following and up until now none of my programming speed dial buddies has been able to help out (most of them not MySQL experts):
I have different tables where the column names and datatypes are auto generated from the 'import table data wizard' using a CSV file, and the table does not contain an AUTO INCREMENT column (yet). This particular table consists of approx: 30.000 rows It starts at row=id(1) from a table that looks like this:
I am trying to correct values in one column that are comma delimited using one 'corrections' table. And to do this I am writing a stored procedure containing a WHILE loop to interate through the corrections table row for row, and check wheter or not an Alias is found in the table that was imported.
| id | material | alias01 | alias02 | alias03 | *up to 12
1 Katoen Cotton Supima Pima
2 Polyester Polyster
3 Lyocell Lycocell Lyocel
4 Linnen Linen
5 Viscose Visose Viskose Viscoe Voscose
6 Scheerwol
7 Polyamide
8 Nylon
9 Leer Leder Lamsleder Varkensleder
10 Polyurethaan Polyurethan PU Polyuretaan
For testing purposes to test any kind of results i am only using alias01 for now ( it needs to check alias01, then 02 etc... but i'll try to solve that at a later time).
It needs to compare `Length' ( alias_string_length = found_string_length) to make sure that a string that consist of 'wo' is not found in 'wool' or 'wol'.
The values from the column that need corrections look like this (the comma's dont need to be there it's just what i was given to work with):
| material |
,Katoen,Elastaan,Voering,Acetaat,Polyester
,Nylon,Polyester,Elastaan
,Katoen
,Leder,in,Leder,Loopzool,Leder
,Polyester
,Polyester,Elastaan,Voering,Polyester
Update
Thanks to Drew's tip i changed the procedure. I added a tmp table that holds materials AND a unique id for each row, and iterate through each one with the alias01. It takes around 11 seconds to do 9000 rows but 0 row(s) affected,. Any tips on increasing speed are most welcome, but insight in what might be the issue would help alot more.
CREATE DEFINER=`root`#`localhost` PROCEDURE `replace_materials`()
BEGIN
set #rownumber = 1;
set #totalrows = 28;
set #um ='';
set #cm ='';
set #corrected ='';
set #correctme ='';
TRUNCATE TABLE tmp;
INSERT INTO tmp (material) SELECT material FROM vantilburgonline.productinfo;
WHILE (#rownumber < #totalrows) DO
SET #um = (SELECT alias01 FROM vantilburgonline.materials WHERE id=#rownumber);
-- gives 'um' value from column alias01, from table materials, row(X)
SET #cm = (SELECT material FROM vantilburgonline.materials WHERE id=#rownumber);
-- gives 'cm' value from column material, from table materials, row(X)
set #tmprow = 1;
set #totaltmprow =9000;
WHILE (#tmprow < #totaltmprow) DO
SET #correctme = (SELECT material FROM vantilburgonline.tmp WHERE id = #tmprow);
-- gives the value from column material from table tmp to correctme(X).
SET #correctme = REPLACE(#correctme,#um,#cm);
-- should run through column material from table productinfo and replace 'alias01' with correct 'material'.
SET #tmprow = #tmprow +1;
END WHILE;
SET #rownumber = #rownumber +1;
END WHILE;
END
though i'm certain alias01 contains strings it should've found in the materials. Also Workbench was using 9GB at this point and i was only able to counter that by restarting..
I would recommend an alteration from your materials table which is unwieldy with multiple columns (alias01 .. alias12). A transition to a normalized, extensible system. It would have a materials table and a materials_alias table. As it sits alongside your current table that you created, I named them with a 2.
Schema
drop table if exists materials2;
create table materials2
( material varchar(100) primary key, -- let's go with a natural key
active bool not null -- turn it LIVE and ON for string replacement of alias back to material name
-- so active is TRUE for ones to do replacement, or FALSE for skip
-- facilitates your testing of your synonyms, translations, slangs, etc
)engine=INNODB;
insert materials2 (material,active) values
('KARTON',true),
('Polyester',false),
('Lyocell',false),
('Linnen',true),
('Viscose',true),
('Scheerwol',false),
('Nylon',false),
('Leer',true),
('Polyurethaan',true),
('Polyacryl',true),
('Acryl',false),
('Modal',true),
('Acetaat',true),
('Papier',false),
('Wol',true),
('Zijde',true),
('Temcal',false),
('Polyamide',true),
('Wol-Merino',true),
('Elastan',true),
('Elastomultiester',true);
-- 21 rows
-- a few rows were skipped. The intent of them read as gibberish to me. Please review.
-- we need to restructure the materials2_alias table (after the first attempt)
-- 1. it might need special handling when `alias` is a legitimate substring of `material` (those 2 columns)
-- 2. it needs a unique composite index
drop table if exists materials2_alias;
create table materials2_alias
( id int auto_increment primary key,
material varchar(100) not null,
alias varchar(100) not null,
ais bool not null, -- Alias is Substring (alias is a legitimate substring of material, like Wo and Wol, respectively)
unique key(material,alias), -- Composite Index, do not allow dupe combos (only 1 row per combo)
foreign key `m2alias_m2` (material) references materials2(material)
)engine=INNODB;
insert materials2_alias (material,alias,ais) values
('KARTON','Cotton',false),('KARTON','Katoen',false),('KARTON','Pima',false),
('Polyester','Polyster',false),
('Lyocell','Lycocell',false),('Lyocell','Lyocel',false),
('Linnen','Linen',false),
('Viscose','Visose',false),('Viscose','Viskose',false),('Viscose','Viscoe',false),('Viscose','Voscose',false),
('Leer','Leder',false),('Leer','Lamsleder',false),('Leer','Varkensleder',false),('Leer','Schapenleder',false),('Leer','Geitenleder',false),
('Polyurethaan','Polyurethan',false),('Polyurethaan','PU',false),('Polyurethaan','Polyuretaan',false),('Polyurethaan','Polyurathane',false),('Polyurethaan','Polyurtaan',false),('Polyurethaan','Polyueretaan',false),
('Polyacryl','Polyacrylic',false),
('Acetaat','Leder',false),('Acetaat','Lamsleder',false),
('Wol','Schuurwol',false),('Wol','Wool',false),('Wol','WO',false),('Wol','Scheerwol',false),
('Zijde','Silk',false),('Zijde','Sede',false),
('Polyamide','Polyamie',false),('Polyamide','Polyamid',false),('Polyamide','Poliamide',false),
('Wol-Merino','Merino',false),
('Elastan','Elastaan',false),('Elastan','Spandex',false),('Elastan','Elataan',false),('Elastan','Elastane',false),
('Elastomultiester','elastomutltiester',false),('Elastomultiester','Elasomultiester',false);
-- this cleans up the above, where false should have been true
update materials2_alias
set ais=true
where instr(material,alias)>0;
-- 4 rows
There are several alter table statements and other things. I will try to document them or link to them. I am merely trying to capture something to share considering it is several hundred lines of code from you. But mine comes down to a simple chunk of code you would put in a loop.
The Update put in a loop:
UPDATE productinfo pi
join materials2_alias ma
on instr( pi.material, concat(',',ma.alias,',') )>0
join materials2 m
on m.material=ma.material and m.active=true
set pi.material=replace(lower(pi.material),lower(ma.alias),lower(ma.material)),
pi.touchCount=pi.touchCount+1;
A few notes on the update:
-- Note, pi.material starts and ends with a comma.
-- I forced that during the ETL. But `ma.alias` does not contain commas.
-- So add the commas with a concat() within the "Update with a Join" pattern shown
--
-- Note that the commas solved the problem with the Wol - Wo
Well, the following 4 in particular.
select * from materials2_alias
where ais=true
order by material,alias;
+----+------------+----------+-----+
| id | material | alias | ais |
+----+------------+----------+-----+
| 6 | Lyocell | Lyocel | 1 |
| 33 | Polyamide | Polyamid | 1 |
| 28 | Wol | WO | 1 |
| 35 | Wol-Merino | Merino | 1 |
+----+------------+----------+-----+
-- instr() is not case sensitive except for binary strings
-- REPLACE(str,from_str,to_str); -- case sensitive
-- http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_replace
--
-- so the update uses lower() or this won't work due to replace() case sensitivity
--
Stored Procedure:
DROP PROCEDURE if exists touchCounts;
DELIMITER $$
CREATE PROCEDURE touchCounts()
BEGIN
select touchCount,count(*) as rowCount
from productinfo
group by touchCount
order by touchCount;
END $$
DELIMITER ;
When that stored procedure returns the same count of rows on a successive call (the next call), you are done modifying the material column via the update.
That stored procedure could naturally return an out parameter for the rowcount. But it is late and time to sleep.
For your last data set from your side, the update statement would need to be called 4 times. That is like 13 seconds on my mediocre laptop. The idea is naturally flexible, for hundreds of aliases per material if you want.
I parked it up on github as it is too much otherwise.

SQL Trigger: New row :tableA to new col in :tableB

I have a database with a couple of tables. I need to add a column in one table after the insertion of a new row in another table.
Table A: id | Type | Category | ShortDesc | LongDesc | Active
Row 1 int(11), varchar, varchar,varchar,varchar,int
Row 2
Row 3
Table B: id | Row1-ShortDesc | Row2-ShortDesc | Row3-ShortDesc
Row 1 int(11), tiny(1), tiny(1), tiny(1) etc...
Row 2
Row 3
When I occasionally add a new row (item) to TableA, I want a new column in TableB. TableA is a long evolving collection. A Row in TableA can not be removed for obvious legacy reasons.
So when I insert a row to TableA I need to have another column inserted/appended into TableB.
Any help would be appreciated.
TIA.
Answer derived from training in SQL
I was finally able to derive and create my trigger solution utilizing a class in SQL Server at MAX TRAINING in CINCINNATI OHIO.
--SQL CODE
-- Create a table called TableA that just holds some data for the trigger
-- This table has a primary Key seeded with 1 and incremented by 1
CREATE TABLE TableA(
id int identity(1,1) PRIMARY KEY,
name varchar(60) NOT NULL,
shortDesc varchar(60) NOT NULL,
longDesc varchar(60) NOT NULL,
bigDesc TEXT NOT NULL
)
GO
-- Create a table TableB that only has a ID column. ID as a primary key seeded with 1, incremented by 1
CREATE TABLE TableB(
id int identity(1,1) PRIMARY KEY
)
GO
-- Just to see the two tables with nothing in it.
select * from TableA
select * from TableB
GO
-- The actual trigger in TableA based upon an insert
CREATE TRIGGER TR_myInserCol
ON TableA
AFTER INSERT
AS
BEGIN
-- Don't count the trigger events
SET NOCOUNT ON;
-- Because we are making strings we declare some variables
DECLARE #newcol as varchar(60);
DECLARE #lastRow as int;
DECLARE #sql as varchar(MAX);
-- Now fill the variables
-- make sure we are looking at the last, freshly inserted row
SET #lastRow = (SELECT COUNT(*) FROM TableA);
-- Make a SELECT statement for the last row
SET #newcol = (SELECT shortDesc FROM TableA WHERE id = #lastRow);
-- Adds a new column in TableB is inserted based on a
-- TableA.shortDesc as the name of the new column.
-- You can use any row data you want but spaces and
-- special characters will require quotes around the field.
SET #sql = ('ALTER TABLE TableB ADD ' + #newcol + ' char(99)');
-- And run the SQL statement as a combined string
exec(#sql);
END;
GO
--Insert a new rows into TableA
--The trigger will fire and add a column in TableB
INSERT INTO TableA
(name,shortDesc,longDesc,bigDesc)
VALUES ('attract','Attraction','Attractions','Places to go see and have
fun');
GO
INSERT INTO TableA
(name,shortDesc,longDesc,bigDesc)
VALUES ('camp','Camp','CAMP GROUND','Great place to sleep next to a creek');
GO
(name,shortDesc,longDesc,bigDesc)
VALUES ('fuel','GasStation','Fueling Depot','Get gas and go');
GO
INSERT INTO TableA
(name,shortDesc,longDesc,bigDesc)
VALUES ('petstore','PetStore','Pet Store','Get a friend');
GO
-- See the newly created rows in TableA and the new Columns created in TableB
select * from TableA
select * from TableB
GO
-- Do not execute unless you want to delete the newly created tables.
-- Use this to delete your tables
-- Clean up your work space so you can make changes and try again.
DROP TABLE TableA;
DROP TABLE TableB;
GO
Thanks again to those that tried to help me out. And yes, I still understand this may not be the best solution but for me this works as I will only insert rows in TableA maybe a couple of times a year and will more than likely max out with less than 300 rows over the next several years as the data I am working with doesn't change that frequently and have a single row to access with a single bit (T/F) allows me to now quickly assign TableB's to locations and people for their search criteria and to generate a nice SQL query string without multiple reads across potentially several pages. Thanks again!
And if someone wants to add or modify what I have done, I'm all ears. It's all about learning and sharing.
Michael

MySql table with not sequential id

Is there a way to create a table in MySql that it has an automatic ID field, but the ID is not sequential. For example, a random or pseudo random ID.
I have found solutions that suggest generating an ID and try to insert it until an unused ID is found (generating an sequential five digit alphanumerical ID).
but nothing that can be done directly in the table definition, or a simpler trick.
MySQL has a native function UUID() which will generate a globally unique identifier:
mysql> SELECT UUID();
-> '6ccd780c-baba-1026-9564-0040f4311e29'
You can store its output in a CHAR(36) column.
INSERT INTO table (`uuid`, `col1`, `col2`) VALUES (UUID(), 'someval', 'someval');
According to the documentation though,
Although UUID() values are intended to be unique, they are not necessarily unguessable or unpredictable. If unpredictability is required, UUID values should be generated some other way.
Addendum Another option is UUID_SHORT() for a 64-bit unsigned INT rather than a character field.
mysql> SELECT UUID_SHORT();
-> 92395783831158784
Since you asked for a trick, you could use a common auto_incremented id and "fake" it by multiplying with a big prime (and then modulo 2^32):
CREATE TABLE AutoIncPrime
(id int unsigned auto_increment primary key
) ;
Insert values, from 1 to 10:
INSERT INTO AutoIncPrime
VALUES (),(),(),(),(),(),(),(),(),() ;
SELECT * FROM AutoIncPrime ;
Output:
id
---
1
2
3
4
5
6
7
8
9
10
Fake the id, with a View:
CREATE VIEW AutoIncPrime_v AS
SELECT
((id*1798672429 ) & 0xFFFFFFFF)
AS FakeUUID
FROM AutoIncPrime ;
Lets see our "UUIDs":
SELECT * FROM AutoIncPrime_v ;
Output:
FakeUUID
----------
1798672429
3597344858
1101049991
2899722420
403427553
2202099982
4000772411
1504477544
3303149973
806855106
You could even make it look more random with (more complicated bit mixing):
CREATE VIEW AutoIncPrime_v2 AS
SELECT
( (((id*1798672429 ) & 0x55555555) << 1)
| (((id*1798672429 ) & 0xAAAAAAAA) >> 1)
)
AS FakeUUID
FROM AutoIncPrime ;
SELECT * FROM AutoIncPrime_v2 ;
FakeUUID
----------
2537185310
3918991525
2186309707
1558806648
604496082
1132630541
3719950903
2791064212
3369149034
808145601
The trick is that you still have a sequential id in the table - which you can use to join to other tables. You just don't show it to the users - but only show the fake one.
If the table is to get big and the calculations slow, you can add another column in the table and store the FakeUUID value there with an INSERT trigger.
Would a composite key work? A regular standard auto_increment field. You insert your new record, retrieve its new ID, then hash that ID with a salt, and update the record with that hash value.
If you do this all within a transaction, the in-progress version of the record without the hash will never be visible until the hash is generated. And assuming you've done proper salting, the resulting hash value will be for all intents and purposes 'random'.
Note that you can't do this in a single step, as the value of last_insert_id() in mysql is not updated with the new id until the record is actually written. The value retrieved during the actual insert parseing stage would be whatever id was inserted BEFORE this one.
The only automatically generated default in the table definition allowed would be autoincrement (MySQL Guide).
You should be able to write a trigger to automate this process though, maybe through the UUID function as Michael suggested.

MySQL make two columns UNIQUE

Don't know if this is possible for MySQL because I know that it doesn't support check constraints, but what I want is to make two columns unique. Before you answer with
ALTER TABLE <table_name> ADD UNIQUE(<col1>, <col2>);
That's not what I want. I would like to ensure that col1 and col2 have unique values so if they are INTs, number "1" can exist only once between both columns, which means if col1 contains "1", col2 cannot contain "1" and "1" can only appear once in col1. Hopefully that makes sense.
I know I can do it from a php level, but right now there is a lot of code, and if I miss a spot, I don't want to destroy data integrity; I rather throw an error from the database. Some ideas that I've come across is using triggers. If someone can give me an example of using triggers to accomplish this, that would be great.
UPDATE
It might help if you knew what I was doing, then maybe you can propose a better way of doing this:
I have two fields, email and new_email. When someone changes their email address, I store it into the new_email field until the accept the change. Since email is unique because it's used as their login, I HAVE to ensure that the email is unique across both fields.
Solution
Create a table MakeColsUnique with one column ColumnBoth
Create a Primary Key on ColumnBoth
All all values from Col1 and Col2 to this table (if you have existing duplicates, this will fail)
Add a trigger on OriginalTable on INSERT or UPDATE to insert the value from Col1 and Col2 into the new table MakeColsUnique
If the value has already been inserted, the insert or update will fail.
I think you should try to reorganize your database. Let's say currently you have this:
Table: users
id name email new_email
102 foo foo#mail.com foo2#mail.com
103 bar bar#mail.com bar2#mail.com
104 baz baz#mail.com NULL
This could be changed to:
Table: users
id name
102 foo
103 bar
104 baz
Table: emails
user_id is_new email
102 0 foo#mail.com
102 1 foo2#mail.com
103 0 bar#mail.com
103 1 bar2#mail.com
104 0 baz#mail.com
You can then add a unique index on the final table on the column email.
You can't enforce that with a key constraint. Honestly the requirement sounds a little bit odd, and I think you're probably better off extracting col1 and col2 into a separate table, say cols.
As per you update on the question a database constraint is not a valid option, because you want to store the email for a while in both the fields and then accept it once the user accepts it. It looks more like a logic that needs to be implemented in the application business logic than the database
Have you solved your problem? I have encountered this problem too, and finally I solve my problem with trigger, following your idea. Here is my sql:
delimiter |
CREATE TRIGGER unique_AB BEFORE INSERT ON test
FOR EACH ROW
BEGIN
DECLARE msg varchar(200);
DECLARE flag int;
set flag = (select count(*) from test where A = new.A or B = new.A or A = new.B or B = new.B);
IF flag > 0 THEN
set msg = "column duplicate!!!";
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END;
|
my table is as follow:
CREATE TABLE `test` (
`A` varchar(255) DEFAULT NULL,
`B` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
in the trigger, I made a select first and judge if there is already a value that is equals to my new line, and this worked for me.