[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Database Index Advisor

Build a Database Index Advisor

Build a database index advisor that analyzes query patterns, recommends optimal indexes, detects unused indexes, estimates impact, and generates migration scripts for PostgreSQL optimization.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Peter leads backend at a 25-person SaaS. Database performance degrades as data grows — queries that were fast on 100K rows crawl on 10M rows. They have 50 indexes but some are unused (wasting disk and slowing writes), while missing indexes cause sequential scans on large tables. Adding an index is trial-and-error: sometimes it helps, sometimes it makes things worse. They need an index advisor: analyze actual query patterns from pg_stat_statements, recommend indexes based on WHERE/JOIN/ORDER BY clauses, detect unused indexes, estimate impact, and generate ready-to-run migration SQL.

Step 1: Build the Index Advisor

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

interface IndexRecommendation { table: string; columns: string[]; type: "btree" | "gin" | "gist" | "hash"; reason: string; estimatedImpact: string; queriesAffected: number; createSQL: string; }
interface UnusedIndex { name: string; table: string; columns: string; size: string; scans: number; reason: string; dropSQL: string; }

// Analyze queries and recommend indexes
export async function analyzeAndRecommend(): Promise<{ recommendations: IndexRecommendation[]; unusedIndexes: UnusedIndex[]; existingIndexes: number; totalTableSize: string }> {
  const recommendations: IndexRecommendation[] = [];

  // Get slow queries from pg_stat_statements
  const { rows: slowQueries } = await pool.query(
    `SELECT query, calls, mean_exec_time, rows FROM pg_stat_statements WHERE mean_exec_time > 100 AND calls > 10 ORDER BY mean_exec_time * calls DESC LIMIT 50`
  ).catch(() => ({ rows: [] }));

  // Analyze sequential scans
  const { rows: seqScans } = await pool.query(
    `SELECT schemaname, relname, seq_scan, seq_tup_read, idx_scan, n_live_tup
     FROM pg_stat_user_tables WHERE seq_scan > 100 AND n_live_tup > 10000
     ORDER BY seq_tup_read DESC LIMIT 20`
  );

  for (const table of seqScans) {
    if (table.seq_scan > table.idx_scan * 2 && table.n_live_tup > 50000) {
      // Find which columns are frequently in WHERE clauses
      const relatedQueries = slowQueries.filter((q: any) => q.query.toLowerCase().includes(table.relname.toLowerCase()));
      const whereColumns = extractWhereColumns(relatedQueries.map((q: any) => q.query), table.relname);

      for (const col of whereColumns) {
        // Check if index already exists
        const { rows: existing } = await pool.query(
          `SELECT indexname FROM pg_indexes WHERE tablename = $1 AND indexdef LIKE $2`,
          [table.relname, `%${col}%`]
        );
        if (existing.length === 0) {
          const indexName = `idx_${table.relname}_${col}`;
          recommendations.push({
            table: table.relname, columns: [col], type: "btree",
            reason: `Table '${table.relname}' has ${table.seq_scan} seq scans vs ${table.idx_scan} idx scans with ${table.n_live_tup} rows. Column '${col}' appears in WHERE clauses of slow queries.`,
            estimatedImpact: `Could reduce seq scans by ~${Math.round((table.seq_scan / (table.seq_scan + table.idx_scan)) * 100)}%`,
            queriesAffected: relatedQueries.length,
            createSQL: `CREATE INDEX CONCURRENTLY ${indexName} ON ${table.relname} (${col});`,
          });
        }
      }
    }
  }

  // Composite index recommendations for multi-column WHERE
  for (const q of slowQueries) {
    const compositeColumns = extractCompositeWhere(q.query);
    if (compositeColumns.length >= 2) {
      const table = compositeColumns[0].table;
      const cols = compositeColumns.map((c: any) => c.column);
      const indexName = `idx_${table}_${cols.join("_")}`.slice(0, 63);
      const { rows: existing } = await pool.query(
        `SELECT indexname FROM pg_indexes WHERE tablename = $1 AND indexdef LIKE $2`,
        [table, `%(${cols.join(", ")})%`]
      );
      if (existing.length === 0) {
        recommendations.push({
          table, columns: cols, type: "btree",
          reason: `Slow query (avg ${Math.round(q.mean_exec_time)}ms, ${q.calls} calls) uses WHERE with columns ${cols.join(", ")}`,
          estimatedImpact: `Could improve query from ${Math.round(q.mean_exec_time)}ms to <10ms`,
          queriesAffected: 1,
          createSQL: `CREATE INDEX CONCURRENTLY ${indexName} ON ${table} (${cols.join(", ")});`,
        });
      }
    }
  }

  // Detect unused indexes
  const { rows: unused } = await pool.query(
    `SELECT indexrelname as name, relname as table, pg_size_pretty(pg_relation_size(indexrelid)) as size,
       idx_scan as scans, indexdef
     FROM pg_stat_user_indexes JOIN pg_indexes ON indexrelname = indexname
     WHERE idx_scan = 0 AND indexrelname NOT LIKE 'pg_%' AND indexrelname NOT LIKE '%_pkey'
     ORDER BY pg_relation_size(indexrelid) DESC`
  );

  const unusedIndexes: UnusedIndex[] = unused.map((idx: any) => ({
    name: idx.name, table: idx.table, columns: idx.indexdef.match(/\((.+)\)/)?.[1] || "",
    size: idx.size, scans: idx.scans, reason: "Zero scans since last stats reset",
    dropSQL: `DROP INDEX CONCURRENTLY ${idx.name};`,
  }));

  // Stats
  const { rows: [{ count: indexCount }] } = await pool.query("SELECT COUNT(*) as count FROM pg_indexes WHERE schemaname = 'public'");
  const { rows: [{ size: totalSize }] } = await pool.query("SELECT pg_size_pretty(pg_database_size(current_database())) as size");

  return { recommendations: recommendations.slice(0, 20), unusedIndexes, existingIndexes: parseInt(indexCount), totalTableSize: totalSize };
}

function extractWhereColumns(queries: string[], tableName: string): string[] {
  const columns = new Set<string>();
  for (const query of queries) {
    const whereMatch = query.match(/WHERE\s+(.+?)(?:ORDER|GROUP|LIMIT|$)/is);
    if (!whereMatch) continue;
    const whereClauses = whereMatch[1];
    const colMatches = whereClauses.matchAll(/(\w+)\s*(?:=|>|<|>=|<=|<>|LIKE|IN|IS|BETWEEN)/gi);
    for (const match of colMatches) {
      const col = match[1].toLowerCase();
      if (!['and', 'or', 'not', 'null', 'true', 'false'].includes(col)) columns.add(col);
    }
  }
  return [...columns];
}

function extractCompositeWhere(query: string): Array<{ table: string; column: string }> {
  const fromMatch = query.match(/FROM\s+(\w+)/i);
  if (!fromMatch) return [];
  const table = fromMatch[1];
  const columns = extractWhereColumns([query], table);
  return columns.map((col) => ({ table, column: col }));
}

// Generate migration file
export function generateMigration(recommendations: IndexRecommendation[]): string {
  let migration = `-- Auto-generated index migration\n-- Generated at: ${new Date().toISOString()}\n\n-- UP\n`;
  for (const rec of recommendations) {
    migration += `-- ${rec.reason}\n${rec.createSQL}\n\n`;
  }
  migration += `-- DOWN\n`;
  for (const rec of recommendations) {
    const indexName = rec.createSQL.match(/INDEX CONCURRENTLY (\w+)/)?.[1];
    migration += `DROP INDEX IF EXISTS ${indexName};\n`;
  }
  return migration;
}

Results

  • Missing indexes found — advisor identified 8 tables with high seq_scan/idx_scan ratio; adding 5 indexes reduced avg query time from 800ms to 15ms
  • Unused indexes cleaned — 12 indexes (2.3GB) with zero scans dropped; write performance improved 15%; disk space recovered
  • Composite index recommended — WHERE clause with tenant_id AND status AND created_at → composite index; query: 2500ms → 3ms
  • Ready-to-run SQL — CREATE INDEX CONCURRENTLY generated; copy-paste to migration; CONCURRENTLY means no table lock; safe for production
  • Impact estimated — "Could reduce seq scans by ~85%" and "improve from 800ms to <10ms"; DBA prioritizes high-impact indexes first

Related use cases