Chapter 03 · Schemas, Tables, Data Types, Keys, Constraints, and SQL Modes

AUTO_INCREMENT, Surrogate vs Natural Keys, and Identifier Design

Understand MySQL AUTO_INCREMENT as an identifier allocator, observe LAST_INSERT_ID connection semantics and gaps, and choose InnoDB primary-key shapes with locality and index footprint in mind.

Beginner60–80 minIdentifier-design labMySQL 8.4 LTS · current released baseline 8.4.11AUTO_INCREMENT + keysLast reviewed: August 2026

Learning outcomes

Identifiers are not row counts. They are durable labels used by foreign keys, URLs, APIs, logs, caches, and indexes. MySQL’s AUTO_INCREMENT is a convenient allocator, but it does not promise a gapless sequence and should not be used as an accounting counter or proof of table cardinality.

InnoDB adds another dimension: the primary key is the clustered index, and every secondary index record carries the primary-key value needed to locate the row. Key width and insertion locality therefore affect more than aesthetics.

01

Explain AUTO_INCREMENT allocation, gaps, explicit values, and connection-specific LAST_INSERT_ID().

02

Distinguish surrogate keys, natural keys, and business UNIQUE constraints without treating one strategy as universal.

03

Explain why InnoDB primary-key width is repeated in secondary indexes and why locality matters.

04

Compare monotonic integer keys with random UUID-like identifiers and outline practical tradeoffs.

05

Build an identifier experiment that proves rollback/deletion does not imply safe identifier reuse.

AUTO_INCREMENT allocates identity values

An integer column with AUTO_INCREMENT can receive a generated value when an insert omits the column or supplies the documented generating form. The generated value is unique under the table’s rules, but the sequence may contain gaps due to failed statements, rollbacks, deletes, explicit values, concurrency, or allocation behavior.

sql · basic allocation and connection-local observation
DROP TABLE IF EXISTS servicehub_lab.id_probe;CREATE TABLE servicehub_lab.id_probe (  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,  label VARCHAR(80) NOT NULL,  PRIMARY KEY (id)) ENGINE=InnoDB;INSERT INTO servicehub_lab.id_probe (label) VALUES ('first');SELECT LAST_INSERT_ID() AS generated_id;INSERT INTO servicehub_lab.id_probe (label)VALUES ('second'), ('third'), ('fourth');SELECT LAST_INSERT_ID() AS first_id_from_multirow_insert;SELECT * FROM servicehub_lab.id_probe ORDER BY id;

For a multi-row insert, LAST_INSERT_ID() reports the first generated value for that statement. Its value is connection-specific, so another session inserting rows does not overwrite the current session’s LAST_INSERT_ID(). Applications should still use their driver’s documented generated-key API rather than guessing with MAX(id).

Failure drill: treating IDs as a gapless business sequence

Suppose invoices are legally required to have a controlled business document number. Reusing the table’s auto-increment primary key as that legal sequence couples persistence identity to an allocator that is explicitly not a gapless business counter.

sql · observe a gap after rollback
START TRANSACTION;INSERT INTO servicehub_lab.id_probe (label) VALUES ('rolled-back');SELECT LAST_INSERT_ID() AS allocated_inside_tx;ROLLBACK;INSERT INTO servicehub_lab.id_probe (label) VALUES ('after-rollback');SELECT LAST_INSERT_ID() AS next_allocated_id;SELECT * FROM servicehub_lab.id_probe ORDER BY id;

The exact next value can depend on server/table state, but the safe invariant is that you must not expect rollback to make an allocated identifier available as a gapless accounting number. Likewise, deleting row 3 does not turn “3” into a safe reusable identity.

Wrong approach

Never calculate the next key with SELECT MAX(id)+1 in application code. Concurrent sessions can race, and the pattern recreates a problem the server’s allocator already solves.

Surrogate key versus natural key is a design decision, not a religion

A natural key comes from the domain, such as a stable externally assigned code. A surrogate key is introduced specifically as row identity, often a numeric auto-increment value. A common MySQL design uses a surrogate primary key for InnoDB organization plus a UNIQUE constraint on the natural business identifier.

sql · surrogate identity plus natural uniqueness
SHOW CREATE TABLE servicehub_lab.customers;SELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX,       COLUMN_NAME, CARDINALITYFROM INFORMATION_SCHEMA.STATISTICSWHERE TABLE_SCHEMA='servicehub_lab'  AND TABLE_NAME='customers'ORDER BY INDEX_NAME, SEQ_IN_INDEX;

This pattern is useful when the business key is long, externally mutable, composite, or inconvenient for referencing tables. But do not automatically add meaningless surrogates everywhere. Small stable composite keys can be excellent primary keys when they match the access pattern and lifecycle.

Why InnoDB cares about primary-key shape

