4

My table have this style:

ID - EMAIL - VERSION - LASTUPDATE

where id = is auto increment and Primary:

and email is UNIQUE

so everytime someone use my Application it get the user Email and try to insert, if the email already exist, it UPDATE the row to new values for $version and $lastupdate

it almostly works 100%, its already updating the row perfectly, the problem is that when it do the UPDATE, still increase the auto increment id, so the next INSERT will not be the lastID plus one, and will be another number example:

ID - EMAIL - VERSION - LASTUPDATE
-----------------------------------
1 - test1@... - 1.0 - currentTime();

if the test2 load my app it will add and the database will be:

ID - EMAIL - VERSION - LASTUPDATE
-----------------------------------
1 - test1@.. - 1.0 - currentTime();
2 - test2@.. - 1.0 - currentTime();

so imagine now that the test2 has the new version and it update to this:

ID - EMAIL - VERSION - LASTUPDATE
-----------------------------------
1 - test1@.. - 1.1 - currentTime();
2 - test2@.. - 1.0 - currentTime();

perfectly isnt it? so the problem start when the test3 open my app, the id will not be 3, it will be 4 because the update added 1 more to the id(i dont know why)

ID - EMAIL - VERSION - LASTUPDATE
-----------------------------------
1 - test1@.. - 1.1 - currentTime();
2 - test2@.. - 1.0 - currentTime();
4 - test3@.. - 1.1 - currentTime();

why its increasing the plus one to the ID when do the update? i dont want it =(

my sql syntax:

$sql = "INSERT INTO `backups`.`regs` (`email`, `version`, `lastupdate`) VALUES ('$email', '$version', '$lastupdate')  ON DUPLICATE KEY UPDATE version='$version', lastupdate='$lastupdate'";
user2582318
  • 1,607
  • 5
  • 29
  • 47
  • You can accomplish that by using transaction – Branimir Đurek Aug 14 '15 at 20:01
  • @BranimirĐurek any tutorial please? i have no idea how – user2582318 Aug 14 '15 at 20:01
  • here is a simple php example of using transaction http://www.w3schools.com/php/func_mysqli_commit.asp. Basically, transaction is something like IF state. If all queries were successfully executed, then commit that changes, but if one of your queries fail, then do nothing :) – Branimir Đurek Aug 14 '15 at 20:08
  • 1
    i would love to see the create table and test this. that is bizarre – Drew Aug 14 '15 at 20:14
  • I remember now how not bizarre this issue was, @BK435 – Drew Aug 14 '15 at 20:32
  • If a duplicate entry is "hit", the auto_increment will be wasted. Not for the **current** record, its auto_increment will remain the same. You will have gaps due to this, meaning if your auto_increment value for record was 1, if you inserted and went to `ON DUPLICATE KEY UPDATE` part, the number 2 will be used, will be discarded - and that's just perfectly normal, you shouldn't "defend" against it and just leave it be. There are reasons why it works like that, if you touch anything - kiss your integrity, performance and durability good bye. – N.B. Aug 14 '15 at 21:39

3 Answers3

3

ok yeah, now I remember this issue. There was a guy once that wanted to do inserts, but each insert had to be in increments of 100 if you could imagine, starting @1000. And we had to wrap the whole thing in a stored proc to have one place of vulnerability. Your issue surfaced and it threw off his numbering by 1 or so.

By wrapping it, we could sanely have one point of doing it, with a lock, and maintain the auto_inc value with ALTER TABLE

The other approach I said to him was have an incrementer table, lock the 1 row, get the value in that row, use it, update that incTable by 100. unlock.

The whole time we were laughing about OCD issues. I think he liked multiples of 10 only, idk

Edit:

Schema:

-- drop table ocd_nextnums;
create table ocd_nextnums
(   -- id table for nextnum, for the OCD impaired
    tableName varchar(100) not null,
    nextnum int not null
    -- won't bother with indexes, go for it if you want
)engine=INNODB; -- note engine type

insert ocd_nextnums(tableName,nextnum) values('thing',1);
insert ocd_nextnums(tableName,nextnum) values('some_other_table',1);

-- drop table thing;
create table thing
(   id int primary key, -- NOT an auto_increment, but is a PK
    email varchar(100) not null,
    version varchar(20) not null,
    lastupdate datetime not null,
    UNIQUE KEY (email)
)engine=MyIsam;

Stored Proc:

-- drop procedure putInThing;
delimiter $$
create procedure putInThing
(
    email_In varchar(100), version_In varchar(20)
)
BEGIN
    declare toUse int;
    declare theCount int;

    select count(*) into theCount from thing where email=email_In;
    select id into toUse from thing where email=email_In;   -- useful for result set @end
    IF theCount=1 THEN
        -- was there, do UPDATE
        update thing set version=version_In,lastupdate=now() where email=email_In;
    ELSE
        -- new row, do INSERT (please note the FOR UPDATE clause)
        select nextnum into toUse from ocd_nextnums where tableName='thing' FOR UPDATE;
        update ocd_nextnums set nextnum=nextnum+1 where tableName='thing';

        insert thing(id,email,version,lastupdate) values (toUse,email_In,version_In,now());
    end if;
    select toUse;   -- <------- that was your id
END
$$

Test:

call putInThing('t1@t.com','111');
call putInThing('t2@t.com','121');
call putInThing('t3@t.com','107');
select * from thing;
+----+----------+---------+---------------------+
| id | email    | version | lastupdate          |
+----+----------+---------+---------------------+
|  1 | t1@t.com | 111     | 2015-08-14 17:08:10 |
|  2 | t2@t.com | 121     | 2015-08-14 17:08:54 |
|  3 | t3@t.com | 107     | 2015-08-14 17:08:56 |
+----+----------+---------+---------------------+

call putInThing('t3@t.com','101111007'); -- is an update
call putInThing('t3@t.com','42'); -- is an update
call putInThing('t3@t.com','10007'); -- is an update
call putInThing('h@hillaryclinton.com','1'); -- is an insert

select * from thing;
+----+----------------------+---------+---------------------+
| id | email                | version | lastupdate          |
+----+----------------------+---------+---------------------+
|  1 | t1@t.com             | 111     | 2015-08-14 17:08:10 |
|  2 | t2@t.com             | 121     | 2015-08-14 17:08:54 |
|  3 | t3@t.com             | 10007   | 2015-08-14 17:22:09 |
|  4 | h@hillaryclinton.com | 1       | 2015-08-14 17:22:47 |
+----+----------------------+---------+---------------------+

From the Mysql INNODB part of Manual:

To implement reading and incrementing the counter, first perform a locking read of the counter using FOR UPDATE, and then increment the counter.....

Will you see me using this, probably not. Just showing it. I am fine with gaps and sleeping at night. Which is why I named the first table what I did :>

Drew
  • 24,851
  • 10
  • 43
  • 78
  • I got the message ^^! so the best way is to before insert check if it exist, if not insert, if exist = update row, isn't it? so I avoid the gaps and the OCD issues =P – user2582318 Aug 15 '15 at 05:27
  • marked as question because it teach me the way and the problems that I can cause, so I will prefer to first check if exist, if not insert, if exist update the row (I think doing this update out of the ON DUPLICATE will not increase the counter, since this is the normal behavior) thx Drew – user2582318 Aug 15 '15 at 05:30
  • solved using this -> http://stackoverflow.com/questions/17465468/how-to-check-if-user-already-exists-in-mysql-with-php just added the UPDATE inside the IF ;) thank you again Drew – user2582318 Aug 15 '15 at 05:58
  • my pleasure. whatever you choose, hopefully it is safe ! – Drew Aug 15 '15 at 13:58
2

Fro what you describe you have the wrong value in $version before you carried out the insert. And that is populated in code you've not shown us.

You should be using the version number already held in the database:

INSERT INTO `backups`.`regs` 
(`email`, `version`, `lastupdate`) 
VALUES ('$email', 1, '$lastupdate')  
ON DUPLICATE KEY 
UPDATE version=version+1, lastupdate='$lastupdate'"
symcbean
  • 47,736
  • 6
  • 59
  • 94
1

Because you already passed the values, then you should use version = VALUES(version), lastupdate = VALUES(lastupdate) for ON DUPLICATE, Try this:

$sql = "INSERT INTO `backups`.`regs` (`email`, `version`, `lastupdate`) VALUES ('$email', '$version', '$lastupdate')  ON DUPLICATE KEY UPDATE version = VALUES(version), lastupdate = VALUES(lastupdate)";
Shafizadeh
  • 9,960
  • 12
  • 52
  • 89