File formats for big data: CSV, JSON, Avro, Parquet and ORC

The same 10 GB of orders can take four minutes or four seconds to query, depending only on the file format. In this lesson you see how CSV, JSON, Avro, Parquet and ORC lay data out on disk, why columnar formats are so fast for analytics, how Avro lets Kafka messages change safely, and why sorting a table can make queries skip most of it.

Module 2 · Data and Storage⏱ 30 minBeginner → AdvancedRuns in your browser (Python)3 real-time scenariosSix-format race on your PC9 practice problems
1 The story

Four minutes or four seconds?

For his practice project Kabir now has a bigger file: ten months of TiffinGo orders, a 10 GB CSV. Every time he restarts his notebook, loading it takes four minutes. He asks a senior engineer at TiffinGo for help. The reply is one line: “Convert it to Parquet.” He does, and the same question now takes four seconds.

orders.csv · 10 GBO1,Pune,thali,220,…O2,Thane,dosa,150,…O3,Pune,chai,20,…… 12 crore rowsload time: 4 min, every timerewriteorder_idcitydishamountdayorders.parquet · 1.8 GB · columns stored togetherquery reads only city + amount: 4 seconds Same data, different layout on disk: sixty times less waiting. myphatshala.in
Kabir

Sir, it’s the same data. How can a different file type make it sixty times faster? Is Parquet some kind of magic?

Arjun Sir

No magic, Kabir, just a smarter layout. A CSV stores data row by row as plain text. Parquet stores it column by column, in binary, compressed, with a small index at the end. Your question needed only two of the columns, so it read only those two.

Riya

Then why does anyone still use CSV?

Arjun Sir

Because anyone can open a CSV, even in Notepad. Every format is a trade-off between being easy for people, fast to write, fast to read and safe when the data changes. Today you’ll meet the five formats you’ll see in every big data job: CSV, JSON, Avro, Parquet and ORC.

Kabir

And how do I choose?

Arjun Sir

One question first: will this file be written one record at a time, or read many times by analysts? You’ll see why that question decides almost everything.

2 Definition

What is a file format?

Formal definition (write this in exams)

A file format defines how records are encoded as bytes in a file: the layout (row-oriented or column-oriented), the encoding (text or binary), whether a schema is stored, and which compression is applied. CSV and JSON are human-readable, row-oriented text formats. Avro is a row-oriented binary format that stores its schema with the data and supports schema evolution. Parquet and ORC are column-oriented binary formats with embedded schemas, per-column compression and statistics that let query engines read only the needed columns and skip irrelevant data.

In simple words

A file format is the way data is laid out inside a file. CSV and JSON are plain text you can read yourself. Avro is compact binary that remembers its own structure. Parquet and ORC store each column separately and compress it, so analytics tools can read just the parts they need, much faster.

Words you’ll meet in this lesson

Row-orientedAll values of one record are stored next to each other.
Column-oriented (columnar)All values of one column are stored next to each other.
SerializationTurning data in memory into bytes for a file or network message (and back: deserialization).
Compression codecThe algorithm used to shrink the bytes: gzip, snappy, zstd, lz4.
SplittableA file that can be cut into pieces and processed by many workers in parallel.
Row group / stripeA large block of rows inside a Parquet / ORC file, with stored statistics.
Predicate pushdownUsing file statistics to skip data that can’t match a filter, before reading it.
Schema evolutionChanging a schema over time (adding or removing fields) without breaking old data or readers.

The five formats in one line each

CSVPlain text, comma-separated, header row. No types, no schema. Universal, but large and slow for big data.
JSONPlain text with field names and nesting. Used by web APIs and app events. Even larger than CSV.
AvroBinary, row-oriented, schema stored in the file (Apache, 2009). The usual choice for Kafka messages.
ParquetBinary, columnar (Twitter and Cloudera, 2013, based on Google’s Dremel). The default for analytics in Spark, lakes and lakehouses.
ORCBinary, columnar (built for Hive, 2013). Excellent compression; common in Hadoop and Hive platforms.

File formats at three levels

Pick your level. New to this? Start at Beginner and climb.

Beginner Five ways to keep a register

  • CSV is a handwritten register: one line per order, values separated by commas. Anyone can read it, but there are no data types and no rules.
  • JSON is the same register where every value carries a label: "city": "Pune". Clear, but the labels repeat on every line, so files get big.
  • Avro is a printed form book: the form template (the schema) is printed once on the first page, and each record just fills in the boxes in compact binary.
  • Parquet and ORC reorganise the register by column: all dates on one page, all cities on the next, all amounts on another. To add up amounts, you open just one page.

Simple rule: sharing a small file with a person → CSV. Sending messages between apps → JSON or Avro. Storing big tables for analysis → Parquet (or ORC).

