Project Info
π 1,600Γ Faster than Baseline β 39ms vs 62 seconds on 5 example queries
Inspiration
Ad networks process massive volumes of event data. AppLovin needed instant query execution on 245M rows of ad events (~20GB). Traditional full-scan approaches were too slow; we needed a smarter architecture that could handle arbitrary queries without sacrificing speed.
What it does
A two-tier query engine that executes ad-hoc analytics queries on massive event datasets in <100ms: 11 pre-aggregated rollup tables (Arrow IPC, ~340MB) handle (hopefully!) 80β90% of queries in <10ms via direct lookup Sorted DuckDB fallback (~2.5GB) handles complex multi-dimensional queries in <400ms Smart query router analyzes query patterns and routes to the optimal data structure Supports 20+ concurrent queries with <200ms total execution time
How we built it
Phase 1: Rollup Design Analyzed baseline query patterns to identify high-impact dimensions (day, country, advertiser, publisher, type) Pre-aggregated data into 11 rollup tables covering 80β90% of query patterns Each rollup stores pre-computed aggregates: sum, count, avg, min, max Phase 2: Build Optimization Single-pass incremental folding over 49 CSV files (reduces peak memory to ~10GB) Parallel CSV reading (8 threads) + Polars multi-threading Arrow IPC format with LZ4 compression (3:1 ratio, instant memory-mapping) Phase 3: Query Execution Query router parses dimensions and filters, matches against rollup definitions Rollup queries: memory-mapped load β filter β aggregate (done) Fallback queries: DuckDB sorted scans (sorted by week, country, type for efficient range queries) Tech Stack: Python 3.9+, Polars, DuckDB, Apache Arrow, LZ4 compression
Challenges we ran into
Memory explosion during build β Solved with incremental folding (merge accumulators every 50 batches) Build time >15 min on M3 Air β Attempted multiprocessing but file-level parallelism added merge overhead; reverted to sequential single-pass (more efficient) Choosing which dimensions to pre-aggregate β Analyzed baseline patterns; 11 rollups cover 80β90% with diminishing returns beyond Query routing accuracy β Built intelligent matcher that handles partial dimension sets and falls back to DuckDB safely Storage format trade-offs β Arrow IPC (memory-mappable, instant zero-copy reads) vs Parquet; chose IPC for 5β10Γ faster load
Accomplishments we're proud of
β 1,600Γ speedup (39ms vs 62s) on example queries β Sub-100ms query execution on 245M rows β Compact storage (~3GB rollups + fallback vs ~20GB raw) β Incremental folding keeps peak memory at ~10GB (safe on 12β16GB systems) β Intelligent routing β no manual query tuning needed β Validated against baseline β results are pixel-perfect identical
What we learned
Pre-aggregation is powerful but requires careful dimension selection (diminishing returns beyond 11 rollups) Sequential single-pass is faster than parallelism for merge-heavy workloads (join overhead outweighs parallelism gains) Physical layout beats indexes β sorting DuckDB by (week, country, type) eliminates need for indexes, helps massively in reducing build time Memory-mapped I/O matters β Arrow IPC's zero-copy semantics cut load time from 2β3s to 450ms Constraints drive design β 16GB RAM limit forced us toward incremental folding and bounded memory strategies
What's next
for AppLovin Query Planner Dynamic rollup generation β auto-detect optimal rollup dimensions from query workloads Adaptive routing β learn query patterns over time and reorganize rollups on-the-fly Distributed execution β scale to multi-machine clusters for even larger datasets Real-time updates β stream new events into rollups without rebuilding Query optimizer β cost-based planner to choose between rollup vs fallback more intelligently Quick Start
CalHacks AppLovin Database Challenge - Optimized Solution
π 1,600Γ Faster than Baseline on the 5 example queries - 39ms vs 62 seconds
Pre-aggregated rollup tables + intelligent query routing = sub-100ms query execution.
Quick Start
pip install -r requirements.txt
python3 prepare.py --data-dir ./data # One-time build: ~11-15 min on m3 air, should be faster on m2 pro
python3 run.py # Query execution on 5 example queries: ~40ms
For Judges: Testing Your Queries
# Step 1: Prepare
python3 prepare.py --data-dir <your-data-path>
# Step 2: Run with your 20 queries (queries should be a json array)
python3 run.py --query-file ./your_queries.json
# Results appear in ./results/q1.csv through q20.csv
Your JSON file should be an array of query objects:
[
{
"select": ["day", {"SUM": "bid_price"}],
"from": "events",
"where": [{"col": "type", "op": "eq", "val": "impression"}],
"group_by": ["day"]
},
{
"select": ["country", {"AVG": "total_price"}],
"from": "events",
"where": [{"col": "type", "op": "eq", "val": "purchase"}],
"group_by": ["country"]
}
// ... your remaining 18 queries
]
Expected output:
- Execution time: <200ms for 20 queries
- Summary showing which rollup handled each query
- Individual CSV files for each query result
π See queries/README.md for complete query format reference.
Performance
| Metric | Baseline (M3 Air) | Optimized | Speedup |
|---|---|---|---|
| 5 queries | 61.9s | 39ms | 1,600Γ |
| Per query avg | 12.4s | 8ms | 1,550Γ |
| Memory | ~4GB | ~10GB build, ~2-4GB query | Within limits |
| Disk | ~20GB raw | ~3GB | 85% smaller |
Query breakdown m3 mac air:
- Q1: 13.5s β 7ms (day aggregation)
- Q2: 11.4s β 8ms (publisher-country)
- Q3: 11.0s β 9ms (country average)
- Q4: 11.7s β 10ms (multi-group)
- Q5: 14.2s β 5ms (minute-level)
Architecture
Two-tier system:
- Rollup tables (11 pre-aggregated) β handles 80-90% of queries in <10ms
- DuckDB fallback (sorted layout) β handles complex queries in <20ms
Smart router β analyzes query β routes to optimal data structure
System Requirements
- Python 3.9+, 12GB+ RAM, 5GB disk
- Dependencies:
polars,duckdb,pyarrow - Apple Silicon optimized (works on Linux/Windows too)
Project Structure
prepare.py # Build rollups (11-15 min)
run.py # Execute queries (<100ms)
src/core/ # Query engine
βββ rollup_builder.py
βββ query_router.py
βββ query_executor.py
βββ fallback_executor.py
rollups/ # 11 pre-aggregated tables (~340MB)
fallback.duckdb # Sorted DuckDB table (~2.5GB)
queries/ # Example query templates
baseline/ # Original baseline for comparison
Validation
python3 validate_setup.py # Check setup
# Compare with baseline
cd baseline && python3 main.py --data-dir ../data --out-dir ./baseline_results
cd .. && python3 run.py --output-dir ./optimized_results
diff -r baseline/baseline_results optimized_results # Should be identical
Key Optimizations
- Pre-aggregation - 11 rollup tables eliminate 99.9% of data scanning
- Incremental folding - Process data in batches (~10GB peak during build)
- Sorted DuckDB - Physical layout optimization (no indexes needed)
- Smart routing - Automatic selection of optimal rollup per query
- Apple Silicon - Multi-threaded Polars (8 cores) + DuckDB (7 cores)
Troubleshooting
| Issue | Fix |
|---|---|
| "Rollup directory not found" | Run python3 prepare.py first |
| Build >10 min | Normal on M3 Air (11-15 min typical), should be faster on M2 pro |
| Out of memory | Close other apps during build (needs ~10GB free RAM) |
| Wrong results | Run validate_setup.py |
Documentation
- queries/README.md - Query format reference
Architecture Deep Dive
Data Flow
Raw CSV (20GB, 245M rows)
β
prepare.py (11-15 min build)
β
βββ 11 Rollup Tables (Arrow IPC, ~340MB)
β ββ day_type, hour_type, minute_type, week_type
β ββ country_type, advertiser_type, publisher_type
β ββ day_country_type, day_advertiser_type, hour_country_type, day_publisher_country_type
β
βββ DuckDB Fallback (sorted table, ~2.5GB)
Sorted by: (week, country, type)
β
run.py (<100ms execution)
β
βββ Query Router β analyze dimensions & filters
β
βββ 80-90% queries β Rollup Executor (<10ms)
β - Direct lookup in pre-aggregated table
β - Apply additional filters
β - Return results
β
βββ 10-20% queries β DuckDB Fallback (<400ms)
- Scan sorted table (efficient range queries)
- Complex multi-dimensional aggregations
- Return results
Rollup Table Design
Each rollup pre-aggregates data by specific dimensions and event type:
| Rollup Name | Dimensions | Aggregates | Rows | Use Case |
|---|---|---|---|---|
| day_type | day, type | bid_price, total_price, count | ~1,500 | Daily trends (Q1) |
| hour_type | hour, type | bid_price, total_price, count | ~100 | Hourly patterns |
| minute_type | minute, type | bid_price, total_price, count | ~240 | Minute-level (Q5) |
| week_type | week, type | bid_price, total_price, count | ~220 | Weekly trends |
| country_type | country, type | bid_price, total_price, count | ~100 | Geographic (Q3) |
| advertiser_type | advertiser_id, type | bid_price, total_price, count | ~4K | Advertiser performance |
| publisher_type | publisher_id, type | bid_price, total_price, count | ~4K | Publisher performance |
| day_country_type | day, country, type | bid_price, total_price, count | ~40K | Daily geo trends |
| day_advertiser_type | day, advertiser_id, type | bid_price, total_price, count | ~1M | Daily advertiser stats |
| hour_country_type | hour, country, type | bid_price, total_price, count | ~2K | Hourly geo patterns |
| day_publisher_country_type | day, publisher_id, country, type | bid_price, total_price, count | ~900K | Publisher geo (Q2) |
Aggregates stored per dimension combination:
sum_bid_price,sum_total_price(for SUM queries)count(for COUNT queries)avg_bid_price,avg_total_price(for AVG queries)min/maxvalues (for MIN/MAX queries)
Query Routing Logic
Decision tree for query routing:
1. Parse query dimensions and filters
2. Extract: group_by dimensions + WHERE filter columns
3. Match against rollup definitions:
If group_by == ["day"] and "type" in filters:
β Use day_type rollup
If group_by == ["day", "publisher_id", "country"] and "type" in filters:
β Use day_publisher_country_type rollup
If no exact match:
β Use DuckDB fallback
4. Execute query on selected data source
5. Apply additional filters (date ranges, etc.)
6. Return results
Build Process Details
Phase 1: Rollup Building (10-11 min)
For each CSV file (49 files):
1. Read batch with PyArrow (256MB blocks, 8 threads)
2. Add time dimensions (day, hour, minute, week)
3. Aggregate by all 11 rollup dimensions simultaneously
4. Every 50 batches:
- Merge with accumulator (incremental folding)
- Keep memory bounded (~10GB peak)
5. Write final rollups as Arrow IPC (LZ4 compression)
Phase 2: DuckDB Fallback (4-5 min)
1. Read all CSVs into DuckDB table
2. Add time dimensions (day, hour, minute, week)
3. Sort by (week, country, type) - physical layout optimization
4. Write to fallback.duckdb (~2.5GB)
Why this is fast:
- Single pass through data (read once)
- Parallel processing (8 threads for CSV reading)
- Incremental folding prevents memory explosion
- Arrow IPC = zero-copy reads during query time
Storage Format
Arrow IPC (Rollups):
- Columnar format (only read columns needed)
- LZ4 compression (3:1 ratio, 3GB/s decompression)
- Memory-mappable (instant load, no deserialization)
- Total size: ~340MB for all 11 rollups
DuckDB (Fallback):
- Row-oriented sorted table
- Sorted by (week, country, type) enables range scans
- No indexes needed (physical sort is the optimization)
- Total size: ~2.5GB
Query Execution
Rollup Query (80-90% of queries, <10ms):
1. Load rollup from disk (memory-mapped, instant)
2. Filter by type (if specified)
3. Filter by date range (if specified)
4. Filter by country/advertiser/publisher (if specified)
5. Select aggregates (already computed)
6. Apply ORDER BY (if specified)
7. Return results
Example Q1 (day aggregation):
- Load day_type rollup (~1,500 rows)
- Filter: type == "impression"
- Select: day, sum_bid_price
- Result: 366 rows, 7ms
DuckDB Query (10-20% of queries, <400ms):
1. Connect to fallback.duckdb
2. Execute SQL query with WHERE clauses
3. Leverage sorted layout for efficient scans
4. Return results
Example Q4 (multi-group):
- Scan fallback table (sorted helps)
- Group by: advertiser_id, type
- Aggregate: count(*)
- Order by: count DESC
- Result: 6,616 rows, 10ms
Performance Analysis
Why 1,600Γ faster than baseline:
| Factor | Baseline | Optimized | Speedup |
|---|---|---|---|
| Data scanned | 245M rows | 1.5K-900K rows | 100-1000Γ less |
| Aggregation | On-demand | Pre-computed | Instant |
| I/O | Full CSV scan | Memory-mapped rollups | 100Γ faster |
| Parsing | Parse 20GB | Parse 340MB | 60Γ less |
| Total | 62 seconds | 39ms | 1,600Γ |
Per-query breakdown:
- Q1 (13.5s β 7ms): Scan 245M rows β lookup 366 rows in day_type
- Q2 (11.4s β 8ms): Scan 245M rows β lookup ~1K rows in day_publisher_country_type
- Q3 (11.0s β 9ms): Scan 245M rows β lookup 12 rows in country_type
- Q4 (11.7s β 10ms): Full scan + group β DuckDB on sorted 245M (still faster)
- Q5 (14.2s β 5ms): Scan 245M rows β lookup 1,440 rows in minute_type
Design Decisions
1. Why 11 rollups instead of more/fewer?
- Analyzed query patterns from baseline
- Covered 80-90% with pre-aggregation
- Diminishing returns beyond 11 (more = slower build, marginal query benefit)
- Total storage: 340MB (well under disk limits)
2. Why Arrow IPC instead of Parquet?
- Memory-mappable (no deserialization overhead)
- Faster load time (450ms vs 2-3s for Parquet)
- Zero-copy reads (direct pointer to mmap'd memory)
- LZ4 compression still gives 3:1 ratio
3. Why DuckDB fallback instead of more rollups?
- Some queries too complex for pre-aggregation (multi-group, no type filter)
- DuckDB with sorted layout is "fast enough" (<400ms)
- Simpler system (11 rollups + fallback vs 50+ rollups)
4. Why sort DuckDB by (week, country, type)?
- Week: coarse partitioning (52 values)
- Country: mid-level partitioning (~30 values)
- Type: fine-grained partitioning (4 values)
- Enables efficient range scans for date/country filters
- No indexes needed (physical layout is the index)
5. Why incremental folding during build?
- Without: Memory grows unbounded (OOM on large datasets)
- With: Fold every 50 batches, keep memory at ~10GB peak
- Slight performance cost (merging overhead) but stays within memory limits
Constraints Met
| Constraint | Requirement | Our System | Status |
|---|---|---|---|
| RAM | β€16 GB | ~10GB build, ~2GB query | β Within |
| Disk | β€100 GB | ~3GB (rollups + DuckDB) | β Within |
| Network | No access during run | None (all local) | β Compliant |
| Query time | <1s target | 39ms (0.039s) | β 25Γ under |
Analysis
View
Metric
- 12
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
461 KB
Source files
63
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
georgeIshaq/Calhacks_AppLovin_Challenge
87 files Β· 472 KB Β· @ d4296c1
Structure
Application logic
50 files Β· 57%Domain rules, services and shared utilities.
+2 more
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
- Python65%
- Markdown34%
- Shell0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
requirements.txt
pypi Β· 12- duckdb
- memory_profiler
- numpy
- pandas
- polars
- psutil
- pyarrow
- pytest
- pytest-benchmark
- python-dateutil
- tqdm
- tzlocal
baseline/requirements.txt
pypi Β· 2- duckdb
- pandas
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.
This projectβs features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.