InterviewKata
System design · Technology guide T09 · Builds on Modules 12 and 23

Data warehouses and lakehouses

The database behind your app answers "show order 81" in a millisecond. "Revenue by country for the last 90 days" scans hundreds of millions of rows, and a different kind of store does it in seconds: a data warehouse. This guide explains why analytical stores are built the way they are, how BigQuery, Snowflake, Redshift, Databricks and the others differ, and which to choose, with a chooser and code you can run.

A 2-minute conversation: Sam asks, Priya explains
0:00 / 2:28

1. OLTP and OLAP

Operational database (OLTP)Analytical store (OLAP)
Typical queryRead or change a few rows by keyScan and aggregate millions of rows, a few columns
StorageRows together (a row store), with B-tree indexesColumns together (a column store), compressed, in large files
ScaleMany small concurrent transactionsFewer, much bigger queries, spread over many machines
ExamplesPostgreSQL, MySQL, DynamoDB (T03)BigQuery, Snowflake, Redshift, ClickHouse

Data reaches the warehouse by batch loads, change data capture or streams (T08), usually transformed with SQL (ELT, often with dbt). A lakehouse keeps the data as open files (Parquet) in object storage (T06), with a table format such as Apache Iceberg or Delta Lake adding transactions, schema changes and time travel, so several engines can query the same tables.

2. How a warehouse runs a query

3. The products

Serverless warehouse · Google Cloud

BigQuery

Storage and compute are separate: tables live in Google's columnar storage, and each query borrows slots (units of compute) from a large shared pool. Pay per data scanned on demand, or reserve slots. Partitioning and clustering cut what a query scans; BigLake and Iceberg tables extend it to open formats.

Best for: analytics on Google Cloud with nothing to size or run.

Warehouse · any cloud

Snowflake

Separate storage and independent compute clusters (virtual warehouses) that start, stop and resize in seconds, so teams don't slow each other. Runs on AWS, Azure and Google Cloud, with data sharing between accounts and support for Iceberg tables.

Best for: a multi-cloud warehouse with strong isolation between workloads.

Warehouse · AWS

Amazon Redshift

A columnar, massively parallel warehouse, provisioned on RA3 nodes with managed storage, or Redshift Serverless. Queries data in S3 through Redshift Spectrum; Athena is AWS's separate serverless SQL engine for S3.

Best for: a warehouse inside an AWS estate.

Lakehouse · any cloud

Databricks SQL

SQL warehouses over Delta Lake tables in your own object storage, alongside Spark jobs, streaming and ML on the same data.

Best for: one platform for data engineering, ML and BI on open files.

Analytics · Azure

Microsoft Fabric (and Azure Synapse)

Fabric combines a warehouse, lakehouse, pipelines and Power BI on OneLake, which stores tables in Delta format. Microsoft positions Fabric Warehouse as the path forward from Synapse dedicated SQL pools, which remain supported.

Best for: new analytics on Azure, especially with Power BI.

Real-time analytics · open source

ClickHouse

A very fast columnar database for analytics on fresh data: inserts become queryable within seconds, and aggregations over billions of rows return in well under a second. Self-run or ClickHouse Cloud. Apache Druid and Apache Pinot fill the same niche.

Best for: user-facing dashboards and observability-style analytics.

In-process · open source

DuckDB

An analytical database that runs inside your program, like SQLite for analytics, reading Parquet and CSV directly. Fast enough for tens or hundreds of gigabytes on one machine.

Best for: local analysis, notebooks, small pipelines and tests.

Open table formats

Apache Iceberg and Delta Lake

Metadata layers over Parquet files that add atomic commits, schema evolution, partition evolution and time travel. Most warehouses can now read and write at least one of them.

Best for: keeping data in open files that several engines share.

4. Differences at a glance

