Clustered vs Nonclustered Index Explained
A clustered index determines how a table's rows are physically stored; a nonclustered index is a separate lookup structure that points back to them. The distinction is central to SQL Server and MySQL/InnoDB — here's exactly how each works, plus how Oracle and PostgreSQL handle the same underlying idea.
A quick scope note before we start
"Clustered vs nonclustered index" is primarily SQL Server and MySQL/InnoDB terminology. Oracle and PostgreSQL organise tables differently by default, so this comparison is written for SQL Server first, with a dedicated section at the end on how Oracle and PostgreSQL relate to the same idea.
The core difference
A clustered index determines the physical order rows are stored on disk — the table's data is the index's leaf level. Because a table can only be physically sorted one way, a table can have at most one clustered index (usually built automatically on the primary key).
A nonclustered index is a separate structure: a sorted list of key values, each pointing back to the actual row's location (via the clustered index key, if one exists, or a row identifier if not). A table can have many nonclustered indexes.
Clustered vs nonclustered index (SQL Server / MySQL InnoDB)
| Clustered index | Nonclustered index | |
|---|---|---|
| How many per table? | at most one | many |
| Determines physical row order? | yes — the table IS the index | no — a separate structure with pointers |
| Leaf level contains | the actual row data | the indexed column(s) + a pointer to the row |
| Created automatically on | the primary key, by default | nothing — created explicitly |
| Best for | range scans on the key, and the most common lookup path | additional lookup paths on other columns |
| Extra storage cost | none — it IS the table | a separate copy of the indexed columns, plus pointers |
SQL Server syntax
The clustered index is usually implicit (from the primary key); nonclustered indexes are explicit.
-- Clustered index, created automatically with the primary key
CREATE TABLE employees (
employee_id INT PRIMARY KEY, -- clustered index on employee_id by default
last_name VARCHAR(50),
department_id INT
);
-- An additional, explicit nonclustered index for a common lookup
CREATE NONCLUSTERED INDEX idx_employees_dept ON employees (department_id);
Why a nonclustered index lookup costs more
Querying by a nonclustered index key is a two-step process: first find the entry in the index, then follow its pointer back to the actual row (a "bookmark lookup" or "key lookup"). Querying by the clustered index key skips that second step entirely, because the index leaf is the row.
This is why picking the right clustered index — usually the column most frequently used for range scans or the primary lookup path — matters more than any single nonclustered index choice.
What about Oracle and PostgreSQL?
Oracle tables are heap-organized by default — rows are not stored in any particular key order, and every index (including the primary key's) is a separate B-tree structure pointing back to rows via a ROWID, similar in spirit to a nonclustered index. Oracle does offer index-organized tables (IOT), which store the table's data directly within a B-tree keyed on the primary key — conceptually the closest match to a clustered index, but they're an explicit, deliberate choice (CREATE TABLE ... ORGANIZATION INDEX), not the default.
PostgreSQL tables are also heap-organized by default, with all indexes — including the primary key's — pointing back to heap rows. Postgres has a CLUSTER command that physically reorders a table's rows to match an index once, but it is a one-time operation, not a maintained property: new rows inserted afterward are not kept in that order automatically.