Intermediate Row vs column, text vs binary, compression and splitting

  • Row-oriented (CSV, JSON, Avro): a whole record is stored together. Great for writing one record at a time and reading whole records.
  • Column-oriented (Parquet, ORC): each column is stored together. Great for analytics that read a few columns of many rows; values in one column are similar, so they compress far better.
  • Text vs binary: CSV and JSON are text (numbers stored as characters); Avro, Parquet and ORC are binary with real data types (an int takes 4 bytes, not 6 characters).
  • Schema: CSV has none (just a header); JSON has field names but no types; Avro, Parquet and ORC store the full schema inside the file.
  • Compression codecs: gzip squeezes most but is slow; snappy and lz4 are fast with moderate savings; zstd is a good balance and increasingly the default.
  • Splittable: a cluster wants to split a big file into pieces for many workers. A gzipped CSV cannot be split (it must be read from the start); Avro, Parquet and ORC are designed to be split.

Advanced Inside Parquet and ORC, and schema evolution

  • Parquet layout: a file holds row groups (often around 128 MB); each row group holds one column chunk per column; chunks are divided into pages. A footer at the end stores the schema and statistics (min, max, null count) for every chunk.
  • Predicate pushdown: for WHERE day = '2026-10-19', the engine reads the footer first and skips every row group whose min–max range can’t contain that date. Sorting data by commonly filtered columns makes skipping far more effective.
  • Encodings: dictionary (store “Pune” once, then small numbers), run-length (“Pune × 5,000”), bit-packing and delta encoding for sorted numbers or timestamps. Compression is applied on top.
  • ORC (from the Hive world) uses stripes with light indexes and optional bloom filters; Parquet is the more common default in Spark and the cloud.
  • Avro schema evolution: readers and writers each have a schema. Adding a field with a default is safe; removing or renaming without an alias breaks old readers. Kafka teams enforce these rules with a Schema Registry.
  • Table formats are not file formats: Delta Lake, Iceberg and Hudi store data as Parquet files and add a transaction log on top (Lesson 4.3).

Exam tip: For a 5- or 10-mark question, make a table comparing CSV, JSON, Avro, Parquet and ORC on row vs columnar, text vs binary, schema, compression, splittable and best use. Then explain with one example why columnar formats are faster for analytical queries (read fewer columns + better compression + skipping with statistics).

3 Types and ways to use it

CSV, JSON, Avro, Parquet and ORC compared

CSV

Simple text rows. Best for small exports, spreadsheets and sharing with people.

Row · text

JSON

Labelled, nested text. Best for APIs, app events and configuration.

Row · text

Avro

Compact binary rows with a stored schema. Best for streaming (Kafka) and writing records one by one.

Row · binary

Parquet

Compressed columns with statistics. Best for analytics, data lakes and lakehouse tables.

Column · binary

ORC

Compressed columns with indexes. Best for Hive and Hadoop-based warehouses.

Column · binary

Side by side

CSVJSONAvroParquetORC
LayoutRowRowRowColumnColumn
EncodingTextTextBinaryBinaryBinary
Schema in fileNo (header only)Field names, no typesYesYesYes
Nested dataNoYesYesYesYes
CompressionWhole file (gzip)Whole filePer blockPer column, very goodPer column, very good
SplittableYes, but not when gzippedJSON lines: yesYesYesYes
Human-readableYesYesNoNoNo
Best forSmall exportsAPIs, eventsKafka, writesAnalytics, lakesHive warehouses

In real platforms formats are used together: apps send JSON, Kafka carries Avro, the raw zone keeps the original files, and cleaned tables are stored as Parquet (often inside Delta Lake or Iceberg). Choose by job, not by fashion.

4 Step by step

