Slow queries kill application performance and are notoriously hard to debug without expertise. AI tools that analyze query plans, suggest index changes, and rewrite queries have made query optimization accessible to developers who don’t spend their days in EXPLAIN ANALYZE output.
Tools Covered
- EverSQL — Dedicated AI query optimizer, supports MySQL/Postgres/MariaDB
- Metis — Query intelligence platform with CI integration
- pganalyze — Postgres-focused monitoring with AI-powered index advisor
- Claude / ChatGPT — General LLMs with EXPLAIN output as context
Getting the Right Input
Before tools can help, you need the right input. For PostgreSQL:
-- Always use EXPLAIN (ANALYZE, BUFFERS) for actual runtime data
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT
o.order_id,
u.email,
SUM(oi.quantity * oi.unit_price_cents) AS total_cents
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at > NOW() - INTERVAL '30 days'
AND o.status = 'paid'
GROUP BY o.order_id, u.email
ORDER BY total_cents DESC
LIMIT 100;
Sample output showing the problems:
Limit (cost=15823.45..15823.70 rows=100) (actual time=342.891..342.912 rows=100)
Buffers: shared hit=289 read=8934
-> HashAggregate
-> Hash Join
-> Seq Scan on order_items (actual time=0.012..89.234 rows=280178)
-> Hash
-> Hash Join
-> Seq Scan on orders (actual time=0.008..38.234 rows=8003)
Filter: ((status = 'paid') AND (created_at > ...))
Rows Removed by Filter: 91997
Two problems visible: sequential scan on orders (no index on status, created_at) and sequential scan on order_items (no index on order_id).
EverSQL
EverSQL is a web-based tool where you paste a query and schema, and it returns optimization recommendations with reasoning.
Workflow:
- Paste your slow query
- Paste your table DDL (CREATE TABLE statements)
- Optionally paste EXPLAIN output
- Get back: rewritten query, index recommendations, explanation
EverSQL output for the above query:
CREATE INDEX idx_orders_status_created ON orders (status, created_at);
CREATE INDEX idx_order_items_order_id ON order_items (order_id);
-- Estimated improvement: 342ms -> ~8ms
EverSQL also suggests covering indexes that eliminate heap fetches:
CREATE INDEX idx_orders_covering ON orders (status, created_at, order_id, user_id);
Strengths: Fast, explains its reasoning, good for MySQL. Weaknesses: Estimates are approximate; doesn’t connect to your actual database statistics.
Metis
Metis integrates into your CI pipeline and flags slow queries before they reach production. It captures query plans from your test suite and surfaces regressions.
- name: Run Metis query analysis
uses: metis-data/metis-action@v1
with:
api-key: ${{ secrets.METIS_API_KEY }}
db-connection: ${{ secrets.DATABASE_URL }}
Metis captures EXPLAIN output for all queries run during the test suite and comments on PRs when a query plan regresses. A query that was fast with 1,000 rows in tests may be slow with 10M rows in production — Metis catches this by analyzing the query plan and flagging sequential scans that don’t scale.
pganalyze
pganalyze is a Postgres monitoring platform. The Index Advisor uses statistics from your actual production database to recommend indexes based on real workload patterns.
curl https://packages.pganalyze.com/setup.sh | bash
pganalyze-collector --setup
After connecting, the Index Advisor shows:
Recommended: CREATE INDEX CONCURRENTLY idx_orders_status_created ON orders (status, created_at);
Estimated impact:
- Affects 23 query patterns seen in last 7 days
- Total query time reduction: ~4.2 hours/day
- Current total time: 6.8 hours/day (62% reduction)
The time-reduction estimates come from actual query execution data, making them far more reliable than synthetic estimates from EverSQL.
Using Claude/ChatGPT for Query Optimization
General LLMs can explain query plans and suggest optimizations when given sufficient context:
I have a slow PostgreSQL query taking 342ms. Here is the EXPLAIN (ANALYZE, BUFFERS) output:
[paste full EXPLAIN output]
Here are the relevant table definitions:
[paste CREATE TABLE statements]
Table sizes:
- orders: 100,000 rows
- order_items: 280,000 rows
What indexes would help? Show the CREATE INDEX statements and explain why each helps.
Claude’s response correctly identifies both sequential scans and recommends:
-- Partial index (smaller, only indexes rows that match the predicate)
CREATE INDEX CONCURRENTLY idx_orders_paid_recent
ON orders (status, created_at DESC)
WHERE status = 'paid';
-- Index for order_items join
CREATE INDEX CONCURRENTLY idx_order_items_order_id
ON order_items (order_id);
-- Covering index to avoid heap fetch for orders table
CREATE INDEX CONCURRENTLY idx_orders_paid_recent_covering
ON orders (status, created_at DESC)
INCLUDE (order_id, user_id)
WHERE status = 'paid';
Claude also explains why the partial index is better than a full composite index (smaller index, only indexes rows matching the predicate). The limitation: without connecting to your actual database, it cannot account for column correlation, TOAST access patterns, or vacuum state.
Scenario: N+1 Query Detection and Fixing
A common optimization challenge:
# Slow: N+1 queries
def get_users_with_posts(limit: int = 100):
users = User.query.limit(limit) # 1 query
result = []
for user in users:
posts = Post.query.filter_by(user_id=user.id).all() # 100 queries
result.append({"user": user, "posts": posts})
return result
Claude analysis:
Paste this function and I'll identify the N+1 query pattern:
- Initial query loads 100 users
- Loop runs 100 iterations, each querying posts
- Total: 101 queries
Fix:
posts = Post.query.filter(Post.user_id.in_([u.id for u in users])).all()
posts_by_user = group_by_user_id(posts)
for user in users:
user_posts = posts_by_user[user.id]
EverSQL analysis: Would require manually identifying N+1, doesn’t auto-detect from code.
Metis detection: If run in test suite, Metis captures each query execution and counts. It would flag “100 identical queries with different parameters” as a red flag.
Cost-Benefit Analysis by Tool
| Scenario | Best Tool | Why |
|---|---|---|
| Quick one-off query analysis | Claude | Free, no setup, instant answers |
| Catch performance regressions in CI | Metis | Integrated test suite analysis |
| Production optimization at scale | pganalyze | Real workload statistics, measurable impact |
| MySQL/MariaDB without pganalyze | EverSQL | Purpose-built for MySQL ecosystem |
| Learning query optimization | Claude | Explains reasoning for recommendations |
Integration with Development Workflow
Before Deployment: Using Metis in CI
# .github/workflows/test.yml
- name: Run tests with query analysis
run: pytest tests/
- name: Analyze slow queries
uses: metis-data/metis-action@v1
with:
api-key: ${{ secrets.METIS_API_KEY }}
db-connection: postgresql://test_db:5432/testdb
- name: Comment on PR if regressions found
if: ${{ always() }}
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('metis-report.json'));
if (report.regressions.length > 0) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠ Query Performance Regression Detected\n${report.summary}`
});
}
This workflow catches slow queries before they reach production — tests that take 2 seconds locally may take 20 seconds with production data volume.
Complex Optimization: Materialized Views
For expensive aggregations that are queried repeatedly:
-- Original query (expensive, runs on-demand)
SELECT
category,
DATE(order_date) as order_day,
SUM(amount) as daily_revenue,
COUNT(*) as order_count
FROM orders
GROUP BY category, DATE(order_date)
ORDER BY order_date DESC;
-- Materialized view (pre-computed nightly)
CREATE MATERIALIZED VIEW daily_category_revenue AS
SELECT
category,
DATE(order_date) as order_day,
SUM(amount) as daily_revenue,
COUNT(*) as order_count
FROM orders
GROUP BY category, DATE(order_date);
CREATE INDEX idx_daily_revenue_category ON daily_category_revenue (category, order_day DESC);
Claude prompt:
I run this aggregation query 1,000 times per day. It joins three tables
and groups by two fields. Average current runtime: 850ms.
Should I create a materialized view? If yes, how do I handle incremental
updates? Can I avoid full refresh every night?
Claude suggests incremental refresh strategies:
- Partition the view by date
- Only refresh the last N days instead of full history
- Use triggers to update pre-aggregated summary tables
- Consider trade-off: materialized view maintenance cost vs query speed gain
Head-to-Head Comparison
| Tool | Accuracy | Requires DB Access | CI Integration | Cost | Best For |
|---|---|---|---|---|---|
| EverSQL | Good | No | No | Freemium | Quick one-off analysis |
| Metis | Very Good | Yes (test DB) | Yes | Paid | Catching CI regressions |
| pganalyze Index Advisor | Excellent | Yes (production) | Partial | Paid | Production optimization |
| Claude / GPT-4o | Good | No | Via prompt | API cost | Ad-hoc understanding |
Practical Recommendation
For most engineering teams:
- pganalyze for Postgres performance — the workload-based index advisor is the only tool with actual production statistics.
- Metis in CI to catch query regressions before production.
- Claude with EXPLAIN output for ad-hoc understanding — paste the query plan and ask it to explain what’s happening.
- EverSQL for MySQL shops where pganalyze doesn’t apply.
Related Articles
- AI Tools for Database Performance Optimization Query
- Best AI Tools for SQL Query Optimization and Database
- Best AI Assistant for SQL Query Optimization
- Best AI Tools for SQL Query Optimization 2026: EverSQL.
- AI-Powered Database Migration Tools Comparison 2026
Built by theluckystrike — More at zovo.one