0%
Big Data Engines: Spark, Lakehouse, and Streaming at Scale
Distributed Execution
Spark
Query Engines
Streaming at Scale
Pipelines in Production
Columnar Formats, Parquet and ORC
Every file format answers one question before it answers any other: in what order do the bytes of a table appear. A row oriented format writes all of record one, then all of record two. A columnar format writes all the values of column one, then all the values of column two. Nothing else about the format matters as much as that choice, because it determines what a reader can skip, how well the bytes compress, and whether the processor can work on many values at once.
The question is worth taking seriously rather than treating as settled, because the right answer depends entirely on the access pattern. A system that reads whole records by key, writes them back, and never aggregates is better served by rows. A system that reads six columns out of two hundred across billions of records is better served by columns, by a margin that is not close.
What a query actually touches
Consider a table of clickstream events with a hundred and eighty columns and four hundred billion rows, and the entirely ordinary question of how many sessions started on mobile devices in the last week. The query references four columns: a timestamp, a device type, a session identifier and a flag. It does not reference the other hundred and seventy six.
In a row oriented file the four columns the query wants are interleaved with the hundred and seventy six it does not. The reader cannot avoid them, because they sit between the bytes it needs. It reads a page from disk, extracts four fields, discards the rest, and reads the next page. If the four columns are two percent of the record width, the reader is discarding ninety eight percent of everything it transfers, decompresses and parses.
In a columnar file the values of each column are contiguous. The reader locates the four column chunks it needs, reads those byte ranges, and never transfers the others. The same query moves two percent of the bytes. This is not a tuning improvement, it is a different amount of work, and no amount of faster hardware closes the gap because the row oriented reader is doing fifty times more of it.
Compression likes similarity
The second consequence is less obvious and often larger. Compression works by finding redundancy, and a column of values is far more redundant than a row of values. A column of country codes contains perhaps two hundred distinct strings repeated billions of times. A column of timestamps contains values that differ from their neighbours by small amounts. A column of booleans contains one of two values.
A row, by contrast, is a timestamp followed by a string followed by an integer followed by a float. Each field has a different type, a different distribution and a different byte pattern, so a general purpose compressor working over a row finds much less to exploit. Put the same data in columns and the compressor is handed long runs of similar values, which is the case it was designed for.
The practical effect is that the same table typically occupies two to four times less space in a columnar format than in a row oriented one with the same compression codec, before any format specific encoding is applied. With encodings the gap widens further, and sections later in this lesson show why.
Vectorised execution needs contiguous values
There is a third consequence that matters for processing speed rather than for input and output. Modern processors execute a single instruction against several values at once when those values are adjacent in memory and have the same type. A column chunk, once decoded, is exactly that: an array of values of one type, laid out contiguously.
This lets an engine evaluate a filter over a thousand values with a handful of instructions rather than a thousand branch heavy comparisons, and it lets it sum a column without a per row function call. Engines that take this seriously, which now means most of them, gain several times the throughput on scans and aggregations purely from the memory layout the format handed them.
A row oriented reader cannot do this without first transposing the data, which costs what transposition always costs. In practice it means the row format pays twice, once to read bytes it does not need and once to process the bytes it does need less efficiently.
Where rows still win
It would be dishonest to present columns as strictly better, because they are not. Three patterns favour rows and recognising them prevents expensive mistakes.
Writing a single record is cheap in a row format and awkward in a columnar one, because a columnar writer has to buffer many records before it can produce column chunks with useful compression. This is why operational databases are row oriented and why streaming ingestion usually lands in a row format such as Avro before being rewritten.
Reading a whole record by key is cheap in a row format and expensive in a columnar one, because the reader must touch every column chunk and reassemble. A point lookup against a wide columnar table does more work than the same lookup against a row format.
Updating a record in place is possible in a row format and generally impossible in a columnar one, since the columnar file is written once and the value sits inside a compressed, encoded block shared with thousands of others. Columnar systems handle updates by writing new files and marking old rows deleted, which is a design decision with consequences of its own.
Hybrid layouts, which is what both formats actually use
Pure column orientation has a practical problem at scale. If a column's values for an entire table are one contiguous run, then reconstructing any record requires reading from positions scattered across the whole file, and splitting the file across tasks becomes impossible because no task has a complete set of records.
Both Parquet and ORC solve this the same way, with a hybrid called PAX. The table is divided horizontally into groups of rows, typically between a hundred thousand and a million of them, and within each group the data is stored column by column. The group is the unit of parallelism, because it contains complete records, and the column chunk within the group is the unit of reading and compression.
This is why both formats talk about row groups or stripes. It is also why row group sizing is one of the most consequential tuning decisions in either format, a topic that a later section returns to with measurements.
The size of a row group is therefore a trade between two opposing pressures. A larger group gives the compressor more to work with, amortises the per chunk metadata over more values, and produces fewer, larger reads. A smaller group gives the planner finer granularity for skipping, because statistics are kept per group, and it lowers the memory a writer must hold before it can flush. The conventional answer of a hundred and twenty eight megabytes per group is a compromise rather than a law, and workloads with highly selective filters often do better with smaller groups than the default.
The two formats, in one paragraph each
Parquet grew out of a collaboration between Twitter and Cloudera, drawing on the Dremel paper for its representation of nested data. It is the de facto standard across the Spark, Python and cloud warehouse ecosystems, and it is the format that table formats such as Iceberg and Delta Lake assume by default. Its nested type handling, using definition and repetition levels, is the most general of any widely used format.
ORC grew out of Hive at Hortonworks, and its design leans toward the Hive and Trino side of the ecosystem. It has richer built in indexes, including optional bloom filters and lightweight indexes every ten thousand rows, and historically it had better support for ACID style updates within Hive. Its compression of certain integer patterns is noticeably better.
For most workloads the difference between them is smaller than the difference between a well configured file and a badly configured one in either format, which is the real subject of this lesson.