DBRaven
Pattern · data storage

Index Table

established

Summary

Maintain a secondary lookup table that maps a non-primary-key attribute to primary keys, enabling efficient queries that the primary key structure does not support without requiring full-table scans.

Problem

The primary key structure of a NoSQL store supports only one access pattern efficiently. Secondary access patterns (lookups by email, customer ID, status) require either full scans or a manually maintained secondary index.

Description

In relational databases, a secondary index provides this functionality natively. In NoSQL systems (DynamoDB, Cassandra, HBase), the primary key determines the physical storage layout and the one access pattern the system is optimized for. Any secondary access pattern requires a manually maintained index table or a costly full-table scan.

Example: a DynamoDB orders table with primary key order_id supports O(1) lookup by order ID. To support "list all orders for customer X", without an index table, you must scan the entire table. An index table customer_orders with partition key customer_id and sort key created_at maps each customer to their order IDs, enabling O(log N) range lookups.

Cassandra example: primary table partitioned by user_id supports "get user profile." An index table email_to_user_id with partition key email enables login-by-email lookups without a full partition scan.

Write-time consistency challenge: every write to the primary table requires a corresponding write to the index table. If the index write fails after the primary write succeeds, the index is inconsistent: it does not reflect the primary table's state. Mitigations: - Outbox pattern: write index update as part of the same transaction (where

supported) or via an outbox event that is idempotently applied.

- Eventually consistent index: accept that the index may lag the primary

table by seconds; use CDC (change data capture) to maintain it.

- Application-layer dual-write with retry: write to both tables, retry on

failure, accept brief inconsistency windows.

DynamoDB Global Secondary Indexes (GSI) automate this at the cost of eventual consistency: DynamoDB maintains the GSI asynchronously, so reads from a GSI may be slightly behind primary table writes. This is usually acceptable and removes the dual-write burden from the application.

Heap table vs clustered primary index: even where a relational database makes an index table unnecessary, how the primary table itself is stored still shapes index behavior. PostgreSQL's default table storage is a heap: rows have no particular physical order, and every index, including the one enforcing the primary key, is a separate B-Tree that points at a heap row via a TID (a row identifier: block number plus offset within the page). A primary-key range scan therefore still requires following TIDs into the heap, and physical row order depends on insert order and VACUUM's row movement, not on key order. InnoDB (MySQL's default and only storage engine for the primary key) instead uses a clustered primary index: the leaf pages of the primary key's own B-Tree are the row data, so there is no separate heap and a primary-key range scan is physically sequential I/O by construction. The cost lands on secondary indexes instead: an InnoDB secondary index stores the primary key value rather than a physical location, so a secondary-index lookup does two B-Tree traversals (secondary index to primary key, then primary key to row), where a PostgreSQL secondary index's stored TID gets there in one. This is a property of which engine was chosen, not something an application opts into the way it opts into an index table.

Local vs global secondary index does not arise on a single, unpartitioned table: one secondary index simply covers the whole table and every write updates it directly. The distinction appears once the base data is sharded (see the sharding pattern) and a secondary index is needed on a column that is not the shard key. A local secondary index is built and maintained independently per shard, indexing only that shard's rows: a write only touches its own shard's index, which is cheap, but a query that filters on the indexed column alone cannot know which shard holds a match and must scatter-gather to every shard. A global secondary index is instead a separate index structure, itself partitioned independently of the base table's shard key, covering rows from every shard: a query on the indexed column hits one place instead of fanning out, but every write must now keep two independently-partitioned structures consistent, which is materially harder. DynamoDB's GSI is exactly this: a global secondary index maintained asynchronously precisely because synchronously updating a separately-partitioned structure on every write is expensive to keep consistent; the eventual-consistency cost described above is not incidental to DynamoDB, it is inherent to any global secondary index over sharded data.

Tradeoffs

Secondary read performance
+0.9

Enables O(log N) secondary lookups instead of full-table scans; critical for non-primary access patterns

Write amplification
-0.6

Every primary write requires N index table writes; amplification factor equals number of maintained indexes

Consistency complexity
-0.5

Dual-write consistency requires outbox or CDC; application must handle partial failure where primary succeeds but index write fails

Operational overhead
-0.4

Index tables must be versioned, backfilled on schema changes, and monitored for consistency drift

GSI convenience (DynamoDB)
+0.6

DynamoDB GSIs remove dual-write burden at the cost of eventual consistency; standard for DynamoDB-native applications

Local vs global secondary index (sharded deployments)
-0.2

A local per-shard index is cheap to write but forces scatter-gather fan-out on every query; a global cross-shard index answers a query in one place but requires keeping two independently-partitioned structures consistent on every write, typically asynchronously

When to use

Data is stored in a NoSQL system that optimizes for a single primary key

DynamoDB, Cassandra, HBase require explicit index tables for non-primary access patterns

A secondary access pattern is frequent and cannot tolerate full-table scan latency

Index table cost is justified when the secondary query is in the hot path

The secondary attribute has reasonable cardinality

Very low-cardinality attributes (e.g., boolean status) may be better served by filtered scans or partitioned hot-path logic

When not to use

Using a relational database with native secondary index support

RDBMS secondary indexes are maintained transactionally by the engine; manual index tables are unnecessary overhead

Write volume is high and write amplification from dual-write is already a bottleneck

Each index table adds one write per primary write; multiplied across many indexes, write amplification compounds

Operational Requirements

mandatory

Define a consistency strategy for index updates (outbox, CDC, or eventual via GSI)

Silent index inconsistency causes query correctness bugs that are hard to detect

mandatory

Backfill index table when the primary table already contains data at index creation time

New index tables on existing data must be populated; partial backfill causes incomplete query results

recommended

Monitor index table for consistency drift relative to primary table

Periodic reconciliation jobs catch index gaps caused by failed dual-writes or CDC lag

Characteristics

Scales on
read
Implementation complexitymedium
Operational complexitymedium
Scaling ceilingIndex tables scale with the NoSQL backend's throughput model. For DynamoDB GSI, throughput is provisioned separately from the primary table. Write throughput ceiling is the combined write capacity of all maintained index tables: each primary write fans out to N index writes.

Technologies

Canonical

dynamodbcassandra

Alternatives

mongodbhbase

Relationships

Evolves to

cqrs

Complements

cqrsmaterialized viewoutbox patternsharding

Basis

Standard NoSQL design pattern; DynamoDB documentation and Cassandra data modeling guides document this explicitly. Heap-vs-clustered and local-vs-global secondary index behavior are documented directly in PostgreSQL and MySQL/InnoDB reference documentation and DDIA's partitioning chapter.

Sources & Claims

PostgreSQL heap rows are located via TID (row identifier), which every index including the primary key index stores and dereferences.

pending

official documentation · PostgreSQL documentation on physical storage and TID

storage-engine-internals-spine batch 4

InnoDB stores the primary key as a clustered index, so row data lives in the leaf pages of the primary key's own B-Tree; an InnoDB secondary index stores the primary key value rather than a physical row location, so a secondary-index lookup requires a second B-Tree traversal into the clustered index.

pending

official documentation · MySQL Reference Manual: InnoDB Index Types (Clustered and Secondary Indexes)

storage-engine-internals-spine batch 4

A local secondary index in a sharded or partitioned system is built and maintained per shard and indexes only that shard's rows, so a write updates only its own shard's index while a query on the indexed column alone must scatter to every shard.

pending

ddia or accepted reference · Kleppmann, Designing Data-Intensive Applications, Chapter 6, Partitioning and Secondary Indexes

storage-engine-internals-spine batch 4

DynamoDB Global Secondary Indexes are independently partitioned from the base table and updated asynchronously, so a GSI read can briefly lag a base table write.

pending

official documentation · AWS DynamoDB documentation: Global Secondary Indexes

storage-engine-internals-spine batch 4

Used In Architecture Scenarios

Audit and Compliance Platformhigh

Financial Ledger

An append-only audit log architecture for capturing system actions: user operations, data access events, configuration changes, and financial operations: with cryptographic integrity chaining, immutable storage, and dual-path query serving. PostgreSQL stores the authoritative append-only event log in time-partitioned tables; ClickHouse serves historical aggregate queries and retention analytics; Kafka streams audit events in real time to SIEM integrations and security dashboards; Redis caches hot audit query results for compliance-facing read paths. No record is ever updated or deleted: only inserts are permitted. Each record includes a hash of the previous record, forming a tamper-evident chain verifiable without external tooling.

Content Management Platformmoderate

Read-Heavy Application

A CMS for publishing and serving structured content: articles, documentation, product pages, and localized variants: where read APIs serve 50–100x more traffic than editorial write APIs. PostgreSQL stores the content graph (articles, authors, categories, taxonomy) and workflow state (draft, in-review, scheduled, published). Redis caches published content objects for read APIs. Elasticsearch powers full-text content search with faceting and relevance ranking. Cache invalidation on publish must be fast and complete; N+1 query patterns on content relationship traversal are the dominant database performance risk during reads.

Healthcare Records Platformexpert

Financial Ledger

An electronic health record (EHR) architecture built around strict auditability, HIPAA compliance, and append-only correctness. Clinical records are mutable by design (amendments, addenda) but corrections must be explicitly attributed, not silently overwritten. Event sourcing provides a reconstructable audit log; PostgreSQL row-level security enforces patient-level access control at the database layer; Kafka streams HL7 FHIR events to downstream clinical systems. The architecture must support breach detection, access auditing, and state reconstruction at any historical point: not just current state retrieval.

Index Table: DBRaven