Diagram illustrating the structure and relationship between InnoDB clustered indexes and secondary indexes in MySQL.

Understanding InnoDB Clustered Indexes and Secondary Indexes

InnoDB indexing behaviour is at the core of MySQL performance. Understanding how clustered and secondary indexes are laid out on disk and used at runtime will help you design better schemas, choose primary keys wisely, and interpret EXPLAIN output with confidence.

1. InnoDB pages and B+‑trees in brief

InnoDB stores data and indexes in fixed-size pages (typically 16 KB). Each index is a B+‑tree:

┌───────────────┐
│  Root page    │
├───────┬───────┤
│ Int.  │ Int.  │  <─ Internal pages (pointers + key ranges)
│ page  │ page  │
├───────┴───────┤
│ Leaf pages... │  <─ Contain actual index entries
└───────────────┘

Key properties:

  • All lookups, inserts, updates and deletes navigate these trees.
  • The tree is kept balanced; depth is usually small (often 3–4 levels).
  • Leaf pages are linked in key order, enabling efficient range scans.

2. What is the clustered index?

InnoDB stores table rows in a single clustered index. This is the primary organisation of the table on disk.

  • The clustered index key is the table's PRIMARY KEY.
  • If you do not define a PRIMARY KEY, InnoDB picks or creates one internally.
  • Leaf pages of the clustered index contain the full row.

Conceptually:

Clustered index (PRIMARY)

Leaf entry layout (simplified):

  [primary_key]  [all other columns]

Example table:

  CREATE TABLE orders (
      order_id   BIGINT PRIMARY KEY,
      user_id    BIGINT NOT NULL,
      status     TINYINT NOT NULL,
      created_at DATETIME NOT NULL,
      amount     DECIMAL(10,2) NOT NULL
  ) ENGINE=InnoDB;

Leaf page entries:

  [order_id] [user_id] [status] [created_at] [amount]

Every row is physically ordered by order_id. A lookup by primary key is a single B+‑tree search in this clustered index.

2.1 How InnoDB chooses the clustered key

  1. If a PRIMARY KEY is defined, InnoDB uses it.
  2. Else, if there is a UNIQUE NOT NULL index, InnoDB may choose the first such index.
  3. Else, InnoDB creates a hidden 6‑byte row ID and uses that.

Relying on hidden row IDs is rarely ideal; it wastes space and makes secondary indexes larger and slower.

3. Secondary indexes: pointers into the clustered index

Secondary indexes are additional B+‑trees that allow efficient lookup by non-primary key columns. The crucial detail: secondary index leaf entries do not store the full row. Instead, they store:

  • The secondary index key columns.
  • The clustered index key (PRIMARY KEY) as a logical pointer.

Diagram for a secondary index on user_id:

Secondary index (idx_user_id)

Leaf entry layout (simplified):

  [user_id]  [order_id]

To get full row:

  1) Search idx_user_id by user_id.
  2) From leaf entry, read order_id.
  3) Search clustered index (PRIMARY) by order_id.

This two-step process is often called a "bookmark lookup" or "row lookup by primary key".

4. Walk-through: how queries use the indexes

4.1 Lookup by primary key

SELECT * FROM orders WHERE order_id = 12345;
  1. InnoDB navigates the clustered index B+‑tree using order_id.
  2. Leaf entry holds the full row; no secondary index is involved.

This is the cheapest possible lookup in InnoDB.

4.2 Lookup by secondary index (non-covering)

CREATE INDEX idx_user_status
    ON orders (user_id, status);

SELECT amount, created_at
FROM orders
WHERE user_id = 42 AND status = 1;

Execution steps:

  1. Search idx_user_status for entries with (user_id=42, status=1).
  2. For each matching leaf entry, read the stored PRIMARY KEY (order_id).
  3. Use order_id to look up the row in the clustered index.
  4. Read amount and created_at from the clustered index leaf.

Each matched secondary index row can cause an additional clustered index lookup. If many rows match, this becomes expensive.

4.3 Covering secondary index

A secondary index is covering for a query when all referenced columns are stored in the index leaf, so no clustered lookup is required.

CREATE INDEX idx_user_status_created
    ON orders (user_id, status, created_at);

SELECT user_id, status, created_at
FROM orders
WHERE user_id = 42 AND status = 1;

Now, the leaf entry layout is:

[user_id] [status] [created_at] [order_id]

Execution:

  1. Search idx_user_status_created for (user_id=42, status=1).
  2. Read user_id, status, created_at directly from the leaf pages.
  3. No need to visit the clustered index for this query.

