ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-04-26Advanced

A Multi-Agent SQL Tuning Workflow with Antigravity — Splitting Analysis, Strategy, Implementation, and Verification

Treat SQL tuning as a relay race instead of a single chat. This guide shows how to wire four specialised Antigravity agents — Analyzer, Strategist, Implementer, Verifier — into a repeatable workflow you can actually trust in production.

antigravity435sqlpostgresql5multi-agent50performance12database4

The strange thing about database performance is that it almost never breaks during development. Tables stay small, queries return in milliseconds, and you ship with a quiet confidence that everything is fine. Then one morning, several months later, the dashboard takes ten seconds to load and you find yourself staring at an EXPLAIN ANALYZE output that looks like a tax form filled in by someone hostile.

I've been there more times than I'd like to admit while running a small portfolio of indie apps. The pattern is always similar — different bottleneck, same human reaction. You squint at the plan, guess at an index, write a migration, hope for the best. When you do this alone with a single chat AI session, the AI will happily nod along and suggest the most generic answer it can find, because that's what undifferentiated context invites. The output is plausible. It is also frequently wrong.

What I've found genuinely useful, after a few false starts, is to treat SQL tuning as a relay race instead of a single conversation, and let Antigravity's multi-agent capability play four different roles. Each role is small, each role is constrained, and each role is forbidden from doing the other roles' jobs. This article walks through that workflow end to end, with the prompts, sample outputs, and the mistakes I made before it stabilised.

Why splitting the workflow matters more than picking a smarter prompt

When you paste an EXPLAIN ANALYZE blob into one chat and say "make this faster," the AI is forced to make several decisions at once: parse the plan, judge the bottleneck, propose a fix, predict side effects, and write a migration. With ambiguous priorities and limited tokens, it tends to default to "add an index" because that answer is the safest in the abstract. It will almost never be the answer that is best for your specific table size, write pattern, and existing index footprint.

Three things change once you split the work across multiple agents.

First, single-purpose prompts produce sharper outputs. A prompt that only asks "extract facts from this plan" cannot drift into recommendations. A prompt that only asks "propose three candidates with trade-offs" has nowhere to hide a half-baked guess, because the schema requires three concrete options with concrete numbers. Constraint, not creativity, is what makes AI output reliable here.

Second, separate logs make decisions auditable. Three months from now, when a different engineer (or your future self) wonders why you added a particular index, having distinct outputs from the analysis stage and the strategy stage means you can replay the reasoning step by step instead of trusting your memory. This matters more for solo developers than it does for teams, because you don't have a colleague to remember the context for you.

Third, it maps cleanly onto the Manager Surface. Antigravity already supports running sub-agents in parallel and gathering their outputs into a single review. If you haven't used the Manager Surface before, the Antigravity Manager Surface complete guide covers the basics — the workflow below assumes you can run four named agents from one Manager view and inspect their handoffs.

There's a quieter benefit too. When the four agents disagree — and they will — the disagreement itself is informative. If the Analyzer says "row estimation error is 10x" and the Strategist somehow returns three index candidates without a rewrite, you immediately see the gap and can adjust the Strategist's prompt. A single chat session would have hidden that mismatch under one fluent paragraph.

The four roles, and what each one is forbidden from doing

Here's the full cast for the workflow I run on my own projects. Each one is a Custom Agent in Antigravity with its own short system prompt.

  • Analyzer: Takes a JSON EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) output and produces a structured summary of the slowest node, the row-estimation error, and any suspicious buffer pattern. It is forbidden from suggesting fixes.
  • Strategist: Takes only the Analyzer's structured summary (not the original SQL) and produces three candidates spanning index addition, query rewrite, and schema change. Each candidate must include trade-offs and a confidence score. It is forbidden from emitting fewer than three candidates.
  • Implementer: Takes the chosen candidate and produces a production-grade migration file. It is forbidden from omitting IF NOT EXISTS, downtime estimates, and rollback steps.
  • Verifier: Runs the migration on a staging replica, re-executes the query, and reports a before/after diff. It is forbidden from emitting APPROVED if the row-estimation error did not improve.

The "forbidden" lines are the most important parts of these prompts. It's tempting to soften them ("avoid", "try not to") but the moment you do, the agents start hedging and the workflow collapses back into a single fuzzy conversation. Hard prohibitions are what make the relay race work.

