Data Warehouse Interview Questions and Answers

Data warehouse interviews ask how the platform behaves, not just what terms mean. Six questions answered on millions of real New York taxi trips: why columnar files are smaller, how partition pruning skips data, why reloads duplicate rows, what schema drift looks like, where stray rows go, and when to summarise.

GloryTecks Team

Training & Placement Faculty

30 October 2025 8 min read

Last updated: 8 October 2026

Share:

Data warehouse interview questions often start with definitions, such as OLTP versus OLAP or a fact table, and then move to behaviour. Why is this query cheap and that one expensive? What happens when a load runs twice? What breaks when the source adds a column? These answers are shown on real data, with DuckDB standing in for a warehouse engine, so you can describe what you have seen rather than what you have read.

The data is New York City yellow taxi trips for January 2024 and January 2025, published monthly by the Taxi and Limousine Commission (NYC TLC: trip record data), almost three million trips in the first month alone.

python
import os
import shutil

import duckdb

db = duckdb.connect()
JAN24, JAN25 = "yellow_tripdata_2024-01.parquet", "yellow_tripdata_2025-01.parquet"   # NYC TLC yellow taxi trips

print("W1 row storage vs columnar storage: the same January 2024 trips")
db.execute(f"COPY (SELECT * FROM '{JAN24}') TO 'trips.csv' (HEADER)")
mb = lambda f: os.path.getsize(f) / 1e6
print(f"   CSV {mb('trips.csv'):,.0f} MB | Parquet {mb(JAN24):,.0f} MB | rows {db.sql(f'SELECT COUNT(*) FROM {JAN24!r}').fetchone()[0]:,}")
os.remove("trips.csv")

print("\nW2 partition pruning: one day out of a month partitioned by pickup date")
shutil.rmtree("lake", ignore_errors=True)
db.execute(f"""COPY (SELECT *, CAST(tpep_pickup_datetime AS DATE) AS pickup_date FROM '{JAN24}'
                    WHERE tpep_pickup_datetime >= '2024-01-01' AND tpep_pickup_datetime < '2024-02-01')
               TO 'lake' (FORMAT parquet, PARTITION_BY (pickup_date))""")
plan = db.execute("""EXPLAIN ANALYZE SELECT COUNT(*) FROM read_parquet('lake/*/*.parquet', hive_partitioning = true)
                     WHERE pickup_date = DATE '2024-01-15'""").fetchall()[0][1]
print("   partition folders:", len(os.listdir("lake")), "|", next(l.strip("│ ") for l in plan.splitlines() if "Scanning Files" in l))

print("\nW3 reloading a day: plain INSERT vs delete-then-insert")
db.execute("CREATE TABLE fact_trips AS SELECT * FROM read_parquet('lake/*/*.parquet', hive_partitioning = true) WHERE false")
load_day = "INSERT INTO fact_trips SELECT * FROM read_parquet('lake/*/*.parquet', hive_partitioning = true) WHERE pickup_date = DATE '2024-01-15'"
db.execute(load_day)
db.execute(load_day)                                           # the job is retried after a failure
print(f"   rows for 15 January after two plain inserts: {db.sql('SELECT COUNT(*) FROM fact_trips').fetchone()[0]:,}")
db.execute("DELETE FROM fact_trips")
for _ in range(2):                                             # the same retry, made idempotent
    db.execute("BEGIN")
    db.execute("DELETE FROM fact_trips WHERE pickup_date = DATE '2024-01-15'")
    db.execute(load_day)
    db.execute("COMMIT")
print(f"   rows after two delete-then-insert loads:    {db.sql('SELECT COUNT(*) FROM fact_trips').fetchone()[0]:,}")

print("\nW4 schema drift between January 2024 and January 2025")
cols = lambda f: [r[0] for r in db.sql(f"DESCRIBE SELECT * FROM '{f}'").fetchall()]
print("   columns only in 2025:", sorted(set(cols(JAN25)) - set(cols(JAN24))))
print("  ", db.sql(f"""SELECT YEAR(tpep_pickup_datetime) AS year, COUNT(*) AS trips, COUNT(cbd_congestion_fee) AS with_cbd_fee
                       FROM read_parquet(['{JAN24}', '{JAN25}'], union_by_name = true)
                       WHERE MONTH(tpep_pickup_datetime) = 1 AND YEAR(tpep_pickup_datetime) IN (2024, 2025)
                       GROUP BY year ORDER BY year""").fetchall())

print("\nW5 rows in the January 2024 file that are not from January 2024")
print("  ", db.sql(f"""SELECT COUNT(*), MIN(tpep_pickup_datetime), MAX(tpep_pickup_datetime) FROM '{JAN24}'
                       WHERE tpep_pickup_datetime < '2024-01-01' OR tpep_pickup_datetime >= '2024-02-01'""").fetchone())

print("\nW6 a daily summary table answers daily questions from far fewer rows")
db.execute("""CREATE TABLE daily AS SELECT pickup_date, COUNT(*) AS trips, SUM(total_amount) AS revenue
              FROM read_parquet('lake/*/*.parquet', hive_partitioning = true) GROUP BY pickup_date""")
