Prevent auto increment on MySQL duplicate insert - mysql

Using MySQL 5.1.49, I'm trying to implement a tagging system
the problem I have is with a table with two columns: id(autoincrement), tag(unique varchar) (InnoDB)
When using query, INSERT IGNORE INTO tablename SET tag="whatever", the auto increment id value increases even if the insert was ignored.
Normally this wouldn't be a problem, but I expect a lot of possible attempts to insert duplicates for this particular table which means that my next value for id field of a new row will be jumping way too much.
For example I'll end up with a table with say 3 rows but bad id's
1 | test
8 | testtext
678 | testtextt
Also, if I don't do INSERT IGNORE and just do regular INSERT INTO and handle the error, the auto increment field still increases so the next true insert is still a wrong auto increment.
Is there a way to stop auto increment if there's an INSERT duplicate row attempt?
As I understand for MySQL 4.1, this value wouldn't increment, but last thing I want to do is end up either doing a lot of SELECT statements in advance to check if the tags exist, or worse yet, downgrade my MySQL version.

You could modify your INSERT to be something like this:
INSERT INTO tablename (tag)
SELECT $tag
FROM tablename
WHERE NOT EXISTS(
SELECT tag
FROM tablename
WHERE tag = $tag
)
LIMIT 1
Where $tag is the tag (properly quoted or as a placeholder of course) that you want to add if it isn't already there. This approach won't even trigger an INSERT (and the subsequent autoincrement wastage) if the tag is already there. You could probably come up with nicer SQL than that but the above should do the trick.
If your table is properly indexed then the extra SELECT for the existence check will be fast and the database is going to have to perform that check anyway.
This approach won't work for the first tag though. You could seed your tag table with a tag that you think will always end up being used or you could do a separate check for an empty table.

I just found this gem...
http://www.timrosenblatt.com/blog/2008/03/21/insert-where-not-exists/
INSERT INTO [table name] SELECT '[value1]', '[value2]' FROM DUAL
WHERE NOT EXISTS(
SELECT [column1] FROM [same table name]
WHERE [column1]='[value1]'
AND [column2]='[value2]' LIMIT 1
)
If affectedRows = 1 then it inserted; otherwise if affectedRows = 0 there was a duplicate.

The MySQL documentation for v 5.5 says:
"If you use INSERT IGNORE and the row is ignored, the AUTO_INCREMENT counter
is **not** incremented and LAST_INSERT_ID() returns 0,
which reflects that no row was inserted."
Ref: http://dev.mysql.com/doc/refman/5.5/en/information-functions.html#function_last-insert-id
Since version 5.1 InnoDB has configurable Auto-Increment Locking. See also http://dev.mysql.com/doc/refman/5.1/en/innodb-auto-increment-handling.html#innodb-auto-inc...
Workaround: use option innodb_autoinc_lock_mode=0 (traditional).

I found mu is too short's answer helpful, but limiting because it doesn't do inserts on an empty table. I found a simple modification did the trick:
INSERT INTO tablename (tag)
SELECT $tag
FROM (select 1) as a #this line is different from the other answer
WHERE NOT EXISTS(
SELECT tag
FROM tablename
WHERE tag = $tag
)
LIMIT 1
Replacing the table in the from clause with a "fake" table (select 1) as a allowed that part to return a record which allowed the insert to take place. I'm running mysql 5.5.37. Thanks mu for getting me most of the way there ....

The accepted answer was useful, however I ran into a problem while using it that basically if your table had no entries it would not work as the select was using the given table, so instead I came up with the following, which will insert even if the table is blank, it also only needs you to insert the table in 2 places and the inserting variables in 1 place, less to get wrong.
INSERT INTO database_name.table_name (a,b,c,d)
SELECT
i.*
FROM
(SELECT
$a AS a,
$b AS b,
$c AS c,
$d AS d
/*variables (properly escaped) to insert*/
) i
LEFT JOIN
database_name.table_name o ON i.a = o.a AND i.b = o.b /*condition to not insert for*/
WHERE
o.a IS NULL
LIMIT 1 /*Not needed as can only ever be one, just being sure*/
Hope you find it useful

