Back to Stories

Apache Parquet Explained: Complex Data Structures, Statistics, Page Indexes, and Bloom Filters

Part 4: nested data, optional fields, definition and repetition levels, column statistics, page indexes, and bloom filters.

Apache Parquet Explained: Complex Data Structures, Statistics, Page Indexes, and Bloom Filters

So far, we have explored how Parquet stores simple columns such as integers, strings, and dates. However, real-world datasets are rarely this straightforward. Modern applications often generate complex and nested data structures, and Parquet is designed to store them efficiently without giving up query performance.

Parquet high performance query flow with metadata and column pruning
Parquet uses metadata, statistics, page indexes, bloom filters, and column pruning to reduce unnecessary reads.

Supporting complex data structures in Apache Parquet

Consider a customer record from an e-commerce platform.

{
  "customerId": 1001,
  "name": "Alice",
  "addresses": [
    {
      "city": "New York",
      "country": "USA"
    },
    {
      "city": "Boston",
      "country": "USA"
    }
  ],
  "orders": [
    {
      "orderId": 501,
      "amount": 120
    },
    {
      "orderId": 502,
      "amount": 340
    }
  ]
}

This record contains arrays and nested objects rather than a flat table. Unlike CSV files, Apache Parquet can store these complex structures while still maintaining efficient column-oriented reads.

  • Nested structures (Struct)
  • Arrays (Lists)
  • Maps
  • Optional fields
  • Repeated fields

How nested structures are stored

Suppose we have the following employee record.

Employee

├── Id
├── Name
└── Address
      ├── City
      ├── State
      └── Country

Although the data appears hierarchical, Parquet does not physically store nested objects as one large document. Instead, it flattens the hierarchy into separate columns.

Employee.Id

Employee.Name

Employee.Address.City

Employee.Address.State

Employee.Address.Country

This lets query engines perform column pruning on nested fields. For example, a query can read only Address.City while skipping Address.State and Address.Country.

SELECT Address.City
FROM employees;

Flattening also improves compression because values of the same type stay grouped together.

Handling arrays and repeated values

Collections such as arrays are common in analytical datasets.

{
  "product": "Laptop",
  "tags": [
      "electronics",
      "office",
      "premium"
  ]
}

Internally, Parquet stores collections using metadata that preserves the relationship between elements while avoiding unnecessary duplication. Query engines can reconstruct the original hierarchy during reads without storing every nested object repeatedly.

Understanding optional and required fields

Not every record contains values for every column. Consider the following example.

| Customer | Email               | Phone      |
| -------- | ------------------- | ---------- |
| Alice    | alice@example.com   | 9999999999 |
| Bob      | bob@example.com     | NULL       |
| Charlie  | NULL                | 8888888888 |

Traditional formats simply leave missing values blank. Parquet explicitly distinguishes between required and optional fields, allowing null values to be represented efficiently without wasting space.

Definition levels and repetition levels

One of the most sophisticated features inside Parquet is its use of definition levels and repetition levels. Developers rarely interact with these concepts directly, but they are fundamental to how Parquet stores nested data.

Definition levels

Definition levels indicate whether a value exists or is null. If Address.City is missing, Parquet does not need to store an explicit NULL value for every record. Instead, it records the appropriate definition level.

Employee

Name

Address.City

Repetition levels

Repetition levels describe whether a value belongs to a repeated structure such as an array. Suppose a customer has multiple phone numbers.

9876543210

9988776655

9123456789

Instead of repeatedly storing the complete parent object, Parquet records only enough structural information to rebuild the hierarchy during reads. Together, definition and repetition levels allow Parquet to represent deeply nested JSON documents while preserving strong compression ratios.

Column statistics: the secret behind fast queries

Earlier, we discussed predicate pushdown. The reason Parquet can skip data so effectively is that every row group stores metadata such as minimum value, maximum value, null count, and number of values.

Row Group 1

Min Year = 2019

Max Year = 2020
Row Group 2

Min Year = 2021

Max Year = 2022

Now consider this query.

SELECT *
FROM transactions
WHERE year = 2023;

Spark immediately recognizes that neither row group can contain records from 2023. Both row groups are skipped without reading their data pages, dramatically reducing disk access.

Page indexes and bloom filters

Newer versions of the Parquet specification introduce additional metadata structures that further improve query performance.

Page indexes

Instead of reading an entire column chunk, query engines can identify exactly which pages contain relevant values. This enables finer-grained skipping during query execution.

Bloom filters

Bloom filters provide a probabilistic way of determining whether a value might exist inside a data block. If a bloom filter determines that a customer ID definitely does not exist within a page, the query engine skips reading that page altogether.

Bloom filters may occasionally return false positives, but they never return false negatives. That makes them effective for equality-based lookups.

Why these internal structures matter

Many developers think Parquet is simply a compressed columnar file. In reality, the format combines several sophisticated mechanisms.

  • Column-oriented storage
  • Nested data representation
  • Efficient null handling
  • Definition levels
  • Repetition levels
  • Column statistics
  • Page indexes
  • Bloom filters
  • Metadata-driven query planning

Together, these features enable analytical engines to process datasets containing billions of records while reading only a small fraction of the underlying storage. As datasets grow, these optimizations become increasingly important for reducing execution time and cloud infrastructure costs.

What is next?

Now that we have explored how Parquet stores both simple and complex data, the next step is learning how to write Parquet files efficiently in production environments.

  • Partitioning strategies
  • Choosing the right file size
  • The Small Files Problem
  • Compaction
  • Compression codec selection
  • Spark write optimizations
  • Real-world production best practices

These recommendations can make the difference between a fast, scalable data lake and one that becomes increasingly difficult to maintain over time.

Share this article: Twitter LinkedIn Email

Stay ahead of the curve.

Join our newsletter for weekly insights on technology, design, and the future of business.