4

My problem is the same as Why does MySQL autoincrement increase on failed inserts?, but instead of increasing my id field, I would prefer just to rewrite the INSERT query that is causing me trouble. Pretend I have a database with two fields, id and username, where id is a primary key and username is a unique key. I'm essentially looking for syntax that would do INSERT...IF NOT EXISTS. Right now, I do

INSERT INTO `table`(`username`) 
    VALUES ('foo') 
    ON DUPLICATE KEY UPDATE `id`=`id`

I only have one thread writing to the database, so I don't need any sort of concurrency protection. Suggestions?

Community
  • 1
  • 1
Andy Shulman
  • 1,895
  • 3
  • 23
  • 32

5 Answers5

6

You should use this:

INSERT INTO tableX (username) 
  SELECT 'foo' AS username
  FROM dual
  WHERE NOT EXISTS
        ( SELECT *
          FROM tableX 
          WHERE username = 'foo'
        ) ;

If you want to include values for more columns:

INSERT INTO tableX (username, dateColumn) 
  SELECT 'foo'                       --- the aliases are not needed             
       , NOW()                       --- actually
  FROM dual
  WHERE NOT EXISTS
        ( SELECT *
          FROM tableX 
          WHERE username = 'foo'
        ) ;                      
ypercubeᵀᴹ
  • 113,259
  • 19
  • 174
  • 235
  • What's `FROM dual`? **EDIT** Ignore me. Found it in the manual. Great answer +1 – eggyal May 15 '12 at 12:23
  • `dual` is a dummy table with one row exactly (originally in Oracle DBMS, where it had 2 rows, thus the name). It can be used to construct a 1-row table. – ypercubeᵀᴹ May 15 '12 at 12:24
  • Why does this work when the manual says "*you cannot insert into a table and select from the same table in a subquery*"? Is it because one only inserts if nothing was selected? Or does `EXISTS` avoid actually selecting anything? – eggyal May 15 '12 at 12:30
  • @eggyal: Where does it say that? (Avctually I'm selecting from 'dual' here, not from the same table but that's anotehr issue). – ypercubeᵀᴹ May 15 '12 at 12:31
  • Third bullet - http://dev.mysql.com/doc/en/insert-select.html; won't the `EXISTS` subquery lock the table and prevent a write? – eggyal May 15 '12 at 12:31
  • No it works fine. Maybe the docs are wrong or are refering to previous versions, I don't know. – ypercubeᵀᴹ May 15 '12 at 12:49
  • @ypercube How does this work if you want to insert multiple columns? On my real table, I have a `DATETIME` column that I want to fill with `NOW()`, but doing an `INSERT INTO...VALUES` with your select statement as one of the args isn't working. – Andy Shulman May 15 '12 at 21:10
  • I haven't `VALUES` into my example. – ypercubeᵀᴹ May 15 '12 at 21:12
  • @eggyal: What is not allowed is this kind of subqueries (inside `VALUES`): `INSERT INTO tableX VALUES( 1, 2, (SELECT MIN(id) FROM tableX) )` – ypercubeᵀᴹ May 15 '12 at 21:18
  • I have used the same table in `FROM` clause in much more complex statements, like: `INSERT INTO t SELECT a.id * m + b.id + m FROM t AS a JOIN t AS b ON b.id <= a.id CROSS JOIN (SELECT MAX(id) AS m FROM t) AS maxt` – ypercubeᵀᴹ May 15 '12 at 21:21
  • @Andy: see my edit. You can't mix `INSERT ... VALUES` with `INSERT ... SELECT` syntax. Use either one or the other. – ypercubeᵀᴹ May 15 '12 at 21:25
4

I don't think you can prevent the counter from being incremented, for the reasons given in the answers the question to which you've linked.

You have three options:

  1. Live with skipped identifiers; do you really expect to use up 64-bits?

  2. Check for existence of the existing record prior to attempting the INSERT:

    DELIMITER ;;
    
    IF NOT EXISTS (SELECT * FROM `table` WHERE username = 'foo') THEN
      INSERT INTO `table` (username) VALUES ('foo');
    END IF;;
    
    DELIMITER ;
    

    Or, better yet, use the FROM dual WHERE NOT EXISTS ... form suggested by @ypercube.

  3. Reset the counter after each insert. If performing the INSERT operation within a stored procedure, you could do this using a handler for duplicate key errors:

    DELIMITER ;;
    
    CREATE PROCEDURE TEST(IN uname CHAR(16) CHARSET latin1) BEGIN
      DECLARE CONTINUE HANDLER FOR SQLSTATE '23000' BEGIN
        -- WARNING: THIS IS NOT SAFE FOR CONCURRENT CONNECTIONS
        SET @qry = CONCAT(
          'ALTER TABLE `table` AUTO_INCREMENT = ',
          (SELECT MAX(id) FROM `table`)
        );
        PREPARE stmt FROM @qry;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
        SET @qry = NULL;
      END;
      INSERT INTO `table` (username) VALUES (uname);
    END;;
    DELIMITER ;
    

    Bearing in mind the (valid) concerns @ypercube raised in his comments beneath regarding this strategy, you might instead use:

    SELECT AUTO_INCREMENT - 1
    FROM   INFORMATION_SCHEMA.TABLES
    WHERE  table_schema = 'db_name' AND table_name = 'table';
    