Covering indexes can dramatically reduce I/O, at the cost of extra index size and write overhead.

5. Visualising the relationship

Putting it together:

Secondary index (idx_user_status)

          user_id,status,order_id
          ┌───────────────────────┐
          │  42,1,1001            │ ─┐
          │  42,1,1005            │ ─┼─▶ Clustered index (PRIMARY)
          │  42,2,1010            │ ─┘      order_id,...
          └───────────────────────┘

Clustered leaf entries (simplified):

  order_id=1001  user_id=42  status=1  created_at=...
  order_id=1005  user_id=42  status=1  created_at=...
  order_id=1010  user_id=42  status=2  created_at=...

6. Choosing a good primary key

The primary key choice affects:

  • Physical row ordering.
  • Size of all secondary indexes (they store the PK).
  • Insert patterns and potential page splits.

6.1 Recommended properties

  • Stable: avoid updating the primary key; it is stored in every secondary index.
  • Short: smaller PK means smaller secondary index entries.
  • Uniquely identifying: natural or surrogate, but truly unique.
  • Monotonically increasing (for insert-heavy tables): reduces page splits and fragmentation.

A typical pattern is a BIGINT AUTO_INCREMENT or a ULID/UUID variant that is insertion-friendly. Plain random UUIDs often cause heavy fragmentation and large indexes.

6.2 Example: impact on secondary index size

Compare:

-- Wide natural primary key
PRIMARY KEY (email VARCHAR(255))

-- Narrow surrogate primary key
id BIGINT PRIMARY KEY,
UNIQUE KEY uk_email (email)

In the first case, every secondary index entry stores a 255‑byte email as the PK pointer. In the second, each stores an 8‑byte BIGINT. On large tables, this difference is significant for memory and disk usage.

7. Designing secondary indexes effectively

7.1 Match your access patterns

Start from your most important queries:

  • Identify WHERE, JOIN, and ORDER BY columns.
  • Design composite indexes with the most selective / leading filter columns first.
  • Add extra columns to make the index covering only when it clearly pays off.

7.2 Composite index ordering

Given:

CREATE INDEX idx_user_status_created
    ON orders (user_id, status, created_at);

This index can efficiently support:

  • WHERE user_id = ?
  • WHERE user_id = ? AND status = ?
  • WHERE user_id = ? AND status = ? AND created_at >= ?

But it will not use the index efficiently for WHERE status = ? alone, because user_id is the leading column.

7.3 Avoid redundant indexes

Because secondary indexes store the primary key, some indexes become redundant:

PRIMARY KEY (order_id)
KEY idx_user (user_id)
KEY idx_user_order (user_id, order_id)

Here, idx_user_order can serve all queries that idx_user can, and more. Keeping both wastes space and slows writes.

Before dropping an index, review application queries and check EXPLAIN plans in a safe environment.

8. Observing index usage with EXPLAIN

EXPLAIN helps you see which index MySQL chooses and whether a secondary index is covering.

EXPLAIN SELECT amount, created_at
FROM orders
WHERE user_id = 42 AND status = 1;

Key columns to review:

  • key: which index is used.
  • rows: estimated rows to examine.
  • Extra: look for "Using index" (covering index) vs "Using where" only.

On large tables, aim for plans that:

  • Use an appropriate index (PRIMARY or a selective secondary).
  • Minimise "rows" for critical queries.
  • Use covering indexes for the highest QPS lookups where feasible.

9. Maintenance and operational considerations

  • Index creation cost: Building large secondary indexes is I/O heavy. On RHEL/Rocky Linux, monitor with tools like iostat and pidstat during index builds.
  • Backups: Logical backups (e.g. mysqldump) recreate indexes; physical backups (e.g. file-based) copy them as-is.
  • Replication: DDL and index changes replicate; consider lag on replicas when adding large indexes.

Always test index changes on a staging system with similar data volume before modifying production.

10. Conclusion

In InnoDB, the clustered index defines how rows are stored, and every secondary index points back to it via the primary key. This design explains why primary key choice matters, why secondary indexes affect write performance, and how covering indexes can speed up reads. By aligning your primary and secondary indexes with real query patterns, and validating plans with EXPLAIN, you can achieve predictable, efficient performance for most workloads.

This article offers general technical guidance. Validate all configurations in a safe environment before applying them to production.

Smart reads for curious minds

We don’t spam! Read more in our privacy policy

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *