# Week 13 - C-Store: A Column-Oriented DBMS

Last edited: 2026-07-28

Paper: C-Store: A Column-oriented DBMS — Stonebraker et al., VLDB 2005.

C-Store is a read-optimised relational DBMS that stores data by column rather than by row. It is the research prototype that became Vertica, and whose ideas — columnar storage, aggressive compression, overlapping projections — are now the foundation of every modern analytical database (Redshift, BigQuery, Snowflake, DuckDB).

# Why Row Stores Fail for Analytics

Traditional row-store DBMSs write all attributes of a record contiguously on disk. A single disk write pushes an entire record out — optimal for OLTP where writes dominate. But for analytical queries that read two or three columns out of fifty, a row store must read (and buffer) all fifty columns to retrieve the two it needs.

Two trends make column storage attractive in 2005:

  • CPU is outpacing disk bandwidth: it now makes sense to trade CPU cycles (for decompression) for disk bandwidth (reading fewer bytes).

  • Read-mostly workloads (data warehouses, CRM, ad-hoc analytics) have a fundamentally different access pattern — the write optimisation of row stores is a pure liability.

The fundamental advantage of columns:

  1. Read only the columns a query touches — irrelevant attributes never leave disk.

  2. Compress aggressively — a column of a single type with sorted values compresses far better than a row of mixed types. C-Store uses 40% of the disk space of the equivalent row store.

  3. Operate on compressed data directly — many operators never decompress, saving both I/O and memory bandwidth.

# The Hybrid Architecture: RS + WS

C-Store separates storage into two components (Figure 1):

Read-optimised Store (RS): the large, compressed, read-only component. Columns are stored sorted on a chosen sort key, heavily compressed, and densely packed. No in-place updates — RS is append-only from the perspective of the tuple mover.

Writeable Store (WS): a small, insert-optimised component built on BerkeleyDB B-trees. Stores the same column-oriented projections as RS but uncompressed, with explicit storage keys, to support efficient transactional inserts and deletes. WS is designed to be entirely main-memory resident.

Tuple Mover: a background process that periodically merges blocks of tuples from WS into RS using a merge-out process (MOP). MOP reads all WS records inserted at or before the Low Water Mark (LWM), merges them with RS column data, and writes a new RS segment. Old-master/new-master swap: RS is replaced atomically by RS'.

Queries must access both RS and WS and merge their results. Inserts go to WS; deletes are marked in RS for later purging by the tuple mover.

# The Data Model: Projections, Not Tables

C-Store does not physically store tables. It stores projections — overlapping subsets of columns from a logical table, each sorted on a chosen sort key.

A projection is denoted: EMP1(name, age | age) — columns name and age, sorted by age.

The same column can appear in multiple projections sorted on different attributes. This redundancy is intentional: it allows the query optimiser to pick the projection with the best sort order for a given query, and it provides the K-safety redundancy for fault tolerance.

Covering set: for any SQL query, there must exist a covering set of projections — at least one projection containing every column referenced by the query. The physical design problem is choosing which projections to materialise.

Segments: every projection is horizontally partitioned into segments by value ranges of the sort key. Each segment is associated with a key range.

Storage keys (SK): each record in RS has an implicit storage key — its ordinal position within the segment. SKs are not stored; they are inferred from position. In WS, SKs are explicitly stored integers.

Join indexes: to reconstruct a logical row from multiple projections, C-Store uses join indexes — collections of (segment_id, storage_key) pairs that map a record in one projection to the corresponding record in another. A join index from EMP3 to EMP1 maps each row of EMP3 (in its sort order) to the storage key of the same logical row in EMP1.

# RS Encoding Schemes

Columns in RS are compressed using one of four encodings, chosen based on whether the column is self-ordered (sorted by its own values) and the number of distinct values:

Type 1 — Self-order, few distinct values: run-length encoding as triples (value, start_position, run_length). One triple per distinct value. Enables very compact storage and direct operation without decompression. A clustered B-tree on the value field supports search.

Type 2 — Foreign-order, few distinct values: bitmap encoding — one bitmap per distinct value, indicating which positions hold that value. Bitmaps are run-length encoded. Supports set operations (AND, OR, NOT) directly on compressed bitmaps. Offset indexes (B-trees mapping column values to bitmap positions) support lookup.

Type 3 — Self-order, many distinct values: delta encoding — store the first value, then deltas from the previous value. A block-oriented form stores the first value and its SK at the start of each block, then deltas. This is how B-tree index keys are compressed in systems like VSAM.

Type 4 — Foreign-order, many distinct values: store values uncompressed (Type 4). A densepack B-tree supports indexing.

Note

The key insight is that C-Store operators can work directly on compressed representations without decompressing first. A Select on a Type 1 column simply scans the triples; a Select on a Type 2 column ANDs bitmaps. Decompression (Decompress operator) only happens when needed for output.

# WS Storage

WS stores the same projections as RS but using B-trees for efficient transactional updates.

Each column in a WS projection is stored as a B-tree on (sort_key, storage_key) pairs, plus a separate B-tree from storage_key to the sort key for reverse lookup. Storage keys are explicitly stored integers (unlike RS where they are implicit positions).

Each insert to a logical table allocates a globally unique SK (via a per-site counter initialised above the largest RS SK) and inserts one record per column per projection.

# Updates and Snapshot Isolation

C-Store expects many concurrent read-only analytical transactions with occasional smaller update transactions. Conventional locking would cause severe contention — every large scan would block writers and vice versa.

Solution: snapshot isolation for read-only queries.