InnoDB stores row data in the clustered index, normally the primary key. Secondary indexes contain their own key plus the primary-key value for the row. That means a wide primary key is repeated across every secondary index entry. A 32-byte or multi-column primary key can therefore amplify storage and cache cost in a table with many secondary indexes.

Primary-key shapeAdvantagesCosts / risks
BIGINT AUTO_INCREMENTCompact, simple, strong insert locality, easy foreign keysCentral allocator semantics; not globally unique across independent systems without coordination.
Natural VARCHAR/compositeEncodes business identity directlyCan be wide/mutable; repeated in secondary indexes; collation affects comparison.
Random UUID textPortable globally unique representationVery wide and random insertion order; text form increases index footprint.
16-byte binary UUID-likeMore compact than text UUIDStill may have random locality; harder for humans to inspect.
Time-ordered UUID-like/application IDDistributed generation plus better locality potentialRequires a clearly specified generation/ordering format and connector support.

Avoid unsupported claims such as “random UUIDs are always slow.” Workload, table size, page fill, caching, storage, insert concurrency, and secondary-index count all matter. The correct lesson is to understand the mechanism and measure on representative data.

Composite keys and AUTO_INCREMENT restrictions

MySQL’s rules for AUTO_INCREMENT interact with indexes and storage engines. For InnoDB, treat an auto-increment column as a simple integer identity that is indexed appropriately, normally the leading/sole primary-key column. Avoid exotic composite auto-increment patterns unless current storage-engine documentation explicitly supports the design and you have a strong reason.

sql · inspect allocator-related metadata
SELECT TABLE_SCHEMA, TABLE_NAME, AUTO_INCREMENTFROM INFORMATION_SCHEMA.TABLESWHERE TABLE_SCHEMA='servicehub_lab'  AND TABLE_NAME IN ('customers','work_orders','id_probe');SHOW CREATE TABLE servicehub_lab.id_probe;SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';

AUTO_INCREMENT metadata is useful operationally but is not a trustworthy row-count substitute. Estimates and allocator state answer different questions from COUNT(*).

Connection semantics: never guess another session’s generated key

Open two clients if possible. Insert from Session A, read LAST_INSERT_ID() there, then insert from Session B. Return to Session A and observe that its connection-specific value is not replaced by Session B’s insert. This is why connection pooling requires discipline: read the generated key from the same logical database operation/connection context that performed the insert.

text · two-session timeline
Session A                         Session B---------                         ---------INSERT row A;SELECT LAST_INSERT_ID(); -> A_id                                  INSERT row B;                                  SELECT LAST_INSERT_ID(); -> B_idSELECT LAST_INSERT_ID(); -> A_idDo not replace this with SELECT MAX(id).

Hands-on lab: compare key designs with evidence

  1. Create id_probe and record its SHOW CREATE TABLE.
  2. Insert one row and a multi-row batch; record LAST_INSERT_ID() behavior.
  3. Run the rollback experiment and record observed gaps without trying to “repair” them.
  4. Create a second table with a natural VARCHAR primary key and one secondary index; inspect INFORMATION_SCHEMA.STATISTICS.
  5. Create a third table with a BIGINT primary key plus a unique business key; compare the DDL/index metadata.
  6. If you want to benchmark UUID-like keys, generate a representative local dataset and measure insert/index size under identical conditions; do not copy benchmark numbers from the lesson.

Knowledge check

  1. Why can AUTO_INCREMENT values contain gaps?
  2. What does LAST_INSERT_ID() return after a multi-row insert?
  3. Why is SELECT MAX(id)+1 unsafe for key allocation?
  4. Why can a wide InnoDB primary key increase every secondary index?
  5. When might a surrogate primary key plus UNIQUE natural key be preferable to the natural key as PRIMARY KEY?
Reveal answers
  1. Allocation, failed/rolled-back statements, deletes, explicit values, and concurrency can all prevent a gapless sequence.
  2. The first automatically generated value from that multi-row statement for the current connection.
  3. Two concurrent sessions can observe the same maximum and race to choose the same next value.
  4. InnoDB secondary index entries carry the primary-key value used to locate the clustered row.
  5. When the natural key is long, mutable, composite, externally controlled, or otherwise expensive/inconvenient as the clustered/reference key.

Production judgment and references

Choose identifier strategy alongside replication, sharding, offline creation, API exposure, privacy, and data-retention requirements. A local auto-increment integer is excellent for many single-primary MySQL applications. Distributed ID generation may be justified when independent writers must create globally unique identifiers without a central round trip. Keep that architectural requirement separate from fashion.

Monitor key-range exhaustion for bounded integer types, allocator surprises after bulk imports/restores, and index growth caused by wide keys. Never renumber primary keys merely to make them visually contiguous.

The final lesson in this chapter makes another hidden dependency explicit: sql_mode. The same insert text can fail, warn, coerce, or group differently if sessions run under different mode contracts.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.