eggyal
  • 122,705
  • 18
  • 212
  • 237
  • No to 4, never use option 4. It's not safe. – ypercubeᵀᴹ May 15 '12 at 12:36
  • I tried adding your "ALTER TABLE" in a stored procedure, but MySQL yelled at me. What's wrong here? `CREATE DEFINER=\`me\`@\`localhost\` PROCEDURE \`TEST\`(IN \`uname\` CHAR(16) CHARSET latin1) NOT DETERMINISTIC MODIFIES SQL DATA SQL SECURITY DEFINER INSERT INTO \`test\` ( \`username\` ) VALUES ( uname ) ON DUPLICATE KEY (ALTER TABLE \`table\` AUTO_INCREMENT = (SELECT MAX(id) FROM \`table\`));` – Andy Shulman May 15 '12 at 12:36
  • @ypercube: Why is it not safe, given the OP stated he has "*only one thread writing to the database, so I don't need any sort of concurrency protection*"? – eggyal May 15 '12 at 12:37
  • @AndyShulman: You can only `ON DUPLICATE UPDATE ...`; trying to `ALTER TABLE` in that fashion is a syntax error. You must use separate statements as shown. – eggyal May 15 '12 at 12:39
  • I guess I misunderstood your comment about "handler for duplicate key errors", then. Could you elaborate? – Andy Shulman May 15 '12 at 12:40
  • @eggyal: Because he may need it in the future. Will he (or the guy responsible in 5 years) remember to change this code? – ypercubeᵀᴹ May 15 '12 at 12:43
  • And even more crucial, what happens if some records have been previously deleted? Then the `MAX(id)` will be smaller than some `id` which has been already used (and deleted). This may be fine but it may not be (if you had referencing rows in other tables that were left orphaned). In any case, re-using an `id` is not best practice. – ypercubeᵀᴹ May 15 '12 at 12:45
  • @ypercube That's why foreign key constraints are fantastic. – Andy Shulman May 15 '12 at 12:46
  • @ypercube: Agree with your first concern re future requirements/understanding; to allay the second, the approach in my updated answer (obtaining the discarded value by proxy through the information schema) should suffice? Besides which, restarting the server would resort to `MAX(id)` and cause those same concerns anyway... – eggyal May 15 '12 at 13:01
  • @eggyal Could you check that? That fails with a syntax error, for me. – Andy Shulman May 15 '12 at 13:04
  • @AndyShulman: I think you will have to go via a variable; see my updated answer. – eggyal May 15 '12 at 18:30
  • @eggyal Now it's `near 'id; END; INSERT INTO \`test_table\` (\`username\`) VALUES ( uname ); END' at line 7` – Andy Shulman May 15 '12 at 18:34
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/11275/discussion-between-eggyal-and-andy-shulman) – eggyal May 15 '12 at 18:45
2

you can simply reset the auto increment field after the failed insert

ALTER TABLE table_name AUTO_INCREMENT =1;

this will not reset the auto increment to 1 but will reset it to the current maximum plus one

also note that the user you are connected with to the DB should has alter privilege to the targeted table

0

You can also check before insert that user if it was inserted before, it will cost an extra call into your DB but, if the user already exists, you can return the information that you found and a fake insert is avoided.

eloibm
  • 899
  • 2
  • 11
  • 27
-1

use INSERT IGNORE INTO table. Check the below post

http://bogdan.org.ua/2007/10/18/mysql-insert-if-not-exists-syntax.html

You can do it like:

INSERT INTO `table`(`username`) 
VALUES ('foo')      
ON DUPLICATE KEY UPDATE `username`= 'foo' 

OR

INSERT IGNORE INTO `table`
SET `username` = 'foo'
Rahul
  • 76,197
  • 13
  • 71
  • 125
  • 1
    No. This still autoincrements `id`, even when no row is inserted. – Andy Shulman May 15 '12 at 11:54
  • Sorry, I don't think so. If a duplicate key exist then it will skip it; if not then a new row will be inserted and obhiously id will be incremented. are you looking for a different behavior? – Rahul May 15 '12 at 12:00
  • 1
    It does. I tested it. I ran `INSERT IGNORE INTO \`test\`(\`username\`) VALUES ('testuser')` 5 times, and then checked what the next auto-increment value was. – Andy Shulman May 15 '12 at 12:02
  • You mean, even though it ignores and doesn't insert but still id gets autoincremented? that's strange ... haven't tested though. Would suggest you to post the same question in mysql forum as well. – Rahul May 15 '12 at 12:05
  • Yup. Inserted with `id` of 1, but on successful insert of a different username the next `id` is 6. – Andy Shulman May 15 '12 at 12:07