Implement Column Pruning and Filter Pushdown on a Query Plan Tree

Quick Overview

Given a SQL query represented as a tree of scan, filter, project and join nodes, write runnable rewrites that prune unused columns from every scan and push filter conditions down toward the tables. Tests query-optimizer fundamentals, recursive tree rewriting and preserving query results.

Implement Column Pruning and Filter Pushdown on a Query Plan Tree

Company: Databricks

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Onsite

A query engine represents a SQL query as a tree of plan nodes. You are handed such a tree and asked to "optimize" it. The interviewer does not say which optimizations are expected, and the code must actually run on a sample plan, not just be described. The rewrites were not named up front in the interview; the two that were eventually required are the Parts below. The exact node classes were not reported. Assume this minimal logical plan model (Python dataclasses are fine): - `Scan(table, columns)`: reads the listed columns of a base table. - `Filter(conjuncts, child)`: keeps the child's rows for which every conjunct is true. A conjunct is a comparison `column op literal`. - `Project(columns, child)`: outputs only the listed columns of the child (plain column references, no expressions). - `Join(left, right, left_key, right_key)`: an inner equi-join on `left.left_key = right.right_key` that outputs all columns of both sides. Column names are globally unique across tables, so a column name identifies its table. A typical input is: ```text Project([order_id, name], Filter([amount > 100, country = 'US'], Join(Scan(orders, [order_id, customer_id, amount, status, created_at]), Scan(customers, [cust_id, name, country, signup_date]), left_key=customer_id, right_key=cust_id))) ``` ### Clarifying Questions - Can conjuncts reference more than one column (for example, comparing two columns), and can projections compute expressions? - Can the plan contain outer joins, aggregations or sorts, or only the four node types above? - Should the optimizer mutate the tree in place or return a new tree? - Must the optimized plan preserve the exact column order and row order of the original output? ### Part 1 — Column pruning Implement a pass that rewrites the plan so every `Scan` reads only the columns that some node above it actually needs, without changing the query's output. ```hint Direction of information Decide whether the set of needed columns is known at the top of the tree or at the leaves, and let the traversal carry it in that direction. ``` #### What This Part Should Cover - Which columns each node type needs from its child, including filter columns and join keys - Keeping the root's output unchanged while shrinking every scan - Runnable code, and its cost in terms of plan size ### Part 2 — Filter pushdown Implement a pass that moves filter conditions as close to the scans as possible (predicate pushdown): through projections and into the correct side of joins, without changing the query result. ```hint Split before you push A filter with several conditions does not have to move as one unit. ``` #### Clarifying Questions for this Part - If a conjunct references columns from both sides of a join, where should it stay? #### What This Part Should Cover - Splitting conjunctions, and deciding for each conjunct how far down it may legally move - Why pushing into either side of an inner join is safe, and when it would not be safe for other join types - How the two passes compose, plus a runnable check that the optimized plan returns the same rows ### What a Strong Answer Covers - Recognizing standard logical rewrites without being told which ones to apply - Clean recursive tree rewrites that return a new plan and preserve the output schema - A correctness argument for each rewrite, including join semantics - Executable code with a tiny evaluator or test that compares results before and after - The complexity of each pass relative to the number of nodes and columns ### Follow-up Questions - How would you push a filter through a projection that computes expressions, such as `total = price * qty`? - Which conjuncts could become join conditions, and why does that matter for performance? - How would you decide the order in which rewrite rules run, and when to stop?

Overview: Given a SQL query represented as a tree of scan, filter, project and join nodes, write runnable rewrites that prune unused columns from every scan and push filter conditions down toward the tables. Tests query-optimizer fundamentals, recursive tree rewriting and preserving query results.

|Home/Software Engineering Fundamentals/Databricks
Databricks logo
Databricks
Sep 11, 2026
mediumSoftware EngineerOnsiteSoftware Engineering Fundamentals
3
0

A query engine represents a SQL query as a tree of plan nodes. You are handed such a tree and asked to "optimize" it. The interviewer does not say which optimizations are expected, and the code must actually run on a sample plan, not just be described. The rewrites were not named up front in the interview; the two that were eventually required are the Parts below.

The exact node classes were not reported. Assume this minimal logical plan model (Python dataclasses are fine):

  • Scan(table, columns) : reads the listed columns of a base table.
  • Filter(conjuncts, child) : keeps the child's rows for which every conjunct is true. A conjunct is a comparison column op literal .
  • Project(columns, child) : outputs only the listed columns of the child (plain column references, no expressions).
  • Join(left, right, left_key, right_key) : an inner equi-join on left.left_key = right.right_key that outputs all columns of both sides.

Column names are globally unique across tables, so a column name identifies its table. A typical input is:

Project([order_id, name],
  Filter([amount > 100, country = 'US'],
    Join(Scan(orders,    [order_id, customer_id, amount, status, created_at]),
         Scan(customers, [cust_id, name, country, signup_date]),
         left_key=customer_id, right_key=cust_id)))

Clarifying Questions Guidance

  • Can conjuncts reference more than one column (for example, comparing two columns), and can projections compute expressions?
  • Can the plan contain outer joins, aggregations or sorts, or only the four node types above?
  • Should the optimizer mutate the tree in place or return a new tree?
  • Must the optimized plan preserve the exact column order and row order of the original output?

Part 1 — Column pruning

Implement a pass that rewrites the plan so every Scan reads only the columns that some node above it actually needs, without changing the query's output.

What This Part Should Cover Guidance

  • Which columns each node type needs from its child, including filter columns and join keys
  • Keeping the root's output unchanged while shrinking every scan
  • Runnable code, and its cost in terms of plan size

Part 2 — Filter pushdown

Implement a pass that moves filter conditions as close to the scans as possible (predicate pushdown): through projections and into the correct side of joins, without changing the query result.

Clarifying Questions for this Part Guidance

  • If a conjunct references columns from both sides of a join, where should it stay?

What This Part Should Cover Guidance

  • Splitting conjunctions, and deciding for each conjunct how far down it may legally move
  • Why pushing into either side of an inner join is safe, and when it would not be safe for other join types
  • How the two passes compose, plus a runnable check that the optimized plan returns the same rows

What a Strong Answer Covers Guidance

  • Recognizing standard logical rewrites without being told which ones to apply
  • Clean recursive tree rewrites that return a new plan and preserve the output schema
  • A correctness argument for each rewrite, including join semantics
  • Executable code with a tiny evaluator or test that compares results before and after
  • The complexity of each pass relative to the number of nodes and columns

Follow-up Questions Guidance

  • How would you push a filter through a projection that computes expressions, such as total = price * qty ?
  • Which conjuncts could become join conditions, and why does that matter for performance?
  • How would you decide the order in which rewrite rules run, and when to stop?
Loading comments...