What is the difference between a clustered and a non-clustered index?
A clustered index determines the physical order of the rows in the table itself. There can be at most one per table, because the rows can only be stored in one order. A non-clustered index is a separate structure that holds a copy of the indexed columns plus a pointer back to the row, and a table can have many of them.
The consequence is that a clustered index is not a separate thing that can be dropped — it is the table's storage. This is why InnoDB tables always have a clustered index, whether you asked for one or not: if you declare an INTEGER PRIMARY KEY, that becomes the clustered index; if you do not, InnoDB adds a hidden one on a row id. And it is why you cannot have a table without a clustered index, because there would be no order to store the rows in.
SQLite works the same way at the storage level. A table with an INTEGER PRIMARY KEY stores the rows in a B-tree keyed by that column, and the primary key lookup is a search of that tree. The rowid is the clustered structure and every other index points back into it.
The practical differences come down to three things. A clustered index makes range scans on its key fast, because the matching rows are physically adjacent. A non-clustered index is a narrower structure — it holds only the indexed columns plus the row pointer — so it is smaller and more likely to fit in memory. And a non-clustered index lookup is a two-step operation: find the key in the index, then follow the pointer to the row, which is a second random access.
Here is the same thing as runnable code:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
email VARCHAR(200) NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
INSERT INTO orders (id, customer_id, email, amount) VALUES
(1, 1, '[email protected]', 120.00),
(2, 2, '[email protected]', 75.25);
-- The primary key lookup uses the clustered structure (the rowid B-tree).
EXPLAIN QUERY PLAN
SELECT id, amount FROM orders WHERE id = 1;
-- A non-clustered index on email gets its own structure with row pointers.
CREATE INDEX idx_orders_email ON orders (email);
EXPLAIN QUERY PLAN
SELECT id, amount FROM orders WHERE email = '[email protected]';