21

I've read some posts about this but none cover this issue.

I guess its not possible, but I'll ask anyway.

I have a table with more than 50.000 registers. It's an old table where various insert/delete operations have taken place.

That said, there are various 'holes' some of about 300 registers. I.e.: ..., 1340, 1341, 1660, 1661, 1662,...

The question is. Is there a simple/easy way to make new inserts fill these 'holes'?

Paulo Bueno
  • 2,499
  • 6
  • 42
  • 68
  • 3
    Consider this: `shoes` have id 1, `shirts` have id 2, `jackets` have id 3. You decide to delete `shirts`, then you decide to add `pants`. If you do fill in the gap, then for a brief period of time, the search engines will lead those who are looking for shirts to `example.com/product?id=2`, which is...`pants`. – Hello World Jan 26 '15 at 16:44
  • 1
    @HelloWorld that's a great point. Although these days search engines update fairly quickly, so depending on how fast holes get filled, that may or may not be a show stopper. Also, if the gap filling is needed for an internal CRM system, etc... then search engines don't reach that internal area, so in that case search engine consideration would not apply. –  Jan 09 '20 at 21:35

7 Answers7

15

I agree with @Aaron Digulla and @Shane N. The gaps are meaningless. If they DO mean something, that is a flawed database design. Period.

That being said, if you absolutely NEED to fill these holes, AND you are running at least MySQL 3.23, you can utilize a TEMPORARY TABLE to create a new set of IDs. The idea here being that you are going to select all of your current IDs, in order, into a temporary table as such:

CREATE TEMPORARY TABLE NewIDs
(
    NewID INT UNSIGNED AUTO INCREMENT,
    OldID INT UNSIGNED
)

INSERT INTO NewIDs (OldId)
SELECT
    Id
FROM
    OldTable
ORDER BY
    Id ASC

This will give you a table mapping your old Id to a brand new Id that is going to be sequential in nature, due to the AUTO INCREMENT property of the NewId column.

Once this is done, you need to update any other reference to the Id in "OldTable" and any foreign key it utilizes. To do this, you will probably need to DROP any foreign key constraints you have, update any reference in tables from the OldId to the NewId, and then re-institute your foreign key constraints.

However, I would argue that you should not do ANY of this, and just understand that your Id field exists for the sole purpose of referencing a record, and should NOT have any specific relevance.

UPDATE: Adding an example of updating the Ids

For example:

Let's say you have the following 2 table schemas:

CREATE TABLE Parent
(
    ParentId INT UNSIGNED AUTO INCREMENT,
    Value INT UNSIGNED,
    PRIMARY KEY (ParentId)
)

CREATE TABLE Child
(
    ChildId INT UNSIGNED AUTO INCREMENT,
    ParentId INT UNSIGNED,
    PRIMARY KEY(ChildId),
    FOREIGN KEY(ParentId) REFERENCES Parent(ParentId)
)

Now, the gaps are appearing in your Parent table.

In order to update your values in Parent and Child, you first create a temporary table with the mappings:

CREATE TEMPORARY TABLE NewIDs
(
    Id INT UNSIGNED AUTO INCREMENT,
    ParentID INT UNSIGNED
)

INSERT INTO NewIDs (ParentId)
SELECT
    ParentId
FROM
    Parent
ORDER BY
    ParentId ASC

Next, we need to tell MySQL to ignore the foreign key constraint so we can correctly UPDATE our values. We will use this syntax:

SET foreign_key_checks = 0;

This causes MySQL to ignore foreign key checks when updating the values, but it will still enforce the correct value type is used (see MySQL reference for details).

Next, we need to update our Parent and Child tables with the new values. We will use the following UPDATE statement for this:

UPDATE
    Parent,
    Child,
    NewIds
SET
    Parent.ParentId = NewIds.Id,
    Child.ParentId = NewIds.Id
WHERE
    Parent.ParentId = NewIds.ParentId AND
    Child.ParentId = NewIds.ParentId

We now have updated all of our ParentId values correctly to the new, ordered Ids from our temporary table. Once this is complete, we can re-institute our foreign key checks to maintain referential integrity:

SET foreign_key_checks = 1;

Finally, we will drop our temporary table to clean up resources:

DROP TABLE NewIds

And that is that.

karlgrz
  • 14,485
  • 12
  • 47
  • 58
  • I had no idea you could do a join on multiple tables in an UPDATE. Very cool, thanks! – Shane N Apr 10 '16 at 02:17
  • 1
    Gaps are not always meaningless at all if data is rotated. Example: MyVals are logged, old MyVals are removed after x time. In a year the AI value could be astronomical and eventually hit the limit. The AI value may be insignificant in use but relevant for providing the primary key. You can't generalise without taking into account every use case. – JBES Feb 04 '23 at 16:51
7

What is the reason you need this functionality? Your db should be fine with the gaps, and if you're approaching the max size of your key, just make it unsigned or change the field type.

Shane N
  • 1,742
  • 2
  • 17
  • 24
  • Thanks Shane, that's exactly my worry. I'm using mediumint and it may be used for some years. But if needed i could change it. – Paulo Bueno Dec 03 '09 at 16:39
  • @PauloBueno If you come close to running out of space, simply update the schema to a larger data-type. *Many, many, many, many, many* values can be represented by 64 bits. –  Dec 11 '11 at 20:51
  • 2
    Another question: what happens when reaching the max size of the auto-increment value? Will it flow over and - when set to `unsigned` - start again with 0 (or 1)? – johk95 Jun 06 '13 at 15:02
  • @johk95 If you're approaching the max size, can you just change the type of the field? According to , an unsigned bigint has a max value of 18,446,744,073,709,551,615 (I don't even know what to call that number but it's huge!). I wouldn't suggest letting things flow over, could get unpredictable results. – Shane N Jun 07 '13 at 15:44
  • @ShaneN well, that would be an option. But I think it would be better for me to just manually reset the autoincrement value since I only maintain the last 50 entries in my database. – johk95 Jun 10 '13 at 06:36
  • 3
    @johk95 the answer is that mysql throws an error and the insert operation simply fails. – Paulo Bueno Jun 20 '13 at 07:27
  • @ShaneN it might be a silly question, but can't it be considered an aesthetical issue? I work on a smaller database at home, and possibly I'm wrong, but I find it extremely irritating that this number doesn't precisely show a row's "rank" in the set of rows. – Zoltán Schmidt Apr 07 '16 at 22:20
  • 2
    @ZoltánSchmidt Not a silly question, I understand that it can be aesthetic. However usually databases are part of the infrastructure of an app/program, so the app will make things appear aesthetically pleasing (ie, calculate the 'rank' and show that instead of the id). To think databases need to be pretty is like saying you'd like the studs in the walls of your home to be painted. – Shane N Apr 10 '16 at 02:23
  • 2
    This does not answer the question and should not be the accepted answer. Practical or not, there is a _right_ answer to this question out there. – Benjamin Kohl Feb 24 '17 at 00:23
  • 1
    @BenjaminKohl is correct. This is not the correct answer. It supposes one should change the field datatypes of forty (random number) other tables with foreign keys to the original key. One could also max his column out on a development environment, for performance benchmarking, for example, while there is no need to update the production database. Further, to say we should not implement efficiency because we have databases that are efficient enough, is self-defeating. The answer to "Is there a simple/easy way" is a straight "No". The right way will depend on the OP's exact environment. –  Jan 09 '20 at 22:26
  • I agree that this should not be the accepted answer, you cannot keep changing types endless! (it could be an emergency fix to buy time for the real solution tough...) https://stackoverflow.com/a/47646618/6280369 shows how to fill the gaps, the only thing left is to update your auto increment field counter to MAX+1 ... that's more of a real solution. – WiRa Apr 03 '20 at 09:09
6

You generally don't need to care about gaps. If you're getting to the end of the datatype for the ID it should be relatively easy to ALTER the table to upgrade to the next biggest int type.

