A CLI tool that analyzes PostgreSQL databases for performance issues: unused / missing / duplicate indexes, foreign keys without indexes, slow queries, table bloat, dead tuples, VACUUM needs, and connection / cache health. Outputs structured JSON for automation or rich Markdown reports for humans.
Working with an AI agent? See .github/copilot-instructions.md for the integrated GitHub Copilot agent workflow and JSON contracts.
- Features
- Requirements
- Installation
- Configuration
- Usage
- Commands
- CLI options
- Output formats
- Health score
- Programmatic usage
- Managed PostgreSQL providers
- Troubleshooting
- Architecture
- Unused indexes (low / no scans, ready-to-run
DROP INDEXstatements). - Missing indexes (tables with high sequential scan activity).
- Duplicate / overlapping indexes.
- Foreign keys without supporting indexes.
- Slow queries via
pg_stat_statements(total / mean / min / max time, calls, hit ratio, rows). - Currently long-running queries.
- Blocking / blocked queries (lock graph).
- Largest tables, dead-tuple ratio, sequential vs. index scans.
- Table bloat detection.
- Tables needing
VACUUM/ANALYZE, with execution support.
- Database size, cache hit ratio, index hit ratio.
- Connection statistics (active / idle / total).
- Server info, configuration settings, installed extensions.
- Composite health score (0–100) with prioritized recommendations.
- Node.js >= 20
- pnpm >= 10
- PostgreSQL 12+
- (Optional but recommended)
pg_stat_statementsextension for slow-query analysis
pnpm install
cp .env.example .env
# edit .env with your connection details# Database connection (use `export` to make them available to ts-node)
export DB_HOST=your-host.example.com
export DB_PORT=5432
export DB_NAME=mydb
export DB_USER=postgres
export DB_PASSWORD=secret
export DB_SSL=true
# Alternative: standard libpq variables
# export PGHOST=...
# export PGPORT=...
# export PGDATABASE=...
# export PGUSER=...
# export PGPASSWORD=...pnpm start# Analysis (JSON output)
pnpm analyze # Full analysis + Markdown report
pnpm analyze:help # Help
pnpm analyze:health # Health score + key metrics
pnpm analyze:indexes # Unused indexes
pnpm analyze:queries # Slow queries
pnpm analyze:tables # Largest tables
pnpm analyze:fk # Foreign keys without indexes
pnpm analyze:connections # Connection statistics
pnpm analyze:config # PostgreSQL configuration
# Server info
pnpm server:info
# Index management
pnpm indexes:drop-sql # Generate DROP INDEX statements
# VACUUM
pnpm vacuum # Run VACUUM ANALYZE on tables that need it
pnpm vacuum:check # List tables that need VACUUM
# pg_stat_statements
pnpm pg-stat-statements:create
pnpm pg-stat-statements:drop
# Development
pnpm build # Compile TypeScript
pnpm lint # Biome check
pnpm lint:fix # Biome auto-fix# Source env first so DB_* are exported into the shell
. ./.env && npx ts-node index.ts -j -c <command>
# Examples
. ./.env && npx ts-node index.ts -j -c health
. ./.env && npx ts-node index.ts -j -c slow-queries
# Without env file
npx ts-node index.ts -h localhost -p 5432 -d mydb -U postgres -W secret --ssl -j -c health| Command | Description |
|---|---|
full |
Complete analysis (default) |
health |
Health score and metrics |
server-info |
Server version and info |
unused-indexes |
Indexes with low / no usage |
missing-indexes |
Tables with high sequential scan activity |
duplicate-indexes |
Overlapping indexes |
fk-without-indexes |
Foreign keys without supporting indexes |
generate-drop-sql |
Ready-to-run DROP INDEX statements |
slow-queries |
Top queries from pg_stat_statements |
long-running |
Currently long-running queries |
blocking |
Locks / blocking sessions |
tables |
Largest tables and table stats |
vacuum-needed |
Tables that need VACUUM |
run-vacuum |
Execute VACUUM ANALYZE |
connections |
Connection statistics |
config |
Configuration settings |
extensions |
Installed extensions |
create-pg-stat-statements |
Create pg_stat_statements extension |
drop-pg-stat-statements |
Drop pg_stat_statements extension |
| Option | Short | Description | Default |
|---|---|---|---|
--host |
-h |
Database host | localhost |
--port |
-p |
Database port | 5432 |
--database |
-d |
Database name | postgres |
--user |
-U |
Database user | postgres |
--password |
-W |
Database password | - |
--ssl |
Enable SSL | false |
|
--command |
-c |
Run a single command (see table) | full |
--json |
-j |
JSON output | false |
--quiet |
-q |
Suppress progress output | false |
--output |
-o |
Reports directory | ./reports |
--slow-query-threshold |
Slow-query threshold (ms) | 100 |
|
--min-index-scans |
Min scans to consider an index "used" | 50 |
|
--interactive |
-i |
Interactive menu | false |
Generated by pnpm analyze. Includes:
- Executive summary with health score
- Database metrics (cache / index hit ratio, dead tuples, connections)
- Index analysis (unused / missing / duplicate)
- Table analysis (largest, high dead tuples, high seq scans)
- Slow queries with recommendations
- Bloat analysis
- Prioritized recommendations
Saved to ./reports/db-analysis-{timestamp}.md.
{
"generatedAt": "2026-01-01T00:00:00.000Z",
"databaseName": "mydb",
"healthScore": 90,
"metrics": {
"databaseSize": "177 MB",
"cacheHitRatio": 99.98,
"indexHitRatio": 99.99,
"deadTuplesRatio": 4.2,
"totalConnections": 4,
"activeConnections": 3
},
"unusedIndexes": [],
"missingIndexes": [],
"duplicateIndexes": [],
"tableStats": [],
"slowQueries": [],
"bloatedTables": [],
"recommendations": []
}| Score | Status | Action |
|---|---|---|
| 90–100 | Excellent | Monitor only |
| 70–89 | Good | Plan optimization |
| 50–69 | Warning | Needs attention |
| 0–49 | Critical | Immediate action needed |
| Metric | Good | Warning | Critical |
|---|---|---|---|
| Cache hit ratio | > 95% | 90–95% | < 90% |
| Index hit ratio | > 95% | 90–95% | < 90% |
| Dead tuples ratio | < 5% | 5–10% | > 10% |
import { DatabaseAnalyzer } from "@deniscuciuc/db-analyzer-postgres";
const analyzer = new DatabaseAnalyzer({
host: "localhost",
port: 5432,
database: "mydb",
user: "postgres",
password: "password",
ssl: { rejectUnauthorized: false },
});
const report = await analyzer.analyze();
analyzer.printSummary(report);
await analyzer.generateReport(report);
await analyzer.close();For full slow-query text and pg_stat_statements visibility, connect with a privileged user (superuser or a role with pg_read_all_stats). Restricted users see <insufficient privilege> instead of query text.
-- Check availability
SELECT * FROM pg_available_extensions WHERE name = 'pg_stat_statements';
-- Create
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Grant access to other users (optional)
GRANT pg_read_all_stats TO your_app_user;Or use the npm script: pnpm pg-stat-statements:create.
| Symptom | Cause | Fix |
|---|---|---|
ECONNREFUSED |
Cannot reach the database | Check host / port and network |
SSL required |
Server requires TLS | Add --ssl or DB_SSL=true |
permission denied for ... |
User lacks privileges | Use a superuser or GRANT pg_read_all_stats TO your_user; |
relation pg_stat_statements ... does not exist |
Extension missing | pnpm pg-stat-statements:create |
| Empty slow queries | No traffic since last reset, or threshold too high | Wait for traffic, or lower --slow-query-threshold |
db-analyzer-postgres/
├── index.ts # Entry point + CLI
├── package.json
├── src/
│ ├── types.ts # Shared types
│ ├── queries.ts # SQL queries
│ ├── interactive.ts # Interactive CLI
│ ├── analyzers/
│ │ ├── index-analyzer.ts
│ │ ├── query-analyzer.ts
│ │ └── table-analyzer.ts
│ ├── collectors/stats-collector.ts
│ └── reporters/report-generator.ts
├── .github/copilot-instructions.md # AI agent workflow
├── .env.example
└── reports/ # Generated reports (gitignored)
| Class | Responsibility |
|---|---|
IndexAnalyzer |
Unused / missing / duplicate / FK indexes |
QueryAnalyzer |
Slow / long-running / blocking queries |
TableAnalyzer |
Stats, bloat, dead tuples, VACUUM |
StatsCollector |
Metrics, connections, configuration |
ReportGenerator |
Markdown + JSON report output |
Contributions are welcome! Here's how to get started:
- Fork the repo and create a feature branch
- Run
pnpm installto install dependencies - Copy
.env.exampleto.envand configure your connection - Make your changes and ensure
pnpm lintpasses - Open a pull request
Coding standards:
- TypeScript with strict mode enabled
- Code formatted with Biome (tab indent, 2-space width)
- All changes must pass
pnpm lintandpnpm build
MIT — see LICENSE.