A heap is a table that has no clustered index.
Rows are stored on data pages without being organized by a clustered B-tree.
A heap can still have nonclustered indexes. In this case, the nonclustered index uses a RID (Row Identifier) to locate the row in the heap.
Nonclustered Index
│
│ RID
│ File : Page : Slot
▼
Heap Page
│
▼
Row
If an updated row becomes larger and no longer fits on its current page, SQL Server can move the row to another page and leave a forwarding pointer.
Original Page
│
│ Forwarding Pointer
▼
Another Page
│
▼
Actual Row
This can introduce additional page reads and make scans less efficient when many forwarded records accumulate.
A nonclustered index on a heap stores the RID of the base row.
If the query needs additional columns that are not present in the index:
Nonclustered Index Seek
↓
RID
↓
RID Lookup
↓
Heap Row
A few RID Lookups are usually not a concern, but thousands or millions can make a query expensive.
A heap does not provide the ordering/access path of a clustered index.
Queries that frequently need ranges or ordered access therefore need an appropriate nonclustered index or may require additional scanning/sorting.
Without an appropriate nonclustered index, SQL Server may scan the heap.
For large tables, that can require reading many data pages.
The important point is that not having a clustered index also means SQL Server does not have to maintain one.
That can be useful for the right workload.
With a clustered table, SQL Server must insert the row into the appropriate location in the clustered B-tree.
Conceptually:
INSERT
↓
Find correct B-tree location
↓
Modify clustered index page
↓
Potential page allocation/split
A heap doesn't have that clustered B-tree to maintain.
INSERT
↓
Find page with sufficient space
↓
Store row
This can make heaps attractive for high-volume loading, particularly when the data does not need a clustered access path afterward.
This does not mean every heap insert is automatically faster; other indexes, logging, constraints, page allocation, concurrency, and the load method still matter.
Consider loading millions of rows:
CSV / External Source
↓
BULK INSERT
↓
StageTransactions
If StageTransactions exists only to receive the data before further processing, building and maintaining a clustered index during the load may be unnecessary work.
A common pattern is:
Heap
↓
Bulk Load 50M rows
↓
Validate / Transform
↓
Move data to final table
↓
TRUNCATE heap
The heap is being used as a landing area, not as the final optimized storage structure.
This is one of the clearest real-world heap use cases.
For example:
Source Database
↓
Extract
↓
STG_TRANSACTIONS ← HEAP
↓
Clean / Transform
↓
DWH_FACT_TRANSACTIONS
If the staging table is mainly:
INSERT
↓
SCAN
↓
TRANSFORM
↓
TRUNCATE
there may be little reason to maintain a clustered index.
One of the major heap disadvantages is forwarded records.
But forwarded records are primarily associated with rows being updated so that they grow and need to move.
Consider a staging workload:
Bulk INSERT
↓
SELECT / Process
↓
TRUNCATE
There may be almost no updates.
Therefore:
No growing UPDATEs
↓
Few/no forwarded records
↓
One major heap disadvantage disappears
This is one reason heaps and staging tables fit together well.
Suppose the processing step is:
SELECT
customer_id,
SUM(amount)
FROM dbo.StageTransactions
GROUP BY customer_id;
and almost every row must be processed.
The workload does not necessarily benefit from locating individual rows through a clustered index.
You're essentially asking SQL Server:
Read all data
↓
Aggregate / Transform
For such transient scan-oriented workloads, maintaining a clustered index may provide little benefit.
A heap can fit naturally into workflows where the table repeatedly goes through:
TRUNCATE
↓
LOAD
↓
PROCESS
↓
TRUNCATE
↓
LOAD
↓
PROCESS
You avoid repeatedly maintaining a clustered structure whose ordering is not actually required by the processing logic.
A heap doesn't mean:
No indexes are allowed.
You can still create targeted nonclustered indexes:
CREATE INDEX IX_StageTransactions_Customer
ON dbo.StageTransactions(customer_id);
So you can have:
HEAP
│
┌──────────┴──────────┐
│ │
Fast/simple loading Nonclustered index
│
▼
Specific access path
This can be useful when you don't need a clustered organization but still need efficient access for one or two processing steps.
A heap is a strong candidate when the table is:
✓ ETL / staging table
✓ Bulk-loaded
✓ Short-lived
✓ Mostly INSERT + SELECT
✓ Rarely updated
✓ Frequently TRUNCATEd
✓ Mostly scanned
✓ Does not need clustered-key range access
Be more careful when it is:
✗ Long-lived OLTP table
✗ Frequently updated
✗ Rows frequently grow after UPDATE
✗ Frequently searched by ranges
✗ Has many RID Lookups
✗ Accumulates many forwarded records