If you absolutely must start filling gaps, here's a query to return the lowest available ID (hopefully not too slowly):

SELECT MIN(table0.id)+1 AS newid
FROM table AS table0
LEFT JOIN table AS table1 ON table1.id=table0.id+1
WHERE table1.id IS NULL

(remember to use a transaction and/or catch duplicate key inserts if you need concurrent inserts to work.)

bobince
  • 528,062
  • 107
  • 651
  • 834
  • A small amendment: if the table is empty this query will return NULL instead of 1. The quick fix is to use SELECT IFNULL(MIN(table0.id), 0) + 1 ... – src091 Nov 07 '11 at 11:13
2
INSERT INTO prueba(id) 
VALUES (
(SELECT IFNULL( MAX( id ) , 0 )+1 FROM prueba target))

IFNULL for skip null on zero rows count

add target for skip error mysql "error clause FROM)

Aziz Shaikh
  • 16,245
  • 11
  • 62
  • 79
danis
  • 69
  • 1
1

There is a simple way but it doesn't perform well: Just try to insert with an id and when that fails, try the next one.

Alternatively, select an ID and when you don't get a result, use it.

If you're looking for a way to tell the DB to automatically fill the gaps, then that's not possible. Moreover, it should never be necessary. If you feel you need it, then you're abusing an internal technical key for something but the single purpose it has: To allow you to join tables.

[EDIT] If this is not a primary key, then you can use this update statement:

update (
    select *
    from table
    order by reg_id -- this makes sure that the order stays the same
)
set reg_id = x.nextval

where x is a new sequence which you must create. This will renumber all existing elements preserving the order. This will fail if you have foreign key constraints. And it will corrupt your database if you reference these IDs anywhere without foreign key constraints.

Note that during the next insert, the database will create a huge gap unless you reset the identity column.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • DB automatically filling gaps is possible, it's just not implemented. Filling gaps could be necessary when running out of space. The single purpose of a key is for O(log n) SELECTs. You can join without using keys. O(log n) SELECTs benefit non-joined tables (single table queries). –  Jan 09 '20 at 21:28
1

As others have said, it doesn't matter, and if it does then something is wrong in your database design. But personally I just like them to be in order anyway!

Here is some SQL that will recreate your IDs in the same order, but without the gaps.

It is done first in a temp_id field (which you will need to create), so you can see that it is all good before overwriting your old IDs. Replace Tbl and id as appropriate.

SELECT @i:=0;
UPDATE Tbl
JOIN
(
    SELECT id
    FROM Tbl
    ORDER BY id
) t2
ON Tbl.id = t2.id
SET temp_id = @i:=@i+1;

You will now have a temp_id field with all of your shiny new IDs. You can make them live by simply:

UPDATE Tbl SET id = temp_id;

And then dropping your temp_id column.

I must admit I'm not quite sure why it works, since I would have expected the engine to complain about duplicate IDs, but it didn't when I ran it.

  • 3
    "it doesn't matter, and if it does then something is wrong in your database design." that may be true in most cases, but not all cases. It is not a fact. On another note, your solution is quite clever, and I like it. Although as it runs two queries that affect the whole table and then a table operation, the table should be locked to prevent race conditions. LOCK TABLES Tbl WRITE; then your two queries, then ALTER TABLE Tbl AUTO_INCREMENT = SELECT (COUNT(*) + 1) FROM Tbl; then UNLOCK TABLES;. Consider adding those to your answer. –  Apr 19 '20 at 21:55
-1

You might wanna clean up gaps in a priority column. The way below will give an auto increment field for the priority. The extra left join on the same tabel will make sure it is added in the same order as (in this case) the priority

SET @a:=0;
REPLACE INTO footable
 (id,priority)
    (
    SELECT tbl2.id, @a 
    FROM footable as tbl
    LEFT JOIN footable as tbl2 ON tbl2.id = tbl.id  
    WHERE (select @a:=@a+1)
    ORDER BY tbl.priority
)