MySQL make two columns UNIQUE - mysql

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.

Related

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

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

How to set it so that 1 of 2 columns is NULL in MySQL

I am going to open by saying that what's happening appears to not be a good practice but it is what myself and some other engineers have arrived at.
I have a table which has a column containing foreign keys that map to products. I want to add a second column that is foreign keys mapping to product categories. However, I only want the product category to be filled in if the product itself isn't filled in. Likewise, if the product category is filled in, I don't want the product to be filled in.
Is there some sort of way to say:
if(colA is NULL)
colB is NOT NULL
if(colB is NULL)
colA is NOT NULL
Thanks for your help in advance.
Since it's MySQL, I guess creating a BEFORE INSERT OR UPDATE trigger is your best bet. Example code:
CREATE TRIGGER Validate_Trigger BEFORE INSERT OR UPDATE ON MyTable FOR EACH ROW
BEGIN
DECLARE msg VARCHAR(255);
IF new.colA IS NOT NULL AND new.colB IS NOT NULL THEN
-- Disallowed: yhrow exception from trigger
SET msg = 'Validate_Trigger: only one column can be NOT NULL: (colA, colB)';
SIGNAL sqlstate '45000' SET message_text = msg;
END IF;
END;
Usually in such cases you would create a CHECK constraint like this:
ALTER TABLE MyTable
ADD CONSTRAINT ConstraintName
CHECK (colA IS NULL OR colB IS NULL)
The problem here is that as of version 5.7, MySQL ignores CHECK constraints:
The CHECK clause is parsed but ignored by all storage engines.

MySql Basic table creation/handing

