◐ ai-teacher

🧱 Foundations · Chapter 5

File formats: CSV, JSON, Parquet, Avro

The same 1 crore rides can be a 9 GB CSV that takes four minutes to query, or a 900 MB Parquet file that answers in four seconds. The data didn't change. The layout did.

Study this lesson free in the app →

The basics

A file format is the agreement about how records are laid out as bytes on disk. For data engineering the four that matter are CSV, JSON, Avro and Parquet.

The problem it solves: analytics queries read a few columns from millions of rows; ingestion writes whole rows one at a time. No single layout is good at both, so we pick the format to match the job.

The mental model: a table can be stored row by row (every field of ride 1, then every field of ride 2…) or column by column (every fare, then every city…). Row layout is great for writing and reading whole records. Column layout is great for "average fare in Shillong", because it reads only the fare and city columns and skips everything else.

Words you must know 5

schema
the column names and types
row group
a horizontal slice of a Parquet file
predicate pushdown
skipping data using stored min/max stats
splittable
can be read in parallel chunks
columnar

Explained

Meghalaya Cabs lands 1 crore rides a month with 20 columns each. The analyst's query: SELECT avg(fare) FROM rides WHERE city = 'Shillong'.

In CSV the engine reads all 20 columns of every row. In Parquet it reads 2 columns — and skips whole chunks where no row has city = 'Shillong'. Same answer, a tenth of the bytes.

The attendance register vs the report card

A school keeps two books.

The attendance register is one line per student per day — name, class, present, time. Easy to add a line at 9 am. That's row layout.

The marks ledger has one page per subject: every student's maths mark on the maths page. When the principal asks "what's the average maths mark?" the clerk opens one page, not 900 report cards. That's columnar layout.

Neither book is better. The register is for writing as things happen; the ledger is for answering questions about a whole subject.

The four, precisely

CSV — text, delimiter-separated. No types (is 0012 a number or a PIN code?), no nesting, quoting/newline bugs. Readable by everything.

JSON / JSON Lines — text, self-describing, supports nesting. Every record repeats every key, so it's bulky. Types are weak: no date type, one number type.

Avro — binary, row-oriented. The writer's schema (JSON) is stored once in the file header; records carry no field names. Built for schema evolution: a reader resolves the writer's schema against its own, filling defaults for new fields. Splittable via sync markers. The usual format for Kafka messages (with a Schema Registry holding the schema, and each message carrying only a schema id).

Parquet — binary, columnar. File = row groups → column chunks → pages, then a footer with the schema and min/max/null-count statistics per column chunk. Engines read the footer first, then fetch only needed columns and row groups.

Inside a Parquet file

rides_2026_03.parquet
┌──────────────────────────────────────────┐
│ "PAR1" magic                             │
│ Row group 0  (~128 MB, rows 0..1.2M)     │
│   ride_id  chunk  [pages…]               │
│   city     chunk  [pages…]               │
│   fare     chunk  [pages…]               │
│   …17 more column chunks                 │
│ Row group 1  (rows 1.2M..2.4M)           │
│   …                                      │
├──────────────────────────────────────────┤
│ FOOTER: schema                           │
│   rg0.city  min='Aizawl' max='Tura'      │
│   rg0.fare  min=40  max=2650 nulls=0     │
│   rg1.city  min='Agartala' max='Guwahati'│
│ footer length + "PAR1"                   │
└──────────────────────────────────────────┘

Query city = 'Shillong' → rg1's range ends at 'Guwahati', so rg1 is skipped without reading it.

Reading only what you need

import pyarrow.parquet as pq

# 1. Column pruning: only 2 of 20 columns are read from disk
table = pq.read_table(
    "rides_2026_03.parquet",
    columns=["city", "fare"],
    # 2. Predicate pushdown: row groups whose min/max
    #    can't contain 'Shillong' are never fetched
    filters=[("city", "=", "Shillong")],
)
print(table.column("fare").to_pandas().mean())