OptionCompute modelYou pay forFresh dataOpen formats
BigQueryServerless slotsBytes scanned, or reserved slotsSeconds with streaming insertsBigLake, Iceberg
SnowflakeVirtual warehouses per teamCredits while warehouses runMinutes; Snowpipe streaming fasterIceberg tables
RedshiftClusters or serverlessNodes, or serverless capacity usedMinutes; streaming ingestion availableSpectrum over S3
Databricks SQLSQL warehousesCompute usedMinutes; streaming tablesDelta (native), Iceberg
FabricCapacity unitsCapacityMinutesDelta on OneLake
ClickHouseServers or cloud serviceServers or compute usedSecondsReads Parquet, Iceberg
DuckDBInside your processNothing (your machine)Whatever you loadParquet, Iceberg, Delta

5. When to choose which, and why

  1. Never run heavy analytics on the production database. Copy the data out (CDC or batch) into an analytical store.
  2. Internal BI and reporting on one cloud? That cloud's warehouse: BigQuery, Redshift or Fabric. Snowflake when you're multi-cloud or want strict workload isolation.
  3. Data engineering and ML on the same data? A lakehouse: Databricks, or Iceberg tables shared by several engines.
  4. Dashboards inside your product, on data seconds old, with many users? ClickHouse, Druid or Pinot.
  5. Small data, one analyst? DuckDB on Parquet files may be all you need.

Try it: choose for your requirements

6. Why column stores scan less, in code

A 3-million-row orders table with 20 columns, and the query "total amount for one day". A row store reads every column of every row: 480 MB. A column store reads only the two columns the query uses: 48 MB. Partitioning by day means only one day's files are opened: 1.6 MB, 300 times less. And a sorted column compresses to almost nothing with run-length encoding. The program was run in all three languages; each prints the output below it.

DAYS, ROWS_PER_DAY, COLUMNS, BYTES = 30, 100_000, 20, 8   # an orders table: 3 million rows, 20 columns of 8 bytes

rows = DAYS * ROWS_PER_DAY
mb = lambda b: f"{b / 1_000_000:,.1f} MB"

# Query: SELECT SUM(amount) FROM orders WHERE day = '2026-11-03'
row_store = rows * COLUMNS * BYTES                  # whole rows are stored together, so every column is read
column_store = rows * 2 * BYTES                     # only the day and amount columns are read
partitioned = ROWS_PER_DAY * 2 * BYTES              # partitioned by day: only one day's files are opened
print("row store, full scan:     ", mb(row_store))
print("column store, 2 columns:  ", mb(column_store))
print("plus partition pruning:   ", mb(partitioned))
print(f"reduction: {row_store // partitioned}x less data read")

# Sorted columns compress well: run-length encoding stores each value once with a count.
day_column = [d for d in range(DAYS) for _ in range(ROWS_PER_DAY)]
runs = []
for d in day_column:
    if runs and runs[-1][0] == d:
        runs[-1][1] += 1
    else:
        runs.append([d, 1])
print(f"day column: {len(day_column):,} values -> {len(runs)} runs of (value, count)")
const DAYS = 30, ROWS_PER_DAY = 100_000, COLUMNS = 20, BYTES = 8; // an orders table: 3 million rows, 20 columns of 8 bytes

const rows = DAYS * ROWS_PER_DAY;
const mb = (b: number) => (b / 1_000_000).toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + " MB";

// Query: SELECT SUM(amount) FROM orders WHERE day = '2026-11-03'
const rowStore = rows * COLUMNS * BYTES; // whole rows are stored together, so every column is read
const columnStore = rows * 2 * BYTES; // only the day and amount columns are read
const partitioned = ROWS_PER_DAY * 2 * BYTES; // partitioned by day: only one day's files are opened
console.log("row store, full scan:     ", mb(rowStore));
console.log("column store, 2 columns:  ", mb(columnStore));
console.log("plus partition pruning:   ", mb(partitioned));
console.log(`reduction: ${Math.floor(rowStore / partitioned)}x less data read`);

// Sorted columns compress well: run-length encoding stores each value once with a count.
const dayColumn: number[] = [];
for (let d = 0; d < DAYS; d++) for (let i = 0; i < ROWS_PER_DAY; i++) dayColumn.push(d);
const runs: [number, number][] = [];
for (const d of dayColumn) {
  if (runs.length && runs[runs.length - 1][0] === d) runs[runs.length - 1][1]++;
  else runs.push([d, 1]);
}
console.log(`day column: ${dayColumn.length.toLocaleString("en-US")} values -> ${runs.length} runs of (value, count)`);
import java.util.*;

