AUTO_INCREMENT and LAST_INSERT_ID(): inserting related rows safely
Part of the classic SQL Help series first published on this domain by David J. Lake (guelphdad). Rewritten from scratch and updated for MySQL 8 — same address, modern SQL.
The scenario: an article gets inserted into one table, and its category links (or tags, or details) go into a second table that references the article's id. The question is always the same — how do I get the new id to use in the second insert, without two users treading on each other?
The two wrong answers
Wrong answer #1: keep a counter yourself — in a file, a config row, or application memory. Two requests read the same counter at the same moment, both increment it, both insert with the same id. You've reinvented a race condition the database already solved.
Wrong answer #2: SELECT MAX(id)+1. Same race,
different clothes. Two connections both read MAX(id) as 100, both
compute 101, and either you get duplicate ids or (with a primary key) one insert
fails — and if related rows were inserted in between, your tables now disagree with
each other.
The right answer: AUTO_INCREMENT
CREATE TABLE articles ( article_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, content TEXT ); CREATE TABLE article_tags ( article_id INT UNSIGNED NOT NULL, tag VARCHAR(50) NOT NULL, PRIMARY KEY (article_id, tag) );
Leave the id column out of your insert (or pass NULL) and MySQL
assigns the next value atomically:
INSERT INTO articles (title, content)
VALUES ('Safe inserts', 'How auto_increment removes the race...');
The id it just assigned is available as LAST_INSERT_ID() — and this
is the crucial part — per connection. Another user inserting a
millisecond later gets their own value; yours doesn't change until you
insert another auto-increment row. No locks to manage, no coordination needed:
INSERT INTO article_tags (article_id, tag)
VALUES (LAST_INSERT_ID(), 'mysql'),
(LAST_INSERT_ID(), 'tutorial');
Several tables, several auto-increments
LAST_INSERT_ID() always reflects the most recent
auto-increment insert on your connection — from any table. So if you insert into two
auto-increment tables and then need the first id, save it in a session
variable before it gets overwritten:
INSERT INTO artists (name) VALUES ('Black Sabbath');
SET @artist_id = LAST_INSERT_ID();
INSERT INTO albums (title) VALUES ('Paranoid');
SET @album_id = LAST_INSERT_ID();
INSERT INTO tracks (artist_id, album_id, track_name)
VALUES (@artist_id, @album_id, 'War Pigs');
Without the SET, the album insert would have silently replaced the
artist's id and the track row would point at the wrong artist.
Wrap related inserts in a transaction. With InnoDB,
START TRANSACTION ... COMMIT around the parent + child inserts means
you never end up with an article that has no tags because the second statement
failed. The auto-increment machinery works exactly the same inside a
transaction.
From application code
Every driver exposes the same value without an extra query:
PHP PDO's $pdo->lastInsertId(), mysqli's
$mysqli->insert_id, Python mysql-connector's
cursor.lastrowid, Node mysql2's result.insertId. They all
read the per-connection value — same guarantees as calling
LAST_INSERT_ID() yourself.
Two things people needlessly worry about
"I need to know the id before inserting." You don't — insert, then read it back. If you truly need pre-generated identifiers (distributed systems, offline clients), that's what UUIDs are for; ordinary web apps don't.
"Deleting rows leaves gaps in the sequence." It does, and that's fine. The id is an internal handle, not a row number — see gaps in AUTO_INCREMENT sequences for why you should leave them alone.