|
| 1 | +# QueryForge — Distributed SQL Query Engine |
| 2 | + |
| 3 | +> A production-grade distributed SQL engine that processes million-row CSV datasets across parallel worker nodes. Upload a dataset, write SQL, and QueryForge distributes execution across 3 workers — returning results faster than any single-machine setup. |
| 4 | +> |
| 5 | +> **Architected after AWS Athena · Google BigQuery · Apache Drill** |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## Benchmark Results |
| 10 | + |
| 11 | +| Metric | Value | |
| 12 | +|--------|-------| |
| 13 | +| Dataset size | 2,000,000 rows | |
| 14 | +| Single-machine time | 5,958ms | |
| 15 | +| Distributed (3 workers) | 3,512ms | |
| 16 | +| **Speedup** | **1.70x faster** | |
| 17 | +| Throughput | ~570,000 rows/sec | |
| 18 | +| Max tested | 5,000,000 rows ✓ | |
| 19 | + |
| 20 | +Query: `SELECT department, COUNT(*), AVG(salary), MAX(salary), MIN(salary), SUM(salary) FROM employees WHERE age > 20 GROUP BY department ORDER BY total DESC` |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## Architecture |
| 25 | + |
| 26 | +``` |
| 27 | +┌─────────────────────────────────────────────────────────────────────┐ |
| 28 | +│ Frontend (React + Vite) │ |
| 29 | +│ Upload · SQL Editor · ⚡ Explain · Live Worker Dashboard │ |
| 30 | +└─────────────────────────┬───────────────────────────────────────────┘ |
| 31 | + │ REST + WebSocket |
| 32 | +┌─────────────────────────▼───────────────────────────────────────────┐ |
| 33 | +│ Coordinator (Node.js) │ |
| 34 | +│ │ |
| 35 | +│ SQL Parser → Execution Plan → Job Manager → Result Merger │ |
| 36 | +│ ⚡ EXPLAIN endpoint · Fault Monitor · WebSocket broadcaster │ |
| 37 | +│ CoordinatorService gRPC server (workers register here) │ |
| 38 | +└──────────┬──────────────────┬───────────────────┬────────────────────┘ |
| 39 | + │ gRPC │ gRPC │ gRPC |
| 40 | + ┌──────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐ |
| 41 | + │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ |
| 42 | + │ │ │ │ │ │ |
| 43 | + │ ① Download │ │ ① Download │ │ ① Download │ |
| 44 | + │ partition │ │ partition │ │ partition │ |
| 45 | + │ ② Filter │ │ ② Filter │ │ ② Filter │ |
| 46 | + │ (WHERE) │ │ (WHERE) │ │ (WHERE) │ |
| 47 | + │ ③ Local │ │ ③ Local │ │ ③ Local │ |
| 48 | + │ GROUP BY │ │ GROUP BY │ │ GROUP BY │ |
| 49 | + │ ④ Stream │ │ ④ Stream │ │ ④ Stream │ |
| 50 | + │ results │ │ results │ │ results │ |
| 51 | + └──────┬──────┘ └───────┬──────┘ └───────┬──────┘ |
| 52 | + └──────────────────┼───────────────────┘ |
| 53 | + │ All read from |
| 54 | + ┌──────────▼──────────┐ |
| 55 | + │ MinIO (S3) │ |
| 56 | + │ datasets/ │ |
| 57 | + │ partitions/ │ |
| 58 | + └─────────────────────┘ |
| 59 | + PostgreSQL ── metadata, jobs, tasks, workers |
| 60 | + Prometheus + Grafana ── metrics, dashboards |
| 61 | +``` |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## Key Features |
| 66 | + |
| 67 | +### ① Predicate Pushdown |
| 68 | +WHERE filters applied **row-by-row during CSV streaming**, before any rows enter memory. Non-matching rows are discarded immediately — never transferred to coordinator. |
| 69 | + |
| 70 | +``` |
| 71 | +Worker reads CSV: |
| 72 | + row 1: age=22 → WHERE age > 25 → ✗ discarded (never in memory) |
| 73 | + row 2: age=31 → WHERE age > 25 → ✓ kept |
| 74 | + row 3: age=19 → WHERE age > 25 → ✗ discarded |
| 75 | +``` |
| 76 | + |
| 77 | +### ② Partial Aggregation (MapReduce-style) |
| 78 | +For GROUP BY queries, each worker builds a **local hash map** on its partition. Coordinator receives 3 compact maps and merges them — not millions of raw rows. |
| 79 | + |
| 80 | +``` |
| 81 | +Worker 1 sends: { Engineering: { count:180000, sum:14B } } ← 8 objects |
| 82 | +Worker 2 sends: { Engineering: { count:181000, sum:14.1B } } ← 8 objects |
| 83 | +Worker 3 sends: { Engineering: { count:180667, sum:14B } } ← 8 objects |
| 84 | +
|
| 85 | +Coordinator merges → final AVG = total_sum / total_count |
| 86 | + (NOT average of averages — mathematically correct) |
| 87 | +``` |
| 88 | + |
| 89 | +### ③ Automatic Fault Recovery |
| 90 | +Workers send heartbeats every 5 seconds. If a worker misses 3 heartbeats (15s), coordinator marks it dead and reassigns its partition to a healthy worker. Max 3 reassignment attempts per partition. |
| 91 | + |
| 92 | +### ④ EXPLAIN Endpoint (like PostgreSQL's EXPLAIN) |
| 93 | +`POST /api/explain` returns the full execution plan before running — shows which predicates are pushed down, partition assignment per worker, aggregation strategy per function. |
| 94 | + |
| 95 | +### ⑤ OpenTelemetry Observability |
| 96 | +Every coordinator and worker exposes metrics on `:9464/metrics`. Custom spans on `query.plan`, `job.execute`, `task.execute` with `rows.scanned` vs `rows.passed_filter` attributes. |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Quick Start |
| 101 | + |
| 102 | +```bash |
| 103 | +git clone https://github.com/vipulpandey7917/queryforge |
| 104 | +cd queryforge |
| 105 | +docker compose up --build |
| 106 | +``` |
| 107 | + |
| 108 | +**That's it.** All 9 services start automatically. No manual setup. |
| 109 | + |
| 110 | +| Service | URL | |
| 111 | +|---------|-----| |
| 112 | +| **Frontend** | http://localhost:5173 | |
| 113 | +| **Coordinator API** | http://localhost:3000 | |
| 114 | +| **MinIO Console** | http://localhost:9001 (minioadmin / minioadmin) | |
| 115 | +| **Prometheus** | http://localhost:9090/targets | |
| 116 | +| **Grafana** | http://localhost:3001 (admin / admin) | |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## SQL Support |
| 121 | + |
| 122 | +```sql |
| 123 | +-- Filtered scan with predicate pushdown |
| 124 | +SELECT name, salary FROM employees WHERE salary > 60000 |
| 125 | + |
| 126 | +-- GROUP BY with multiple aggregations |
| 127 | +SELECT department, |
| 128 | + COUNT(*) as total, |
| 129 | + AVG(salary) as avg_sal, |
| 130 | + MAX(salary) as max_sal, |
| 131 | + MIN(salary) as min_sal, |
| 132 | + SUM(salary) as total_sal |
| 133 | +FROM employees |
| 134 | +WHERE age > 25 |
| 135 | +GROUP BY department |
| 136 | +ORDER BY total DESC |
| 137 | + |
| 138 | +-- COUNT with filter |
| 139 | +SELECT COUNT(*) as total FROM employees WHERE city = 'Mumbai' |
| 140 | + |
| 141 | +-- ORDER BY + LIMIT |
| 142 | +SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 10 |
| 143 | +``` |
| 144 | + |
| 145 | +Supported aggregations: `COUNT`, `SUM`, `AVG`, `MAX`, `MIN` |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +## Query Execution — 20-Step Flow |
| 150 | + |
| 151 | +``` |
| 152 | +1. POST /api/query { sql, datasetId } |
| 153 | +2. node-sql-parser → AST |
| 154 | +3. Extract: predicates, GROUP BY, aggregations, ORDER BY, LIMIT |
| 155 | +4. Look up dataset + 3 partitions in PostgreSQL |
| 156 | +5. Create Job + 3 Tasks in PostgreSQL |
| 157 | +6. Dispatch 3 gRPC ExecuteTask calls in parallel (Promise.all) |
| 158 | +7. Each worker: getObject(MinIO) → write to /tmp/{taskId}.csv |
| 159 | +8. Each worker: stream CSV row-by-row → apply WHERE predicates |
| 160 | +9. Each worker: build local GROUP BY hash map |
| 161 | +10. Each worker: stream AggregationGroup messages → coordinator |
| 162 | +11. Coordinator: merge 3 hash maps (SUM totals, MAX of MAXes, COUNT sums) |
| 163 | +12. Coordinator: compute final AVG = total_sum / total_count |
| 164 | +13. Coordinator: apply ORDER BY on merged result |
| 165 | +14. Coordinator: apply LIMIT |
| 166 | +15. Coordinator: stream rows via WebSocket → frontend |
| 167 | +16. Frontend: render rows as they arrive |
| 168 | +17. Coordinator: UPDATE jobs SET status='completed' |
| 169 | +18. WebSocket: { type: 'complete', totalRows, executionTimeMs } |
| 170 | +19. OTel spans closed with row counts |
| 171 | +20. Prometheus metrics updated |
| 172 | +``` |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Fault Recovery Demo |
| 177 | + |
| 178 | +While a query is running: |
| 179 | + |
| 180 | +```bash |
| 181 | +docker compose stop worker-2 |
| 182 | +``` |
| 183 | + |
| 184 | +Coordinator detects missing heartbeat → reassigns partition → query completes with 2 workers. |
| 185 | + |
| 186 | +```bash |
| 187 | +docker compose start worker-2 # brings it back online |
| 188 | +``` |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Running the Benchmark |
| 193 | + |
| 194 | +```bash |
| 195 | +cd benchmarks |
| 196 | +npm install |
| 197 | +node run_benchmark.js |
| 198 | +``` |
| 199 | + |
| 200 | +Generates 2M rows → uploads → runs distributed query → runs same query single-machine → prints speedup. |
| 201 | + |
| 202 | +--- |
| 203 | + |
| 204 | +## API Reference |
| 205 | + |
| 206 | +| Method | Endpoint | Description | |
| 207 | +|--------|----------|-------------| |
| 208 | +| `POST` | `/api/datasets/upload` | Upload CSV, returns `{ datasetId, rowCount, schema }` | |
| 209 | +| `GET` | `/api/datasets` | List all datasets | |
| 210 | +| `GET` | `/api/datasets/:id` | Dataset + partition details | |
| 211 | +| `POST` | `/api/query` | Submit SQL, returns `{ jobId }` immediately | |
| 212 | +| `GET` | `/api/query/jobs/:id` | Job status + per-task metrics | |
| 213 | +| `POST` | `/api/explain` | Execution plan JSON (no query executed) | |
| 214 | +| `GET` | `/api/workers` | Live worker registry with heartbeat status | |
| 215 | +| `GET` | `/api/health` | Coordinator health check | |
| 216 | + |
| 217 | +**WebSocket:** `ws://localhost:3000/ws` |
| 218 | +```json |
| 219 | +// Subscribe |
| 220 | +{ "type": "subscribe", "jobId": "..." } |
| 221 | + |
| 222 | +// Receive |
| 223 | +{ "type": "row", "data": { "department": "Engineering", "total": 541667 } } |
| 224 | +{ "type": "progress", "completedTasks": 2, "totalTasks": 3 } |
| 225 | +{ "type": "complete", "totalRows": 8, "executionTimeMs": 3512 } |
| 226 | +{ "type": "error", "message": "..." } |
| 227 | +``` |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## Tech Stack |
| 232 | + |
| 233 | +| Layer | Technology | |
| 234 | +|-------|-----------| |
| 235 | +| Coordinator | Node.js 20, Express 4, gRPC (`@grpc/grpc-js`), WebSocket (`ws`) | |
| 236 | +| Workers | Node.js 20, gRPC server-side streaming | |
| 237 | +| SQL Parsing | `node-sql-parser` (PostgreSQL dialect) | |
| 238 | +| Object Storage | MinIO (S3-compatible) | |
| 239 | +| Metadata | PostgreSQL 15 | |
| 240 | +| Observability | OpenTelemetry SDK, Prometheus, Grafana | |
| 241 | +| Containerisation | Docker Compose (9 services) | |
| 242 | +| Frontend | React 18, Vite 5, Tailwind CSS 3 | |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +## Project Structure |
| 247 | + |
| 248 | +``` |
| 249 | +queryforge/ |
| 250 | +├── coordinator/ # Coordinator node |
| 251 | +│ ├── src/ |
| 252 | +│ │ ├── grpc/ # CoordinatorService server + WorkerService client |
| 253 | +│ │ ├── routes/ # datasets, query, workers, explain |
| 254 | +│ │ ├── services/ # queryPlanner, partitioner, jobManager, |
| 255 | +│ │ │ # resultMerger, faultMonitor |
| 256 | +│ │ ├── websocket/ # WebSocket server (ping/pong + job subscriptions) |
| 257 | +│ │ └── db/ # PostgreSQL connection pool |
| 258 | +│ ├── schema.sql |
| 259 | +│ └── tracing.js # OTel SDK (loaded before index.js via -r flag) |
| 260 | +├── worker/ # Worker node |
| 261 | +│ ├── src/ |
| 262 | +│ │ ├── grpc/ # WorkerService server + CoordinatorService client |
| 263 | +│ │ └── services/ # taskExecutor, predicateEvaluator, |
| 264 | +│ │ # aggregator, minioClient |
| 265 | +│ └── tracing.js |
| 266 | +├── frontend/ # React + Tailwind UI |
| 267 | +│ └── src/components/ # DatasetUploader, SQLEditor, ExplainPanel, |
| 268 | +│ # WorkerDashboard, ResultsTable |
| 269 | +├── proto/ |
| 270 | +│ └── queryforge.proto # gRPC service definitions |
| 271 | +├── monitoring/ |
| 272 | +│ ├── prometheus.yml |
| 273 | +│ └── provisioning/ # Grafana auto-provisioned datasource + dashboard |
| 274 | +├── benchmarks/ |
| 275 | +│ └── run_benchmark.js # 2M row benchmark script |
| 276 | +└── docker-compose.yml # 9 services, zero manual setup |
| 277 | +``` |
| 278 | + |
| 279 | +--- |
| 280 | + |
| 281 | +## Architecture Decisions (Interview Q&A) |
| 282 | + |
| 283 | +**Why gRPC between coordinator and workers, not REST?** |
| 284 | +gRPC supports server-side streaming natively — workers stream partial results back as they process, without buffering everything first. REST would require workers to finish completely before sending anything, removing the streaming benefit. |
| 285 | + |
| 286 | +**Why MinIO and not shared filesystem?** |
| 287 | +Shared filesystem doesn't work across distributed nodes. MinIO gives each worker independent object access — worker-1 reads partition-0, worker-2 reads partition-1, both simultaneously, with no coordination needed. |
| 288 | + |
| 289 | +**Why partial aggregation instead of sending all rows?** |
| 290 | +For a GROUP BY query on 2M rows with 8 groups, sending raw rows means 666,667 rows per worker × 3 workers = 2M rows through the coordinator. Partial aggregation sends 8 hash map entries per worker = 24 objects total. The network difference is ~100MB vs ~200 bytes. |
| 291 | + |
| 292 | +**What's the bottleneck right now?** |
| 293 | +Coordinator is a single point of failure and also the merge bottleneck. For production I'd shard the merge across multiple coordinator instances, similar to how Presto uses a coordinator cluster. |
| 294 | + |
| 295 | +**How would you scale beyond 3 workers?** |
| 296 | +Partition count = worker count. Dynamic partitioning would split the dataset into N chunks at upload time based on registered workers. The current round-robin assignment already handles N workers — changing `partitionCount` from 3 to N is the only required change. |
| 297 | + |
| 298 | +**Why 3 partitions specifically?** |
| 299 | +Matched to the 3 workers in this deployment. Coordinator assigns partition[i] to worker[i % workerCount], so adding a 4th worker automatically gets partition-3 if it exists. |
| 300 | + |
| 301 | +--- |
| 302 | + |
| 303 | +*Built by [Vipul Pandey](https://github.com/vipulpandey7917) — GSoC 2026 Contributor (Learning Unlimited) · LeetCode Knight (1881) · B.Tech IT + MBA, IIITM Gwalior* |
0 commit comments