What happens inside each format

  1. Text formats repeat a lot

    rows = [("O1", "Pune", 220), ("O2", "Pune", 150), ("O3", "Thane", 20)]
    csv_text = "id,city,amount\n" + "".join(f"{i},{c},{a}\n" for i, c, a in rows)
    json_text = "".join(f'{{"id": "{i}", "city": "{c}", "amount": {a}}}\n' for i, c, a in rows)
    print("CSV bytes :", len(csv_text))
    print("JSON bytes:", len(json_text))
    CSV bytes : 51 JSON bytes: 132
    JSON repeats every field name on every row, so it is much bigger than CSV for the same data. Both store numbers as characters, so 220 takes three bytes of text instead of a compact binary number.
  2. Row layout vs column layout

    rows = [("O1", "Pune", 220), ("O2", "Pune", 150), ("O3", "Thane", 20)]
    print("Row layout   :", [v for r in rows for v in r])
    columns = list(zip(*rows))
    print("Column layout:", columns)
    Row layout : ['O1', 'Pune', 220, 'O2', 'Pune', 150, 'O3', 'Thane', 20] Column layout: [('O1', 'O2', 'O3'), ('Pune', 'Pune', 'Thane'), (220, 150, 20)]
    A row format keeps each order together. A columnar format keeps each column together. To total the amounts, the columnar file reads one short run of numbers instead of every value of every row.
  3. Dictionary encoding

    city = ["Pune", "Pune", "Thane", "Pune", "Mumbai", "Pune"]
    dictionary = sorted(set(city))
    codes = [dictionary.index(c) for c in city]
    print("dictionary:", dictionary)
    print("codes     :", codes)
    dictionary: ['Mumbai', 'Pune', 'Thane'] codes : [1, 1, 2, 1, 0, 1]
    A column has few distinct cities, so Parquet stores each name once and replaces values with small numbers. This only works well because similar values sit together in one column.
  4. Run-length encoding loves sorted data

    city_sorted = ["Mumbai", "Pune", "Pune", "Pune", "Pune", "Thane"]
    runs = []
    for c in city_sorted:
        if runs and runs[-1][0] == c:
            runs[-1][1] += 1
        else:
            runs.append([c, 1])
    print(runs)
    [['Mumbai', 1], ['Pune', 4], ['Thane', 1]]
    When the column is sorted, repeated values become runs: “Pune × 4” instead of four copies. Sorting large tables by common filters (city, date) can shrink them dramatically.
  5. Statistics let engines skip data

    row_groups = [("2026-10-01", "2026-10-10"), ("2026-10-11", "2026-10-20"), ("2026-10-21", "2026-10-31")]
    want = "2026-10-19"
    for i, (lo, hi) in enumerate(row_groups):
        print(f"row group {i}: min {lo} max {hi} ->", "READ" if lo <= want <= hi else "skip")
    row group 0: min 2026-10-01 max 2026-10-10 -> skip row group 1: min 2026-10-11 max 2026-10-20 -> READ row group 2: min 2026-10-21 max 2026-10-31 -> skip
    The Parquet footer stores the min and max of each column for every row group. A query for 19 October reads only the one row group whose range contains that date. This is predicate pushdown.
  6. The schema travels with the file

    Avro, Parquet and ORC store the column names and types inside the file. Spark, Hive, BigQuery or pandas can open it and know immediately that amount is an integer and day is a date.

    No guessing types from text, no broken loads because someone typed “N/A” in a number column. A stored schema is one of the biggest practical benefits of the binary formats.
5 See it happen

One query: what Kabir sees, and what the disk reads

Step through the six frames. Left: Kabir running the same query on two files. Right: how much of each file is actually read.

One query: what Kabir sees, and what the disk reads 6 steps
What you see Kabir’s notebookSELECT SUM(amount) FROM ordersWHERE city = 'Pune'AND day = '2026-10-19'Same question, two files.Query on CSVSELECT SUM(amount) FROM ordersWHERE city = 'Pune'AND day = '2026-10-19'orders.csv: reading… 2.5 of 10 GBQuery on CSVSELECT SUM(amount) FROM ordersWHERE city = 'Pune'AND day = '2026-10-19'Done in 4 min 05 s · Rs 7,42,180Query on ParquetSELECT SUM(amount) FROM ordersWHERE city = 'Pune'AND day = '2026-10-19'orders.parquet: reading footer…Query on ParquetSELECT SUM(amount) FROM ordersWHERE city = 'Pune'AND day = '2026-10-19'reading 3 columns of 1 row groupQuery on ParquetSELECT SUM(amount) FROM ordersWHERE city = 'Pune'AND day = '2026-10-19'Done in 4 s · Rs 7,42,180 (same answer) myphatshala.in
Behind the screen Same 12 crore orders, two layoutsCSV: rows of textO1,Pune,thali,220,10-19O2,Thane,dosa,150,10-02O3,Pune,chai,20,10-19…Parquet: columnsCSV 10 GB of text · Parquet 1.8 GB binaryorder_idcitydishamountdayRG0RG1RG2RG3CSV: every byte must be read and parsedorder_idcitydishamountdayRG0RG1RG2RG310 GB read · 12 crore rows parsed from textorder_idcitydishamountdayRG0RG1RG2RG3footer: schema + min/max per row groupParquet: read the small footer firstorder_idcitydishamountdayRG0RG1RG2RG3footer: schema + min/max per row groupSkip 3 row groups · read 3 of 5 columnsorder_idcitydishamountdayRG0RG1RG2RG3footer: schema + min/max per row group≈ 150 MB read instead of 10 GB myphatshala.in
6 Real-world use

Real-time scenarios: formats at work

Three live scenarios: a size race between six formats using real numbers, Kafka messages evolving safely with Avro and a Schema Registry, and an advanced look at how sort order lets Parquet skip data.

Real-time scenario 1

The size race: one table, six files

These are real numbers from the program in “Run it on your PC” below: 2 lakh random orders written in six formats. Watch the file sizes appear one by one.

Same data, six formats 5 steps
Animated example Same 2 lakh orders, file size on diskCSV6.76 MBJSON linesCSV.gzAvroParquetORCCSV: plain text, 6.76 MB. Our baseline.Same 2 lakh orders, file size on diskCSV6.76 MBJSON lines17.44 MBCSV.gzAvroParquetORCJSON lines: 2.6× bigger. Field names repeat every row.Same 2 lakh orders, file size on diskCSV6.76 MBJSON lines17.44 MBCSV.gz1.29 MBAvroParquetORCGzipped CSV: 19% of the size, but not splittableand still text: every row must be decompressed and parsed.Same 2 lakh orders, file size on diskCSV6.76 MBJSON lines17.44 MBCSV.gz1.29 MBAvro1.37 MBParquet1.53 MBORCAvro and Parquet: binary with real types and a schema.Both around a fifth of the CSV.Same 2 lakh orders, file size on diskCSV6.76 MBJSON lines17.44 MBCSV.gz1.29 MBAvro1.37 MBParquet1.53 MBORC0.92 MBORC: smallest here, 13.6% of the CSV.Querying 2 columns, Parquet was about 13× faster. myphatshala.in
Real-time scenario 2

