Project Info
Calhacks 2025 – Query Challenge
A submission to AppLovin's query planner challenge at Cal Hacks 2025. See an example run here.
Summary: we achieve a ~1300x speedup compared to the baseline implementation (~45s to ~30ms) on an M3 Pro with 18 GB RAM. Preprocessing takes 300.5 seconds (~5 minutes).

Figure 1: A comparison of the baseline implementation to our submission (v1). Note the log scale: v1 is over three orders of magnitude faster.
On our custom set of 25 additional validation queries, we achieve a ~120x speedup (~205s to ~1.72s).

Setup
This project uses uv to manage Python packages, which can be installed here.
To evaluate a set of queries (provided as a JSON file):
PYTHONPATH=. uv run benchmark/run.py \
--data-dir data/data \
--out-dir results/v1-baseline-full \
--solution=v1 \
--queries queries/baseline.json \
--verbose \
--num-runs=1
You can pass the directory to the expected results for correctness checks. For example:
PYTHONPATH=. uv run benchmark/run.py \
--data-dir data/data \
--out-dir results/v1-baseline-full \
--solution=v1 \
--queries queries/baseline.json \
--expected-dir results/results-full \
--verbose \
--num-runs=1
Design
We use four main techniques to optimize query performance.
Efficient on-disk storage layout
Instead of storing the data in CSV files and re-reading via read_csv on each query, we can pre-process the table into parquet files for significantly more efficient IO. In particular, we allow each thread to store its own parquet shard.

With the default ROW_GROUP_SIZE = 122880, each of the 10 shards (for each thread) has around ~160 row groups.
SELECT num_row_groups FROM parquet_file_metadata('tmp/events.parquet/data_0.parquet');
> 163
This means that there's a fair chunk of overhead spent on reading per-group metadata. We can afford to store much more data in memory - given we have ~10 threads, we should aim for num_row_groups around ~20.