The system maintains:

  • High Water Mark (HWM): the most recent epoch at which all transactions have committed. Read-only queries run as of the HWM — they see a consistent snapshot without acquiring any locks.
  • Low Water Mark (LWM): the earliest epoch at which a read-only transaction can run. The tuple mover only moves records whose insertion epoch ≤ LWM (they are visible to all possible read queries).

Epochs are managed by a timestamp authority (TA) that periodically advances the epoch counter and broadcasts the new HWM.

Visibility: a record in WS is visible if insertion_epoch ≤ HWM AND (deletion_epoch = 0 OR deletion_epoch ≥ LWM). An insertion vector (IV) per WS projection segment tracks the insertion epoch of each record. A deleted record vector (DRV) tracks which records have been logically deleted (stored as a Type 2 bitmap since it is mostly zeros).

Read-write transactions use conventional two-phase locking on WS data and write-ahead logging (NO-FORCE, STEAL policy, REDO-only log). No PREPARE message is sent in commit (unlike 2PC) — the master waits for all workers to finish, then issues commit/abort directly.

# The Tuple Mover

The tuple mover runs as a background process, finding “worthy” WS segments to merge into RS.

Merge-out process (MOP):

  1. Find all WS records with insertion_epoch ≤ LWM.

  2. Split into two groups:

    • Deleted at or before LWM → discard (they are invisible to all queries).

    • Not deleted, or deleted after LWM → move to RS.

  3. Read RS blocks, delete RS items with DRV ≤ LWM, merge in the surviving WS values column by column.

  4. Write a new RS’ segment. Update join indexes.

  5. Atomic swap: RS → RS’. Free old RS disk space.

The merge-out produces a fresh RS segment without fragmentation and without the need for in-place updates.

# Query Execution

C-Store uses a column-oriented executor with 10 operator types operating on projections (sets of co-sorted columns) and bitstrings (bitmaps):

OperatorFunction
DecompressConvert compressed column to uncompressed (Type 4)
SelectApply predicate; produce a bitstring (1 = matches, 0 = no)
MaskApply a bitstring to a projection, emitting only matching rows
ProjectColumn subset (equivalent to π)
SortSort all columns of a projection by a subset of columns
AggregationCompute SQL aggregates per group
ConcatCombine projections with the same sort order into one
PermuteReorder a projection according to a join index
JoinJoin two projections on a predicate
BAnd/BOr/BNotBitwise AND/OR/NOT on bitstrings

Key design: Select produces a bitstring rather than a filtered projection. The bitstring can be used with BAnd/BOr to combine multiple predicates without materialising intermediate results. Mask only materialises the surviving rows when needed — late materialisation.

C-Store iterators return 64K-row blocks of a single column per get_next call (rather than one tuple at a time as in Volcano). This preserves the coupling of data flow and control flow while matching the granularity to column-oriented processing.

# K-Safety and Fault Tolerance

C-Store stores K+1 copies of every column across the grid, where K is configurable. The system tolerates up to K node failures: as long as a covering set of projections remains accessible across surviving nodes, all queries can proceed and all data can be reconstructed.

When a node recovers, it rebuilds its projections by querying surviving nodes for the data it needs and re-running the tuple mover log.

# Performance

Benchmarked against a commercial row store and a commercial column store on 7 TPC-H-style queries over 60M lineitems (1.8 GB), all systems given 2.7 GB storage budget.

Disk usage: C-Store 1.987 GB, Row Store 4.480 GB (given 4.5 GB budget since it couldn’t fit in 2.7 GB), Column Store 2.650 GB. C-Store uses 40% of row store space despite storing redundant projections, because of aggressive compression and no alignment padding.

Query performance (seconds, space-constrained):

QueryC-StoreRow StoreColumn Store
Q1 (count by date)0.036.802.24
Q2 (count by supplier, point date)0.361.090.83
Q3 (count by supplier, range date)4.9093.2629.54
Q4 (max shipdate per orderdate)2.09722.9022.23
Q5 (max shipdate per supplier, join)0.31116.560.93
Q6 (like Q5, range date)8.50652.9032.83
Q7 (revenue by nation, 3-table join)2.54265.8033.24

C-Store is on average 164× faster than the commercial row store and 21× faster than the commercial column store in the space-constrained case.

Four reasons cited for the performance advantage:

  • Column representation — only relevant columns are read from disk.
  • Overlapping projections — multiple sort orders available; the best is chosen per query.
  • Better compression — more sort orders → better compressibility; no padding.
  • Operating on compressed data — avoids decompression for many operations, keeping data smaller throughout the pipeline.

# Key Takeaways

ConceptLesson
Columnar storageRead only the columns a query needs; compress each column independently
Overlapping projectionsRedundant storage enables the best sort order per query; also provides K-safety
WS + RS hybridWrite to a small row-like store; background-merge into the large compressed column store
Snapshot isolationHWM/LWM epoch scheme eliminates lock contention between readers and writers
Tuple mover / LSMBatch migration from WS to RS is the forerunner of the Log-Structured Merge pattern used in modern systems
Late materialisationSelect produces bitstrings; Mask applies them — rows are materialised only when necessary
Compression = performanceCompression reduces I/O and keeps more data in cache; operators on compressed types avoid decompression entirely

C-Store became Vertica (commercialised by Stonebraker in 2005). Its ideas — columnar storage, late materialisation, operating on compressed data — are now universal in analytical databases: Redshift, BigQuery, Snowflake, Parquet/Arrow, and DuckDB all descend directly from this paper.