I'm trying to create a simple table where I insert field and I do some checks in MySql. I've used Microsoft SQL relatively easy. Instead, MySql give evrrytime query errors without even specifying what's going on. Poor MySql software design apart, here's what I'm trying to do:
1 table with 4 fields with an autoincremental autogenerated number to det an ID as primary key
CREATE TABLE `my_db`.`Patients_table` (
`ID_Patient` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`Patient_name` VARCHAR( 200 ) NOT NULL ,
`Recovery_Date` DATETIME NOT NULL ,
`Recovery_count` INT NOT NULL
) ENGINE = MYISAM
a simple stored procedure to insert such fields and check if something exist before inserting:
CREATE PROCEDURE nameInsert(IN nome, IN data)
INSERT INTO Patients_table (Patient_name,Recovery_Date) values (nome,data)
IF (EXISTS (SELECT Recovery_count FROM Tabella_nomi) = 0) THEN
INSERT INTO (Patients_table (Recovery_count)
ELSE
SET Recovery_count = select Recovery_count+1 from Patients_table
END
this seems wrong on many levels and MySQL useless syntax checker does not help.
How can I do this? Thanks.
There seems to be a lot wrong with this block of code. (No offense intended!)
First, Procedures need to be wrapped with BEGIN and END:
CREATE PROCEDURE nameInsert(IN nome, IN data)
BEGIN
...[actually do stuff here]
END
Second, since your table is declared with all fields as NOT NULL, you must insert all fields with an INSERT statement (this includes the Recovery_Date column, and excludes the AUTO_INCREMENT column). You can add DEFAULT CURRENT_TIMESTAMP to the date column if you want it to be set automatically.
INSERT INTO Patients_table (Patient_name,Recovery_Date) values (nome,data)
Third, what exactly is your IF predicate doing?
EXISTS (SELECT Recovery_count FROM Tabella_nomi) = 0
If you want to check if a row exists, don't put the = 0 at the end. Also, Tabella_nomi isn't declared anywhere in that procedure. Also, your SELECT statement should have a WHERE clause, since I'm assuming you want to select a specific row (this is going to select a result set of all recovery_counts).
Fourth, the second INSERT statement seems a little messy. It should look more like the first INSERT, and keep the point I made above in mind.
INSERT INTO (Patients_table (Recovery_count)
Fifth, the ELSE statement
SET Recovery_count = select Recovery_count+1 from Patients_table
Has some problems too. SET is meant for setting variables, not values in rows. I'm not 100% sure what your intent is from this statement, but it looks like you meant to increment the Recovery_count column of a certain row if it already exists. In which case, you meant to do something like this:
UPDATE Patients_table SET Recovery_count = Recovery_count+1 WHERE <conditional predicate>
Where the conditional predicate is something like this:
Patients_name = nome
Try these things, and look at the errors it gives you when you try to execute the CREATE STATEMENT. I bet they're more useful then you think!

Can one obtain the value being inserted with an auto increment pk?

When inserting a row into a table with an auto increment primary key column, is there a way to get the value that is going to be assigned to that row?
To be clear, I want to use this value as a part of a string for a different column on the same row.
Doing MAX(id) + 1 doesn't seem robust enough.
And doing the insert and then an update with LAST_INSERT_ID() is bad because that's 2 separate db calls.
Thanks
EDIT:
What does everyone think of this:
INSERT INTO `mydatabase`.`mytable` (`name`, `description`)
VALUES
(
CONCAT(
'name-',
CAST(
(SELECT
`auto_increment` + 1
FROM
`information_schema`.`TABLES`
WHERE `TABLE_SCHEMA` = 'mydatabase'
AND `TABLE_NAME` = 'mytable') AS CHAR
)
),
'description of this thing'
) ;
This way, you should end up with a row with id of 5 for example, and a name of "name-5". This is kind of a messy way to go about it, but it should work, no?
Thoughts?
You should look in to the SQL command "SHOW TABLE STATUS"
Try it, and google further to find out how to pull the "auto_increment" value out of it.
You can't do with auto_increment field. Instead you can manually generate UID (unique primary key) and use it for this purpose.
Ain't that bad. If you're not generating the key externally, then no. You could put a trigger on the table and make that a non auto increment and then generate the key yourself otherwise.

Copy values from one column to another in the same table

How can I make a copy values from one column to another?
I have:
Database name: list
-------------------
number | test
-------------------
123456 | somedata
123486 | somedata1
232344 | 34
I want to have:
Database name: list
----------------
number | test
----------------
123456 | 123456
123486 | 123486
232344 | 232344
What MySQL query should I have?
Short answer for the code in question is:
UPDATE `table` SET test=number
Here table is the table name and it's surrounded by grave accent (aka back-ticks `) as this is MySQL convention to escape keywords (and TABLE is a keyword in that case).
BEWARE!
This is pretty dangerous query which will wipe everything in column test in every row of your table replacing it by the number (regardless of it's value)
It is more common to use WHERE clause to limit your query to only specific set of rows:
UPDATE `products` SET `in_stock` = true WHERE `supplier_id` = 10
UPDATE `table_name` SET `test` = `number`
You can also do any mathematical changes in the process or use MySQL functions to modify the values.
try this:
update `list`
set `test` = `number`
BEWARE : Order of update columns is critical
GOOD: What I want saves existing Value of Status to PrevStatus
UPDATE Collections SET PrevStatus=Status, Status=44 WHERE ID=1487496;
BAD: Status & PrevStatus both end up as 44
UPDATE Collections SET Status=44, PrevStatus=Status WHERE ID=1487496;
try following:
UPDATE `list` SET `test` = `number`
If list is table name and test and number are columns
it creates copy of all values from "number" and paste it to "test"
Following worked for me..
Ensure you are not using Safe-mode in your query editor application. If you are, disable it!
Then run following sql command
for a table say, 'test_update_cmd', source value column col2, target
value column col1 and condition column col3: -
UPDATE test_update_cmd SET col1=col2 WHERE col3='value';
Good Luck!
IF Anyone wants to put Condition
UPDATE bohf SET Sq=IDNo WHERE Table = 'AOF' AND FormSq BETWEEN 13 AND 17
update `table`
set `firstcolumn` = `secondcolumn`