Using ROW_GROUP_SIZE = 1M, we see significant improvements on the majority of queries (22x improvement on baseline from optimized layout alone).
Materialized views
Materialized views let us pre-aggregate common query patterns and can easily improve query times 10-100x (note: DuckDB doesn't have materialized views, so we just create new tables).
Although we could create arbitrarily many pre-aggregated tables to account for various query patterns, I chose a small set of general MVs (see src/mv.py for the definitions). In particular, I have a set of "slow/large" MVs and a corresponding set of faster/smaller ones for common use cases.

For each of the ID types (advertiser_id, publisher_id, user_id), I've constructed a slow/large table grouped by (type, day, country, <id>). This answers common questions like:
- How much in total bids did a particular advertiser make on a certain day?
- How many impressions were served to users in JP?
Even though these tables are fairly large (advertiser_full is ~10M rows), they are still significantly smaller than events with 245M rows.
On the faster side, I have a set of small MVs (e.g. mv_day_fast, mv_time_fast and mv_auction_fast) that have ~10K rows and act as a quick cache if a query can be written to hit them.
Note: this is a tradeoff between pre-processing time and query performance. I've only set up a handful of common MVs, but have tried to make it as easy as possible to add new MVs. All it takes is updating the registry in src/mv.py – the corresponding SQL will be automatically generated.
Intelligent query planning & rewriting

The query planner (src/planner.py) is responsible for rewriting the query to hit a particular materialized view. This happens in two steps:
- Find the set of feasible MVs that support the query
- Score each feasible MV based on estimated cost heuristics
Feasibility Check
We can only substitute a table for an MV if:
- the group-by of the MV fully covers the group-by of the query and any filters
- all select columns/aggregates can be derived from the MV
For the last point, we make a micro-optimization to derive AVG from SUM and COUNT, which lets us cut down on the number of bytes we need to save.
Intelligent scoring
The key idea here is that the cost of the query is approximately linear to the # of filtered rows and the # of rows that need to be rolled-up. Luckily, we can pre-compute these statistics during the pre-loading phase.
In the preload, we batch together a number of COUNT and COUNT(DISTINCT) calls to cache:
- the number of rows in the MV
- the number of distinct values per column
- the top-k most common values and their count per column (to improve estimates)
Then, estimating the number of filtered rows looks like:
num_filtered_rows = num_rows
for f in filters:
if f.value in top_k:
num_filtered_rows *= top_k[f.col][f.value] / distinct[f.col][f.value]
else:
num_filtered_rows *= 1 / distinct[f.col][f.value]
Next, there's additional cost if the MV has higher granularity than the query. For example, if the query groups by (type, day, hour) but the closest MV groups by (type, day, hour, minute), DuckDB will need to rollup the values in the minute. We can approximate this rollup cost as:
num_rolled_up = 1
for col in mv.group_by:
if col not in query.group_by:
num_rolled_up *= distinct[col]
Finally, I computed the total cost as a linear combination of the two. These are all rough hyperparameters and require more tuning in the future.
Example
This is taken directly from the v1 solution when trying to optimize the following query:
Query:
{'select': ['day', {'SUM': 'bid_price'}], 'from': 'events', 'where': [{'col': 'type', 'op': 'eq', 'val': 'impression'}], 'group_by': ['day']}
Logs from verbose=True:
Considering mv_auction_id_full: cost 80316988.0
Considering mv_advertiser_id_full: cost 7132445.0
Considering mv_publisher_id_full: cost 4868461.0
Considering mv_user_id_full: cost 72091914.0
Considering mv_day_fast: cost 366.0 <--- planner rewrites query to use `mv_day_fast`
Considering mv_time: cost 6252631.0
Efficient caching and warmup
The last significant optimization was warming up the DuckDB buffer manager and OS cache. This lets subsequent queries hit warm pages and avoid direct disk reads.
To do this, I ran ANALYZE mv_... for each of the materialized views. Any similar operation that scans all of the data (e.g. COUNT(*)) should have the same effect.
I timed this on my validation suite; the variation makes it hard to tell, but this step offers some speedup and doesn't hurt subsequent performance in any way.
Miscellaneous: using enums for types; re-computing AVG from SUM/CNT; ZSTD compression.
Implementation
Benchmarking
We implemented a generic Solution class (benchmark/solution.py), which both the baseline and v1 solutions inherit from.
Additionally, we provide options to:
- benchmark over multiple runs (
--num-runs) - plot stats (
benchmark/plot_stats.py) - compare against a ground-truth directory of results (
--expected-dir)
Materialized Views
To avoid duplicated SQL and code, we created a custom MaterializedView class that can automatically generate the corresponding CREATE TABLE SQL statement. This lets us manage the set of MVs solely from Python and automatically handle routing.
The logic for the planner and assembler can be found in src/planner.py and src/assembler.py. We automatically convert aggregates like SUM(bid_price) to a normalized name sum_bid_price.
Testing
For rigorous testing, we generated an additional set of 25 validation queries (found in queries/validation.json). This let us confidently experiment with various choices for the materialized view, without overfitting to the provided 5 examples.
Analysis
View
Metric
- 26
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- PythonIn code
1 of 1 appear in the indexed code.
AI coding agents
No AI coding agent signals were found in this repository.
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
44 KB
Source files
12
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
KevinL10/applovin-query
30 files · 1.2 MB · @ c749e2b
Structure
Application logic
17 files · 57%Domain rules, services and shared utilities.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Python81%
- Markdown19%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
pyproject.toml
pypi · 4- duckdb
- matplotlib
- pytz
- tqdm
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
Feature verification
Automatic SQL generation for materialized viewsVerified
A custom MaterializedView class automatically generates CREATE TABLE SQL, managed solely from Python
Claimed on readmehigh confidencesrc/mv.py:37— generate_create_sql() builds the CREATE TABLE statement from group_by and aggs fields
Buffer manager / OS cache warmup via ANALYZEVerified
Warm up the DuckDB buffer manager and OS cache by running ANALYZE on materialized views so subsequent queries hit warm pages
Claimed on readmehigh confidencesrc/v1.py:116— warmup_cache block runs ANALYZE and a COUNT(*) scan on every MV during load_data, matching the described warmup step
DuckDB-based query engine built with PythonVerified
A fast, intelligent query engine built on DuckDB, built with python
Claimed on Devposthigh confidencesrc/v1.py:3— solution is implemented in Python using the duckdb library for storage, view creation, and query execution
Enum type usage for categorical columnsVerified
Miscellaneous optimization: using enums for types
Claimed on readmehigh confidencesrc/v1.py:61— type column is cast to an ENUM('impression','serve','click','purchase') during load
Generic benchmarking harness comparing baseline vs solutionsVerified
A generic Solution class that both baseline and v1 inherit from, with multi-run benchmarking, plotting, and correctness comparison against expected results
Claimed on readmehigh confidencebenchmark/solution.py:4— abstract Solution base class with load_data/run/closebenchmark/run.py:14— SOLUTION_CLASSES maps baseline and v1 to their implementations; run_solution supports num_runs and expected_dir correctness checksbenchmark/plot_stats.py:1— separate plotting script exists for benchmark stats, matching the claimed plot_stats.py option
Materialized views (pre-aggregated tables)Verified
Pre-aggregate common query patterns into slow/large and fast/small materialized tables since DuckDB lacks native MVs
Claimed on readmehigh confidencesrc/mv.py:58— MV_REGISTRY defines full (slow/large) MVs per id column and fast/small MVs (mv_day_fast, mv_time_fast, mv_*_fast)src/v1.py:106— load_data iterates MV_REGISTRY and executes CREATE TABLE SQL for each MV during preprocessing
Parquet-based on-disk storage with per-thread sharding and ZSTD compressionVerified
Preprocess CSV data into parquet files with per-thread shards and large row groups for efficient IO
Claimed on readmehigh confidencesrc/v1.py:94— COPY events TO parquet with FORMAT PARQUET, COMPRESSION ZSTD, PER_THREAD_OUTPUT, ROW_GROUP_SIZE 1000000 matches the described parquet layout and row-group tuning
Query planner: cost-based scoring of feasible MVsVerified
Score each feasible MV using estimated filtered-row counts and rollup cost, using precomputed distinct-value and top-k statistics
Claimed on readmehigh confidencesrc/planner.py:122— mv_cost computes num_rows_scanned via predicate_selectivity plus a rollup penalty (32.0 * num_groups), matching the described linear cost modelsrc/planner.py:20— compute_mv_stats precomputes num_rows, num_distinct per column, and top-k values per column, exactly as described
Query planner: feasibility check for MV substitutionVerified
Planner finds feasible MVs where the group-by/filters are covered and aggregates are derivable (including deriving AVG from SUM/COUNT)
Claimed on readmehigh confidencesrc/planner.py:65— is_mv_usuable checks group_by subset, filter column coverage, and aggregate derivabilitysrc/planner.py:54— _agg_deriveable derives AVG from SUM and COUNT of the same column
Query rewriting to assemble SQL against chosen MVVerified
Rewritten queries hit the chosen MV, normalizing aggregate names like SUM(bid_price) to sum_bid_price
Claimed on readmehigh confidencesrc/assembler.py:7— assemble_sql_for_mv builds a SELECT against the MV's table name using normalized column aliasessrc/mv.py:5— metric_col_name normalizes op/column pairs like sum_bid_price
Validation query suite of 25 additional queriesCode-supported
Generated an additional set of 25 validation queries to avoid overfitting to the 5 provided examples
Claimed on readmemedium confidencequeries/validation.json— a validation.json query file exists and is substantial (222 lines), consistent with a multi-query validation suite, but the exact count of 25 queries was not individually verified
~1300x speedup over baseline (45s to 30ms) and ~120x speedup on validation queriesClaimed only
v1 achieves ~1300x speedup vs baseline on the challenge queries and ~120x speedup on the 25 validation queries, with 300.5s preprocessing time
Claimed on readmemedium confidence
An AI agent derived these features from the project’s Devpost page and readme, then searched the code for each one. Verified features are backed by cited code; claimed-only features had no supporting code, which is not by itself proof a feature is missing.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.