Below is the actual Strategist prompt I use today. Notice how short it is — under 200 words.

You are a SQL Tuning Strategist for PostgreSQL 16.
You receive a JSON object describing the slowest node of an EXPLAIN ANALYZE plan.
 
Your output MUST be a JSON array of 3 candidates with the following shape:
 
  {
    "kind": "index" | "rewrite" | "schema",
    "summary": "...",
    "ddl_or_sql": "...",
    "tradeoffs": {
      "write_amplification": "low" | "medium" | "high",
      "storage_estimate_mb": number,
      "affects_other_queries": string[]
    },
    "confidence": 0.0 - 1.0
  }
 
Rules:
- Never propose more than one new index per candidate.
- If the plan shows a row-count estimation error >= 10x, ALWAYS include a "rewrite" candidate that fixes statistics or adds a manual hint.
- Reject any "schema" candidate whose downtime estimate is unknown.

If you also rely on Antigravity's Browser Agent for separate parts of your workflow, the browser sub-agent practical guide is a good companion — keeping each sub-agent focused on its lane is the same idea applied to UI testing.

Step 1: Let the Analyzer turn raw plans into structured truth

The first temptation when you ask AI for help with a slow query is to paste the textual EXPLAIN ANALYZE output. Don't. Always run it with FORMAT JSON so both the AI and your future self can parse it deterministically.

-- We want a JSON output containing per-node cost, actual time, and buffer hit ratio.
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON, VERBOSE)
SELECT u.id, u.email, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at >= NOW() - INTERVAL '90 days'
GROUP BY u.id, u.email
ORDER BY order_count DESC
LIMIT 50;

When I fed this into the Analyzer on a real project, the response below came back. Notice the last field — the Analyzer is explicitly required to hand a question over to the next role rather than concluding on its own.

{
  "bottleneck_node": {
    "node_type": "Seq Scan",
    "relation": "orders",
    "actual_rows": 4821000,
    "expected_rows": 482100,
    "row_estimate_error": "10x underestimate",
    "actual_time_ms": 2840,
    "buffers_read": 187432
  },
  "summary": "A sequential scan on orders with a 10x row-estimate underestimate. Either user_id has no usable index, or ANALYZE is stale.",
  "next_question_for_strategist": "Evaluate the impact of a composite index on (user_id, created_at) against existing query patterns."
}

That last line, the handoff question, is where this workflow stops being a gimmick and starts being useful. By construction, the Analyzer cannot trick itself into recommending an index. It can only describe the world. Notice also that the summary mentions a possibility ("ANALYZE is stale") that the Strategist will need to consider — if statistics are simply out of date, no index in the world will fix the planner's behaviour permanently.

A subtle but important detail: I keep the Analyzer's prompt explicit about what counts as a bottleneck. By default I tell it to flag any node whose actual time is more than 30% of the total query time. Without that threshold, it sometimes picks an early node that "looks" expensive but contributes nothing meaningful to the wall-clock time you actually feel.

Step 2: Make the Strategist propose three options, not one

The Strategist receives only the JSON above — not the original SQL, not the schema, not your aspirations. This is deliberate. When I let it see the original SQL, it gravitated toward "add an index" because rewriting feels riskier in isolation. With only the bottleneck description, it considers all three kinds of solutions on a level playing field.

Here's an actual response I got for the example above.

[
  {
    "kind": "index",
    "summary": "Composite index on orders(user_id, created_at DESC)",
    "ddl_or_sql": "CREATE INDEX CONCURRENTLY idx_orders_user_id_created_at ON orders (user_id, created_at DESC);",
    "tradeoffs": {
      "write_amplification": "medium",
      "storage_estimate_mb": 240,
      "affects_other_queries": ["GET /users/:id/orders", "monthly admin report"]
    },
    "confidence": 0.72
  },
  {
    "kind": "rewrite",
    "summary": "Filter users first via CTE, then join to orders",
    "ddl_or_sql": "WITH recent_users AS (SELECT id, email FROM users WHERE created_at >= NOW() - INTERVAL '90 days') SELECT u.id, u.email, COUNT(o.id) FROM recent_users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.email ORDER BY 3 DESC LIMIT 50;",
    "tradeoffs": {
      "write_amplification": "low",
      "storage_estimate_mb": 0,
      "affects_other_queries": []
    },
    "confidence": 0.83
  },
  {
    "kind": "schema",
    "summary": "Maintain a denormalised last_30d_order_count column on users",
    "ddl_or_sql": "ALTER TABLE users ADD COLUMN last_30d_order_count INT DEFAULT 0; -- updated via async job",
    "tradeoffs": {
      "write_amplification": "high",
      "storage_estimate_mb": 80,
      "affects_other_queries": ["users list SELECT"]
    },
    "confidence": 0.41
  }
]

