# 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:
Read only the columns a query touches — irrelevant attributes never leave disk.
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.
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.
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):
Find all WS records with
insertion_epoch ≤ LWM.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.
Read RS blocks, delete RS items with DRV ≤ LWM, merge in the surviving WS values column by column.
Write a new RS’ segment. Update join indexes.
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):
| Operator | Function |
|---|---|
Decompress | Convert compressed column to uncompressed (Type 4) |
Select | Apply predicate; produce a bitstring (1 = matches, 0 = no) |
Mask | Apply a bitstring to a projection, emitting only matching rows |
Project | Column subset (equivalent to π) |
Sort | Sort all columns of a projection by a subset of columns |
Aggregation | Compute SQL aggregates per group |
Concat | Combine projections with the same sort order into one |
Permute | Reorder a projection according to a join index |
Join | Join two projections on a predicate |
BAnd/BOr/BNot | Bitwise 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):
| Query | C-Store | Row Store | Column Store |
|---|---|---|---|
| Q1 (count by date) | 0.03 | 6.80 | 2.24 |
| Q2 (count by supplier, point date) | 0.36 | 1.09 | 0.83 |
| Q3 (count by supplier, range date) | 4.90 | 93.26 | 29.54 |
| Q4 (max shipdate per orderdate) | 2.09 | 722.90 | 22.23 |
| Q5 (max shipdate per supplier, join) | 0.31 | 116.56 | 0.93 |
| Q6 (like Q5, range date) | 8.50 | 652.90 | 32.83 |
| Q7 (revenue by nation, 3-table join) | 2.54 | 265.80 | 33.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
| Concept | Lesson |
|---|---|
| Columnar storage | Read only the columns a query needs; compress each column independently |
| Overlapping projections | Redundant storage enables the best sort order per query; also provides K-safety |
| WS + RS hybrid | Write to a small row-like store; background-merge into the large compressed column store |
| Snapshot isolation | HWM/LWM epoch scheme eliminates lock contention between readers and writers |
| Tuple mover / LSM | Batch migration from WS to RS is the forerunner of the Log-Structured Merge pattern used in modern systems |
| Late materialisation | Select produces bitstrings; Mask applies them — rows are materialised only when necessary |
| Compression = performance | Compression 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.