raw = db.sql("SELECT ROUND(SUM(total_amount)) FROM read_parquet('lake/*/*.parquet', hive_partitioning = true) WHERE pickup_date BETWEEN '2024-01-08' AND '2024-01-14'").fetchone()[0]
summary = db.sql("SELECT ROUND(SUM(revenue)) FROM daily WHERE pickup_date BETWEEN '2024-01-08' AND '2024-01-14'").fetchone()[0]
print(f"   summary rows {db.sql('SELECT COUNT(*) FROM daily').fetchone()[0]} | week of 8 January: raw {raw:,.0f} vs summary {summary:,.0f}")
shutil.rmtree("lake")
text
W1 row storage vs columnar storage: the same January 2024 trips
   CSV 299 MB | Parquet 50 MB | rows 2,964,624

W2 partition pruning: one day out of a month partitioned by pickup date
   partition folders: 31 | Scanning Files: 1/31

W3 reloading a day: plain INSERT vs delete-then-insert
   rows for 15 January after two plain inserts: 154,066
   rows after two delete-then-insert loads:    77,033

W4 schema drift between January 2024 and January 2025
   columns only in 2025: ['cbd_congestion_fee']
   [(2024, 2964606, 0), (2025, 3475204, 3475204)]

W5 rows in the January 2024 file that are not from January 2024
   (18, datetime.datetime(2002, 12, 31, 22, 59, 39), datetime.datetime(2024, 2, 1, 0, 1, 15))

W6 a daily summary table answers daily questions from far fewer rows
   summary rows 31 | week of 8 January: raw 18,008,730 vs summary 18,008,730

W1. Why do analytical warehouses store data by column?

Because analytical queries read a few columns from many rows, and columnar storage lets them read only those columns, compressed. The same January trips took 299 MB as CSV and 50 MB as Parquet, a column-oriented format built for compression and encoding (Apache Parquet: overview). Values in one column are similar, so they compress well. Transactional databases store whole rows together because they mostly read and write one record at a time. That is the practical difference behind "OLTP versus OLAP".

W2. What is partition pruning?

Splitting a table by a column such as date into separate files or folders, so that a query filtering on that column only opens the matching partitions (DuckDB: hive partitioning). Partitioned by pickup date, the month became 31 folders, and a query for 15 January scanned 1 of 31 files. BigQuery, Snowflake, Redshift and lakehouse tables all have a version of this, under names such as partitioning, clustering and micro-partitions.

The trap to mention: pruning only works when the filter is on the partition column itself. Wrap it in a function or filter on a different date column, and every partition is read again.

W3. What happens if a load job runs twice?

With a plain INSERT, the data is loaded twice. Loading 15 January and then retrying, as an orchestrator does after a failure, left 154,066 rows instead of 77,033. Make loads idempotent: delete the partition being loaded and insert it again inside one transaction, or MERGE on a key. Two delete-then-insert runs left exactly 77,033 rows. Interviewers like this question because retries are normal in production, and a pipeline that cannot be rerun safely will eventually double-count.

W4. How do you handle a source that adds a column?

Expect it. From 2025 the TLC files include cbd_congestion_fee for New York's congestion pricing charge, a column the 2024 files do not have. Reading both with union_by_name lines columns up by name rather than position (DuckDB: combining schemas): all 3,475,204 trips from January 2025 have a value, while the 2,964,606 from January 2024 get NULL. A NULL for a column that did not exist yet is honest; a zero would claim that no fee was charged. In a warehouse, decide whether new source columns are added automatically or must pass review, and record when each column started.

W5. Where do rows from the wrong period go?

First find them. The January 2024 file holds 18 trips dated outside January, from 31 December 2002 to just after midnight on 1 February 2024. Some are clearly wrong clocks; others are late or boundary records. If the warehouse partitions by the date in the row, they land in partitions that may already be closed and reported. Decide the rule in advance: quarantine impossible dates, route late but valid rows to their true partition and reprocess it, and monitor the count. The 18 rows would also have created extra partitions if the load had not filtered to the month.

W6. When should you build summary tables?

When many queries ask the same aggregate question. A daily summary of trips and revenue has 31 rows instead of nearly three million, and the week of 8 January gave the same revenue from both, 18,008,730 dollars. The cost is keeping it in step with the facts: every reload or late row must also refresh the summary. Materialised views do this automatically in many warehouses. Say what refresh strategy you would use, not just that you would add the table.

How to answer warehouse questions

  • Explain cost in terms of data read, such as columns, partitions and files.
  • Treat every load as something that will be retried.
  • Assume the source schema will change, and say how you would notice.
  • Name a rule for late and invalid rows before they arrive.

For the concepts behind these answers, see data warehouse concepts and architecture and data lake vs data warehouse vs lakehouse. Loads are covered in idempotency in data pipelines and handling late-arriving data, and design questions in data modeling interview questions.

What are common data warehouse interview questions?

OLTP versus OLAP, columnar storage, star schemas and slowly changing dimensions, partitioning and clustering, incremental and idempotent loading, schema evolution, late-arriving data, and summary tables or materialised views.

What is the difference between a data warehouse and a database?

A transactional database is optimised for many small reads and writes of individual records. A data warehouse is optimised for large analytical queries over history, usually with columnar storage, partitioning and a dimensional model.

What makes a data load idempotent?

Running it twice gives the same result as running it once, for example by deleting and reinserting the partition being loaded in one transaction, or by merging on a unique key.

How does partitioning speed up queries?

Queries that filter on the partition column only read the matching partitions, so far less data is scanned. The filter must be on the partition column itself for this to work.

Warehouse design and loading are part of the Data Engineering course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Interview Questions?

Join GloryTecks’ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.

Explore course