The interesting result is that the rewrite candidate has the highest confidence. Rewrites tend to be safer than they look, because they touch a single query rather than a shared index that ripples across the system. If you're using an ORM-driven codebase, Antigravity with Drizzle ORM for type-safe databases covers how to keep that rewrite type-safe at the application layer without losing the query shape the planner expects.

A point of personal preference: I almost always start by trying the rewrite candidate, even when the index candidate looks attractive on paper. Indexes feel cheap because you don't change application code, but they're paying ongoing storage and write amplification costs forever, and adding three or four of them quietly while debugging is how you end up with a table whose write throughput halves over a year. Rewrites are reversible in a deploy. Indexes, in practice, almost never get removed.

Step 3: Hand the chosen candidate to the Implementer

Once you (the human) pick a candidate, the Implementer turns it into a migration that is safe to run in production. My system prompt enforces three things every time.

  • Indexes use CREATE INDEX CONCURRENTLY to avoid blocking writes.
  • The migration file does not wrap statements in BEGIN/COMMIT, because CONCURRENTLY cannot run inside a transaction.
  • Comments must spell out downtime estimate, rollback procedure, and a pre-deploy verification command.

Here's what came back for the index candidate above.

-- Migration: 20260426_add_orders_user_created_idx.sql
-- Purpose: Speed up "recent active users with order count" report
-- Estimated downtime: 0s (CONCURRENTLY)
-- Estimated build time on staging (3M rows): ~45s
-- Rollback: DROP INDEX CONCURRENTLY IF EXISTS idx_orders_user_id_created_at;
-- Pre-deploy verification:
--   EXPLAIN (ANALYZE, BUFFERS) <<the original query>>
--   ↑ Bottleneck node should change from "Seq Scan on orders" to "Index Scan".
 
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_user_id_created_at
  ON orders (user_id, created_at DESC);
 
-- ANALYZE so the planner picks up new statistics immediately.
ANALYZE orders;

The IF NOT EXISTS clause is the kind of detail an AI will skip unless you tell it to care. I learned to add "the migration must be idempotent on re-apply" to the Implementer's prompt after watching one too many CI runs fail in retry loops, because the previous attempt had partially succeeded and the next attempt found the index already existed.

One more nuance: the Implementer is also responsible for emitting the ANALYZE after the index. PostgreSQL doesn't always pick up new statistics fast enough on its own, and a fresh ANALYZE immediately makes the planner behave consistently in production tests. Without it, you sometimes see the index get used in staging but ignored in production for the first few minutes after deploy, which is the exact kind of "Heisenbug" you don't want to debug under pressure.

Step 4: Make the Verifier run the experiment

The Verifier is the role that tells you whether you actually solved the problem. With Antigravity's Terminal Tool enabled, it can psql into a staging replica, run the migration, re-execute the query, and produce a diff report.

=== Verifier Report (2026-04-26 05:18 JST) ===
Query: monthly_active_users_top50
 
Before
  Bottleneck: Seq Scan on orders
  Total time: 2840 ms
  Buffers: shared hit=1182 read=187432
 
After
  Bottleneck: Index Scan using idx_orders_user_id_created_at
  Total time: 312 ms (-89%)
  Buffers: shared hit=4019 read=2840 (-98% read)
 
Verdict: APPROVED
  Notes:
    - Row estimation error improved from 10x to 1.2x
    - Existing query "GET /users/:id/orders" plan unchanged, no extra cost observed

When the Verdict turns APPROVED, the Manager pings me for a final human review. I deliberately do not let the workflow auto-deploy. The point of automation here isn't to remove human judgment, it's to bring the right information to the moment when human judgment is needed.

