Heap Files
Imagine a database table with no ordering, no index, and no promises about where any given row lives. A new order arrives at an online store. The database must record it somewhere on disk, quickly. It must do this without reorganizing anything else. This is exactly the problem a heap file solves: an unordered collection of records, packed into disk blocks. It is optimized for fast insertion and full-table scans rather than fast lookup. Heap files matter because they are the default. Every relational database management system needs a baseline file organization. It falls back on this before any Index Structures are built. Heap files are almost always that baseline. Understanding them is a prerequisite for understanding why indexes exist at all. You cannot appreciate the speedup an index gives you until you know the cost of a table with none. The big picture is simple but deep: a heap file trades search performance for insertion performance and simplicity. Records are appended wherever there is room. Deletions leave gaps that must be tracked and reused. Finding a specific record means examining blocks until it turns up. This article works out precisely how insertion, deletion, and scanning work at the block level. It shows how free space is tracked so deleted slots don't leak away forever. It derives the I/O cost of each operation in terms of block transfers — the currency of all storage-system analysis. By the end, you will be able to trace, block by block, what happens when a record is inserted into a heap file with a free-space map. You will be able to explain why deletion is usually "lazy." You will state precisely why a full scan of a heap file costs Θ(b) block I/Os for b data blocks — no more, no less. This groundwork sets up later comparisons with sorted files and indexed organizations.
- A heap file stores records in no particular order; new records go wherever space is available, usually at the end.
- Insertion is O(1) amortized block I/O when free space is tracked; deletion marks slots free rather than physically shifting data.
- Scanning the whole file costs b block transfers for a file of b blocks — this is the baseline against which indexes are judged.
- Free-space management (linked free lists or a free-space map) is what keeps heap files from wasting space after deletions.
- Point lookups by a specific attribute value cost O(b) in the worst case — heap files have no way to skip blocks.
- Heap files are the default storage layout for new tables, and the substrate over which Index Structures add fast search.
A heap file achieves the fastest possible insertion by giving up any guaranteed order, which is exactly why point queries need a full scan and indexes become necessary.
The idea
A heap file (also called an unordered file) is a file organization in which records are placed on disk blocks in no particular order relative to any attribute value. The only structural rule is: a record lives entirely within one block (assuming no record spans blocks, the common case for fixed or moderately sized variable-length records), and the file grows by allocating new blocks when existing ones fill up. There is no sort key, no key-based clustering, and no index built into the file itself — a heap file is what you get when a table is created with nothing else specified. The design goal is to make insertion as cheap as possible: find any block with free space, write the record there, done. This is contrasted with a sorted file, which must maintain an order and therefore may need to shift records on insertion, and with indexed/hashed files, which impose extra structure to accelerate search at some cost in maintenance. The heap file sacrifices search speed for this insertion speed and for structural simplicity: because records can be anywhere, answering "find all records with `age = 30`" requires inspecting every block — there is no shortcut. Three operations define the interface of a heap file, and each has a distinct cost profile that this article derives precisely:
- Insert(record) — place the record in some block with available space, updating free-space bookkeeping.
- Delete(record-id) — remove or mark unavailable the slot holding the record, and register the freed space for reuse.
- Scan() — iterate over every block and every valid record within it, typically for a linear search or to support a full query evaluation.
Records are addressed by a record identifier (rid), typically a pair `(block number, slot number)`, which lets other structures — most importantly Index Structures built later — point directly at a record's physical location without needing to know how heap-internal free space is organized.
How it works
Physical layout. Each block holds a slotted page: a small header (record count, pointers/offsets to free space) followed by a directory of slots, each slot holding either a record or a tombstone marking a deleted record. This slotted structure is what allows variable-length records and lets deletion be "logical" — mark a slot empty — without physically compacting the whole block on every delete.
Free-space tracking. A heap file needs a way to answer "which block has room for n more bytes?" without scanning the entire file on every insert. Two classical mechanisms: 1. Free-space linked list. The file header points to the first block with free space; each such block points to the next block with free space, forming a linked list threaded through the blocks themselves. Blocks that fill up are unlinked (a full pointer is not on the list, or is put on a separate "full" list so it can be revisited after a deletion frees space in it). 2. Free-space map (FSM) / bitmap of free-space classes. A small separate structure (often cached in memory) records, per block, roughly how much free space it has (e.g., a few bits encoding "empty," "one-third full," "two-thirds full," "full"). Insertion consults the map directly instead of following pointers, at the cost of maintaining an auxiliary structure. Both approaches answer the same question in O(1) amortized time for a typical insert: they avoid scanning the file to find space.
Insertion algorithm. The database receives a new record, consults the free-space structure for a candidate block, and writes the record there. If no block has room, a new block is allocated at the end of the file. ``` function INSERT(file, record): size = byte_length(record)
# Step 1: consult free-space structure for a block with enough room block_id = FREE_SPACE_STRUCTURE.find_block_with_room(size) if block_id is null:
# No existing block has room: allocate a new block block_id = ALLOCATE_NEW_BLOCK(file) page = EMPTY_PAGE() else: page = READ_BLOCK(file, block_id)
# Step 2: place record into a free slot (or extend the slot directory) slot = page.first_free_slot_or_new() page.write(slot, record)
# Step 3: update bookkeeping WRITE_BLOCK(file, block_id, page) FREE_SPACE_STRUCTURE.update(block_id, page.remaining_free_space()) rid = (block_id, slot) return rid ```
Deletion algorithm. Deletion is deliberately lazy: the slot is marked free rather than the remainder of the block being shifted, because shifting is expensive and unnecessary — the slotted-page directory lets a slot be reused later without moving its neighbors. ``` function DELETE(file, rid): (block_id, slot) = rid page = READ_BLOCK(file, block_id) if page.slot_is_empty(slot): return FAILURE # already deleted / invalid rid page.mark_slot_free(slot) # tombstone; do NOT shift other records WRITE_BLOCK(file, block_id, page) FREE_SPACE_STRUCTURE.update(block_id, page.remaining_free_space()) return SUCCESS ```
Scan algorithm. A scan simply walks every block in file order and yields every occupied slot — this is the operation that touches the entire file and is the reference cost against which indexes are measured. ``` function SCAN(file): for block_id in 1 .. file.num_blocks: page = READ_BLOCK(file, block_id) for slot in page.slots: if not page.slot_is_empty(slot): yield page.read(slot) ```
Point search (no index). Searching for a record matching a predicate on a heap file with no index degenerates to a linear scan with early exit only if a unique match is guaranteed and found: ``` function FIND(file, predicate): for block_id in 1 .. file.num_blocks: page = READ_BLOCK(file, block_id) for slot in page.slots: if not page.slot_is_empty(slot) and predicate(page.read(slot)): yield page.read(slot)
# no early termination in general: predicate may match multiple records,
# or none, and heap file order gives no hint about where matches lie ```
Complexity & correctness
Notation. Let b be the number of blocks currently occupied by the heap file, and let r be the number of records. Assume the free-space structure (map or linked list) is small enough to be cached in memory, or costs O(1) amortized block I/O to consult — this is standard, since it typically occupies a tiny fraction of the space the data itself occupies.
Insertion — O(1) block I/Os (amortized). Consulting the free-space structure is a memory operation (or one extra I/O in the worst case if the structure itself must be paged in). Reading the candidate block, writing the record into it, and writing the block back is exactly one block read + one block write, i.e., 2 I/Os, regardless of b. In the rare case no block has room, the cost is one block allocation and one write — still O(1), independent of file size. This is the entire point of a heap file: insertion cost does not grow with the size of the table. Why is this correct, not just fast? The free-space structure is an invariant-preserving cache: after every insert or delete, `FREE_SPACE_STRUCTURE.update` is called, so the structure always reflects the true occupancy of the block just touched. The one subtlety is staleness for blocks not just touched — a bitmap with coarse buckets (e.g., "under 33% free") can under- or over-estimate slightly, but never claims a full block has room, so `INSERT` never corrupts data; at worst it wastes a check by trying a block that turns out too full and falling through to allocate a new one. This bounded imprecision is the standard tradeoff for keeping the free-space structure cheap to maintain.
Deletion — O(1) block I/Os. Deletion touches exactly the one block containing the target rid: one read, mark the tombstone, one write. No other block is touched, because deletion never triggers compaction of other pages. This is why deletion is called "lazy": the r value drops but b does not shrink automatically, and interior fragmentation (free tombstoned slots inside used blocks) accumulates until the free-space structure directs future inserts to reclaim it.
Scan — Θ(b) block I/Os. A scan must read every block exactly once: `b` reads. This is both an upper and lower bound — you cannot know a block is empty of matching records without reading it (there is no ordering or index to skip blocks), so Θ(b) is tight, not merely an upper bound. This is the number every later article on Cost Comparison of File Organizations will use as the reference point: any index or sorted structure is judged by how far below Θ(b) it can push a point or range query.
Point search (linear scan) — O(b) worst case, Θ(b) if no shortcut exists. Because the heap file gives no information about where a record with a given value lives, a search for `attribute = value` must, in the worst case (value absent, or all matches concentrated in the last block), inspect all b blocks. Average case is still Θ(b) if the query issuer cannot stop early (e.g., counting all matches) and Θ(b/2) in expectation only for the specific sub-case of finding a single guaranteed-unique match with uniformly random placement — a fragile assumption that databases do not rely on for cost estimation. The standard, defensible statement is: point search on a heap file costs O(b) block transfers, full stop.
Space overhead. The free-space structure costs O(b) bits to O(b) bytes depending on granularity (a bitmap of a few bits per block, or a linked pointer per block), which is asymptotically negligible next to the O(b · block-size) data itself, and is the price paid for O(1) insertion.
Tradeoff summary. Heap files achieve the best possible asymptotic insertion cost among file organizations — no reordering, no rebalancing, no split logic — at the cost of the worst possible search cost, since nothing about record placement encodes any information useful for skipping blocks. Every more sophisticated organization (sorted files, hash files, tree indexes) spends some insertion cost to buy back search cost; heap files sit at the "pure insertion speed" end of that spectrum.
Worked example
Consider a heap file `Employees` with block size holding 4 records per block, currently spanning 3 blocks (12 slots, some occupied, some free after earlier deletes). The free-space map is coarse-grained: it tracks only "has ≥1 free slot" per block.
| Step | Operation | Block touched | Free-space map before | Action | I/Os | Free-space map after |
|---|---|---|---|---|---|---|
| 1 | Initial state | — | B1: full, B2: full, B3: 1 free slot | — | — | B1: full, B2: full, B3: 1 free |
| 2 | `INSERT(Carla)` | B3 | B3 has 1 free slot | Read B3, write Carla into free slot 3 | 2 | B1: full, B2: full, B3: full |
| 3 | `INSERT(Deepak)` | none has room | all full | Allocate B4, write Deepak into slot 0 of new page | 2 | B1–B3: full, B4: 3 free |
| 4 | `DELETE(rid=(B2, 1))` — remove "Farah" | B2 | B2: full | Read B2, tombstone slot 1, write back | 2 | B1: full, B2: 1 free, B3: full, B4: 3 free |
| 5 | `INSERT(Grace)` | B2 (map now shows it has room) | B2: 1 free | Read B2, write Grace into the tombstoned slot 1 | 2 | B1: full, B2: full, B3: full, B4: 3 free |
| 6 | `SCAN()` for all employees | B1, B2, B3, B4 | — | Read every block once, yield non-tombstoned records | 4 | unchanged |
| 7 | `FIND(name = "Deepak")` | worst case: B1, B2, B3, B4 | — | Must check every block since no ordering hints location | up to 4 | unchanged |
Two things this trace makes concrete. First, step 5 shows reclamation working as designed: the slot vacated by deleting Farah in step 4 is reused by Grace in step 5 rather than the file growing a fifth block — this is exactly why heap files bound their wasted space to at most one block's worth of fragmentation per free-space-map granularity level, not unboundedly. Second, step 7 shows the fundamental limitation: even though Deepak's record physically sits in block 4, `FIND` has no way to know that without checking B1, B2, and B3 first (in the worst ordering of blocks scanned), incurring the full O(b) cost that a later index would eliminate. Total I/O for this six-operation sequence: 2+2+2+2+4+4 = 16 block transfers for 3 inserts, 1 delete, 1 full scan, and 1 point search over a 4-block file — illustrating that the insert/delete operations (O(1) each) are cheap individually, while the scan and search dominate the total cost, scaling with file size rather than with the number of records touched.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Treating deletion as immediately reclaiming disk space | Confusing logical deletion (tombstone) with physical file shrinkage | File never shrinks automatically; disk usage grows even as row count drops, until compaction/vacuum runs |
| Assuming point search is O(1) or O(log b) on a bare heap file | Habit carried over from indexed tables | Underestimating query cost by orders of magnitude on large unindexed tables |
| Forgetting to update the free-space structure after delete | Treating delete as "just mark the slot" without the bookkeeping step | Freed slots become invisible to future inserts — silent fragmentation, wasted space |
| Assuming insertion always appends at end-of-file | Simplified mental model ignoring free-space reuse | Miscounting I/O cost; also breaks any code relying on insertion order being preserved |
| Believing scan order reflects insertion or logical order | Heap = unordered, but many demos happen to show append-order results | Queries relying on implicit ordering (e.g., "first N rows") give incorrect or inconsistent results across reorganizations |
Pioneers & history
The heap file is not the invention of a single person; it is the natural first file organization anyone builds when persisting records to block-structured disks, and its formalization emerged alongside the earliest systematic treatments of database file structures in the 1970s, as researchers began classifying file organizations (heap, sorted, hashed) and analyzing their access costs in the way this article does.
Origins · 1 minEdgar F. Codd and how this was discovered
Codd's 1970 paper on the relational model reframed data independence: applications should not need to know how records are physically stored — whether in a heap, sorted, or indexed file — only that a relation exists logically. This separation of logical and physical organization is precisely what makes heap files a legitimate, swappable default: a table can start as a heap file and later be reorganized under an index without changing any query's meaning, only its cost.
Where this matters
Every major relational database uses a heap-like organization as its default table storage unless the user explicitly requests otherwise. PostgreSQL literally calls its default table storage a "heap" and maintains an explicit free-space map (FSM) file per table plus a visibility map, exactly mirroring the mechanisms described here, with a background `VACUUM` process reclaiming tombstoned tuple slots. MySQL/InnoDB by contrast clusters tables by primary key (an index-organized table) by default, but heap-style unordered secondary storage patterns appear in its temporary tables and in MyISAM's legacy heap-organized `.MYD` files. Big-data and log-structured systems — write-optimized stores, append-only logs, LSM-tree base layers — inherit the same core tradeoff: cheap appends now, and a cost paid later (compaction, indexing, merging) for cheap reads. Heap files are also the conceptual anchor for teaching query cost estimation: every database course computes "cost of a full table scan" as b I/Os before introducing any index, precisely because that number is the heap file's scan cost derived above. Once a table has a B+-tree or hash index, the same query's cost analysis becomes "index lookup cost + heap file access cost per matching row," making the heap-file cost model the baseline term in every subsequent formula. Understanding heap files is therefore not just historical background — it is the arithmetic every later chapter on indexing, query optimization, and access-path selection builds directly on top of.
Ask the tutor: How does the I/O cost of a heap file scan compare once a B+-tree index is added on top of it, for both point queries and range queries?
Is a heap file the same thing as an unsorted array in memory?
Conceptually similar but not identical. Both lack ordering, but a heap file is organized around disk blocks, not individual elements: records are grouped into blocks, and the unit of I/O is a whole block, not a record. This block granularity is why free-space management operates at the block level (tracking how much room each block has) rather than shifting individual elements as a resizable array would. Also, unlike a memory array, a heap file must handle variable-length records, block allocation on growth, and crash-safe writes — none of which apply to a simple in-memory array.
Does "heap file" have anything to do with the heap data structure (priority queue)?
No — this is a well-known naming collision. The heap data structure (used in heapsort and priority queues) is a binary-tree-based structure maintaining a min/max ordering property. A heap file has no such ordering property at all; the name instead evokes an informal "unordered heap [pile] of records," analogous to a heap of unsorted papers. The two concepts are unrelated beyond sharing a common English word; confusing them is a frequent but harmless terminology trap for new students.
If heap files are so slow to search, why not always sort the file or build an index?
Because sorting or indexing shifts cost from writes to reads. A sorted file must maintain order on every insert, potentially shifting many records (or requiring overflow areas), and an index must be updated on every insert and delete too. For write-heavy workloads — logging, staging tables, bulk-loading — the heap file's O(1) insert with no maintenance overhead is exactly the right tradeoff. The decision of which organization to use is a cost-based one, formalized once you compare heap files against sorted and indexed alternatives directly.
How does a heap file avoid unbounded fragmentation after many deletes?
It doesn't avoid it entirely — it bounds and manages it via the free-space structure. Every delete updates the free-space map to reflect the newly available slot, so subsequent inserts are directed toward blocks with reclaimable space rather than always allocating new blocks. Over time, especially under heavy delete/insert churn, blocks can still end up sparsely populated (many tombstones, few live records), which is why real systems run periodic maintenance (e.g., PostgreSQL's `VACUUM`) to physically compact pages and return fully empty blocks to the operating system.
Are records ever split across two blocks in a heap file?
In the simplest and most commonly taught model, no — a record is assumed to fit entirely within one block, which keeps the slotted-page model clean and the I/O-cost analysis simple (one record touch equals one block touch). Real systems relax this for very large records (e.g., PostgreSQL's TOAST mechanism moves oversized field values to a separate storage area), but the core heap-file operations — insert, delete, scan, and free-space tracking — are defined assuming the simple, single-block-per-record case, which is the version this article analyzes.
What replaces the linear scan cost once an index is added on top of a heap file?
An index (hash or B+-tree) does not replace the heap file — it sits alongside it and maps search-key values to record identifiers (rids) that point directly into the heap file's blocks. A point query then costs "index traversal cost" (often O(log b) or better) plus one block read to fetch the actual record via its rid, instead of O(b) block reads to scan the whole file. The heap file itself is unchanged; only the access path used to reach specific records improves, which is precisely the subject of the next step in this progression.
Why do database systems still default to heap-organized storage instead of always clustering by an index?
Because clustering by an index (an index-organized table) optimizes for one access pattern — typically range scans on the clustering key — at the cost of making inserts more expensive (maintaining sort order) and making all other access patterns no better than a heap file would have been anyway. A plain heap file makes no such bet: it is uniformly cheap to write regardless of query pattern, and the database administrator can layer exactly the indexes the actual workload needs on top, rather than committing to one clustering choice at table-creation time.