Data Engineering & Cloud Platforms
Interview Context
資深 DS 面試常會測試你對「資料從哪裡來、怎麼流到你手上」的理解 — 你不需要會維運 Kafka cluster,但要能講清楚 data lake 和 warehouse 的差異、什麼時候用 Spark、pipeline 掛掉時去哪裡找問題。這些題目篩掉的是「只會在 notebook 裡等別人餵資料」的候選人。
What You Should Understand
- 能畫出一個 modern data platform 的分層架構:sources → ingestion → storage → processing → serving
- 能比較 data lake、data warehouse、lakehouse,並解釋 medallion architecture 和 table formats(Delta / Iceberg / Hudi)
- 知道為什麼 Parquet 是分析工作的預設格式,以及 partitioning 和 predicate pushdown 怎麼省錢
- 能解釋 ETL vs ELT、Airflow DAG 的 idempotency 和 backfill、dbt 的角色
- 理解 Spark 的 driver/executor 模型、shuffle 的成本,並能判斷什麼時候 pandas 就夠了
- 能把 storage / warehouse / processing / streaming / orchestration 對應到 AWS、GCP、Azure 的服務
The Modern Data Platform Landscape
A modern data platform moves data through five logical layers:
| Layer | What Happens | Example Technologies |
|---|---|---|
| Sources | Operational systems generate data | PostgreSQL, MySQL (OLTP), app events, third-party APIs, logs |
| Ingestion | Move data into the platform | Kafka / Kinesis (streaming), Fivetran / Airbyte (batch CDC), SDK event trackers |
| Storage | Durable, cheap, scalable landing zone | S3 / GCS / Azure Blob (data lake), warehouse native storage |
| Processing | Clean, join, aggregate, feature-engineer | Spark, dbt + warehouse SQL, Flink (streaming) |
| Serving | Expose data to consumers | Warehouse (BI dashboards), feature store (ML), reverse ETL (back to apps) |
直覺:OLTP database 是為了「快速處理單筆交易」設計的(row-oriented、高併發、低延遲),分析查詢卻是「掃很多 rows、只看幾個 columns」— 直接在 production DB 上跑分析會拖垮線上服務,所以要先把資料複製到專為分析設計的系統。這就是整個 data platform 存在的理由。
Who Does What
| Role | Owns | Typical Tools | Cares About |
|---|---|---|---|
| Data Engineer | Ingestion, storage, pipeline infrastructure | Kafka, Spark, Airflow, Terraform | Reliability, throughput, cost |
| Analytics Engineer | Transform layer, curated tables, metric definitions | dbt, SQL, warehouse | Data modeling, correctness, documentation |
| Data Scientist | Analysis, experiments, models built on curated data | SQL, pandas, Spark, feature store | Statistical validity, feature quality |
Where a DS touches each layer:
| Layer | DS Involvement |
|---|---|
| Sources / Ingestion | 定義需要哪些 events 和 fields(event taxonomy)— 沒 log 到的資料永遠拿不回來 |
| Storage | 直接讀 lake 上的 raw data 做 exploration,理解 partitioning 才不會掃全表 |
| Processing | 寫 feature pipeline、把 ad-hoc notebook 邏輯 production 化 |
| Serving | 主要消費者 — 定義 metrics tables、訓練資料集、監控報表 |
為什麼 DS 面試考這個
面試官想確認你能和 data engineer 有效協作:你提的需求是否合理(「我要每 5 分鐘更新的全量 join」成本極高)、pipeline 出問題時你能不能自己 debug 到一半(查 partition 是否 late、上游 schema 是否變了),而不是每次都開 ticket 等人救。
Data Lake vs Data Warehouse vs Lakehouse
Data warehouse: a system that stores structured, schema-enforced data optimized for SQL analytics (schema-on-write). Data lake: a repository of raw files in open formats on cheap object storage, interpreted at read time (schema-on-read). Lakehouse: data lake storage plus a transactional table format layer, giving warehouse-like guarantees on lake economics.
| Dimension | Data Warehouse | Data Lake | Lakehouse |
|---|---|---|---|
| Schema | Schema-on-write(寫入時強制) | Schema-on-read(讀取時才解釋) | Schema enforcement + evolution |
| Data types | Structured | Structured + semi/unstructured (logs, images, JSON) | Both |
| Storage cost | Higher (proprietary format, coupled compute) | Very low (object storage) | Very low (object storage) |
| Governance / quality | Strong (constraints, ACLs) | Weak by default(容易變 data swamp) | Strong (ACID, schema checks, time travel) |
| Query performance | Excellent (optimized engine) | Depends on file layout | Near-warehouse with good layout |
| Best for | BI, curated metrics, finance reporting | Raw event archive, ML training data, exploration | Single platform for BI + ML |
| Examples | BigQuery, Redshift, Snowflake | S3 + Parquet, GCS + Avro | Databricks + Delta Lake, Iceberg on S3 |
直覺比喻:warehouse 像整理好的圖書館(上架前先分類編目,找書很快);data lake 像倉庫(什麼都先丟進去,便宜但要找東西時得自己翻);lakehouse 是在倉庫裡加上圖書館的目錄系統 — 保留便宜的儲存,補上交易保證和治理。
Medallion Architecture
Lakehouse pipelines are commonly organized into three quality tiers:
| Tier | Content | Transformations | Consumers |
|---|---|---|---|
| Bronze | Raw data, as ingested, append-only | None(只加 metadata:ingest time, source) | Data engineers, reprocessing |
| Silver | Cleaned, deduplicated, typed, joined | Filtering, dedup, schema conformance | DS exploration, feature pipelines |
| Gold | Business-level aggregates, metrics tables | Aggregation, business logic | BI dashboards, ML training sets, execs |
直覺:Bronze 保留「事實的原始證據」讓你永遠可以重算;Silver 是「可信的明細」;Gold 是「可以直接給老闆看的數字」。錯誤修復的原則是往上游修 — 如果 Gold 的數字錯了,修 Silver 的邏輯再重跑,而不是手動改 Gold。
Table Formats: Delta Lake / Iceberg / Hudi
Plain Parquet files on S3 have no transaction log — concurrent writes can corrupt each other, and there is no way to update or delete rows safely. Table formats add a metadata layer that provides:
| Capability | What It Means | Why DS Cares |
|---|---|---|
| ACID transactions | Concurrent read/write without corruption | Pipeline 寫到一半失敗不會留下半套資料 |
| Time travel | Query the table as of a past version/timestamp | 重現「上週訓練時看到的資料」— reproducibility |
| Schema evolution | Add/rename columns without rewriting all files | 上游加欄位不會弄壞舊查詢 |
| Upserts / deletes | MERGE INTO on object storage | GDPR deletion, CDC 同步, SCD 維護 |
| Compaction / clustering | Rewrite small files into large ones | 避免 small-file problem 拖慢查詢 |
| Format | Origin | Distinguishing Trait |
|---|---|---|
| Delta Lake | Databricks | Tightest Spark/Databricks integration, simple JSON transaction log |
| Apache Iceberg | Netflix | Engine-agnostic (Spark, Trino, Flink, Snowflake, BigQuery 都支援), hidden partitioning |
| Apache Hudi | Uber | 專攻 upsert-heavy / CDC ingestion workloads |
Time Travel 和 ML Reproducibility
面試常見追問:「你的 model 三個月前訓練的,現在要 debug,怎麼拿到當時的訓練資料?」標準答案就是 table format 的 time travel(查詢特定 version / timestamp 的 snapshot),搭配把 data version 記錄在 model registry 的 metadata 裡 — 這連回 ML pipelines 那頁的 data versioning 主題。
Object Storage Foundations
Why S3-style object storage underpins nearly every data lake:
| Property | Detail | Consequence |
|---|---|---|
| Durability | Objects replicated across devices and availability zones (S3 advertises 99.999999999% durability) | 幾乎不用擔心資料遺失 |
| Elastic capacity | No provisioning — store bytes, pay per GB-month | 不用預估容量 |
| Cheap tiers | Standard → Infrequent Access → Glacier | 冷資料成本可以再降一個數量級 |
| Decoupled compute/storage | Any engine (Spark, Trino, DuckDB) reads the same files | 換 query engine 不用搬資料;沒人查詢時 storage 幾乎不花錢 |
| Flat namespace | Objects addressed by key; "folders" are just key prefixes | Immutable objects — 修改 = 重寫整個 object,所以適合 append 型 workload |
和傳統 HDFS 或 warehouse 綁定的儲存相比,decoupled compute/storage 是雲端資料架構最核心的轉變:compute 可以隨用隨開隨關,storage 永遠在線且便宜。
Partitioning
Data lake tables are laid out as directory prefixes, e.g. events/dt=2026-07-01/country=TW/part-0001.parquet. Query engines use these prefixes to prune irrelevant files:
- 查詢帶
WHERE dt = '2026-07-01'時,engine 只列出並讀取那個 prefix 下的檔案 — 掃描量可能從 TB 降到 GB。 - 選 partition key 的原則:低 cardinality、幾乎每個查詢都會 filter 的欄位(date 是最常見的)。用 user_id 這種高 cardinality 欄位做 partition 會產生百萬個小檔案(small-file problem),metadata 操作比讀資料還慢。
File Formats: Parquet vs CSV vs Avro
| Dimension | CSV / JSON | Avro | Parquet (columnar) |
|---|---|---|---|
| Layout | Row-oriented text | Row-oriented binary | Column-oriented binary |
| Schema | None(全是字串,型別靠猜) | Embedded schema, strong evolution support | Embedded schema + column statistics |
| Compression | Poor | Good | Excellent(同欄位值相似 → 壓縮率高) |
| Read pattern | Must read whole row/file | Whole rows | Read only needed columns(column pruning) |
| Predicate pushdown | No | No | Yes — skip row groups using min/max stats |
| Splittable for parallel read | Awkward | Yes | Yes(row groups) |
| Best for | Human-readable exchange, tiny data | Streaming / Kafka messages, write-heavy ingestion | Analytics, ML training data — the lake default |
Predicate pushdown: Parquet 在每個 row group 存了各欄位的 min/max 統計,engine 看到 WHERE amount $>$ 1000 時,會直接跳過 max 小於 1000 的 row groups — 連讀都不讀。加上 column pruning(只讀 SELECT 到的欄位),這就是為什麼同一份資料存 Parquet 的查詢成本常常只有 CSV 的十分之一以下。
直覺:分析查詢通常是「幾百個欄位裡只要 5 個,但要掃很多天」— columnar 格式讓你只付你要的那 5 欄的 I/O。反過來,streaming ingestion 是「一次寫一整筆 record」— row-oriented 的 Avro 比較自然,之後再 compact 成 Parquet。
ETL vs ELT & Orchestration
| Dimension | ETL | ELT |
|---|---|---|
| Order | Extract → Transform(外部 engine)→ Load | Extract → Load(raw 直接進 warehouse/lake)→ Transform(用 warehouse SQL) |
| Transform engine | Dedicated servers (Informatica, custom Spark) | The warehouse itself (BigQuery, Snowflake) |
| Raw data kept? | 通常不留 — transform 錯了要重抽 | 留 — 隨時可以重新 transform |
| Who writes transforms | Data engineers (code) | Analytics engineers / DS (SQL, dbt) |
| Era | Storage 貴、compute 集中的年代 | Cloud warehouse compute 便宜且彈性 |
為什麼 ELT 贏了:cloud warehouse 讓「先全部載入、再用 SQL 慢慢整理」變得便宜,而保留 raw data 意味著 transform 邏輯錯了只要重跑 SQL,不用重新抽取。這催生了 dbt pattern — transformation 全部寫成 SQL SELECT models,用 Git 管版本、寫 tests、自動生成 lineage 文件,把 software engineering 的紀律帶進 SQL 世界。
Orchestration with Airflow-style DAGs
An orchestrator schedules tasks as a DAG (directed acyclic graph) with dependencies, retries, and alerting. Key concepts interviewers probe:
| Concept | Definition | Why It Matters |
|---|---|---|
| Idempotency | 同一個 task 對同一個時間區間跑 N 次,結果都一樣 | Retry 和 backfill 的前提 — 用 overwrite partition 而不是 append |
| Backfill | 對歷史日期重跑 pipeline | 邏輯改了或上游修資料後,重建過去的結果 |
| Retries | 失敗自動重試(含 exponential backoff) | 大部分失敗是 transient(網路、資源),自動恢復 |
| Sensors | 等待上游條件成立才開跑(例如某個 partition 出現) | 避免在上游還沒到齊時就計算出錯的結果 |
| Catchup / schedule | 每個 run 綁定一個 logical date,處理該區間的資料 | 資料和時間區間對齊,才可能 idempotent |
Non-idempotent Pipeline 是經典地雷
如果 task 是「把今天的資料 append 到結果表」,retry 一次就會重複資料、backfill 更是災難。正確設計:每個 run 負責一個明確的時間 partition,執行時整個 partition 覆寫(delete-then-insert 或 overwrite)。面試被問 pipeline design 時主動講出 idempotency,是很強的訊號。
Data Quality Checks in Pipelines
和 ML Pipelines 那頁的 data validation 直接相連 — 在 orchestration 層把品質檢查做成 DAG 中的 blocking tasks:
| Check | Example | On Failure |
|---|---|---|
| Freshness | 最新 partition 的 event time 距今小於 2 小時 | Block downstream, page on-call |
| Volume | 今日 row count 在 7 日移動平均的正負 30% 內 | Block + alert |
| Schema / contract | 欄位型別與 not-null 約束符合預期 | Block(上游 breaking change) |
| Uniqueness | Primary key 無重複 | Block(replay bug) |
原則同樣是 fail loud:寧可 dashboard 晚一天,也不要 model 默默用壞資料訓練。
Distributed Processing with Spark
Driver / Executor Model
A Spark application has one driver(解析你的程式、建立執行計畫、調度工作)and many executors(實際持有 data partitions 並執行運算的 worker processes)。資料被切成 partitions 分散在 executors 上平行處理。
Lazy Evaluation and the DAG
Transformations (filter, select, groupBy) do not execute immediately — Spark 只是把它們記錄成一個 logical plan。直到遇上 action(count, write, collect)才會觸發:optimizer 把整條 transformation 鏈整理成 execution DAG(合併運算、把 filter 往資料源推),一次執行。這讓 Spark 能做全局最佳化,但也是新手困惑的來源 — 程式跑到 write 那行才爆錯,錯誤其實在前面的 transformation。
Narrow vs Wide Transformations
| Type | Data Movement | Examples | Cost |
|---|---|---|---|
| Narrow | 每個 output partition 只依賴一個 input partition — no network | filter, select, withColumn, map | Cheap, pipelined in memory |
| Wide | Output partition 需要多個 input partitions 的資料 — 觸發 shuffle | groupBy, join, distinct, orderBy, repartition | Expensive |
Why shuffle is expensive: 所有 executors 把資料按 key 重新分組,寫到 local disk,再透過網路互相交換(all-to-all)。Disk I/O + network + serialization 三重成本,而且是 stage 之間的同步屏障 — 最慢的 partition 拖住所有人。
Partitioning and Skew Tips
| Problem | Symptom | Fix |
|---|---|---|
| Data skew | 一個 task 跑 1 小時,其他 199 個 3 分鐘就完成 | Salting(把 hot key 加隨機後綴拆開)、AQE skew join handling |
| Join 一大一小表 | 大 shuffle | Broadcast join — 把小表複製到每個 executor,完全避開 shuffle |
| Too many small partitions | Task scheduling overhead 超過運算 | coalesce 減少 partition 數 |
| Too few large partitions | OOM、平行度不足 | repartition 增加,或調 shuffle partitions 設定 |
| 重複使用中間結果 | 同一段 lineage 重算多次 | cache() / persist() 明確物化 |
When Is pandas Enough?
| Situation | Use | Reasoning |
|---|---|---|
| Data fits in memory(單機 RAM 的一半以內,約小於 10-50 GB) | pandas / Polars / DuckDB | 沒有 cluster 開銷、迭代快、除錯容易 |
| 中型資料、單機放不下但查詢簡單 | DuckDB on Parquet / warehouse SQL | Columnar engine 單機就很能打 |
| TB 級 join / aggregation、需要和 lake 深度整合 | Spark | 真正需要 distributed compute |
| Feature pipeline 要 train/serve 共用、排程化 | Spark or warehouse SQL | 和平台整合、可監控、可 backfill |
不要為了履歷用 Spark
面試講到工具選擇時,「我們資料 8GB,我用 pandas 半小時搞定,不需要 Spark」比「什麼都上 Spark」更能展現 seniority。Spark 的啟動成本、除錯難度和 cluster 費用都是真實代價 — 先問資料量和成長速度,再選工具。
Batch vs Streaming in the Data Platform
| Dimension | Batch | Streaming |
|---|---|---|
| Trigger | Schedule(hourly / daily) | Event arrival, continuous |
| Latency | Minutes to hours | Sub-second to minutes |
| Throughput per unit cost | High(大塊處理攤薄開銷) | Lower(常駐資源) |
| Complexity | Low — 容易 backfill、容易測試 | High — event time vs processing time、late data、exactly-once |
| Typical tools | Spark batch, dbt, warehouse SQL | Kafka / Kinesis + Flink / Spark Structured Streaming |
| Use cases | 報表、訓練資料、每日 metrics | Fraud 偵測、即時監控、real-time features |
Streaming Ingestion into the Lake
典型 event pipeline:app SDK 發 events → Kafka / Kinesis(durable, ordered, replayable log — producer 和 consumer 解耦)→ 一個 sink job(Kafka Connect / Firehose / Spark Streaming)持續把 events 落到 lake 的 bronze 層 → 下游 batch jobs 接手清洗聚合。訊息系統的 replay 能力很關鍵:consumer 有 bug 時可以回捲 offset 重新消費。
Micro-batch vs True Streaming
| Approach | How | Latency | Example |
|---|---|---|---|
| Micro-batch | 每隔一小段時間(秒級)收集一批一起處理 | Seconds | Spark Structured Streaming |
| True streaming | 一筆 event 一筆處理 | Milliseconds | Flink |
大部分「real-time」需求(dashboard 每分鐘更新、feature 5 分鐘內 fresh)micro-batch 就綽綽有餘;毫秒級的 true streaming 只有 fraud、bidding 這類場景才需要。
Freshness vs cost tradeoff:latency 每降一個數量級,架構複雜度和成本大約升一個數量級。面試的加分回答是反問:「這個 metric 晚一小時會造成什麼商業損失?」— 大多數分析用途 daily batch 就夠,streaming 是為明確的 use case 服務,不是預設。
Cloud Big Data Services Map
The same logical layer maps to equivalent managed services on each cloud:
| Layer | AWS | GCP | Azure |
|---|---|---|---|
| Object storage (lake) | S3 | Cloud Storage (GCS) | Blob Storage / ADLS Gen2 |
| Data warehouse | Redshift | BigQuery | Synapse Analytics |
| Managed Spark / processing | EMR | Dataproc | Databricks on Azure / HDInsight |
| Serverless batch/stream processing | Glue | Dataflow | Data Factory data flows |
| Streaming ingestion | Kinesis / MSK (Kafka) | Pub/Sub | Event Hubs |
| Orchestration | MWAA (managed Airflow) / Step Functions | Cloud Composer (managed Airflow) | Data Factory |
| NoSQL key-value | DynamoDB | Bigtable / Firestore | Cosmos DB |
| Relational OLTP | RDS / Aurora | Cloud SQL / Spanner | Azure SQL Database |
| BI | QuickSight | Looker | Power BI |
記法:分層記、不要逐一背 — 面試被問「你們是 GCP,我們是 AWS,你 OK 嗎?」時,正確的回答是概念一一對應:BigQuery 對 Redshift、Pub/Sub 對 Kinesis、Composer 對 MWAA,換雲只是換服務名,架構思維完全共通。
Which Database Should I Use? (Workload-driven)
| Workload | Right Tool (AWS example) | Why |
|---|---|---|
| Transactional app backend(orders, users) | RDS / Aurora (PostgreSQL) | ACID, joins, row-oriented |
| Key-value at massive scale, single-digit ms(session, cart) | DynamoDB | Predictable latency, serverless scaling |
| Analytics over TBs(BI, ad-hoc SQL) | Redshift(or BigQuery on GCP) | Columnar, MPP scan performance |
| Cache / leaderboard | ElastiCache (Redis) | In-memory, microsecond reads |
| Full-text / log search | OpenSearch | Inverted index |
| Raw event archive + ML training data | S3 + Parquet (+ Iceberg) | Cheapest, open format, any engine |
| Time-series metrics | Timestream(or InfluxDB) | Time-partitioned compression and rollups |
決策順序
先問 workload 的三件事:(1)access pattern — point lookup 還是大範圍掃描?(2)latency 需求 — ms 還是分鐘?(3)資料量和成長。答案幾乎自動決定資料庫類型;先選類型、再選雲上對應的服務。反著來(先迷戀某個服務)是面試的扣分回答。
Data Modeling for Analytics
Star Schema
Warehouse 的經典模型:中央的 fact table(事件/交易,數字 measures + 外鍵)連到多個 dimension tables(描述性屬性)。
| Table Type | Content | Example | Size |
|---|---|---|---|
| Fact | Events / transactions: measures + dimension keys | fact_orders(order_id, user_key, product_key, date_key, amount) | 巨大、持續成長 |
| Dimension | Descriptive attributes | dim_user(user_key, country, signup_date, plan) | 小、變化慢 |
好處:BI 查詢變成固定模式 — fact join 幾個 dims、GROUP BY 維度、聚合 measures;維度屬性只存一份,改一次全部報表都對。
Grain
Grain = fact table 一列代表什麼(one row per order? per order line? per user per day?)。建模的第一個問題永遠是宣告 grain — grain 不清楚,所有的 join 和聚合都可能默默重複計算。這也是 SQL 面試 fan-out bug 的根源:fact join 到比它 grain 細的表,金額被複製多次,SUM 直接爆掉。
Slowly Changing Dimensions (SCD)
Dimension 屬性會變(user 搬家、改方案),怎麼處理歷史?
| Type | Strategy | History | Example Use |
|---|---|---|---|
| SCD Type 1 | Overwrite the old value | 不保留 | 修正錯字、不影響分析的屬性 |
| SCD Type 2 | Insert a new row with valid_from / valid_to (and is_current flag) | 完整保留 | 分析必須「以當時狀態計算」時 — 例如用戶當時的 plan |
SCD Type 2 是面試熱點:查詢時 fact 要 join 在 event_date BETWEEN valid_from AND valid_to 的那一列 dimension,才能還原「事件發生當下」的屬性。用 current 值回算歷史 = 常見的分析錯誤(例如把用戶現在的 premium 身分套用到他還是 free user 時的行為)。
Wide Tables vs Normalized
| Approach | Pros | Cons | When |
|---|---|---|---|
| Normalized (star) | Single source of truth, storage-efficient, flexible | 查詢要 join,對非技術用戶不友善 | Core warehouse layer |
| One Big Table (denormalized wide) | 查詢簡單、columnar warehouse 掃描快、適合餵 BI 和 ML | 資料重複、dimension 更新要重建、容易 metric 定義發散 | Gold layer / 特定主題的 mart |
現代常見折衷:Silver 層維持 normalized star schema 作為 single source of truth,Gold 層用 dbt 物化成 wide tables 給 dashboard 和 ML 直接用 — 兩者不衝突,是不同層的產物。
Real-World Use Cases
Case 1: 建立實驗數據平台(A/B Testing)
你加入一家還在用 Google Sheets 算實驗結果的公司,要建立可信的實驗平台。設計:app 端 SDK 發 exposure 和 conversion events → Kinesis / Kafka → 落地到 S3 bronze(原始 events,append-only, Parquet, partitioned by date)→ 每小時 batch job 清洗成 silver(dedup、joining assignment logs、修正 late events)→ dbt 在 warehouse 建 gold 層 metrics tables(每個 experiment × variant × metric 一列,含 sample size 和 variance)→ 實驗 dashboard 和統計檢定直接讀 gold 表。
關鍵設計決策:exposure logging 要在 randomization 當下記錄(避免 selection bias);metrics 定義集中在 dbt models(避免每個 DS 自己算一套 CTR);bronze 永遠保留 raw events,實驗定義改了可以 backfill 重算。
Interview follow-ups:
- 為什麼 events 要先落 lake 再進 warehouse,而不是直接寫進 warehouse?(成本、replay 能力、非結構化欄位的彈性、warehouse 不適合高頻小筆寫入)
- Late-arriving events(手機離線補傳)怎麼處理?(watermark + 每天重跑最近 N 天的 partitions)
- 怎麼確保兩個 DS 算同一個實驗得到相同數字?(single source of truth 的 metrics 層 — 這就是 gold 層存在的理由)
Case 2: Feature Pipeline 從 pandas 遷移到 Spark 的時機
你的 churn model feature pipeline 是一個每天跑的 pandas script,讀 warehouse 匯出的 CSV。一年後資料從 2GB 長到 60GB,script 開始 OOM、跑 6 小時。你要決定:加大機器記憶體?改寫成 Spark?還是把邏輯下推到 warehouse SQL / dbt?
分析框架:(1)60GB 其實還在「大機器 + Polars/DuckDB」能處理的範圍,最便宜的修法可能是換格式(CSV → Parquet)+ 換 engine,成本一天就能完成;(2)如果成長趨勢明確會到 TB 級、或需要和 lake 上的 event 資料 join,才值得付出 Spark 的複雜度;(3)如果邏輯全是 SQL 可表達的聚合,下推到 warehouse 用 dbt 管理可能是最好解 — 免維運、有 lineage、和其他表共用品質檢查。遷移到 Spark 時的重點:用 broadcast join 處理小 dimension、確認 shuffle partitions 設定、每天 overwrite 對應的 feature partition 保持 idempotent。
Interview follow-ups:
- 你怎麼驗證遷移後 features 和舊 pipeline 一致?(同一天資料雙跑、逐欄位比對統計量 — 呼應 train-serving skew)
- 什麼訊號告訴你「該上 Spark 了」?(資料超過單機記憶體數倍、需要跨 TB join、單機跑不進 SLA)
- 為什麼 CSV 換 Parquet 就能救回一大段效能?(columnar + 壓縮 + 只讀需要的欄位)
Case 3: 選 BigQuery 還是 Self-host?
新創公司(8 人 data 團隊)要選分析平台:BigQuery(serverless, pay-per-query)還是自建(ClickHouse on Kubernetes)。決策維度:
| Dimension | BigQuery (managed) | Self-hosted ClickHouse |
|---|---|---|
| Ops burden | 幾乎為零 | 需要專人顧 cluster、升級、備份 |
| Cost shape | 隨用量線性成長,query-heavy 時可能失控 | 固定機器成本,大量查詢時單位成本低 |
| Elasticity | 自動 scale,抗尖峰 | 容量要預先規劃 |
| Performance control | 有限(黑盒 optimizer) | 完全掌控(可調 engine、index) |
| Team size fit | 小團隊最佳解 | 需要 infra 能力的團隊 |
給小團隊的合理建議:先用 managed(BigQuery),把工程時間花在產生商業價值的分析上;等 query 帳單成長到「一個工程師年薪」的量級,才值得評估 self-host — 而且因為資料存在開放格式的 lake(Iceberg/Parquet),遷移時搬的是 compute 不是資料,lock-in 有限。
Interview follow-ups:
- Pay-per-query 模式下,怎麼防止一個失控查詢燒掉預算?(partition filter 強制、查詢 byte 上限、按 team 設 quota)
- 什麼情況 self-host 明顯划算?(查詢量極大且穩定、latency 需求特殊、合規要求資料不出機房)
- 怎麼降低對單一雲的 lock-in?(open table formats、SQL 標準化、把 transform 邏輯放 dbt 而不是 proprietary 工具)
Hands-on: Data Engineering in Python
PySpark: Read, Aggregate, Write
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("daily_user_features").getOrCreate()
# Read only one day via partition pruning (dt is a partition column)
events = (
spark.read.parquet("s3://lake/silver/events/")
.filter(F.col("dt") == "2026-07-01") # partition pruning: reads one prefix
.select("user_id", "event_type", "amount") # column pruning: Parquet reads 3 columns
)
# Narrow transformation (no shuffle)
purchases = events.filter(F.col("event_type") == "purchase")
# Wide transformation: groupBy triggers a shuffle by user_id
user_features = purchases.groupBy("user_id").agg(
F.count("*").alias("purchase_cnt_1d"),
F.sum("amount").alias("purchase_amt_1d"),
F.avg("amount").alias("avg_order_value_1d"),
)
# Broadcast join: small dimension table copied to every executor (no shuffle)
users = spark.read.parquet("s3://lake/silver/dim_user/")
enriched = user_features.join(F.broadcast(users), "user_id", "left")
# Control output file count, then overwrite ONE partition (idempotent)
(
enriched.repartition(8)
.write.mode("overwrite")
.parquet("s3://lake/gold/user_features/dt=2026-07-01/")
)
Airflow DAG Sketch
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.sensors.external_task import ExternalTaskSensor
from datetime import datetime, timedelta
default_args = {
"retries": 2, # transient failures auto-recover
"retry_delay": timedelta(minutes=10),
}
with DAG(
dag_id="daily_user_features",
start_date=datetime(2026, 1, 1),
schedule="0 3 * * *", # daily at 03:00, one run per logical date
catchup=True, # enables backfill of past dates
default_args=default_args,
) as dag:
# Sensor: wait until upstream events table for this date is ready
wait_for_events = ExternalTaskSensor(
task_id="wait_for_events",
external_dag_id="ingest_events",
timeout=3600,
)
# Each task processes exactly one date partition -> idempotent reruns
build_features = PythonOperator(
task_id="build_features",
python_callable=run_spark_job, # submits the PySpark job above
op_kwargs={"ds": "{{ ds }}"}, # logical date injected by Airflow
)
validate = PythonOperator(
task_id="validate_output",
python_callable=check_row_count_and_nulls, # fail loud -> blocks downstream
op_kwargs={"ds": "{{ ds }}"},
)
wait_for_events >> build_features >> validate
dbt-style SQL Model
-- models/gold/fct_daily_user_metrics.sql
-- dbt manages dependencies via ref(); materialized as an incremental table
{{ config(materialized="incremental", unique_key="user_date_key") }}
with purchases as (
select
user_id,
date(event_ts) as event_date,
amount
from {{ ref("stg_events") }} -- silver-layer staging model
where event_type = 'purchase'
{% if is_incremental() %}
and date(event_ts) >= dateadd(day, -3, current_date) -- reprocess late data
{% endif %}
)
select
user_id || '_' || event_date as user_date_key, -- declared grain: user x day
user_id,
event_date,
count(*) as purchase_cnt,
sum(amount) as purchase_amt
from purchases
group by user_id, event_date
Interview Signals
What interviewers listen for:
- 你能畫出資料從 OLTP source 到 dashboard/model 的完整路徑,並說出每一層的職責
- 你會先問資料量、latency 需求和成長速度,再選工具 — 而不是所有東西都上 Spark 或 streaming
- 你主動提到 idempotency、backfill、partition overwrite 這些 pipeline 可靠性的關鍵字
- 你能解釋 Parquet + partitioning 為什麼省錢(column pruning、predicate pushdown、partition pruning)
- 你把雲服務當成分層概念的實作(BigQuery ≈ Redshift ≈ Synapse),換雲不換架構思維
Practice
Flashcards
Flashcards (1/10)
Data lake、data warehouse、lakehouse 的核心差異?
Warehouse: schema-on-write、結構化、查詢快、治理強、儲存較貴。Lake: 開放格式檔案放 object storage、schema-on-read、超便宜、什麼型態都收,但預設治理弱。Lakehouse: 在 lake 上加 table format(Delta/Iceberg/Hudi)提供 ACID、schema enforcement、time travel — lake 的成本 + warehouse 的保證。
Quiz
為什麼不直接在 production OLTP database 上跑分析查詢?