Understanding Database Indexes
Index Entry
Read aloud is unavailable in this browser.
The Building Blocks of Indexes
An index, regardless of its type or implementation, is ultimately a collection of index entries. Each entry is the atomic unit of information that the index stores and organizes. Understanding index entries is understanding indexes at the most fundamental level.
An index entry answers a simple question: "Given this search key value, where can I find the corresponding data?" But the way this question is answered—what information is included, how it is formatted, how duplicates are handled—varies significantly across database systems and index configurations. These variations have profound implications for space efficiency, lookup performance, update cost, and concurrency behavior.
By the end of this page, you will understand the anatomy of an index entry, recognize the three major alternatives for index entry design, analyze storage overhead for different entry formats, and comprehend how entry design affects index performance characteristics.
Anatomy of an Index Entry
At its core, an index entry consists of two fundamental components:
Index Entry = Search Key Value + Data Reference
Search Key Value:
This is the value (or combination of values for composite keys) extracted from the indexed column(s). The key value is what makes the entry findable—index traversal algorithms compare the search predicate against key values to locate matching entries.
- For an index on
employee_id: The key might be10042 - For an index on
last_name: The key might be'Smith' - For a composite index on
(department, hire_date): The key might be('Engineering', '2023-01-15')
Data Reference:
This component tells us where to find the actual data record. The nature of this reference varies dramatically across index designs and determines much of the index's behavior. The three main alternatives are explored in detail in subsequent sections.
Additional Entry Components:
Beyond the basic key-reference pair, index entries often include:
- Entry Length Field: For variable-length entries, indicates total entry size
- Key Length Field: For variable-length keys, indicates key size
- Null Indicator: Bitmap or flag indicating which key components are null
- Timestamp/Version: In MVCC systems, indicates entry visibility
- Delete Marker: Indicates logically deleted entries awaiting garbage collection
- Compression Metadata: For prefix-compressed entries, reconstruction information
The physical format of an entry is carefully designed to minimize space while enabling efficient byte-level operations. Real-world entries are packed structures with no wasted bytes.
| Component | Typical Size | Purpose | Variability |
|---|---|---|---|
| Key Value(s) | 4-100+ bytes | Search target | Fixed or variable by data type |
| Record Pointer (RID) | 6-8 bytes | Locate data record | Usually fixed |
| Entry Header | 0-4 bytes | Entry metadata | Often fixed |
| Null Bitmap | 0-N bits | Track null columns | Depends on nullable count |
| Version/Transaction ID | 0-8 bytes | MVCC visibility | System-dependent |
Alternative 1: Data Record as Index Entry
In this design, the index entry is the data record itself. The data is stored directly within the index structure, organized by the search key. There is no separate data file; the index contains all the data.
Conceptual Structure:
┌─────────────────────────────────────────────────────────┐
│ Index Entry = (Search Key Value, Full Data Record) │
│ │
│ Example: │
│ (10042, {emp_id:10042, name:'Smith', dept:'Eng', ...}) │
└─────────────────────────────────────────────────────────┘
This is the design used by clustered indexes (also called primary indexes in some contexts). The data is physically arranged in the order of the index key.
- •No additional lookup: Data is immediately available when entry is found
- •Excellent locality: Records sorted by key cluster together on disk
- •Range queries are optimal: Sequential scan through sorted data
- •No pointer overhead: No RID storage needed
- •Only one per table: Data can be sorted by only one key
- •Large entries: Full records make index traversal slower
- •Expensive updates: Changing key values requires moving entire record
- •Secondary indexes need different design: Must point to this index
MySQL's InnoDB, SQL Server's clustered indexes, and Oracle's Index-Organized Tables (IOTs) all use this approach. The primary key index IS the table—data is stored in primary key order. This is why choosing the right primary key is so critical in these systems.
When to Use This Design:
- When there is a natural ordering key that dominates query patterns
- When range queries on that key are common and performance-critical
- When the table is predominantly read-heavy after initial load
- When the key values rarely change (to avoid costly record movement)
This is the default table organization in systems like InnoDB, where every table has a clustered index on its primary key (or a hidden row ID if no primary key is defined).
Alternative 2: (Key, RID) Pairs
In this design, the index entry contains the search key value and a record identifier (RID) pointing to the data record's location. The data resides in a separate heap file or clustered index.
Conceptual Structure:
┌────────────────────────────────────────────────────────┐
│ Index Entry = (Search Key Value, RID) │
│ │
│ RID typically = (Page Number, Slot Number) │
│ │
│ Example: │
│ ('Smith', Page:1042, Slot:7) │
└────────────────────────────────────────────────────────┘
This is the design used by secondary indexes (non-clustered indexes) in most database systems. Each entry is a compact pointer to a data record.
Handling Duplicate Keys:
When multiple records share the same key value, there are two sub-approaches:
2a. One entry per record (duplicate entries):
('Smith', Page:1042, Slot:7)
('Smith', Page:1089, Slot:3)
('Smith', Page:2001, Slot:15)
- Simple structure; each entry is self-contained
- Key value repeated, consuming more space
- Direct point lookup to each record
2b. One entry per unique key (list of RIDs):
('Smith', [RID:1042.7, RID:1089.3, RID:2001.15])
- Key value stored once; space-efficient for high-duplicate keys
- Variable-length entries complicate storage management
- Must manage RID list growth and overflow
Most systems use 2a (one entry per record) for simplicity, accepting the space overhead in exchange for uniform entry sizes and simpler concurrency control.
- •Compact entries: Only key + pointer (typically 10-20 bytes)
- •Multiple indexes allowed: Any table can have many such indexes
- •Efficient updates: Changing non-key columns doesn't affect index
- •Good fanout: Small entries = more entries per page = shallower trees
- •Extra I/O per lookup: Must follow RID to fetch actual data
- •Random I/O pattern: RIDs may point to scattered pages
- •RID invalidation risk: Page reorganization may break pointers
- •Double-lookup in some systems: Secondary → Clustered → Data
In InnoDB, secondary indexes store (Key, Primary Key Value) rather than (Key, RID). To find data, the system: 1) Searches secondary index for Primary Key, 2) Searches clustered index using Primary Key. This 'double lookup' trades some read performance for immunity to page-level reorganization and eliminates pointer maintenance.
Alternative 3: (Key, List of RIDs)
This variation consolidates all RIDs for a given key value into a single index entry. Instead of repeating the key for each matching record, the key appears once with an associated list of record pointers.
Conceptual Structure:
┌───────────────────────────────────────────────────────────────┐
│ Index Entry = (Search Key Value, RID List) │
│ │
│ Example: │
│ ('Engineering', [RID:101.3, RID:104.7, RID:107.2, RID:203.1])│
└───────────────────────────────────────────────────────────────┘
This design is particularly interesting for columns with many duplicates—such as department names, status codes, or category identifiers.
Implementation Challenges:
-
Variable-length entries: RID list length varies by key. Some keys may have millions of RIDs, others may have one.
-
Entry growth: When a new record with an existing key is inserted, the entry must grow. This may require:
- In-place expansion (if page has space)
- Entry relocation to a different page
- Overflow pages for very long lists
-
Concurrency complexity: Updating an entry that may be read-locked by other transactions requires careful protocol design.
-
Deletion challenges: Removing one RID from a list requires locating it within potentially millions of entries.
Typical Implementation: Overflow Pages
For keys with very many duplicates, most implementations use overflow pages:
- The index entry stores the key and a pointer to a chain of overflow pages
- Overflow pages contain RID lists and pointers to subsequent overflow pages
- Acts like a linked list of RID blocks
| Approach | Space Efficiency | Lookup Speed | Insert/Delete | Use Case |
|---|---|---|---|---|
| Duplicate entries | Lower (key repeated) | Optimal | Simple | Low-duplicate keys |
| RID list (inline) | Higher (one key) | Good for few RIDs | Complex | Moderate duplicates |
| Overflow pages | Highest (one key) | Variable | Most complex | High duplicates |
For columns with very few distinct values (low cardinality) and very many duplicates, bitmap indexes often outperform RID-list approaches. Instead of storing lists of RIDs, a bitmap stores one bit per row indicating presence/absence. Bitmap indexes are covered in detail in the Advanced Index Structures chapter.
Entry Storage and Physical Layout
Index entries must be stored on disk pages, and their physical layout significantly affects performance. Understanding entry storage helps explain why certain operations are fast or slow.
Page Layout for Index Entries:
A typical B+-tree leaf page contains:
┌──────────────────────────────────────────────────────┐
│ Page Header (LSN, flags, pointers) ~20 bytes │
├──────────────────────────────────────────────────────┤
│ Entry 1: (key₁, RID₁) │
│ Entry 2: (key₂, RID₂) │
│ Entry 3: (key₃, RID₃) │
│ ... │
│ Entry N: (keyₙ, RIDₙ) │
├──────────────────────────────────────────────────────┤
│ Free Space │
├──────────────────────────────────────────────────────┤
│ Slot Directory (offsets to entries) ~2N bytes │
│ Page Trailer ~8 bytes │
└──────────────────────────────────────────────────────┘
Entries are typically stored in key order, with a slot directory at the page bottom pointing to each entry. This allows binary search within the page.
Fixed-Length vs. Variable-Length Entries:
Fixed-Length Entries:
- All entries occupy the same space (max key size + RID size)
- Simple to manage: entry N is at offset N × entry_size
- Wastes space when actual keys are shorter than maximum
- Common for integer keys or short fixed-length strings
Variable-Length Entries:
- Each entry occupies only the space needed
- Requires slot directory or length prefixes to locate entries
- More complex insertion/deletion (may require compaction)
- Essential for VARCHAR keys or composite keys with variable components
Key Compression Techniques:
To improve fanout (entries per page), databases employ compression:
-
Prefix Compression: For sequential keys, store only the differing suffix
- Keys: 'application', 'apply', 'appointment'
- Stored: 'application', '+ly' (diff from prev), '+ointment'
-
Suffix Truncation: In internal nodes, store only enough of the key to distinguish children
- Full key: 'Bartholomew'
- Truncated separator: 'Bar' (if sufficient to route)
- •Higher fanout = shallower trees — More entries per page means fewer levels to traverse
- •Shallower trees = fewer I/Os — Each level traversed requires one page read
- •Smaller entries = higher fanout — Compact entry design is critical for performance
- •Compression adds CPU cost — Trade computation for reduced I/O (usually worthwhile)
Index Entries in Different Index Types
While we've focused on B+-tree entries, different index types have different entry structures optimized for their use cases.
B+-Tree Leaf Entries:
- Contain (key, RID) or (key, data)
- Sorted by key within the page
- Linked to sibling leaves for range scans
- Support: equality, range, prefix matching, ordering
B+-Tree Internal Entries:
- Contain (key, child_page_pointer)
- Keys serve as separators (routing guides)
- No RIDs or data—purely navigational
- Often use suffix truncation for space efficiency
Hash Index Entries:
- Contain (key, RID) within hash buckets
- No ordering within buckets
- Bucket determined by hash(key) mod bucket_count
- Support: equality only (no range queries)
Bitmap Index Entries:
- Fundamentally different structure
- Entry per distinct key value: (key, bit_vector)
- Bit vector has one bit per row in the table
- Bit is 1 if that row has this key value, 0 otherwise
- Supports: equality, AND/OR operations efficiently
Full-Text Index Entries (Inverted Index):
- Entry per distinct term: (term, posting_list)
- Posting list: list of (document_id, position, frequency) tuples
- Optimized for text search, not exact equality
- Supports: keyword search, phrase matching, relevance ranking
Spatial Index Entries (R-tree):
- Entry per spatial object: (bounding_box, RID)
- Objects grouped by spatial proximity
- Bounding boxes may overlap (unlike B+-trees)
- Supports: contains, intersects, nearest-neighbor queries
| Index Type | Entry Structure | Key Characteristic | Query Support |
|---|---|---|---|
| B+-tree Leaf | (key, RID) or (key, data) | Ordered by key | Equality, range, prefix, sort |
| B+-tree Internal | (separator_key, child_ptr) | Routing only | Navigation |
| Hash Bucket | (key, RID) unordered | Hash-grouped | Equality only |
| Bitmap | (value, bit_vector) | One bit per row | Set operations, low-cardinality |
| Inverted (Text) | (term, posting_list) | Per-term postings | Text search, ranking |
| R-tree (Spatial) | (MBR, RID) | Spatial grouping | Spatial predicates |
Entry Design Trade-offs and Considerations
Every aspect of index entry design involves trade-offs. Understanding these trade-offs enables you to reason about why databases make the choices they do and how to configure indexes for specific workloads.
Trade-off 1: Entry Size vs. Feature Set
| More Information in Entry | Less Information in Entry |
|---|---|
| Larger entries | Smaller entries |
| Lower fanout | Higher fanout |
| Fewer I/Os for covered queries | More I/Os (need table lookup) |
| Higher write overhead | Lower write overhead |
Covering indexes (including extra columns for index-only scans) exemplify this trade-off: larger entries eliminate table lookups but increase index size and write costs.
Trade-off 2: Pointer Format
| RID (Page, Slot) | Primary Key Value |
|---|---|
| Compact (6-8 bytes) | Variable (key-dependent) |
| Direct access to data | Requires clustered index lookup |
| Broken by page reorganization | Survives reorganization |
| Requires maintenance on splits | Self-maintaining |
InnoDB chose primary key as pointer; PostgreSQL uses RID (ctid). Each has valid rationale.
Trade-off 3: Duplicate Handling
| Duplicate Entries | Aggregated RID Lists |
|---|---|
| Simple, uniform entries | Complex, variable-length entries |
| Key stored repeatedly | Key stored once |
| Easy concurrency | Complex concurrency |
| Byte-aligned operations | List manipulation overhead |
Most production databases use relatively simple entry designs: (key, RID) with one entry per record, variable-length entries with slot directories, and optional prefix compression. Exotic entry structures tend to appear only in specialized index types (bitmaps, full-text) or research prototypes.
- •Minimize entry size — Every byte affects fanout and tree height
- •Use fixed-length when possible — Simpler management, predictable behavior
- •Apply compression judiciously — CPU is cheaper than disk I/O
- •Match pointer format to workload — Consider update frequency and reorganization needs
- •Keep duplicates simple — One entry per record unless space is truly critical
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.