Kafka + Avro: changing a message without breaking anyone

TiffinGo’s order app sends Avro messages through Kafka to several consumers. Watch a safe change go through and an unsafe one get stopped by the Schema Registry.

Schema evolution in practice 5 steps
Animated example Order appproducerKafka topicordersBilling jobconsumerSchema v1: order_id, city, amountstored in the Schema RegistryEach Avro message carries a small schema id, not the schema.Order appv2Kafka topicordersBilling jobstill v1v2 adds: coupon (default = null)Registry: compatible ✓Old consumers simply ignore the new field.Analytics jobreads v2old messagewritten in v1old v1 message has no couponreader fills the default: coupon = nullNew readers can read old data too.Order appv3 (wrong)SchemaRegistryv3 removes amount (no default)Registry: INCOMPATIBLE ✗ · publish refusedThe mistake is stopped before any message is sent.Safe schema changes in Avro✓ add a field with a default✓ remove a field that had a default✓ rename using an alias✗ remove a required field✗ change a field’s type (e.g. int → string)Rules are checked automatically on every new version. myphatshala.in
Real-time scenario 3 · advanced

Inside Parquet: why sorting makes queries skip data

Parquet can skip row groups using min/max statistics, but only if the data is laid out well. Watch the same query on an unsorted and a sorted file.

Statistics and sort order 5 steps
Animated example Query: WHERE day = '2026-10-19'row group 0: day min 10-01 max 10-31READrow group 1: day min 10-01 max 10-31READrow group 2: day min 10-01 max 10-31READrow group 3: day min 10-01 max 10-31READUnsorted data: every row group spans the whole month.min/max can’t rule any out, so all 4 are read.Rewrite the table sorted by dayrow group 0: day min 10-01 max 10-08row group 1: day min 10-09 max 10-16row group 2: day min 10-17 max 10-24row group 3: day min 10-25 max 10-31Same rows, sorted before writing.Each row group now covers about one week.Query: WHERE day = '2026-10-19'row group 0: day min 10-01 max 10-08skiprow group 1: day min 10-09 max 10-16skiprow group 2: day min 10-17 max 10-24READrow group 3: day min 10-25 max 10-31skipOnly row group 2 can contain 19 October.3 of 4 skipped without reading: 75% less I/O.Bonus: sorted columns compress betterday (unsorted)100%day (sorted)~6%city (unsorted)100%city (sorted)~30%Runs of equal values → run-length + dictionary encoding.(Illustrative ratios; real gains depend on the data.)Sort by what you filter by• partition folders by day (Lesson 4.3)• inside a partition, sort or cluster by city• target row groups of ~128 MB, files of 128 MB–1 GB• table formats: Z-order / clustering does this for youLayout decisions often matter more than more machines. myphatshala.in

Which format, where

.csv

1. Government open data and bank statements: CSV

Portals publishing public datasets and banks letting you download statements use CSV (or Excel), because anyone can open it. Data teams convert it to Parquet as soon as it lands.

CSV download→Convert once→Parquet table
{ }

2. Mobile apps and web APIs: JSON

Payment apps, food-delivery apps and partner APIs exchange JSON because every language can read it. High-volume flows often switch to Avro or Protobuf to save bandwidth.

App→JSON over HTTPS→API / Kafka

3. Event streams: Avro with a Schema Registry

Companies running Kafka at scale commonly use Avro (or Protobuf) with a schema registry so that dozens of producer and consumer teams can change messages safely.

Producer→Avro + registry→Many consumers

4. Lakes, lakehouses and cloud warehouses: Parquet

Spark jobs, Delta Lake and Iceberg tables, and external tables in BigQuery, Snowflake, Redshift and Athena all read Parquet. Pay-per-scan engines charge less because fewer bytes are read.

Raw files→Parquet / Delta / Iceberg→SQL + ML
7 Run the code

Row storage vs column storage

The same small table is stored row by row and column by column. The program counts how many values each layout must read to answer “total amount for Pune”. Then try the challenges: text vs binary size, dictionary encoding, compression of sorted data, min/max skipping and Avro-style schema evolution.

orders = [
    ("O1", "Pune", "thali", 220, "10-19"),
    ("O2", "Thane", "dosa", 150, "10-19"),
    ("O3", "Pune", "chai", 20, "10-20"),
    ("O4", "Mumbai", "lassi", 90, "10-20"),
]
columns = ["order_id", "city", "dish", "amount", "day"]

