CS PAPERS DEEP-READ · PAPER 64
Stonebraker, Abadi, Madden, Cherniack, O'Neil et al. · MIT / Brandeis / UMass Boston / Brown · VLDB 2005
In 2005 Michael Stonebraker and a group of database researchers built a prototype called C-Store that changed how a database puts data on disk: store it column by column instead of row by row. It sounds like a mere rearrangement, yet it made "boil a hundred million records down to one number" queries two orders of magnitude faster. BigQuery, Snowflake, ClickHouse, and the Parquet files strewn across every data lake all walk this road.
Think of a database as a supermarket's books. The traditional way is keeping receipts: each sale's date, store, item and amount written on one slip, slips stacked up. Now the owner asks, "How much did we sell nationwide last year?" You need only one number — the amount — yet you must pull out every slip whole, glance at the amount, and put it back. Across a hundred million slips, nearly all the effort goes into hauling fields nobody looks at.
C-Store rearranges the books: copy every slip's "amount" into one long strip, every "store" into another, every "item" into a third. Asked about amounts, you carry only the amounts strip and never touch the rest. Same question, a fraction of the hauling.
And the new arrangement throws in three bonuses.
First, it compresses beautifully. A strip holds all the same kind of thing — in the "store" strip, "Downtown branch" may repeat for thousands of rows in a row, so you write one note: "Downtown branch, starting at row 1, for the next 5,000 rows." Better still, many questions can be answered without unpacking: to count that branch's sales, read the phrase "the next 5,000 rows" and you are done, never spreading it back out into 5,000 rows.
Second, the same data can be kept in several sort orders at once. Keep one copy ordered by date, another ordered by store — as if the same books were bound twice, once by date and once by branch, so every common question meets a copy already in the right order. A traditional database chases the same effect by building separate indexes; here, because columns compress so small, simply keeping extra copies is the better bargain. Those copies double as backups: when a machine dies, you ask the other copies for whatever it was holding.
Third, what about writing? Books that are neatly ordered and tightly packed are miserable to insert a line into. C-Store splits the operation in two: new data first lands in a small scratch pad (fast to write, uncompressed), and a background porter periodically merges what has piled up there into the big tidy ledger in bulk. Queries read both, so you never miss data that was just written.
In the paper, C-Store ran a set of analytical queries two orders of magnitude faster than a commercial row-oriented database of the day, on less than half the disk. It became the company Vertica (bought by HP in 2011), and "columnar plus heavy compression" became the default shape of a data warehouse.
One honest cost: this arrangement is built for workloads that are read far more than written. For transactional work — editing orders one at a time and seeing the change instantly — storing row by row still suits better.
Store by column and a query hauls only the columns it truly needs; a single column looks alike all the way down, so it compresses tiny and can often be computed on without unpacking; keep the same data in several sort orders so the copies serve as index and backup at once; and absorb the slow writes with a scratch pad plus a background bulk merge. Analytical databases changed generation here.
Want the projections, join indices, WS/RS diagrams and the numbers? → switch to the deep read
C-Store flips a relational database's physical storage from rows to columns, and redesigns everything around that choice: heavy compression with operators that work directly on the compressed form, the same data stored as several overlapping "projections" in different sort orders (sorted redundant copies that serve as index and as fault-tolerance replica), and a write-optimized store + read-optimized store + background tuple mover hybrid that absorbs the writes a column store hates. It marks the generational turn in analytical (OLAP) databases and was commercialized as Vertica.
The authors are Michael Stonebraker (MIT), Daniel Abadi, Samuel Madden, Mitch Cherniack (Brandeis), Elizabeth and Patrick O'Neil (UMass Boston), Stan Zdonik (Brown) and others — about a dozen in all — publishing at VLDB 2005. Storing by column was not their invention: the decomposition storage model (1985), the commercial Sybase IQ, and the academic MonetDB all came earlier. C-Store's contribution is placing column storage inside one complete, workable database design — compression, sorted redundancy, a hybrid write path, replica-based recovery and snapshot reads, each leaning on the others. It launches Vertica, and together with the contemporaneous Dremel (columnar storage plus a multi-level execution tree — see that paper here) forms one of the two headwaters of today's warehouses.
The mainstream relational systems of 2005 (Oracle, DB2, SQL Server) all descended from the OLTP world of the 1970s and 80s, and their physical storage was write-optimized: a whole row placed contiguously, so inserting a record writes one place. But the market's center of gravity had moved to the data warehouse — bulk-loaded, rarely modified, repeatedly scanned and aggregated.
On that workload the row store wastes work structurally: disks read in blocks, and a block is packed with whole rows. A query that rolls up sales by year touches two or three fields, yet the disk is forced to drag every row's dozens of fields into memory; the wider the table, the worse the waste. The remedy of the day was piles of indexes and materialized views, which cost space, slowed writes, and only ever covered the queries somebody anticipated.
The authors' judgment is blunt: the problem is not insufficient tuning, it is a physical layout on the wrong side of the trade. If reads vastly outnumber writes, design a read-optimized store from scratch — and pay back the harder writes by other means.
C-Store stores each column contiguously on its own. A query reads exactly the columns it needs and never touches a byte of the rest; on wide tables, analytical queries routinely cut an order of magnitude of I/O this way.
The cost is tuple reconstruction: a query usually has to stitch several columns back into one record. C-Store's answer is position alignment — within a set of columns, the i-th value belongs to the i-th row by construction, so stitching is just indexing by ordinal and no row id is stored alongside each value. That "position is identity" convention became table stakes for every columnar engine since.
A column holds values of one type and one meaning, often in sorted order, so it repeats itself far more than a row does and compresses far better. C-Store picks an encoding along two axes: is this column the sort key of the projection it lives in (ordered or not) × does it have few or many distinct values:
(value, start position, run length), squeezing thousands of rows into one entry.The decisive step is that operators eat the compressed representation directly rather than decompressing first. Counting one store's sales against a run-length encoding means reading the phrase "5,000 rows in a row" — the answer is 5,000. What that saves is not only disk I/O but the CPU of decoding and touching rows one at a time. Compression stops being a "space for CPU" trade and starts saving both.
C-Store does not store "tables"; it stores projections: a set of columns taken from a table (possibly pulling in columns of another table through a foreign key), sorted as a whole on some sort key. One column may appear in several projections, each sorted differently. As long as every column is covered by some projection, any query can be answered.
Why design it this way? Once data is columnar and heavily compressed, keeping one more sorted copy becomes cheap — and a sorted copy does three jobs at once: it acts as an index (range queries on that key can skip whole segments); it acts as a compression multiplier (the sort key's own column is ordered, so run-length encoding takes it to the floor); and it acts as a fault-tolerance replica (see K-safety below). Base table, index and materialized view — three separate things in a classical database — collapse into one kind of object.
Each projection is then partitioned horizontally into segments by ranges of the sort key and spread across machines; each row within a segment has a storage key, simply its ordinal position, which the read-optimized copy never stores at all and computes from position instead. Different projections sort differently, so their row orders do not line up, which is what join indices are for: for each row of a "sending" projection they record the (segment id, storage key) of the matching row in the "receiving" projection, stitching the two orders back into one logical record.
Columnar plus sorted plus heavily compressed amounts to welding the data into a slab — inserting a row in the middle nearly means rewriting a stretch of it. So C-Store splits the database in two:
Inserts go only to WS; a delete marks the row (recorded in a deleted-record vector); an update is a delete plus an insert. A background tuple mover periodically runs a bulk merge, folding WS data that is old enough into RS in blocks, rewriting compressed blocks and updating join indices as it goes. Queries read WS and RS together and merge the results, so freshly written data is never missed. "Small fast write area + big tidy read area + background merge" recurs later in LSM trees, in ClickHouse's part merging, and in Vertica's own WOS/ROS.
Warehouse queries are long and heavy; fighting writers for locks would strangle both. C-Store lets read-only transactions run as of a slightly earlier instant: timestamps are coarse-grained epochs (a few seconds each) and a timestamp authority advances a high water mark. A read-only query picks an epoch no later than that mark and uses each row's insertion and deletion times to decide whether it was visible then — so readers take no locks and never block writers, at the price of seeing data a few seconds stale. Read-write transactions use ordinary two-phase locking.
RS is huge and rewritten in bulk, so write-ahead logging it would be absurdly expensive. C-Store's trade: log only the small WS and let replication carry the fault tolerance — keep enough projections that any K simultaneous machine failures still leave every column covered (K-safety). When a crashed machine restarts, the data it missed is not replayed from a log but queried back out of the other replicas and loaded into its WS. The elegance is that those replicas already exist for query performance; the fault tolerance is nearly free.
The paper reports a preliminary experiment: a TPC-H-derived warehouse schema, seven queries, against a popular commercial row store and a popular commercial column store (both anonymized). In the configuration that limited each system's disk budget, the same data took 1.987 GB in C-Store versus 4.480 GB in the commercial row store (about 40%), and on the queries C-Store averaged roughly 164× faster than the commercial row store and 21× faster than the commercial column store. The authors attribute the gain to three effects stacking: reading only the needed columns, scanning projections rather than whole tables, and better compression.
They are notably restrained about the numbers themselves: the prototype implemented only part of the design — WS and the tuple mover were not yet fully working — so what was measured is essentially the read-optimized half, and they say plainly that a thorough study must wait for the rest. Read it as directional evidence, not as a product comparison.
After this paper, columnar storage went from a niche option to the assumed baseline for analytical databases. C-Store was commercialized as Vertica (acquired by HP in 2011), and its combination — columnar layout, per-column encoding chosen for heavy compression, computing on the compressed form, sorted redundant copies doubling as indexes, a split write/read path with background merging — was inherited by nearly every analytical engine since: Redshift, ClickHouse, Snowflake, BigQuery, and the Parquet / ORC columnar file formats. The lakehouse queries that scan billions of rows in seconds today are living off that dividend.
It also made Stonebraker's slogan concrete: one size does not fit all — OLTP and OLAP deserve engines of different shape, not one engine with different knobs (the same group's H-Store / VoltDB is the other half of that argument). Academically it opened a whole line of work on how compression and execution should cooperate and on when to stitch columns back into rows (late materialization).
① In one line: columnar storage + heavy compression + sorted redundant projections + a split write/read path, a database rebuilt for workloads read far more than written.
② The pain: a row store keeps whole rows together, while analytical queries touch few columns, forcing the disk to haul fields nobody reads.
③ Columnar mechanics: each column stored contiguously, only the needed ones read; rows are stitched back by position ("the i-th value is the i-th row"), with no row id per value.
④ Compression: encoding picked by "sort key or not × few or many distinct values" — RLE / bitmaps / deltas / none; the key is that operators consume the compressed form, saving space and CPU at once.
⑤ Projections: no tables, only overlapping column groups each sorted on a key and cut into segments; a sorted copy serves as index, compression multiplier and replica, while join indices stitch across projections.
⑥ Writes: new data enters the uncompressed WS and a background tuple mover merges it in bulk into the packed RS, with queries reading both — an ancestor of the "small write area, big read area, background merge" pattern.
⑦ Concurrency and recovery: read-only queries take a snapshot at a past epoch and hold no locks; RS is not logged, and K-safety replicas query each other's missing data back.
⑧ Results: on seven TPC-H-style queries, about 164× a commercial row store and 21× a commercial column store, in roughly 40% of the row store's space — but the write side was unfinished and the authors call it preliminary.
⑨ Impact and limits: commercialized as Vertica and the shared foundation under ClickHouse / Redshift / Snowflake / BigQuery and Parquet / ORC; yet the comparison was uneven, the sources of the gain unseparated, join indices never landed in practice, and the physical-design problem was handed off to a tool.