A useful pattern is to have the Verifier also produce a small "what would worry me" section listing planner choices that surprised it, even if they aren't blockers. For example, if the new plan still uses a Bitmap Heap Scan instead of an Index Only Scan, the Verifier should flag that for human attention rather than silently approve. Most of these flags turn out to be fine, but the rare time they aren't, you'll be glad you noticed.

A concrete example from my own logs: on one tuning session, the Verifier reported that the new query was 40% faster but that the planner had switched from a Hash Aggregate to a GroupAggregate, which is more memory-efficient at scale but slightly slower for small result sets. The Verdict was still APPROVED, but the flag prompted me to check that the new plan would still hold up when the result set grew. It did, and I shipped — but I would not have noticed that subtle plan change in a single-session conversation that produced only a final summary.

Five things even this workflow won't decide for you

After running this for a while, I keep a checklist of things I review by hand before any migration goes to production. They aren't AI weaknesses so much as judgment calls that need context the agents don't have.

The first is the impact of a new index on disk and backups. A 240 MB estimate is fine on paper, but on a managed Postgres tier with constrained I/O and a tight backup window, the practical cost can be much higher. If you're on a cloud provider that charges for storage, an index that adds half the table size again can quietly raise your monthly bill in a way that isn't reflected in the Strategist's confidence number. A concrete number that surprised me on a managed Postgres provider: a 240 MB index extended the nightly logical backup by 12 minutes, which pushed it past my replication lag tolerance window during the European morning. I had to reschedule the backup, which was a kind of cost the Strategist couldn't possibly have predicted from the structured input alone.

The second is whether a query rewrite breaks application caching assumptions. If your Redis or CDN caches a rendering of the old query shape, changing the SQL can silently invalidate keys you forgot existed. The Antigravity with Upstash Redis edge cache and rate limit guide goes into how to audit those assumptions deliberately. Even if your cache layer is local, query-shape changes can ripple through generated TypeScript types and break unrelated consumers at compile time.

The third is statistics freshness. Adding ANALYZE orders; is necessary but not always sufficient. On very large tables you'll often want to bump default_statistics_target first, then re-ANALYZE. I never let the AI change planner settings on its own — those changes have global effects and need to be deliberate, ideally with a load test behind them.

The fourth is propagation lag to read replicas. In multi-region setups, an index that exists on the primary may take time to appear on replicas, and during that gap your query plan will flicker depending on which replica handled the request. Plan deploy windows accordingly, and consider running the migration during a low-traffic period the first time you do this.

The fifth is a written rollback condition. Even with a green Verdict, you need to define in advance what symptom triggers a rollback — a CPU spike, a p99 latency regression, a queue length crossing a threshold. Without that, you'll be making the call at 2 AM with bad sleep and worse data, and "should we revert" turns into a debate instead of a decision.

Mistakes I made before this workflow stabilised

A few traps I fell into early, in case they save you a week.

Pasting the textual plan into the Analyzer. Always use FORMAT JSON. Textual plans look readable to humans but introduce parsing ambiguity for the AI, and you lose the structured row-estimate information the Strategist needs. Worse, the textual plan format has subtle indentation rules that vary across PostgreSQL versions; the AI sometimes reads "Seq Scan" as nested under the wrong join, which produces an entirely fictional bottleneck description.

Letting the Strategist see the original SQL. Counterintuitive but consequential. The Strategist's job is to react to the bottleneck description, not the surface-level query. Withholding the SQL forces it to consider rewrites on equal footing with indexes. The first time I tried this workflow with the SQL included, every candidate the Strategist produced was an index — when I removed the SQL, the rewrite suggestions were often the highest-confidence answers.

Trusting confidence scores absolutely. A 0.83 means "the model thinks this is a good idea given the structured input." It does not mean "this will work in production." Use it for prioritisation, not for green-lighting. I treat the confidence field the way I treat a weather forecast — useful for planning, but I still bring an umbrella.

Skipping the Verifier when the Implementer's output looks pretty. The temptation to ship a clean migration is real. Don't. The Verifier is what closes the loop on whether the row-estimation error actually improved. I once shipped a beautiful index that the planner ignored entirely because I'd put the columns in the wrong order — the Verifier would have caught it in 30 seconds.

