A table is a two-dimensional thing, but storage is one-dimensional. Something has to decide the order in which cells get written down, and there are only two sensible answers.
Row-oriented storage keeps each record together: all of customer 1, then all of customer 2. Column-oriented storage keeps each field together: every name, then every age, then every address.
Same data, same schema. The difference is purely in the layout — and nearly every property people attribute to "columnar databases" falls out of it.
What the layout buys you
Compression gets much better. Values in a column are the same type and usually drawn from a small range. A million rows of a country field are a million values drawn from about two hundred distinct strings, which dictionary encoding turns into a million small integers. A million rows of timestamp are nearly sorted, which delta encoding turns into a million tiny numbers. Neither trick is available when a string, a float, and a boolean sit adjacent in memory.
Queries read less. SELECT AVG(price) on a forty-column table touches one column. Row storage has to read every row in full and discard thirty-nine fortieths of what it read; column storage reads only the column it needs. On analytical queries over wide tables, this is most of the win.
Scans vectorise. A column is a contiguous run of one type, which is exactly what SIMD instructions and CPU prefetchers want. Row storage interleaves types and defeats both.
What it costs
Writes get expensive. Inserting one row means touching every column, which is one write per field instead of one write per record. Deleting is worse. This is why columnar systems tend to be append-only, batch-loaded, or paired with a row-oriented write buffer that is periodically merged.
Reconstructing a whole row costs a join. SELECT * FROM t WHERE id = 5 is the case row storage was designed for and the case column storage handles worst.
Unstructured data gains nothing. The benefits all derive from a column being homogeneous. Blobs of text or images have no such structure to exploit.
The split is roughly OLTP against OLAP: many small transactions touching whole records, versus few large queries touching few fields of many records.
A benchmark, and its limits
Encoding 100,000 records both ways with encoding/gob:
type ProductRow struct {
ID int
Name string
Price float64
InStock bool
}
type ProductColumns struct {
IDs []int
Names []string
Prices []float64
InStocks []bool
}Build both representations from the same data, gob-encode each, and compare:
Rows size: 3156545 bytes
Columns size: 2755676 bytes
Ratio: 87.30%
Encoding and decoding rows: 2.382370118s
Encoding and decoding columns: 1.345502811sColumns are 13% smaller and just under twice as fast to round-trip.
NOTE
This benchmark is worth reading carefully, because it does not measure what it appears to measure.
The 13% is almost entirely serialization overhead, not compression. gob tags each field of each struct as it writes it, so the row layout pays that tag 400,000 times. The column layout writes four homogeneous slices, and a slice of int needs no per-element field tag at all. Change the serializer and the number changes.
Real columnar compression is a different and much larger effect — run-length, dictionary, and delta encoding applied to a homogeneous column routinely give 5–10×, not 13%. This benchmark does not do any of that.
The timing difference is more honest: it reflects genuinely less per-element bookkeeping and better cache behaviour on the decode path.
So the experiment demonstrates that layout affects cost. It does not demonstrate the compression case for columnar storage, and I would be overstating things if I claimed it did.
To actually measure that, you would need a format that compresses columns — Parquet or ORC — rather than one that just serialises them.
Making it faster
Most of the practical advice is about keeping columns homogeneous and contiguous:
- Struct of arrays, not array of structs. This is the whole idea, applied at the level of in-memory data structures.
- Fixed-size types where possible, so a column is a flat array with no indirection.
- A format built for this — Parquet or ORC rather than a general-purpose encoder.
- Compression chosen per column, since what works for sorted timestamps is not what works for low-cardinality strings.
The thing worth remembering
Columnar storage is not a compression technique. It is a data layout that makes compression techniques work far better than they otherwise would, and that lets a query skip reading fields it does not need.
Both benefits come from the same source: putting similar things next to each other. That is a much older idea than databases, and it turns up wherever memory latency dominates — which, increasingly, is everywhere.