Every SQL Server professional eventually reaches a point where storage internals stop being abstract. You learn that a table is not just a logical container but a physical structure, and that structure is defined by the clustered index. Rows live at the leaf level of a B-tree, the clustering key defines physical order, and every nonclustered index ultimately points back to that key. Once this model settles in, performance tuning feels almost intuitive. Range scans behave predictably, bookmark lookups make sense, and fragmentation becomes an expected consequence of how data is stored.
That is why the first encounter with PostgreSQL often feels disorienting. You look for clustered indexes because, in your experience, a serious database engine must have them. You eventually discover the CLUSTER command and assume you have found the equivalent. Then you realize it is not persistent, not automatic, and not enforced. At that moment, PostgreSQL feels incomplete, as if a fundamental optimization mechanism were missing.
The truth is that PostgreSQL is not missing clustered indexes. It deliberately chose a different foundation.
👉 If you want to learn more about SQL Server 2025 and PostgreSQL, I highly recommend to have a look on my upcoming online trainings about it.
Why PostgreSQL Refuses to Store Data Inside an Index
In SQL Server, the clustered index is the table. The data itself is stored inside the B-tree, and the physical order of rows is inseparable from the index definition. This makes physical ordering a core design feature, but it also means that every write operation must preserve that structure. Inserts, updates, and deletes all interact directly with the tree, which is why page splits, fragmentation, and index rebuilds are normal operational concerns.
PostgreSQL does not work this way. Tables are stored as heaps, which are essentially unordered collections of data pages. Indexes are separate structures that contain pointers to rows rather than the rows themselves. These pointers reference physical locations known as tuple IDs. The index answers the question “how do I find this row?” but not “where should this row live physically?”
This distinction becomes critical once you consider PostgreSQL’s concurrency model. PostgreSQL is built around multi-version concurrency control, and that choice dominates every other storage decision. When a row is updated, PostgreSQL does not overwrite it. Instead, it creates a new version of the row and leaves the old version in place until it is no longer visible to any transaction. Reads and writes proceed without blocking each other, even under heavy concurrency.
A true clustered index would be fundamentally odds with this approach. If every update creates a new row version, maintaining strict physical order would require constant rebalancing of the index structure. Page splits would be frequent, optimizations like HOT updates would no longer work, and vacuum would need to physically reorder data instead of merely cleaning up dead tuples. The cost would be prohibitive.
Rather than compromise MVCC, PostgreSQL rejects the idea that physical row order should be permanently tied to an index.
The CLUSTER Command and the Illusion of Familiarity
The CLUSTER command often gives SQL Server professionals false hope. PostgreSQL can reorder a table based on an index, and after running CLUSTER, sequential scans can indeed become faster. For a brief moment, the table behaves as if it were clustered.
The key difference is that PostgreSQL makes no attempt to preserve this order. CLUSTER rewrites the table once and then steps away. As soon as new rows are inserted or existing rows are updated, the physical layout begins to drift. PostgreSQL does not consider this drift a problem, because it does not treat physical order as a stable optimization target.
From a SQL Server perspective, this feels like an unfinished feature. From PostgreSQL’s perspective, it is a maintenance operation, not a storage model. Physical order can be imposed temporarily when it helps, but it is never guaranteed and never enforced.
This distinction is subtle but essential. PostgreSQL optimizes for predictable behavior under concurrency and sustained write load, not for preserving a particular physical layout over time.
👉 If you want to learn more about SQL Server 2025 and PostgreSQL, I highly recommend to have a look on my upcoming online trainings about it.
Letting Go of Clustered Indexes and Moving Forward
Many SQL Server professionals only realize in hindsight how much operational complexity clustered indexes introduce. Choosing the wrong clustering key can affect a system for years. Index rebuilds become routine maintenance. Fragmentation management and latch contention are accepted as part of life. PostgreSQL avoids these problems by design, even though it asks you to give up something that feels fundamental.
This is why asking for the PostgreSQL equivalent of a clustered index usually leads to frustration. There is no equivalent, because PostgreSQL does not want one. Instead, it encourages different ways of thinking about performance. Sometimes that means relying on logical access paths rather than physical order. Sometimes it means using partitioning to reduce the size of the data you scan. Sometimes it means exploiting natural data correlation with different index types.
The transition is not easy, especially if clustered indexes have been central to your tuning strategies for years. But once you stop trying to recreate SQL Server’s storage model and start working with PostgreSQL’s assumptions, the system becomes coherent. Concurrency feels smoother, write performance becomes more predictable, and maintenance becomes less dramatic.
The absence of clustered indexes is not a limitation. It is an architectural decision – one that only reveals its value after you accept that PostgreSQL is solving a different set of problems, in a different way.
Thanks for reading,
-Klaus