State the data grain, assumptions, metric, leakage or failure risk, and how you would validate the result before discussing tools.
Question set
9 detailed answers
01How does columnar storage differ from row storage, and why is columnar faster and cheaper for analytics?
middle
Short answer: In a row format the values of a single record sit together; in a columnar format all values of one column sit together. Analytical queries (aggregates over a few of dozens of columns) read only the needed columns, compress far better and support predicate pushdown — so less I/O and lower cost.
In depth:
- Column projection (projection pushdown) — a query needs 3 of 50 columns, and a columnar engine reads only those three off disk. A row format must read the whole row.
- Compression — a column holds homogeneous values (one type, similar data), so dictionary, run-length and delta encoding reach compression ratios several times higher than on a heterogeneous row.
- Predicate pushdown — using stored per-block min/max statistics, a filter like
WHERE date = ...skips whole blocks without reading them. - Row format wins for point operations: read/write a whole record (OLTP, one-record-at-a-time streaming).
Row layout: [id=1,name=A,ts=..][id=2,name=B,ts=..][id=3,...]
└── record 1 ──┘└── record 2 ──┘
Columnar layout:
id: [1, 2, 3, ...] ← read only the columns
name: [A, B, C, ...] you need, each one
ts: [.., .., .., ...] compressed separately
⚠️ Common mistake: treating columnar as universally better. For row-at-a-time writes and whole-record reads (streaming, transactional updates) a row format (Avro) is more efficient.
02How is Parquet structured internally — row groups, column chunks, encoding — and how does it do predicate and projection pushdown?
middle
Short answer: Parquet is a columnar format where a file is split into horizontal row groups; within each, data is stored by column (column chunks), and a column is made of pages. The footer holds the schema and per-column-chunk statistics (min/max, null counts) that let the engine skip data it doesn't need.
In depth:
- Row group — a horizontal slice (typically 128–512 MB), the unit of parallelism: different workers read different row groups.
- Column chunk — one column's data inside a row group; only projected columns are read.
- Pages — a column is split into pages (~1 MB) with their own statistics and encoding (dictionary, RLE, bit-packing) on top of compression (Snappy/Zstd).
- Footer — metadata at the file's end: schema, offsets, per-chunk min/max. It's read first, so a filter prunes row groups before any data is read.
Parquet file
┌──────────────────────────────────────────┐
│ Row Group 0 │
│ col A chunk [page|page|...] (min/max) │
│ col B chunk [page|page|...] (min/max) │
│ Row Group 1 │
│ col A chunk ... col B chunk ... │
├──────────────────────────────────────────┤
│ Footer: schema + row-group/column stats │
└──────────────────────────────────────────┘
⚠️ Common mistake: expecting pushdown but actually producing thousands of tiny files of a couple of row groups each — statistics prune almost nothing, and the overhead of opening files eats the win.
03Parquet, ORC, Avro, CSV/JSON — what's the difference and when do you pick each?
middle
Short answer: Parquet and ORC are columnar formats for analytics (compression, pushdown). Avro is a row-based binary format for streaming and messaging with strong schema evolution. CSV/JSON are text formats for exchange and debugging, but not for analytics at scale.
In depth:
| Format | Type | Schema | Strength | When to use |
|---|---|---|---|---|
| Parquet | columnar | embedded | compression, projection/predicate pushdown | analytics, data lake, Spark/Trino |
| ORC | columnar | embedded | compression + built-in indexes | Hive ecosystem |
| Avro | row-based | separate (JSON) | schema evolution, compactness | Kafka, streaming, service-to-service |
| CSV | row text | none | simplicity, human-readable | manual exchange, small extracts |
| JSON | row text | none | nesting, flexibility | APIs, logs, debugging |
- Column analytics — Parquet or ORC: you read few of many columns and statistics prune blocks.
- Streaming and messaging — Avro: you write whole records and the schema evolves without rewriting data.
- Text CSV/JSON — handy and portable, but no types, no statistics and no efficient compression — expensive for large tables.
⚠️ Common mistake: keeping the analytics layer in CSV or raw JSON. At scale that is several times more I/O and money than the same dataset in Parquet.
04Open table formats: Apache Iceberg, Delta Lake, Hudi — what do they add on top of raw Parquet?
senior
Short answer: A table format is a metadata layer over Parquet/ORC files in object storage that turns a bag of files into a table with ACID transactions, atomic commits, time travel, and schema and partition evolution. Iceberg, Delta and Hudi solve the same problem, differing in engine details and ecosystem.
In depth:
- ACID and atomic commits — readers see either the old or the new table version, never half-written files mid-write.
- Time travel — snapshots by version/time: you can read the table "as it was" and roll back.
- Schema and partition evolution — add/rename a column and even change the partitioning scheme without rewriting data.
- Hidden partitioning (Iceberg) — the partition is derived from a value (e.g. from a timestamp), so users needn't filter on a partition column by hand.
| Format | Metadata model | Distinctive trait |
|---|---|---|
| Iceberg | manifest files, snapshots | hidden partitioning, engine-agnostic |
| Delta Lake | transaction log _delta_log |
tight Spark integration, MERGE |
| Hudi | commit timeline | upserts and incremental pull out of the box |
-- Iceberg time travel: read the table at a specific snapshot
SELECT * FROM orders
FOR SYSTEM_VERSION AS OF 3821550127947089009;
⚠️ Common mistake: thinking "I dropped Parquet in S3, so I have a table." Without a table format there is no atomicity under concurrent writes, no time travel and no safe schema evolution.
05How do you partition data on object storage, and what are the traps of small files and over-partitioning?
middle
Short answer: Partitioning lays data out into col=value directories so a filtered query reads only the relevant partitions (partition pruning). Pick a column with moderate cardinality that you actually filter on; avoid partitions that are too small and avoid over-partitioning.
In depth:
- Partition = path prefix — the engine sees
year=2026/month=06and skips other partitions without reading data. - Choosing the key — pick a column from frequent
WHEREclauses (date, region) with moderate cardinality; high-cardinality keys (user_id) spawn millions of partitions. - File size — aim for 128 MB–1 GB files; lots of tiny files = the small-files problem and slow listing in S3.
- Don't overdo it — thousands of partitions of a couple of files each make metadata listing expensive and barely speed reads.
s3://lake/events/
year=2026/
month=06/
day=30/
part-0001.parquet (~256 MB)
part-0002.parquet (~256 MB)
month=07/
day=01/
part-0001.parquet
⚠️ Common mistake: partitioning on a high-cardinality column (e.g. user_id) — you get thousands of directories with tiny files, and listing metadata becomes more expensive than the query itself.
06Compression codecs Snappy, Zstd, Gzip: how do you trade speed against ratio, and why does splittability matter?
middle
Short answer: A codec is a tradeoff between speed and compression ratio. Snappy is fast but compresses less; Gzip compresses more but is slow and not splittable when raw; Zstd offers the best balance with a tunable level. For analytics, splittability matters so a file can be read in parallel.
In depth:
- Speed vs ratio — Snappy optimizes CPU (frequent reads), Gzip optimizes size (archive/extract), Zstd spans the range via its compression level.
- Splittability — whether different parts of a file can be read in parallel. Raw Gzip is not splittable: one worker pulls the whole file, losing parallelism.
- Rescued inside Parquet — in Parquet/ORC compression is per page/chunk, so even Gzip inside Parquet is read in parallel by row groups.
- In practice — Snappy as the default for hot data, Zstd when storage matters more at acceptable CPU.
| Codec | Speed | Ratio | Splittable (raw) |
|---|---|---|---|
| Snappy | very high | medium | no (on its own) |
| Zstd | high, tunable | high | no (on its own) |
| Gzip | low | high | no |
| inside Parquet | — | — | yes (by row groups) |
⚠️ Common mistake: dropping a big .csv.gz as a single non-splittable file — one worker reads the whole gigabyte and the cluster's parallelism sits idle. Split into files or use Parquet.
07What is schema evolution, and how do you safely add, rename and drop columns in Avro and table formats?
senior
Short answer: Schema evolution is changing the data structure without rewriting already-written files and without breaking old readers/writers. Safety depends on compatibility: adding a column with a default is usually safe, dropping and renaming are risky. Avro handles it via schema resolution, table formats via tracking columns by stable ids.
In depth:
- Add a column — safe with a default: old files still read and the missing value is filled with the default.
- Drop a column — safe for new-code readers, but an old reader that requires the field breaks — you need backward compatibility.
- Rename — the most dangerous done naively: by name it's a drop + add. Avro solves it with aliases; Iceberg tracks stable column ids, not names.
- Compatibility — backward (new schema reads old data), forward (old schema reads new data), full (both at once); in Kafka the Schema Registry guards it.
| Operation | Avro | Iceberg/Delta | Risk |
|---|---|---|---|
| Add column | default in schema | safe, new id | low |
| Drop column | reader ignores it | marked deleted | medium |
| Rename | alias | by column id, not name | high without support |
⚠️ Common mistake: renaming a column by name in a raw Parquet dataset — old files under the old name stop reading as the new column and data silently disappears from queries.
08Object storage (S3/GCS) as the data lake substrate: how does it differ from HDFS/POSIX, and what matters about consistency and cost?
middle
Short answer: Object storage is a flat key-value store of objects over HTTP, not a filesystem: there are no real directories, no cheap rename, and you pay for storage and for requests. Unlike HDFS/POSIX it separates storage from compute and scales almost limitlessly, but renaming a folder means copying every object.
In depth:
- Flat key space — "directories" are just prefixes in the object name; listing walks a prefix, not a tree.
- No atomic rename — rename/move = copy + delete per object, so commit-by-folder-rename (as in HDFS) is expensive; hence committers and table formats.
- Consistency — modern S3 is strongly read-after-write consistent, but historically it was eventually consistent; account for possible delays in older systems.
- Cost model — you pay per GB stored, per GB transferred and per number of requests (GET/PUT/LIST) — hence the penalty for millions of tiny files.
| Property | Object (S3/GCS) | HDFS/POSIX |
|---|---|---|
| Model | key-value over HTTP | filesystem |
| Folder rename | copy+delete (costly) | atomic, cheap |
| Scale/cost | near-limitless, pay per request | bounded by cluster |
| Storage vs compute | decoupled | tied to nodes |
⚠️ Common mistake: treating S3 like a POSIX filesystem and doing lots of LIST/rename over millions of keys — request bills and listing latency unexpectedly become the pipeline's bottleneck.
09What is the small-files problem, why does it kill performance, and how do you fix it with compaction?
middle
Short answer: The small-files problem is when a table is made of thousands of tiny files instead of a few large ones. Each file needs a separate open, footer read and task scheduling, so overhead outweighs useful reading. It's fixed with compaction — periodically merging small files into target-sized ones.
In depth:
- Where it comes from — frequent micro-batches and streaming, partitions that are too small, many workers each writing its own tiny file.
- Why it hurts — per file: an S3 request, a metadata read, a separate split/scheduler task; thousands of files = thousands of opens and slow listing.
- Compaction — a background job reads small files and rewrites them into large ones (aim for 128 MB–1 GB); Iceberg/Delta have built-in operations (
OPTIMIZE,rewrite_data_files). - Prevention — write less often and larger, buffer batches, sensible partition granularity, don't spawn needless partitions.
Before compaction: [f1][f2][f3][f4][f5]...[f999] ← 999 files × ~1 MB
tiny files problem
│ compaction
▼
After: [ big-0001.parquet ][ big-0002.parquet ] ← 2 files × ~256 MB
⚠️ Common mistake: streaming straight into the lake in one-second micro-batches with no follow-up compaction — within a day the table accumulates hundreds of thousands of files and every analytical query over it slows down several-fold.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.