Throwing away agent logs. I save every agent's output as Markdown into a db-tuning/ directory in the repo. Three months later, when a different bottleneck appears in the same area, having the previous decisions on disk is what keeps me from undoing my own work. It also makes it easier to revisit Strategist prompts that produced bad candidates and tighten the constraints over time.

Letting one agent "remember" the other agents' outputs through chat history. Each role should start fresh on every invocation. If you let the Strategist remember last week's session, it begins biasing toward whatever it suggested then, even when the new query has no relationship to the old one. I treat each tuning session as a fresh relay race — the agents have no memory between sessions, only the structured handoffs in the current one. Antigravity makes this easy by letting you scope context per agent invocation rather than per chat thread.

Running all four agents in fully automated mode. I tried this once during an experimental phase and immediately regretted it. The Verifier said APPROVED, the Manager auto-applied the migration to staging, and a side-effect on a different query went unnoticed for a day. The lesson stuck: the human checkpoint between Verifier approval and production deployment isn't a bottleneck, it's the safety mechanism that makes the rest of the automation worth running.

A small step you can try today

You don't need to wire up all four agents to start. The single most impactful change you can make today is to take your slowest query, run it with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON), paste the JSON into a fresh Antigravity session, and ask only for "the structured description of the bottleneck node, no recommendations." That's the Analyzer's job, manually.

Once that feels comfortable, peel off the next role. Add a Strategist that only sees the Analyzer's output. Then an Implementer that only sees the chosen candidate. Then a Verifier that only sees the migration plus the original query. By the time you've separated all four, you'll have a workflow that doesn't depend on you remembering everything at once — and that, honestly, is what I was hoping for when I started building this. SQL tuning stops being a personality trait and starts being a procedure.

If your team has only one person — yourself — this workflow buys you a useful illusion: it feels like you have a database review buddy who never gets tired and never gets defensive about their own suggestions. The relay structure is what produces that feeling, not any single prompt. You can rebuild the same workflow with any other agent runtime, but Antigravity's Manager Surface and Custom Agents are the lowest-friction way I've found to set it up without writing infrastructure code first.

A note on cost — the workflow is cheaper than it looks

A reasonable concern when reading any "use four agents" article is that you'll pay four times the API cost. In practice, the Analyzer's input is the largest (a JSON plan can be a few thousand tokens for a complex query), but its output is small. The Strategist's input is tiny (the Analyzer's summary), and its output is structured JSON, so it stays compact. The Implementer and Verifier both work on focused, scoped inputs. Across all four roles, my average tuning session ends up using fewer total tokens than a single open-ended "make this faster" conversation, because the long, rambling back-and-forth that single-session debugging produces simply doesn't happen here.

There's also a hidden cost in the alternative. A single-session AI that hallucinates an index recommendation can cost you a 240 MB index that runs for a year before anyone removes it, plus the developer time spent tracing why writes got slower. The dollar cost of an extra Strategist call is rounding error compared to that.

One thing I would not automate, even if I could

People sometimes ask whether the human approval step could be replaced by a sixth agent that "reviews" the Verifier's output. I've tried it. It doesn't work — or rather, it works in the average case and fails catastrophically in the edge case, which is the worst possible failure mode for a database change.

The reason is that production database decisions are rarely about the technical numbers. They're about whether tonight is a deployable night, whether the feature this query supports is critical right now, whether the on-call engineer has the bandwidth to revert. Those judgments live entirely outside the data the agents can see. Keeping a human at the final gate isn't a sign of incomplete automation; it's the design.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-05-01
Zero-Downtime Database Migrations with Antigravity: The Expand-Contract Pattern in Production
A complete production guide to running breaking schema changes—type swaps, column renames, table splits—with zero user-facing downtime, using the Expand-Contract pattern with Antigravity's AI assistance.
App Dev2026-03-28
Building Real-Time Full-Stack Apps with Antigravity and Supabase: A Practical Guide
Learn how to combine Antigravity IDE with Supabase to build full-stack apps featuring authentication, database management, and real-time sync — step by step.
App Dev2026-07-18
After Compose-First: Choosing Which View Screens to Migrate, Ranked by Churn Instead of Count
Google has declared Android development Compose-first. Here is how I rank View-based screens for migration using git history rather than screen counts, with the scoring script I actually ran and the three places partial migration quietly duplicates state.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →