Row vs column storage: an interactive comparison of OLTP and OLAP layouts
A row store keeps each row’s values together on disk, which is ideal for reading or changing one record: the shape of most application traffic, or OLTP. A column store keeps each column’s values together, which is ideal for queries that scan millions of rows but only a few columns: the shape of analytics, or OLAP. PostgreSQL and MySQL store rows; ClickHouse, BigQuery, Snowflake, Redshift, DuckDB and Parquet files store columns.
The playground stores one orders table both ways and shows, for four queries, which cells each layout has to read and what that costs at the table size you choose.
SELECT country, sum(amount) FROM orders GROUP BY country10 million orders: 1.5 GB as rows of about 147 bytes, 409 MB as compressed columns. This query needs 2 of the table's seven columns. The row store has no way to read part of a row, so it scans every page: 1.5 GB, about 1.5 s at 1 GB/s. The column store reads only country and amount, compressed: 38 MB, 39 times less.
- Read from disk
- 1.5 GB
- Reads
- 181,819 pages
- Time, roughly
- 1.5 s
- Read from disk
- 38 MB
- Reads
- 2,442 blocks
- Time, roughly
- 38 ms
Why columns win for analytics
Totalling revenue by country needs 2 of the table’s 7 columns. A row store cannot read part of a row, so for 10 million orders it scans all 1.5 GB of pages. A column store reads just the country and amount files, and because a column holds similar values side by side it compresses well: 38 MB, 39 times less. Counting orders by status reads one column of 5 distinct values, 4 MB. Three more effects add to that:
- Compression per column. Dictionary encoding turns repeated strings into small integers, run-length encoding stores long runs once, and delta encoding stores sorted timestamps as small differences.
- Skipping blocks. Each block records the minimum and maximum of each column, so a filter on the sort key skips whole blocks without reading them.
- Vectorized execution. Processing a column as an array lets the CPU apply one operation to many values at once, which is much faster than walking rows.
Why rows win for applications
Fetching one order by id is a B-tree walk to a single page in a row store: 4 page reads, 33 KB. A column store has to read and decompress a block from each of the 7 column files to rebuild that one row, 335 KB. Looking up a customer’s latest orders is worse without an index: the column store scans the whole customer_id column, 47 MB against the row store’s 188 KB. Writes follow the same pattern. A row store updates one page in place; a column store has to touch every column file, so it is built for large batched inserts and treats updates and deletes as expensive background rewrites.
So most systems use both: the application writes to a row store, and change data capture or a nightly export copies the data into a column store or Parquet files for analytics. Note that “wide-column” databases such as Cassandra and HBase are not column stores; they store rows grouped by partition key.
A columnar table in ClickHouse
-- ClickHouse: a MergeTree table stores each column in its own file, sorted by ORDER BY
CREATE TABLE orders (
order_id UInt64,
customer_id UInt64,
created_at DateTime,
country LowCardinality(String), -- dictionary-encoded
status LowCardinality(String),
amount Decimal(12, 2),
note String
) ENGINE = MergeTree
ORDER BY created_at;
-- reads 2 of the 7 columns, compressed, in blocks of 8,192 rows
SELECT country, sum(amount) FROM orders GROUP BY country;In a system design interview
Separate the two workloads when you see both: “Orders live in PostgreSQL, where checkout reads and writes single rows through indexes. Every change flows through CDC into ClickHouse, sorted by time, where dashboards aggregate months of orders by reading only the columns they need. The dashboards are a few seconds behind, which is fine for analytics.” Expect the follow-ups: how fresh the analytics need to be, how deletes for privacy reach the column store, and how you choose its sort key.
Read the guide to analytics and OLAP or data lakes and lakehouses, see the B-tree behind row stores’ indexes, or watch a strong candidate design an ad click aggregator.
Questions people ask
What is the difference between row and column storage?
A row store keeps all of a row’s values together on disk, so reading or writing one record touches one place. A column store keeps each column’s values together, so a query reads only the columns it needs. Row stores suit transactional workloads; column stores suit analytics over many rows and few columns.
Why are columnar databases faster for analytics?
They read only the columns a query uses, those columns compress far better because similar values sit together, blocks whose min and max rule them out are skipped, and whole columns are processed as arrays with vectorized CPU instructions. Together this can cut the data read by one or two orders of magnitude.
Why are column stores slow for point lookups and updates?
Rebuilding one row means reading and decompressing a block from every column file, and changing one row means rewriting compressed blocks in each of them. Column stores also rarely have secondary indexes. They are built for large batched inserts and scans, not single-row reads and writes.
Is Cassandra a columnar database?
No. Cassandra and HBase are wide-column stores: rows are grouped and sorted by partition key and can have many sparse columns, but the data is stored row by row within a partition. Columnar databases such as ClickHouse, BigQuery and Redshift store each column separately.
What is Parquet?
An open columnar file format. A Parquet file splits rows into row groups and stores each column of a row group as a compressed column chunk with statistics such as min and max, so engines like Spark, Trino and DuckDB can read only the columns and row groups a query needs.
How does columnar compression work?
Each column is compressed on its own, with encodings that suit its values: dictionary encoding for repeated strings, run-length encoding for runs of equal values, delta encoding for sorted numbers and timestamps, and bit-packing for small integers, often followed by a general compressor such as LZ4 or ZSTD.
Should I use a row store or a column store?
Use a row store such as PostgreSQL or MySQL for the application: single-row reads, writes and transactions. Use a column store such as ClickHouse, BigQuery or Snowflake for analytics and reporting. Many systems run both, copying data from the row store to the column store with change data capture.