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.
Sir, it’s the same data. How can a different file type make it sixty times faster? Is Parquet some kind of magic?
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.
Then why does anyone still use CSV?
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.
And how do I choose?
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.
What is a file format?
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.
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
The five formats in one line each
| CSV | Plain text, comma-separated, header row. No types, no schema. Universal, but large and slow for big data. |
|---|---|
| JSON | Plain text with field names and nesting. Used by web APIs and app events. Even larger than CSV. |
| Avro | Binary, row-oriented, schema stored in the file (Apache, 2009). The usual choice for Kafka messages. |
| Parquet | Binary, columnar (Twitter and Cloudera, 2013, based on Google’s Dremel). The default for analytics in Spark, lakes and lakehouses. |
| ORC | Binary, columnar (built for Hive, 2013). Excellent compression; common in Hadoop and Hive platforms. |
File formats at three levels
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).
CSV, JSON, Avro, Parquet and ORC compared
CSV
Simple text rows. Best for small exports, spreadsheets and sharing with people.
Row · textJSON
Labelled, nested text. Best for APIs, app events and configuration.
Row · textAvro
Compact binary rows with a stored schema. Best for streaming (Kafka) and writing records one by one.
Row · binaryParquet
Compressed columns with statistics. Best for analytics, data lakes and lakehouse tables.
Column · binaryORC
Compressed columns with indexes. Best for Hive and Hadoop-based warehouses.
Column · binarySide by side
| CSV | JSON | Avro | Parquet | ORC | |
|---|---|---|---|---|---|
| Layout | Row | Row | Row | Column | Column |
| Encoding | Text | Text | Binary | Binary | Binary |
| Schema in file | No (header only) | Field names, no types | Yes | Yes | Yes |
| Nested data | No | Yes | Yes | Yes | Yes |
| Compression | Whole file (gzip) | Whole file | Per block | Per column, very good | Per column, very good |
| Splittable | Yes, but not when gzipped | JSON lines: yes | Yes | Yes | Yes |
| Human-readable | Yes | Yes | No | No | No |
| Best for | Small exports | APIs, events | Kafka, writes | Analytics, lakes | Hive 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.
What happens inside each format
Text formats repeat a lot
JSON repeats every field name on every row, so it is much bigger than CSV for the same data. Both store numbers as characters, sorows = [("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: 132220takes three bytes of text instead of a compact binary number.Row layout vs column layout
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.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)]Dictionary encoding
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.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]Run-length encoding loves sorted data
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.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]]Statistics let engines skip data
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.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 -> skipThe 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
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.amountis an integer anddayis a date.
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.
- Kabir writes one question: total amount for Pune on 19 October. He will run it on the CSV and on the Parquet copy.
- On the CSV the progress bar crawls: a quarter of the 10 GB file read so far.
- Four minutes later the CSV query finishes.
- On the Parquet file, the first thing that happens is a quick read of the file’s footer.
- Then the bar jumps: only a small part of the file is read.
- Four seconds, and exactly the same answer.
- The same orders stored two ways: CSV as rows of text, Parquet as compressed binary columns.
- With CSV there is no way to skip anything: every row, every column, is read and parsed from text.
- All 10 GB pass through the disk and CPU to answer a question about one city on one day.
- Parquet’s footer holds the schema and the min and max of every column in every row group: a few kilobytes.
- Only one row group can contain 19 October, so three are skipped. Only city, amount and day are read.
- Roughly 150 MB is read instead of 10 GB. Less reading, plus no text parsing, gives the huge speed-up. (Sizes and times are illustrative.)
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 1The 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.
- The CSV file is 6.76 MB. Every number and date is stored as text characters.
- JSON lines is 17.44 MB, about 2.6 times the CSV, because every row repeats every field name.
- Gzip shrinks the CSV to 19% of its size, but a gzipped CSV can’t be split across workers and must still be decompressed and parsed as text.
- Avro (1.37 MB) and Parquet (1.53 MB) are binary formats with real types and an embedded schema.
- ORC is the smallest at 13.6% of the CSV. On this random data the columnar formats don’t beat gzip on size, but for a query on two columns Parquet read only those columns and was about 13 times faster. On real, sorted data the columnar formats usually win on both. (Exact sizes vary slightly with library versions.)
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.
- The producer writes Avro messages to a Kafka topic. The schema (version 1) is stored once in a Schema Registry; each message carries only a small schema id.
- Version 2 adds a
couponfield with a default of null. The registry checks compatibility and allows it. The billing job, still on version 1, ignores the new field. - A new analytics job reads with version 2. When it reads an old version-1 message, Avro fills in the default, so new readers can read old data.
- A careless version 3 removes the required
amountfield. The registry rejects it as incompatible, so no broken messages ever reach consumers. - The rules to remember: add fields with defaults, remove only fields that had defaults, rename with aliases, never remove required fields or change types in place.
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.
- In an unsorted file, orders from every day are mixed into every row group, so each group’s min and max cover the whole month. The engine must read all four.
- The table is rewritten sorted by day. The rows are the same; only their order changes, so each row group now covers about a week.
- For 19 October, the footer statistics show that only row group 2 can match. The other three are skipped without reading a byte of their data.
- Sorting also puts equal values next to each other, so dictionary and run-length encoding shrink the columns much more.
- The practical rules: partition by the main filter (often date), sort or cluster within it by the next filter, and keep row groups and files large. Lakehouse table formats offer clustering and Z-ordering to do this automatically.
Which format, where
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.
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.
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.
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.
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)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 onlycityandamount, 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.pyRecorded 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.
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.
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.
monthly report for finance, Kafka order events, partner webhook, lake orders table
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
- Describe each job by who reads it.
- Use if/elif rules to pick a format.
- 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.
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.
3 sample records, scale to 1 crore
CSV : 12.7 bytes/record -> 0.12 GB JSON: 50.7 bytes/record -> 0.47 GB JSON is 4.0x bigger
Approach
- Make the CSV header plus one line per record.
- Make one JSON object per line with
json.dumps. - 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.
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.
10,000 cities in random order
random order: 63999 -> 5483 bytes, ratio 12x sorted : 63999 -> 151 bytes, ratio 424x
Approach
- Generate the column with a fixed random seed so results repeat.
- Join the values into bytes and compress with
zlib.compress. - 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.
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?
12 row groups × 128 MB, query 2026-10-17 to 2026-10-19
Row groups read: [9] MB read with skipping: 128 MB read without : 1536
Approach
- Build the min/max range for each month.
- A row group must be read if its range overlaps the query range.
- 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.
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.
Row to columns
Turn a list of rows into a dictionary of columns.
(O1, Pune, 220), (O2, Thane, 150)
{'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)How it works: This is the reorganisation a columnar format does when it writes data.
Bytes of a CSV line
Print the number of bytes in the CSV line and in the JSON version of the same record.
O1, Pune, 220
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))How it works: Field names and quotes make JSON larger than CSV.
Pick a codec
Print the codec for each need: fastest → snappy, smallest → gzip, balanced → zstd.
fastest, smallest, balanced
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])How it works: A simple lookup; in real projects zstd is increasingly the default.
Run-length encode
Encode the sorted column as (value, count) pairs.
A A A B B C A A
[('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)How it works: Runs break when the value changes; sorting first would merge the two runs of A.
Decode a dictionary column
Rebuild the original column from a dictionary and codes.
dictionary [Mumbai, Pune, Thane], codes [1,1,2,0,1]
['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])How it works: Readers reverse the encoding by looking each code up in the dictionary.
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.
(100,450), (480,700), (20,90), (590,990)
[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])How it works: A group is read only if its [min, max] range overlaps the query range.
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.
old: id, amount, note(default) · new: id, amount, coupon(default)
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")How it works: A simplified version of the checks a Schema Registry runs for full compatibility.
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.
80 TB, 25%, Rs 1,800/TB-month
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))How it works: Format choice is also a cost decision, before even counting faster queries.
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.
600 GB vs 4 GB, 300/day, Rs 400/TB
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")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.
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.
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
- Convert the logs to Parquet with snappy or zstd: typically several times smaller, and queries read only the columns they need.
- Partition by date (and perhaps region) so queries scan only the days they ask for.
- Sort or cluster within partitions by common filters to improve min/max skipping.
- Compact small files into files of a few hundred MB.
- 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.
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.
“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.