Skip to content
AFM Studio
  • Pages

    AFM Studio
  • Work
  • Lab
  • About
  • CV
  • Contact
  • Projects

    AksaraA searchable library for Otorita IKN's knowledge, where the server checks every reader's access across three levels
  • Daily TaqwaTamper-resistant prayer attendance system for ~2,000 OIKN government employees at Masjid Negara IKN
  • Lantara v2A permit engine for Otorita IKN where administrators add new permit types from a screen, without a developer
  • eKiosk IKNTwo control rooms for Nusantara: kiosk content management and live bus tracking with arrival times that grade themselves
  • E-Monev IKNA monitoring platform for Nusantara's 23-year build, tracking every unit's quarterly progress against 24 national KPIs
  • JDIH OIKNOIKN's public legal document portal, with a controlled publishing workflow and AI summaries that never leave the server
  • Jejak PenanamanA check-in app for IKN tree-planting events that proves each participant was on site with a geofenced selfie
  • Dashboard SAKTIA nightly pipeline and dashboard that replaces OIKN's manual budget downloads for all six of its budget units
  • DoseRxA bedside dose calculator for 127 drugs that rounds each dose to tablets and syrups Indonesian pharmacies sell
  • CubiqA speedcubing trainer with competition-grade timing and a solver I wrote for each of the 8 major puzzles
  • ScimotionAn interactive science library of 86 articles, each explained next to animations the reader can control
  • WA Daily Scrum BotA WhatsApp bot that chases missing standup reports for a 14-person team and flags tasks stuck for days

↑ ↓ to move · Enter to open · Esc to close

Query Planner

A browser-based database engine that shows every plan it considered and where its row estimates went wrong

Screenshots
estimate error at the root, shown node by node
300×
to re-plan and re-run a four-way join
< 16 ms
distinct visualisations, all hand-built
8
network requests; nothing leaves the browser
0
On this page

The problem

When a database query is slow, the usual advice is to add an index. Often that isn't the problem. Before running a query, a database estimates how many rows each step will produce, using a small statistical summary of the table. When those estimates are wrong, it confidently picks a slow plan. Nothing is broken and no index is missing. The plan was chosen correctly from a false belief.

A common cause is the independence assumption. Asked for rows where the city is Balikpapan and the province is Kalimantan Timur, a database multiplies the two odds as if the city said nothing about the province. It predicts one row in ten thousand. The truth is one in a hundred.

The standard tool, EXPLAIN, shows only the winning plan. It hides the candidates that lost and the gap between belief and reality. I built Query Planner for backend and data engineers, database administrators, and anyone teaching or learning query optimisation.

What I built

I wrote a whole database engine that runs in the browser

Wrapping an existing engine would have been far less work. But no existing engine shows the plans it threw away, or puts its estimate next to the true count at every step. Only an engine I control can do both. So the app has its own query parser, table storage, index, statistics, cost model, plan search and executor with nine operators. Operator names match Postgres, so what people learn here carries over to real EXPLAIN output.

The planner is not allowed to see the data

A planner that can peek at the real rows isn't planning. I made the planner code unable to import the storage or executor code, and the linter fails the build if anyone tries. The planner reads statistics and nothing else. Every estimate also carries its own explanation: which method it used, its inputs and its assumptions. The screen draws that explanation directly, so the display can't drift from the maths.

I keep every losing plan

The classic plan search throws away a candidate as soon as a cheaper one appears. I keep every candidate, plus the order in which the search filled them in. For eight tables that's 255 cells and very little memory. It is what lets the animation replay the real search. I also keep plans that are only best because of their sort order. Without them, merge join can never win, and the app would teach something false.

I checked it against real Postgres

A copy of Postgres compiled to run in the browser serves as the test reference. The same data goes into both engines, and tests check that my planner picks the same plan shape. Where the two disagree, I documented the cause in the app instead of tuning it away. A separate test checks that every possible plan for a query returns the same result, which is what catches executor bugs.

A data detail almost hid the effect

My realistic Indonesian dataset listed cities grouped by province. The generator picks cities by list position, so that grouping shrank the independence error from about 30× to 3×. I re-ordered the list by population, which is also more realistic, and the effect came back.

Result

Query Planner is live and public. Drag the storage cost setting from 4.0 toward 1.1, the usual adjustment for SSDs, and the cost bars cross as the winning plan changes. A four-table join re-plans and re-runs in under 16 milliseconds, one frame at 60 frames per second. The plan tree shows the estimate beside the real count and the error growing: 1.2× at a scan, 8× after one join, 300× at the top.

A correlation chart draws the wrong assumption as a rectangle away from the real data. One click adds a multi-column statistic, and the estimate snaps to the truth. Nothing leaves the browser, and any plan can be shared as a link. I built it in one session of about 16.5 hours on 3 September 2026.

Under the hoodTechnical detail for engineers
  • Engine: recursive-descent SQL parser for a narrow subset, columnar storage, bulk-loaded B+tree index, reservoir sampling (seeded, default 30,000 rows), equi-depth histograms, MCV lists, and all three multivariate statistic kinds (functional dependencies, n-distinct, multivariate MCV).
  • Cost is a (startup, total) pair, so LIMIT 10 can prefer a different plan. Selinger dynamic programming with interesting-order retention; tests/orders.test.ts covers a case that inverts when retention is off.
  • Volcano-model executor: seq scan, index scan, nested loop, hash join, merge join, sort, hash aggregate, group aggregate, limit. Per-node timing is sampled against a coarse counter so instrumentation doesn't distort it.
  • ESLint no-restricted-imports stops src/planner/ from importing src/storage/ or src/executor/. Math.random is banned in the engine; all randomness is seeded.
  • Each selectivity returns a SelectivityTrace (mcv, histogram, independence, dependency, …) that the histogram and correlation views render.
  • PGlite (Postgres in WebAssembly) is a devDependency only. src/planner/divergences.ts lists each disagreement, and the oracle test asserts each one still diverges. Commit 5ba8322 is titled "the PGlite oracle, and what it caught".
  • The city list lives in src/storage/datasets/wilayah.ts; the sampler is Zipf by list position.
  • Eight hand-written SVG instruments, including a Reingold–Tilford tidy tree, one requestAnimationFrame loop, and a two-theme token system rebuilt in a mid-project design revision (a073496).
  • tests/copy.test.ts bans em-dashes and marketing filler in shipped strings. Theme is kept out of the shared URL.
  • Three runtime dependencies (React, React DOM, the Geist typeface). 98 KB gzipped JS against a 350 KB budget. Zero runtime network requests.
  • CI: typecheck, lint, test, oracle, build, deploy to GitHub Pages.
  • 294 tests (261 unit and integration, 33 oracle) in 13 files. ~13,500 lines. 36 commits.

Screenshots

Beranda
Evidence - Correlation
Evidence - Histogram
Evidence - Sample
Evidence - Timeline
Evidence - Recovery

Hiring, or building something similar?

I'm open to full-time roles and select freelance work.