Working with Large-Scale Datasets Questions
Analyzing data that does not fit comfortably in memory or a single query. Covers distributed processing concepts, partitioning and sampling strategies, query and pipeline performance, and trade-offs when scaling an analysis. Focuses on getting correct answers efficiently at scale.
A 1B-row fact table must be joined with a 10k-row dimension to enrich features in Spark. Describe concrete strategies to optimize the join: broadcast join, repartitioning and bucketing, map-side joins, caching, and use of adaptive query execution. Explain how to choose among them given memory and cluster constraints.
Sample Answer
Start by clarifying sizes: 1B-row fact table and a 10k-row dimension. The key question is the dimension’s serialized size (MBs vs GBs) and available executor memory. With a typical 10k-row dim (few MBs–tens of MBs), broadcasting is usually best; if rows are wide (many columns, embeddings), it may be larger and need a different approach.
Concrete strategies and trade-offs
-
Broadcast join
- How: let Spark send the entire dimension to every executor and do a local hash join.
- When to choose: dim size < spark.sql.autoBroadcastJoinThreshold (default ~10MB) or adjustable; network cost minimal; fastest (no shuffle of big fact table).
- Trade-offs: needs memory on each executor for the broadcasted table; if too large it OOMs.
-
Repartitioning and bucketing (co-partitioning)
- How: repartition both datasets by join key (df.repartition(numPartitions, col)) or write both as bucketed tables with same bucket count and join keys so Spark can avoid full shuffle for repeated joins.
- When to choose: dim too large to broadcast but join is frequent or pipelines reuse pre-bucketed data; cluster has spare shuffle bandwidth and storage.
- Trade-offs: initial shuffle cost to repartition/bucket; good for repeated joins and for reducing future shuffles.
-
Map-side joins (pre-join before shuffle / local lookup)
- How: load the dimension into a distributed key-value store in memory (e.g., map of partitions keyed by hash) or use broadcast-like mechanisms; similar to broadcast but implemented manually when Spark-level broadcast not possible.
- When to choose: custom memory-managed lookups, or when you want finer control over serialization/deserialization.
- Trade-offs: complexity and risk of OOM if not carefully sized.
-
Caching
- How: cache/broadcast the dimension (sparkContext.broadcast) or cache the fact table partitions if reused.
- When to choose: when the same datasets are used across multiple joins/transformations (e.g., feature engineering pipelines).
- Trade-offs: consumes memory; evictions hurt performance.
-
Adaptive Query Execution (AQE)
- How: enable spark.sql.adaptive.enabled = true. AQE can switch join strategy at runtime (e.g., convert shuffle join to broadcast if actual size is small), coalesce shuffle partitions, and optimize skewed joins.
- When to choose: cluster with AQE-enabled Spark (2.4+ improvements); helpful when size estimates are inaccurate or data skew exists.
- Trade-offs: slight planning overhead; rely on runtime info.
How to choose given memory and cluster constraints (practical decision flow)
- Estimate dimension serialized size. If << broadcast threshold and executors have memory headroom → broadcast. Set spark.sql.autoBroadcastJoinThreshold or use df.broadcast().
- If too big for broadcast but cluster has spare network/shuffle capacity and join will be repeated → bucket both tables by join key (same number of buckets) or repartition the fact table by join key once and reuse.
- If cluster memory is tight and broadcasting would cause OOMs → use shuffle join (sort-merge) with AQE enabled to coalesce partitions and mitigate skew. Increase number of partitions to avoid large tasks.
- If join key is skewed → use AQE’s skew handling or salting keys manually to balance partitions.
- If you reuse the dim across many jobs → persist it (MEMORY_ONLY or MEMORY_AND_DISK) or keep it as a broadcast variable across tasks.
Tuning tips
- Monitor executor memory, broadcast size, shuffle read/write. Use spark UI.
- Adjust spark.sql.autoBroadcastJoinThreshold conservatively.
- For bucketing, ensure identical bucket counts and join keys; use spark.sql.bucketing.enabled and save as ORC/Parquet with bucketing in metastore.
- Use smaller shuffle partitions for tiny tasks; AQE can auto-coalesce.
Example: typical ML feature enrichment
- If dim (10k rows) serializes to < 10MB → broadcast it and perform map-side enrichments; cache if reused across training folds.
- If dim is 200MB and cluster has limited executor memory → repartition fact by join key, use sort-merge join, enable AQE, and increase shuffle partitions to avoid OOMs.
This approach balances speed (broadcast), stability (repartition/bucket), and adaptiveness (AQE) based on concrete size + memory constraints.
You're asked to implement automated quality checks for incoming datasets used for model training. List essential validations (schema and type checks, null rates, range checks, uniqueness, foreign-key consistency, cardinality checks) and outline how to implement them using Great Expectations or custom scripts. Describe alerting and remediation workflows for failures.
Sample Answer
Essential validations
- Schema & type checks: exact columns, dtypes, required/optional columns.
- Null / missing-rate checks: per-column null percentage thresholds.
- Range checks: numeric min/max, percentiles to catch outliers.
- Uniqueness: primary-key uniqueness and dedup detection.
- Foreign-key consistency: referenced keys exist in dimension/master tables.
- Cardinality checks: expected distinct counts for ids, enums; growth/shift detection.
- Distributional checks: histogram or KS tests vs. reference dataset (population drift).
- Row counts & freshness: expected min/max rows and ingestion timestamp checks.
Implementation with Great Expectations (GE)
- Create a suite and datasource pointing at your storage (S3, GCS, SQL).
- Define Expectations for each validation. Example:
from great_expectations.dataset import PandasDataset
# example expectation config using GE CLI is typical; Python example for clarity
batch = context.get_batch(batch_kwargs)
batch.expect_table_columns_to_match_set(["id","ts","feature1","feature2"])
batch.expect_column_values_to_be_in_type_list("id", ["IntegerType","int64"])
batch.expect_column_values_to_not_be_null("id")
batch.expect_column_values_to_be_between("feature1", min_value=0, max_value=100)
batch.expect_column_unique_value_count_to_equal("id", batch.row_count)
batch.expect_column_values_to_be_in_set("country", ["US","CA","GB"])
- Use built-in validators for null rates, uniqueness, table row counts, and custom expectations for FK checks (e.g., join to lookup table and expect zero mismatches).
- Persist baselines/validation results as Data Docs; configure GE to run via Airflow / Prefect / step functions on each ingest.
Custom scripts (when GE not used)
- Small Python scripts using pandas / PySpark:
- Schema: compare expected schema dict to actual.
- Null rates: df.isna().mean() and assert <= threshold.
- Range: df[column].between(min,max).all()
- Uniqueness: df[id].nunique() == len(df)
- FK: left_outer_join to lookup; assert nulls in lookup_id == 0
- Package these in a CI job or orchestration task with clear exit codes and JSON output.
Alerting & remediation workflow
- On failure: validation job emits structured JSON with failing checks, sample failing rows, and severity label (blocker/warning).
- Alerting:
- Blockers -> immediate page/email/Slack to data owners + ML on-call; create ticket (Jira) automatically with payload.
- Warnings -> daily digest to owner and data steward.
- Integrate with PagerDuty for production-blocking failures.
- Remediation steps:
- Auto-retry ingestion if transient (e.g., partial file). Limit retries and backoff.
- For data issues: quarantine dataset to separate storage path, mark pipeline as paused, attach failing sample to ticket.
- Provide developer tools: a "validate-and-preview" endpoint/CLI that runs checks locally and returns actionable messages.
- Post-remediation: require re-run of validations; block model training until all critical checks pass; maintain audit log of fixes and validation history.
- Monitoring & governance:
- Track validation KPIs (failure rates, time-to-fix), store expectations history to detect drifting expectations and update baselines periodically.
- Enforce SLA: auto-fail training if any critical expectation failed within last N hours.
Why this works
- Clear, automated checks prevent garbage-in -> garbage-out for models.
- GE provides reusable, documented expectations and data docs; custom scripts give lightweight control.
- Structured alerts + quarantine + replayability make remediation fast and auditable.
Design an algorithm and outline a Python implementation to compute top pairwise feature co-occurrences (high-frequency pairs of categorical values) from a dataset with millions of rows and millions of unique categories using limited memory. Discuss partitioning by hash to disk, streaming aggregation, approximate algorithms, and how you'd validate correctness and handle skew.
Sample Answer
Approach (overview)
- Partition pairs by hashing to disk so each partition fits memory; stream rows once to generate pairs, write pair occurrences to partition files. Then process partitions one-by-one to aggregate exact counts (or apply an approximate heavy-hitter algorithm in-memory). Use a two-phase pipeline: map (hash+spill) → reduce (aggregate per-partition) → global top-K merge. To reduce work, pre-filter low-frequency singletons via approximate counts.
Python outline (core pieces)
import os, hashlib, heapq, pickle
from collections import Counter, defaultdict
def shard_key(k, num_shards):
return int(hashlib.md5(k.encode()).hexdigest(),16) % num_shards
def map_phase(rows_iter, num_shards, shard_dir):
os.makedirs(shard_dir, exist_ok=True)
writers = [open(os.path.join(shard_dir,f"shard_{i}.bin"),"ab") for i in range(num_shards)]
for row in rows_iter:
cats = row # list of categorical values in this row
for i in range(len(cats)):
for j in range(i+1,len(cats)):
pair = f"{cats[i]}\t{cats[j]}"
shard = shard_key(pair, num_shards)
writers[shard].write((pair+"\n").encode())
for w in writers: w.close()
def reduce_phase(num_shards, shard_dir, topk):
global_heap = []
for i in range(num_shards):
cnt = Counter()
with open(os.path.join(shard_dir,f"shard_{i}.bin"),"rb") as f:
for line in f:
pair = line.decode().strip()
cnt[pair]+=1
# keep local topk
for pair, c in cnt.items():
if len(global_heap)<topk:
heapq.heappush(global_heap,(c,pair))
else:
if c>global_heap[0][0]:
heapq.heapreplace(global_heap,(c,pair))
return sorted(global_heap, reverse=True)
Approximate alternatives
- Use Count-Min Sketch per shard to reduce memory (gives overestimates; combine conservatively).
- Use Space-Saving / Frequent algorithm for heavy hitters to track top pairs without storing all counters.
- Use HyperLogLog for cardinalities if you need distinct counts per category before pairing.
Handling skew
- Detect large shards: sample row distribution or keep approximate per-key counts (CMS) for hot keys.
- For very hot categories, special-case: process their co-occurrences separately (broadcast join style) and shard by the other item.
- Use adaptive partitioning: split heavy-key ranges into multiple sub-shards.
Validation & correctness
- Validate on a holdout dataset small enough to compute exact counts; compare top-K overlap, rank correlation, recall@K.
- Use unit tests for map/reduce invariants and idempotency.
- For approximations: measure error bounds empirically vs exact; tune sketch size to guarantee acceptable epsilon/delta.
Complexity & trade-offs
- Single-pass write cost O(N * avg_pairs_per_row) I/O. Reduce memory by increasing num_shards.
- Exact counting costs disk + CPU but is accurate. Sketches trade accuracy for memory/speed.
- Choose num_shards so each shard's in-memory counter fits (estimate unique pairs per shard = total_pairs/num_shards).
Notes
- Use compressed writes, batching, and concurrency for throughput.
- Ensure canonical ordering within pairs (sorted) to avoid duplicates (A,B) vs (B,A).
On a Spark job, a hot join key causes a single task to process an outsized partition and fail with OOM. Describe how you'd debug this (what metrics to inspect) and propose concrete remedies: salting, pre-aggregation, broadcasting, custom partitioners, or sampling. For each remedy explain pros/cons.
Sample Answer
Situation: A Spark job fails with OOM because one reduce task processes an outsized partition due to a hot join key. I would debug and remediate as follows.
Debug / Metrics to inspect:
- Stage/task metrics in Spark UI: task duration, input size, shuffle read/write size, peak memory, GC time, and executor logs for OOM stack traces.
- Shuffle block distribution: sizes per partition (Spark UI Storage/Stages -> Shuffle Read Distribution) to confirm skew.
- Job DAG and join type (sort-merge vs shuffle-hash), and whether data is already partitioned/clustered by key.
- Sample key cardinality and frequency (count distinct and top-k keys) using a small Spark job or DataFrame.sample() + groupBy.
- Data skew visualization: histogram of key frequencies and partition sizes.
Concrete remedies (when to use, how to implement, pros/cons):
- Salting (key padding)
- How: Add random salt int (0..N-1) to hot key for the large side before join; duplicate the other side’s hot-key rows across salts or similarly salt both sides and join on (key,salt); afterwards remove salt and aggregate.
- Pros: Simple, effective at spreading load across partitions without complex shuffles.
- Cons: Increases data size by factor ~N; requires re-aggregation and careful handling to avoid correctness bugs; choosing N needs tuning; not suitable if downstream operations expect exact per-key ordering.
- Pre-aggregation (map-side combine)
- How: Aggregate the large side by join key (sum/count) before the join to reduce size; use reduceByKey or DataFrame.groupBy().agg().
- Pros: Lowers shuffle volume; preserves correctness; cheap if aggregation is associative/commutative.
- Cons: Not applicable if you need full row-level joins (e.g., non-aggregable features); may still leave hotspot if hot key has many rows after aggregation.
- Broadcasting small side
- How: If one side is small, use broadcast(joined_df) or spark.conf spark.sql.autoBroadcastJoinThreshold. Use broadcast join to avoid shuffle of small side.
- Pros: Eliminates shuffle for small table and avoids skew-induced large reducer; fast.
- Cons: Requires small side fits in executor memory; not possible when both sides are large; broadcasting a table with a hot key doesn’t help if the large side’s partitioning is skewed in subsequent stages.
- Custom partitioner / range partitioning
- How: Use repartitionByRange or map-side custom Partitioner to control partition distribution; create partitioner that maps hot key to multiple partitions (logical salt) or distribute keys by hash of (key, ordinal).
- Pros: Deterministic, integrates with shuffle, avoids random salt incompatibilities.
- Cons: Implementation complexity; may require re-partitioning upstream and careful job planning; if one key still has too much data, you need to split it.
- Sampling + targeted handling
- How: Sample keys, detect top heavy keys, then handle heavy keys separately: process non-hot keys via normal join, process hot key with a specialized pipeline (e.g., broadcast small side, or chunked join).
- Pros: Minimizes changes to general pipeline; focused resource allocation for hot keys.
- Cons: Extra orchestration; risk of logic divergence; needs repeat monitoring as data distribution changes.
Additional operational tactics:
- Increase shuffle partitions (spark.sql.shuffle.partitions) to reduce per-partition size, but beware of overhead.
- Tune executor memory and off-heap/serialization (Kryo).
- Use persisted/cached pre-partitioned datasets to avoid repeated shuffles.
- Add monitoring and alerts for skew metrics; automate top-k detection and dynamic salting.
Recommendation: Start by confirming skew (metrics + sampling). If small side is small, broadcast. If aggregation is possible, pre-aggregate. Otherwise prefer salting or custom partitioner combined with targeted sampling for hot keys; choose N conservatively and re-aggregate after join. Always test on a representative subset and add automated checks for evolving skew.
Explain why feature scaling matters for machine learning. Compare standardization (z-score), min-max normalization, and robust scaling (using median and IQR). For which algorithms is scaling critical and how would you integrate scaling into a large-scale training pipeline?
Sample Answer
Feature scaling matters because many ML algorithms assume features are on comparable scales; unscaled features can bias distance metrics, gradient updates, regularization, and convergence speed.
Definitions & differences:
- Standardization (z-score): x' = (x - mean) / std. Centers to zero mean and unit variance. Good when data is approximately Gaussian and you care about variance-based weighting.
- Min–max normalization: x' = (x - min) / (max - min). Maps features to [0,1] (or any range). Preserves relative ordering and bounds; sensitive to outliers.
- Robust scaling (median & IQR): x' = (x - median) / IQR. Centers using median and scales by interquartile range; resilient to outliers and skewed distributions.
When scaling is critical:
- Distance-based algorithms: k-NN, K-means, DBSCAN — distances directly affected.
- Gradient-based models: logistic regression, neural networks, SVM (with RBF), and models with L1/L2 regularization — scaling stabilizes optimization and ensures regularization applies uniformly.
- Tree-based models (random forests, decision trees, gradient-boosted trees) are largely invariant to monotonic scaling and usually don’t require scaling.
Integrating into a large-scale pipeline:
- Fit scaler on training data (or training shard) only; persist parameters (mean/std/min/max/median/IQR) with model artifact.
- Implement scaling as a reusable, versioned preprocessing component (e.g., TF Transform, scikit-learn Pipeline, Spark ML Transformer) so same logic runs in training and serving.
- For distributed training, compute global statistics via aggregation (map-reduce: compute counts, sums, sums of squares or approximate quantiles) to avoid bias from shards.
- Validate: include unit tests for transforms, monitor feature distributions in production, and handle novel/unseen values (clipping, fallback).
Choice guideline: use standardization for most continuous features, min–max when bounded ranges are desired (e.g., image pixels), and robust scaling when outliers/skew are present.
Unlock Full Question Bank
Get access to all Working with Large-Scale Datasets interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.