# Row layout: whole records stored together
row_file = [value for row in orders for value in row]
# Column layout: each column stored together
col_file = {name: [row[i] for row in orders] for i, name in enumerate(columns)}

# Question: total amount for Pune
row_reads = len(row_file)                                   # must read every value
col_reads = len(col_file["city"]) + len(col_file["amount"])  # only 2 columns
total = sum(a for c, a in zip(col_file["city"], col_file["amount"]) if c == "Pune")

print("Values read, row layout   :", row_reads)
print("Values read, column layout:", col_reads)
print("Total for Pune: Rs", total)
Values read, row layout : 20 Values read, column layout: 8 Total for Pune: Rs 240
  • row_file = [value for row in orders for value in row]A row format (CSV, JSON, Avro) writes record after record: O1, Pune, thali, 220, …
  • col_file = {name: [row[i] for row in orders] for i, name in enumerate(columns)}A columnar format (Parquet, ORC) keeps each column together: all cities, then all amounts, and so on.
  • col_reads = len(col_file["city"]) + len(col_file["amount"]) # only 2 columnsThe query touches only city and amount, so a columnar engine reads 2 of the 5 columns.

Run it on your PC: race six file formats

This program creates 2 lakh random orders and writes them as CSV, gzipped CSV, JSON lines, Avro, Parquet and ORC, then compares file sizes and answers one question from CSV and from Parquet. First install two libraries: pip install pyarrow fastavro. Save the program as formats_race.py in an empty folder and run it.

Six formats, one table

"""Write the same 2 lakh orders as CSV, CSV.gz, JSON lines, Avro, Parquet and ORC,
then compare file sizes and the time to answer one question.
Needs:  pip install pyarrow fastavro"""
import csv, gzip, json, os, random, time
import fastavro
import pyarrow as pa
import pyarrow.compute as pc
import pyarrow.orc as orc
import pyarrow.parquet as pq

def inr(n):
    """Indian digit grouping: 18977156 -> 1,89,77,156"""
    s = str(n)
    head, tail = s[:-3], s[-3:]
    while len(head) > 2:
        tail = head[-2:] + "," + tail
        head = head[:-2]
    return (head + "," + tail) if head else tail


random.seed(7)
N = 200_000
cities = ["Pune", "Mumbai", "Nashik", "Nagpur", "Thane"]
dishes = ["thali", "dosa", "biryani", "pav bhaji", "lassi", "chai"]
rows = [{"order_id": i, "city": random.choice(cities), "dish": random.choice(dishes),
         "amount": random.randint(40, 900), "day": f"2026-10-{random.randint(1, 31):02d}"} for i in range(N)]
os.makedirs("formats", exist_ok=True)
cols = list(rows[0])