You can always add ON DUPLICATE KEY UPDATE Read here (not exactly, but solves your problem it seems).
From the comments, by #ravi
Whether the increment occurs or not depends on the
innodb_autoinc_lock_mode setting. If set to a non-zero value, the
auto-inc counter will increment even if the ON DUPLICATE KEY fires

I had the same problem but didn't want to use innodb_autoinc_lock_mode = 0 since it felt like I was killing a fly with a howitzer.
To resolve this problem I ended up using a temporary table.
create temporary table mytable_temp like mytable;
Then I inserted the values with:
insert into mytable_temp values (null,'valA'),(null,'valB'),(null,'valC');
After that you simply do another insert but use "not in" to ignore duplicates.
insert into mytable (myRow) select mytable_temp.myRow from mytable_temp
where mytable_temp.myRow not in (select myRow from mytable);
I haven't tested this for performance, but it does the job and is easy to read. Granted this was only important because I was working with data that was constantly being updated so I couldn't ignore the gaps.

modified the answer from mu is too short, (simply remove one line)
as i am newbie and i cannot make comment below his answer. Just post it here
the query below works for the first tag
INSERT INTO tablename (tag)
SELECT $tag
WHERE NOT EXISTS(
SELECT tag
FROM tablename
WHERE tag = $tag
)

I just put an extra statement after the insert/update query:
ALTER TABLE table_name AUTO_INCREMENT = 1
And then he automatically picks up the highest prim key id plus 1.

Related

In SQL, can you nest a SELECT statement in the VALUES clause of the INSERT statement?

In SQL is it possible to nest a SELECT statement in the VALUES clause of the INSERT statement? I'm using MySQL and would like to query the table for the max value of a field plus one when creating a new record, as shown below.
INSERT into attornies (
LawOfficeId, LawOfficeName
) VALUES (
(select max(LawOfficeID)+1 from attornies),
'Wee, Sue Em and Howe'
);
I'm not sure if my syntax is bad, what I'm trying to do is not possible, etc. Of course, If I try this as two separate statements it works but I would like to make it one statement. I know one suggestion is to use auto increment but I don't want to.
If this question has already been answered please point me in that direction. If not .... Help.
Yes you can do it
INSERT into attornies (
LawOfficeId, LawOfficeName
)
select max(LawOfficeID) + 1,'Wee, Sue Em and Howe' from attornies;
However this is not recommended over auto increment since you are likely going to run into duplicate primary key situation if multiple threads are doing the insert
Your query should work. Scalar subqueries are allowed in the values clause of the insert.
In general, it would more commonly be written as:
insert into attornies (LawOfficeId, LawOfficeName)
select max(LawOfficeID)+1, 'Wee, Sue Em and Howe'
from attornies;
However, this is not the right way to do what you seem to want. Instead, create attornies -- which I am renaming to lawOffices because that seems to be the intention -- with an auto-increment column:
create table lawOffices (
lawOfficeId int auto_increment primary key,
lawOfficeName varchar(255)
);
And then do:
insert into lawOffices (lawOfficeName)
values ( 'Wee, Sue Em and Howe' );
The database does the work of incrementing the id.
You can't use the syntax you show.
mysql> INSERT into attornies (
-> LawOfficeId, LawOfficeName
-> ) VALUES (
-> (select max(LawOfficeID)+1 from attornies),
-> 'Wee, Sue Em and Howe'
-> );
ERROR 1093 (HY000): You can't specify target table 'attornies' for update in FROM clause
https://dev.mysql.com/doc/refman/8.0/en/subquery-errors.html says:
You can use a subquery for assignment within an UPDATE statement because subqueries are legal in UPDATE and DELETE statements as well as in SELECT statements. However, you cannot use the same table (in this case, table t1) for both the subquery FROM clause and the update target.
The doc is talking about using a subquery in an UPDATE statement, but the same rule applies in an INSERT or DELETE statement.
However, this works:
mysql> INSERT into attornies (
-> LawOfficeId, LawOfficeName
-> )
-> select max(LawOfficeID) + 1,'Wee, Sue Em and Howe' from attornies;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
https://dev.mysql.com/doc/refman/8.0/en/insert-select.html says:
The target table of the INSERT statement may appear in the FROM clause of the SELECT part of the query. However, you cannot insert into a table and select from the same table in a subquery.
When selecting from and inserting into the same table, MySQL creates an internal temporary table to hold the rows from the SELECT and then inserts those rows into the target table.
I agree with the other answers and comments who warn you against using the MAX()+1 method of getting the next id value. This method is susceptible to race conditions. Use an auto-increment column instead.
Okay, you've all convinced me. In addition, I did a bit of testing of import/export with an auto-increment field in MySQL. My concerns are no longer justified. I'll discard my max()+1 idea as unnecessary and go with auto-increment.
I would also like to thank Steve, Bill Karwin and Gordon Linoff for correcting my SQL and pointing me in the proper direction if I had continued with my original idea.

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.

