How Apache Parquet Stores Data: Internals, Compression, and Performance Explained


Data is growing, and fast. Whether you're querying petabytes in a data lake or running analytics in a cloud warehouse, the format you store your data in can make or break performance.
Parquet is your saviour.
If you've ever used tools like Apache Spark, AWS Athena, or Google BigQuery, chances are you've come across .parquet
files. But what makes them so special? Let us explore what happens under the hood when you store your data in a Parquet file. I’ll break down its file structure, encoding tricks, compression magic, and how it manages to stay consistent and lightning fast.
Let’s dive right in.
So what the hell is Apache Parquet?
Apache Parquet is a columnar storage format developed to handle the kind of analytical workloads we see in modern data systems. Originally built by Twitter and Cloudera, and now an Apache top-level project, it was designed with performance, scalability, and flexibility in mind.
Think of Parquet as the opposite of formats like CSV or JSON. Instead of storing data row by row, it stores data column by column. That might sound like a small difference, but it has huge implications, especially when you're only interested in querying a few columns from a massive dataset.
For you to understand it fully, you would need to understand how parquet files are built internally and store data. So shall we?
The Anatomy of a Parquet File
Now that we’ve established what Parquet is, let’s pop open the hood and take a look at how it’s built.
A Parquet file isn’t just a dump of data, it’s neatly organized and self-aware. It follows a well-defined structure that helps tools quickly scan, read, and even skip over unnecessary parts of the file. Here’s the basic layout:
File Header: It starts with a few magic bytes (
PAR1
) to signal, “Hey, I’m a Parquet file!”Row Groups: The file is divided into chunks of rows. Each row group contains data for all columns, but is stored column-by-column.
Column Chunks: Within each row group, data for each column is stored separately.
Pages: Column chunks are further split into pages (we’ll talk more about this in a bit). Pages are where compression and encoding shine.
Footer: The file ends with metadata about the schema, row groups, column statistics, and more.
Magic Bytes Again: A second
PAR1
at the end lets tools scan files backwards efficiently, yes, that’s a thing!
What’s cool here is that everything is indexed. Want just two columns from a file with a hundred? You don’t have to scan the whole thing. Want rows that match a filter? Stats in the footer can help skip irrelevant row groups entirely.
So yeah, it’s not just a file, it’s a smart, structured, query-friendly block of data.
Not clear yet? Lets understand, columnar storage to understand it better.
Columnar Storage Explained
Imagine you're in a library looking for all the books written by a particular author. Would you rather flip through every page of every book, or just look in a catalog that lists authors and their works? That’s the difference between row-based and columnar storage.
Parquet, being a columnar format, stores data column by column instead of row by row. That means all values for a single column are physically stored together. So if you’re running a query that only needs, say, user_id
and purchase_amount
Parquet can read just those columns and completely ignore the rest.
Here’s why that’s a big deal:
Speed: Less data read from disk = faster queries.
Compression: Similar values grouped (as is often the case in columns) compress way better than mixed data types in rows.
Analytics Optimization: Most analytical queries focus on aggregates and filters over specific columns, not entire rows.
So in a way, Parquet plays to the strengths of modern analytics: focus on what you need, skip what you don’t. And that’s why columnar storage isn’t just a design choice, but it’s a performance superpower.
Row Groups and Column Chunks
Let’s zoom in a bit on how Parquet organizes your data within the file. This is where the structure starts to flex its performance muscles.
A row group in Parquet is a horizontal partition of your data. Think of it like a block of rows, maybe a million at a time. Each row group contains all the columns, but (and here’s the twist) each column’s data is stored separately, in what are called column chunks.
So, for every row group:
You have one column chunk per column.
These column chunks are physically stored together for that row group.
Why does this matter?
Because row groups are the unit of parallelism. If your data is split into multiple row groups, tools like Spark or Presto can read them in parallel. And since each column is isolated, Parquet can apply compression and encoding to each column independently, tailored to that column’s data type and values.
There’s also a bonus: column stats (like min/max values) are stored at the row group level. That means your engine can skip entire row groups if it knows they don’t match your query filter.
So while it might sound like we’re slicing and dicing data for fun, there’s real logic behind the madness, i.e., more structure = smarter reads.
Pages and Encoding Techniques
Alright, let’s go one level deeper, into the pages inside each column chunk. If row groups are the big boxes, and column chunks are the compartments, then pages are the actual storage containers holding the data.
Each column chunk is split into multiple pages. And these pages aren’t just raw dumps, they’re where Parquet starts showing off its efficiency tricks.
There are mainly two types of pages:
Data Pages: These contain the actual values, possibly encoded and compressed.
Dictionary Pages: If dictionary encoding is used, this page holds a lookup table mapping short codes to actual values.
Now let’s talk about encoding techniques, this is Parquet’s secret sauce:
Dictionary Encoding: Instead of repeating the same string like “Pending” a thousand times, Parquet stores it once in a dictionary and just writes a reference (e.g.,
1, 1, 1, 1...
). Super effective for repetitive data.Run-Length Encoding (RLE): Perfect for data like status flags (
1, 1, 1, 0, 0, 0
), where repeated values are stored as a single value + count.Delta Encoding: Handy for sequences like timestamps or incremental numbers. Instead of storing full values, it stores the difference between them.
These encoding methods are column-specific. That means Parquet can choose the best strategy for each column. A string-heavy column might get dictionary-encoded, while an integer column might use delta encoding.
And here's the fun part: encoding isn’t just about size, it also speeds up reads because there's less data to scan in memory.
So next time you hear someone brag about how “small” their Parquet files are, now you know it’s not just compression, it’s clever encoding too.
Compression in Parquet
Okay, we’ve talked about encoding, but what about compression, the thing everyone loves Parquet for?
In Parquet, compression is applied after encoding and per column. That’s right, not the whole file, not even the whole row group; each column chunk gets its compression treatment. And that’s a game changer.
Here’s why that’s smart:
Similar values (which columns often contain) compress way better than mixed ones.
Different columns can use different algorithms based on what suits them best.
Parquet supports a handful of popular compression algorithms:
Snappy (default): Fast and decent compression. Great for real-time reads.
GZIP: Slower but better compression ratio, ideal for storage-heavy workloads.
Brotli: Very efficient, but also CPU-hungry.
ZSTD: The cool kid on the block, good balance of speed and size.
This combo of encoding + compression is where Parquet truly shines. You end up with files that are tiny on disk, but can be read quickly by distributed engines like Spark or Athena. And the best part? Compression is invisible to the reader. It just works.
So next time your pipeline writes Parquet files and you see that tiny file size, know there’s a little party of codecs and encoders happening inside.
Schema and Metadata
Parquet isn’t just storing your data, it’s storing your data about your data, too. That’s where its schema and metadata capabilities come in.
Every Parquet file is self-describing, meaning it carries its schema right in the file. This is especially handy in big data environments where you don’t always know the structure ahead of time. Tools can infer the schema just by reading the file footer. No separate schema registry is needed.
Here’s what Parquet keeps track of:
Logical Types: Like
timestamp
,decimal
, or evenlist
. These help map data more accurately to your application or analytics engine.Physical Types: How the data is stored on disk, like
int32
,binary
, etc.Column-level Statistics: Min, max, null count, distinct count (in some cases). These are a goldmine for query engines trying to skip irrelevant data.
Why does this matter?
Tools like AWS Athena or Presto use these stats for predicate pushdown, skipping row groups that don’t match your
WHERE
clause.You can do schema evolution. For example, add new columns without breaking old queries.
It helps with data validation and consistency. No more guessing column types.
In short, Parquet’s metadata layer is like having a mini-database index baked right into the file; smart, lightweight, and incredibly useful.
How Parquet Maintains Data Consistency and Integrity?
Parquet isn’t just fast and efficient, it’s also reliable. Behind the scenes, it includes a few thoughtful features to make sure your data stays intact and easy to trust.
Let’s start with the basics:
Every Parquet file begins and ends with a set of magic bytes, specifically,
PAR1
. These act as a signature, helping tools recognize and validate the file format.The file footer, which contains schema, metadata, and block offsets, is stored at the end of the file, along with its length, so tools can scan files in reverse without reading the whole thing. Yes, reverse scanning is a thing, and it’s brilliant.
For data integrity, Parquet includes:
Checksums on pages: These ensure that if a page gets corrupted, the reader knows and can handle it (usually by skipping or failing fast).
Versioning: Parquet files contain metadata about the writer version. This allows readers to understand how to interpret the data, even as the format evolves.
And when it comes to compatibility, Parquet is built to be cross-platform and cross-language. A file written with PyArrow can be read with Spark, and vice versa. That level of interoperability only works because of the consistency baked into the format.
So even though Parquet feels like just a file on disk, it’s acting a lot like a database, complete with structure, checks, and contracts.
Conclusion
The next time someone asks, “Why Parquet?” you’ll have a better answer than just “because it’s small.” You’ll know it’s because Parquet thinks differently about structure, speed, and scale.
Subscribe to my newsletter
Read articles from Raju Mandal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Raju Mandal
Raju Mandal
A digital entrepreneur, actively working as a data platform consultant. A seasoned data engineer/architect with an experience of Fintech & Telecom industry and a passion for data monetization and a penchant for navigating the intricate realms of multi-cloud data solutions.