public class Main {
    static final int DAYS = 30, ROWS_PER_DAY = 100_000, COLUMNS = 20, BYTES = 8; // an orders table: 3 million rows, 20 columns of 8 bytes

    static String mb(long b) { return String.format(Locale.US, "%,.1f MB", b / 1_000_000.0); }

    public static void main(String[] args) {
        long rows = (long) DAYS * ROWS_PER_DAY;
        // Query: SELECT SUM(amount) FROM orders WHERE day = '2026-11-03'
        long rowStore = rows * COLUMNS * BYTES;            // whole rows are stored together, so every column is read
        long columnStore = rows * 2 * BYTES;               // only the day and amount columns are read
        long partitioned = (long) ROWS_PER_DAY * 2 * BYTES; // partitioned by day: only one day's files are opened
        System.out.println("row store, full scan:      " + mb(rowStore));
        System.out.println("column store, 2 columns:   " + mb(columnStore));
        System.out.println("plus partition pruning:    " + mb(partitioned));
        System.out.println("reduction: " + rowStore / partitioned + "x less data read");

        // Sorted columns compress well: run-length encoding stores each value once with a count.
        int[] dayColumn = new int[DAYS * ROWS_PER_DAY];
        for (int d = 0, i = 0; d < DAYS; d++) for (int r = 0; r < ROWS_PER_DAY; r++) dayColumn[i++] = d;
        List<int[]> runs = new ArrayList<>();
        for (int d : dayColumn) {
            if (!runs.isEmpty() && runs.get(runs.size() - 1)[0] == d) runs.get(runs.size() - 1)[1]++;
            else runs.add(new int[] {d, 1});
        }
        System.out.printf(Locale.US, "day column: %,d values -> %d runs of (value, count)%n", dayColumn.length, runs.size());
    }
}
row store, full scan:      480.0 MB
column store, 2 columns:   48.0 MB
plus partition pruning:    1.6 MB
reduction: 300x less data read
day column: 3,000,000 values -> 30 runs of (value, count)

7. Say it in the interview

Two-sentence answers
  • "Where do analytics run?" Not on the production database: change data capture streams rows into a warehouse such as BigQuery or Snowflake, partitioned by date. Analysts and dashboards query there, so their scans never slow checkout.
  • "Warehouse or lakehouse?" A warehouse for SQL and BI with the least to manage; a lakehouse with Iceberg or Delta when several engines (Spark, ML jobs, the warehouse) must share the same tables in open files.
  • "Live dashboards for customers?" A real-time analytical store such as ClickHouse or Pinot, fed from the event stream, because warehouses aren't built for thousands of small concurrent queries.

8. Common mistakes

Analytics on the primary databaseOne big report locks or slows the tables users are writing. Copy the data out.
SELECT * on a column storeIt reads every column and, on pay-per-scan pricing, bills for them. Name the columns.
No partitioningEvery query scans the whole history. Partition by date and cluster by common filters.
A warehouse behind a customer-facing pageWarehouses favour big queries, not thousands of small ones a second. Use a real-time store or pre-computed tables.
Many tiny filesStreaming small files into a lake slows every query; compact them regularly.

9. Check yourself

A query sums one column of a 20-column table. Roughly how much less does a column store read than a row store?

It reads only the columns the query names.

A daily report scans the whole orders history every run. The cheapest fix?

Partition pruning opens only the days the query asks for.

Your product shows each customer a live dashboard; thousands load it per minute. Which fits?

They're built for many small, fast queries on fresh data.

What does an open table format such as Iceberg add to Parquet files?

It's a metadata layer; engines such as Spark, Trino or a warehouse do the querying.

10. Sources and further reading

Reviewed on 4 November 2026. Pricing models and features change; check before relying on them.

SourceUsed for
BigQuery overviewSeparate storage and compute, slots
Snowflake key conceptsArchitecture and virtual warehouses
Apache IcebergOpen table format features
Microsoft Fabric blog: a turning point for enterprise data warehousingFabric Warehouse and Synapse dedicated pools
Module 12: Indexes and storage enginesRow and column storage