How to UPDATE just one record in DB2?

In DB2, I need to do a SELECT FROM UPDATE, to put an update + select in a single transaction.
But I need to make sure to update only one record per transaction.
Familiar with the LIMIT clause from MySQL's UPDATE option
places a limit on the number of rows that can be updated
I looked for something similar in DB2's UPDATE reference but without success.
How can something similar be achieved in DB2?
Edit: In my scenario, I have to deliver 1000 coupon codes upon request. I just need to select (any)one that has not been given yet.
The question uses some ambiguous terminology that makes it unclear what needs to be accomplished. Fortunately, DB2 offers robust support for a variety of SQL patterns.
To limit the number of rows that are modified by an UPDATE:
UPDATE
( SELECT t.column1 FROM someschema.sometable t WHERE ... FETCH FIRST ROW ONLY
)
SET column1 = 'newvalue';
The UPDATE statement never sees the base table, just the expression that filters it, so you can control which rows are updated.
To INSERT a limited number of new rows:
INSERT INTO mktg.offeredcoupons( cust_id, coupon_id, offered_on, expires_on )
SELECT c.cust_id, 1234, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 30 DAYS
FROM mktg.customers c
LEFT OUTER JOIN mktg.offered_coupons o
ON o.cust_id = c.cust_id
WHERE ....
AND o.cust_id IS NULL
FETCH FIRST 1000 ROWS ONLY;
This is how DB2 supports SELECT from an UPDATE, INSERT, or DELETE statement:
SELECT column1 FROM NEW TABLE (
UPDATE ( SELECT column1 FROM someschema.sometable
WHERE ... FETCH FIRST ROW ONLY
)
SET column1 = 'newvalue'
) AS x;
The SELECT will return data from only the modified rows.
You have two options. As noted by A Horse With No Name, you can use the primary key of the table to ensure that one row is updated at a time.
The alternative, if you're using a programming language and have control over cursors, is to use a cursor with the 'FOR UPDATE' option (though that may be probably optional; IIRC, cursors are 'FOR UPDATE' by default when the underlying SELECT means it can be), and then use an UPDATE statement with the WHERE CURRENT OF <cursor-name> in the UPDATE statement. This will update the one row currently addressed by the cursor. The details of the syntax vary with the language you're using, but the raw SQL looks like:
DECLARE CURSOR cursor_name FOR
SELECT *
FROM SomeTable
WHERE PKCol1 = ? AND PKCol2 = ?
FOR UPDATE;
UPDATE SomeTable
SET ...
WHERE CURRENT OF cursor_name;
If you can't write DECLARE in your host language, you have to do manual bashing to find the equivalent mechanism.

Edit the latest row in the database?

How can I edit the latest row in the database. I only know it's the last one. I don't know its id.
I don't know which language you are working with, in PHP's mySQL functions you can use
mysql_insert_id()
there are similar function in every other mySQL client library I know of.
Also, there is a native mySQL function!
LAST_INSERT_ID() (with no argument)
returns the first automatically
generated value that was set for an
AUTO_INCREMENT column by the most
recently executed INSERT statement to
affect such a column. For example,
after inserting a row that generates
an AUTO_INCREMENT value, you can get
the value like this:
mysql> SELECT LAST_INSERT_ID();
-> 195
Of course, a primary key with AUTO_INCREMENT is required for these functions to work.
For a table with an auto_increment id field:
UPDATE tbl SET col1 = 'val1' WHERE id = MAX(id);
If it's a row that has been inserted in your script (the same script from which you want to update it) and there is an auto_increment column on your table, you can get that auto_increment value, using functions such as those, for PHP :
mysql_insert_id
mysqli_insert_id
PDO::lastInsertId
There should be an equivalent for probably any language you can possibly be using for your application.
If your are trying to do an update from another script than the one in which you did the insert, and still have an auto_increment column, the best way will probably be to update the row that has the biggest value for that column :
update your_table
set your_column = ...
where id = max(id)
Or, in two steps (not sure it'll work in one) :
select max(id) as id from your_table
update your_table set your_column = ... where id = [what you got with thr first query]
You can also use UPDATE table SET ... WHERE id=LAST_INSERT_ID() (supposing the last insert was on the table you want to query).
I would not use TWO steps to find the last insert ID simply because a new record could be added in the mean time.
Depending on your version, you should be able to call $handle->last_id(); or $handle->{mysql_insertid};
Chris

Is it really no solution to update multiple records in MySQL?

I want to do all these update in one statement.
update table set ts=ts_1 where id=1
update table set ts=ts_2 where id=2
...
update table set ts=ts_n where id=n
Is it?
Use this:
UPDATE `table` SET `ts`=CONCAT('ts_', `id`);
Yes you can but that would require a table (if only virtual/temporary), where you's store the id + ts value pairs, and then run an UPDATE with the FROM syntax.
Assuming tmpList is a table with an id and a ts_value column, filled with the pairs of id value, ts value you wish to apply.
UPDATE table, tmpList
SET table.ts = tmpList.ts_value
WHERE table.id = tmpList.id
-- AND table.id IN (1, 2, 3, .. n)
-- above "AND" is only needed if somehow you wish to limit it, i.e
-- if tmpTbl has more idsthan you wish to update
A possibly table-less (but similar) approach would involve a CASE statement, as in:
UPDATE table
SET ts = CASE id
WHEN 1 THEN 'ts_1'
WHEN 2 THEN 'ts_2'
-- ..
WHEN n THEN 'ts_n'
END
WHERE id in (1, 2, ... n) -- here this is necessary I believe
Well, without knowing what data, I'm not sure whether the answer is yes or no.
It certainly is possible to update multiple rows at once:
update table table1 set field1='value' where field2='bar'
This will update every row in table2 whose field2 value is 'bar'.
update table1 set field1='value' where field2 in (1, 2, 3, 4)
This will update every row in the table whose field2 value is 1, 2, 3 or 4.
update table1 set field1='value' where field2 > 5
This will update every row in the table whose field2 value is greater than 5.
update table1 set field1=concat('value', id)
This will update every row in the table, setting the field1 value to 'value' plus the value of that row's id field.
You could do it with a case statement, but it wouldn't be pretty:
UPDATE table
SET ts = CASE id WHEN 1 THEN ts_1 WHEN 2 THEN ts_2 ... WHEN n THEN ts_n END
I think that you should expand the context of the problem. Why do you want/need all the updates to be done in one statement? What benefit does that give you? Perhaps there's another way to get that benefit.
Presumably you are interacting with sql via some code, so certainly you can simply make sure that the three updates all happen atomically by creating a function that performs all three of the updates.
e.g. pseudocode:
function update_all_three(val){
// all the updates in one function
}
The difference between a single function update and some kind of update that performs multiple updates at once is probably not a very useful distinction.
generate the statements:
select concat('update table set ts = ts_', id, ' where id = ', id, '; ')
from table
or generate the case conditions, then connect it to your update statement:
select concat('when ', id, ' then ts_', id) from table
You can use INSERT ... ON DUPLICATE KEY UPDATE. See this quesion: Multiple Updates in MySQL
ts_1, ts_2, ts_3, etc. are different fields on the same table? There's no way to do that with a single statement.