with open("formats/orders.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=cols); w.writeheader(); w.writerows(rows)
with gzip.open("formats/orders.csv.gz", "wt", newline="") as f:
    w = csv.DictWriter(f, fieldnames=cols); w.writeheader(); w.writerows(rows)
with open("formats/orders.jsonl", "w") as f:
    for r in rows:
        f.write(json.dumps(r) + "\n")
schema = {"name": "Order", "type": "record", "fields": [
    {"name": "order_id", "type": "long"}, {"name": "city", "type": "string"},
    {"name": "dish", "type": "string"}, {"name": "amount", "type": "int"}, {"name": "day", "type": "string"}]}
with open("formats/orders.avro", "wb") as f:
    fastavro.writer(f, fastavro.parse_schema(schema), rows, codec="deflate")
table = pa.Table.from_pylist(rows)
pq.write_table(table, "formats/orders.parquet", compression="snappy")
orc.write_table(table, "formats/orders.orc", compression="zstd")

print("File sizes for", inr(N), "orders:")
base = os.path.getsize("formats/orders.csv")
for name in ["orders.csv", "orders.jsonl", "orders.csv.gz", "orders.avro", "orders.parquet", "orders.orc"]:
    size = os.path.getsize("formats/" + name)
    print(f"  {name:15} {size / 1024 / 1024:6.2f} MB   ({size / base * 100:5.1f}% of CSV)")

# Question: total amount for Pune. A row format must read every column of every row.
t = time.perf_counter()
total = 0
with open("formats/orders.csv") as f:
    for r in csv.DictReader(f):
        if r["city"] == "Pune":
            total += int(r["amount"])
csv_s = time.perf_counter() - t

t = time.perf_counter()
part = pq.read_table("formats/orders.parquet", columns=["city", "amount"])
pq_total = pc.sum(pc.filter(part["amount"], pc.equal(part["city"], "Pune"))).as_py()
pq_s = time.perf_counter() - t

print(f"\nTotal for Pune from CSV    : Rs {inr(total)}")
print(f"Total for Pune from Parquet: Rs {inr(pq_total)}  (read only 2 of 5 columns)")
print(f"Parquet was about {csv_s / pq_s:.0f}x faster on this machine")
meta = pq.ParquetFile("formats/orders.parquet").metadata
print(f"Parquet footer: {meta.num_row_groups} row group(s), {meta.num_columns} columns, schema stored inside the file")

# In a terminal:
#   pip install pyarrow fastavro
#   python formats_race.py
File sizes for 2,00,000 orders: orders.csv 6.76 MB (100.0% of CSV) orders.jsonl 17.44 MB (258.0% of CSV) orders.csv.gz 1.29 MB ( 19.0% of CSV) orders.avro 1.37 MB ( 20.3% of CSV) orders.parquet 1.53 MB ( 22.7% of CSV) orders.orc 0.92 MB ( 13.6% of CSV) Total for Pune from CSV : Rs 1,89,77,156 Total for Pune from Parquet: Rs 1,89,77,156 (read only 2 of 5 columns) Parquet was about 13x faster on this machine Parquet footer: 1 row group(s), 5 columns, schema stored inside the file

Recorded from a real run with Python 3.13, pyarrow 26 and fastavro 1.13. Sizes can differ slightly with other library versions, and the speed-up depends on your machine. The data is random, which is the hardest case for compression; real business data (sorted dates, repeated cities) usually compresses much better in Parquet and ORC. Open the formats folder and try opening each file in Notepad: only the CSV and JSON are readable.

8 Line by line

Dictionary encoding, one line at a time

A city column is encoded the way Parquet does it: each new city is added to the dictionary once, and every value becomes a small code. Watch the dictionary stop growing while the codes list keeps growing.

Worked examples: real problems, solved and traced in memory

Four format decisions, from choosing a format to estimating what min/max statistics save. Each runs live and is traced.

Example 1BeginnerPlatform design · choosing

Pick a format for each job

Problem statement

Apply simple rules to choose a format: exports for people → CSV; messages between services → Avro (JSON if the partner can't use Avro); tables for analytics → Parquet.

Input
monthly report for finance, Kafka order events, partner webhook, lake orders table
Expected output
monthly report for finance  -> CSV      (opens anywhere)
Kafka order events          -> Avro     (compact, typed, schema registry)
partner webhook             -> JSON     (every language reads it)
lake orders table           -> Parquet  (columnar, compressed, skippable)

Approach

  1. Describe each job by who reads it.
  2. Use if/elif rules to pick a format.
  3. Print the choice and the reason.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 2IntermediateStorage · estimation

How much bigger is JSON than CSV?

Problem statement

Build the CSV line and the JSON line for the same records, measure their sizes, and print how much bigger JSON is for 1 crore records.

Input
3 sample records, scale to 1 crore
Expected output
CSV : 12.7 bytes/record -> 0.12 GB
JSON: 50.7 bytes/record -> 0.47 GB
JSON is 4.0x bigger

Approach

  1. Make the CSV header plus one line per record.
  2. Make one JSON object per line with json.dumps.
  3. Compare average bytes per record and scale up.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 3IntermediatePerformance · compression

Sorted vs unsorted: measure the difference

Problem statement

Compress the same column of 10,000 values with zlib, once in random order and once sorted, and report both compression ratios.

Input
10,000 cities in random order
Expected output
random order: 63999 -> 5483 bytes, ratio 12x
sorted      : 63999 -> 151 bytes, ratio 424x

Approach

  1. Generate the column with a fixed random seed so results repeat.
  2. Join the values into bytes and compress with zlib.compress.
  3. Ratio = original size ÷ compressed size.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 4AdvancedQuery engine · predicate pushdown

How many bytes does min/max skipping save?

Problem statement

A Parquet file has 12 row groups of 128 MB, one per month, with min and max day stored in the footer. A query asks for 3 days in October. How many MB are read with and without skipping?

Input
12 row groups × 128 MB, query 2026-10-17 to 2026-10-19
Expected output
Row groups read: [9]
MB read with skipping: 128
MB read without      : 1536

Approach

  1. Build the min/max range for each month.
  2. A row group must be read if its range overlaps the query range.
  3. Compare bytes read with bytes in the whole file.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

9 Quiz and revision

Check yourself

Quick revision

Lesson 2.2 in one screen

  • Row-oriented: CSV, JSON, Avro. Column-oriented: Parquet, ORC.
  • Text: CSV, JSON (readable, large, no types). Binary: Avro, Parquet, ORC (compact, typed, schema inside).
  • Use: CSV for people, JSON for APIs, Avro for Kafka/streams, Parquet for analytics and lakes, ORC for Hive.
  • Codecs: gzip (small, slow), snappy/lz4 (fast), zstd (balanced). Gzipped CSV is not splittable.
  • Parquet: row groups → column chunks → pages; footer with schema + min/max → predicate pushdown.
  • Encodings: dictionary, run-length, bit-packing, delta. Sorting improves skipping and compression.
  • Avro evolution: add fields with defaults; never remove required fields or change types. Enforce with a Schema Registry.

Interview and viva questions

Compare CSV, JSON, Avro, Parquet and ORC.

CSV and JSON are row-oriented text formats: human-readable and universal, but large, untyped and slow to parse; JSON supports nesting. Avro is a row-oriented binary format with an embedded schema and strong schema-evolution support, ideal for streaming and Kafka. Parquet and ORC are column-oriented binary formats with embedded schemas, per-column compression and encodings, and statistics for skipping data; they are best for analytical queries in lakes, lakehouses and warehouses. Parquet is the common default in Spark and the cloud; ORC is popular in Hive.

Why are columnar formats better for analytics?

Analytical queries usually read a few columns of many rows. Columnar formats read only those columns, store values of one type together so they compress and encode very well, and keep statistics per block so whole blocks can be skipped. Row formats must read and parse every column of every row.

When should you not use Parquet?

When writing or reading single records with low latency (streaming messages, OLTP), when data is tiny, when frequent row-level updates are needed without a table format like Delta or Iceberg, and when people need to open files directly. Use Avro, JSON, a database or CSV in those cases.

What does “splittable” mean and why does it matter?

A splittable file can be divided into independent pieces that different workers process in parallel. Big data engines rely on this for speed. Avro, Parquet, ORC and uncompressed or block-compressed text are splittable; a single large gzip file is not, so one worker must read it all.

What is predicate pushdown?

Pushing a query's filter conditions down to the storage layer so that data which cannot match is skipped before being read. In Parquet and ORC, the engine compares the filter with min/max statistics in the file metadata and skips whole row groups or stripes, and with partitioned tables it skips whole folders.

Practice problems

Format and encoding exercises. The last two are business tasks about storage cost and query cost.

P1Easy

Row to columns

Turn a list of rows into a dictionary of columns.

Input
(O1, Pune, 220), (O2, Thane, 150)
Expected output
{'id': ['O1', 'O2'], 'city': ['Pune', 'Thane'], 'amount': [220, 150]}
Show solution and explanation
rows = [("O1", "Pune", 220), ("O2", "Thane", 150)]
names = ["id", "city", "amount"]
cols = {n: [r[i] for r in rows] for i, n in enumerate(names)}
print(cols)
{'id': ['O1', 'O2'], 'city': ['Pune', 'Thane'], 'amount': [220, 150]}

How it works: This is the reorganisation a columnar format does when it writes data.

P2Easy

Bytes of a CSV line

Print the number of bytes in the CSV line and in the JSON version of the same record.

Input
O1, Pune, 220
Expected output
12 44
Show solution and explanation
import json
csv_line = "O1,Pune,220\n"
json_line = json.dumps({"id": "O1", "city": "Pune", "amount": 220}) + "\n"
print(len(csv_line), len(json_line))
12 44

How it works: Field names and quotes make JSON larger than CSV.

P3Easy

Pick a codec

Print the codec for each need: fastest → snappy, smallest → gzip, balanced → zstd.

Input
fastest, smallest, balanced
Expected output
fastest -> snappy
smallest -> gzip
balanced -> zstd
Show solution and explanation
codec = {"fastest": "snappy", "smallest": "gzip", "balanced": "zstd"}
for need in ["fastest", "smallest", "balanced"]:
    print(need, "->", codec[need])
fastest -> snappy smallest -> gzip balanced -> zstd

How it works: A simple lookup; in real projects zstd is increasingly the default.

P4Medium

Run-length encode

Encode the sorted column as (value, count) pairs.

Input
A A A B B C A A
Expected output
[('A', 3), ('B', 2), ('C', 1), ('A', 2)]
Show solution and explanation
col = ["A", "A", "A", "B", "B", "C", "A", "A"]
runs = []
for v in col:
    if runs and runs[-1][0] == v:
        runs[-1] = (v, runs[-1][1] + 1)
    else:
        runs.append((v, 1))
print(runs)
[('A', 3), ('B', 2), ('C', 1), ('A', 2)]

How it works: Runs break when the value changes; sorting first would merge the two runs of A.

P5Medium

Decode a dictionary column

Rebuild the original column from a dictionary and codes.

Input
dictionary [Mumbai, Pune, Thane], codes [1,1,2,0,1]
Expected output
['Pune', 'Pune', 'Thane', 'Mumbai', 'Pune']
Show solution and explanation
dictionary = ["Mumbai", "Pune", "Thane"]
codes = [1, 1, 2, 0, 1]
print([dictionary[c] for c in codes])
['Pune', 'Pune', 'Thane', 'Mumbai', 'Pune']

How it works: Readers reverse the encoding by looking each code up in the dictionary.

P6Advanced

Row groups that match a filter

Given min/max of amount per row group, print the groups a query for amount between 500 and 600 must read.

Input
(100,450), (480,700), (20,90), (590,990)
Expected output
[1, 3]
Show solution and explanation
stats = [(100, 450), (480, 700), (20, 90), (590, 990)]
lo, hi = 500, 600
print([i for i, (a, b) in enumerate(stats) if a <= hi and b >= lo])
[1, 3]

How it works: A group is read only if its [min, max] range overlaps the query range.

P7Advanced

Compatible schema change?

A new schema adds and removes fields. Print “compatible” only if every removed field had a default and every added field has a default.

Input
old: id, amount, note(default) · new: id, amount, coupon(default)
Expected output
removed: ['note'] added: ['coupon']
compatible
Show solution and explanation
old = {"id": None, "amount": None, "note": ""}      # field -> default (None = required)
new = {"id": None, "amount": None, "coupon": "NONE"}
removed = [f for f in old if f not in new]
added = [f for f in new if f not in old]
ok = all(old[f] is not None for f in removed) and all(new[f] is not None for f in added)
print("removed:", removed, "added:", added)
print("compatible" if ok else "INCOMPATIBLE")
removed: ['note'] added: ['coupon'] compatible

How it works: A simplified version of the checks a Schema Registry runs for full compatibility.

P8Business problem

Storage saving from Parquet

The lake holds 80 TB of CSV. Parquet stores it in about a quarter of the space. Storage costs Rs 1,800 per TB per month. Print the yearly saving.

Input
80 TB, 25%, Rs 1,800/TB-month
Expected output
Parquet size: 20.0 TB
Yearly saving: Rs 1296000
Show solution and explanation
csv_tb = 80
parquet_tb = csv_tb * 0.25
saving = (csv_tb - parquet_tb) * 1800 * 12
print("Parquet size:", parquet_tb, "TB")
print("Yearly saving: Rs", int(saving))
Parquet size: 20.0 TB Yearly saving: Rs 1296000

How it works: Format choice is also a cost decision, before even counting faster queries.

P9Business problem

Pay-per-scan query cost

A dashboard query runs 300 times a day. On CSV it scans 600 GB; on partitioned Parquet it scans 4 GB. At Rs 400 per TB scanned, print the monthly cost of each.

Input
600 GB vs 4 GB, 300/day, Rs 400/TB
Expected output
CSV      Rs 2,109,375 per month
Parquet  Rs 14,062 per month
Show solution and explanation
runs = 300 * 30
for name, gb in [("CSV", 600), ("Parquet", 4)]:
    cost = gb / 1024 * 400 * runs
    print(f"{name:8} Rs {cost:,.0f} per month")
CSV Rs 2,109,375 per month Parquet Rs 14,062 per month

How it works: Engines that bill per byte scanned reward columnar formats and partitioning directly. (Rates are illustrative.)

Real-world scenario questions (asked in company interviews)

Format decisions you will face in real data teams. Decide your answer first, then open the model answer.

Data platform · designFresher

Choose formats for a new pipeline

An app sends order events. They go through Kafka, land in a lake, and analysts query them daily. Your lead asks which file format to use at each step.

The interviewer is checking: do you choose formats by job rather than by habit?

Show a strong answer
  • App → backend: JSON, because it is easy for mobile developers.
  • Kafka: Avro (or Protobuf) with a Schema Registry: compact, typed and safe to evolve.
  • Raw zone: keep messages as received (Avro or JSON) for replay.
  • Analytics tables: Parquet (in Delta Lake or Iceberg), partitioned by date, compressed with snappy or zstd.
  • Exports to business users: CSV or Excel, generated from the Parquet tables.

Avoid: using CSV everywhere because it is familiar, or Parquet for single-message streaming.

Performance · cost1–3 years

The Athena bill is too high

Analysts query 40 TB of gzipped CSV logs with a pay-per-scan engine. Each dashboard refresh costs a lot and takes minutes.

The interviewer is checking: do you know how format and layout change cost and speed?

Show a strong answer
  1. Convert the logs to Parquet with snappy or zstd: typically several times smaller, and queries read only the columns they need.
  2. Partition by date (and perhaps region) so queries scan only the days they ask for.
  3. Sort or cluster within partitions by common filters to improve min/max skipping.
  4. Compact small files into files of a few hundred MB.
  5. Measure bytes scanned before and after; it often drops by 90% or more.

Avoid: buying more compute or caching results without fixing the storage layout.

Streaming · reliability1–3 years

A field removal broke three consumers

A producer team removed a field from their Kafka messages. Three downstream jobs crashed at night.

The interviewer is checking: do you understand schema evolution and how to enforce it?

Show a strong answer

Move to Avro (or Protobuf) with a Schema Registry and set a compatibility mode such as BACKWARD or FULL. The registry then rejects incompatible schemas at publish time. Teach producers the rules: add fields with defaults, remove only optional fields, never change types in place. Add the compatibility check to the producer’s CI pipeline, and give consumers time to upgrade before removing anything.

Avoid: asking every team to “be careful” without an automated check.

Fundamentals · interviewFresher

“Why is Parquet faster than CSV?”

A classic interview question, often followed by “when would you not use Parquet?”

The interviewer is checking: can you explain the mechanism, and its limits?

Show a strong answer

Parquet is columnar, so a query reads only the columns it needs; it is binary with types, so there is no text parsing; each column is compressed and encoded (dictionary, run-length) very efficiently; and min/max statistics let engines skip row groups. It is not ideal for writing one record at a time (streaming), for frequent single-row updates without a table format, for tiny files, or when people need to open the file directly, where CSV or JSON is friendlier.

Avoid: saying “because it is compressed” and stopping there.