# 3. Look at the stats the engine used
meta = pq.ParquetFile("rides_2026_03.parquet").metadata
print(meta.num_row_groups)
print(meta.row_group(0).column(1).statistics)  # min/max for city

The same query on CSV has no footer, no stats and no columns to skip — pd.read_csv parses every byte.

At scale

Small files kill Parquet. A streaming job writing a 50 KB Parquet file every 10 seconds makes 8,640 files a day per partition. Each file means a footer read and an object-store GET. Target files of 128 MB–1 GB; compact the small ones.

Row-group size is a dial. Bigger row groups = better compression and fewer footer entries; smaller = finer skipping and less memory per reader.

Statistics only help if data is sorted/clustered. If city is randomly scattered, every row group has min='Agartala' max='Tura' and nothing gets skipped. Sort by the column you filter on before writing.

The typical lake flow: land raw as JSON/Avro (whatever the source speaks, cheap to append), convert to Parquet in the curated layer where it will be queried a thousand times.

Gotchas

Write rows, read columns.

Avro/JSON for landing and messages (row, append-friendly). Parquet for anything queried (columnar, footer stats, skip what you don't need). CSV only at the edge with humans.

Words to know

Row-orientedAll fields of a record stored together; efficient for writes and full-record reads.
ColumnarAll values of one column stored together; efficient for scans over few columns.
Row groupA horizontal slice of a Parquet file holding one column chunk per column.
FooterParquet's metadata block at the end of the file: schema plus per-chunk statistics.
Predicate pushdownUsing stored statistics to skip data that cannot match the filter, before reading it.
Column pruningReading only the columns a query references.
SplittableA file that can be divided and read in parallel by multiple workers.

In two minutes

Test yourself

1. Which format is row-oriented AND binary with its schema stored in the file header?

  1. Parquet
  2. CSV
  3. Avro
  4. JSON Lines
Show the answer

Avro — Avro writes the schema once in the header and stores records row by row in compact binary. Parquet is binary but columnar, with the schema in the footer.

2. Where does Parquet keep the min/max statistics engines use to skip data?

  1. In the footer, per column chunk of each row group
  2. In a separate .stats file
  3. In the first row of the file
  4. Parquet keeps no statistics
Show the answer

In the footer, per column chunk of each row group — Readers fetch the footer first, then decide which row groups and columns to fetch. That's why Parquet readers seek to the end of the file first.

3. What is column pruning?

  1. Deleting unused columns from the table
  2. Reading only the columns the query references
  3. Compressing each column separately
  4. Sorting columns alphabetically
Show the answer

Reading only the columns the query references — In a columnar file each column is a separate byte range, so unused columns are never read from disk.

4. A vendor sends a daily 20 GB rides.csv.gz. Spark reads it with only one task. Why?

  1. CSV can never be read in parallel
  2. Spark limits CSV to one core
  3. The file has no header
  4. gzip is not splittable, so the whole file must be decompressed by one reader
Show the answer

gzip is not splittable, so the whole file must be decompressed by one reader — gzip streams can't be entered mid-file. Ask for many smaller files, or a splittable format/codec, or convert once to Parquet.

5. Meghalaya Cabs filters on city constantly, but Parquet row-group skipping almost never happens. Most likely fix?

  1. Sort or cluster the data by city before writing so each row group covers a narrow range
  2. Switch to CSV
  3. Increase the number of columns
  4. Disable the footer
Show the answer

Sort or cluster the data by city before writing so each row group covers a narrow range — If cities are scattered randomly, every row group's min/max spans almost all cities, so no row group can be ruled out. Sorting makes the ranges narrow.

6. Producers to a Kafka topic are adding new optional fields every few weeks. Which format handles this best on the wire?

  1. CSV with a header row
  2. Avro with a Schema Registry and defaults for new fields
  3. Parquet
  4. Plain JSON with no schema
Show the answer

Avro with a Schema Registry and defaults for new fields — Avro's reader/writer schema resolution fills defaults for fields an old producer didn't write, and the registry enforces compatibility rules. Parquet is a file format for analytics, not a per-message wire format.

There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →