@vasoyaprince14/sql-analyzer
Version:
🚀 Enhanced SQL database analyzer with AI-powered insights, comprehensive security analysis, RLS policy auditing, and beautiful HTML reports
452 lines • 18.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DatabaseHealthAuditor = void 0;
class DatabaseHealthAuditor {
constructor(client) {
this.client = client;
}
/**
* Perform comprehensive database health audit
*/
async performHealthAudit() {
console.log('🔍 Starting comprehensive database health audit...');
const [databaseInfo, schemaHealth, indexAnalysis, performanceIssues, securityIssues, costAnalysis] = await Promise.all([
this.getDatabaseInfo(),
this.analyzeSchemaHealth(),
this.analyzeIndexes(),
this.detectPerformanceIssues(),
this.detectSecurityIssues(),
this.analyzeCosts()
]);
const optimizationRecommendations = this.generateOptimizationRecommendations(schemaHealth, indexAnalysis, performanceIssues, securityIssues);
const maintenanceRecommendations = this.generateMaintenanceRecommendations();
return {
databaseInfo,
schemaHealth,
indexAnalysis,
performanceIssues,
securityIssues,
optimizationRecommendations,
costAnalysis,
maintenanceRecommendations
};
}
/**
* Get comprehensive database information
*/
async getDatabaseInfo() {
console.log('📊 Gathering database information...');
const versionResult = await this.client.query('SELECT version()');
const sizeResult = await this.client.query(`
SELECT pg_size_pretty(pg_database_size(current_database())) as size
`);
const tableCountResult = await this.client.query(`
SELECT COUNT(*) as count
FROM information_schema.tables
WHERE table_schema = 'public'
`);
const indexCountResult = await this.client.query(`
SELECT COUNT(*) as count
FROM pg_indexes
WHERE schemaname = 'public'
`);
const connectionsResult = await this.client.query(`
SELECT
setting::int as max_connections,
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_connections
FROM pg_settings
WHERE name = 'max_connections'
`);
const settingsResult = await this.client.query(`
SELECT name, setting, unit
FROM pg_settings
WHERE name IN (
'shared_buffers', 'effective_cache_size', 'work_mem',
'maintenance_work_mem', 'checkpoint_completion_target',
'wal_buffers', 'random_page_cost'
)
`);
const settings = {};
settingsResult.rows.forEach(row => {
settings[row.name] = row.unit ? `${row.setting} ${row.unit}` : parseFloat(row.setting);
});
return {
version: versionResult.rows[0].version,
size: sizeResult.rows[0].size,
tableCount: parseInt(tableCountResult.rows[0].count),
indexCount: parseInt(indexCountResult.rows[0].count),
connectionInfo: {
maxConnections: connectionsResult.rows[0].max_connections,
activeConnections: connectionsResult.rows[0].active_connections
},
settings: {
sharedBuffers: settings.shared_buffers || 'unknown',
effectiveCacheSize: settings.effective_cache_size || 'unknown',
workMem: settings.work_mem || 'unknown',
maintenanceWorkMem: settings.maintenance_work_mem || 'unknown',
checkpointCompletionTarget: settings.checkpoint_completion_target || 0.5,
walBuffers: settings.wal_buffers || 'unknown',
randomPageCost: settings.random_page_cost || 4.0
}
};
}
/**
* Analyze schema health and detect issues
*/
async analyzeSchemaHealth() {
console.log('🔍 Analyzing schema health...');
const issues = [];
let scores = { normalization: 8, indexEfficiency: 7, foreignKeyIntegrity: 9, dataTypes: 8, naming: 9 };
// Check for tables without primary keys
const noPkResult = await this.client.query(`
SELECT table_name
FROM information_schema.tables t
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
AND NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints tc
WHERE tc.table_name = t.table_name
AND tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = 'public'
)
`);
noPkResult.rows.forEach(row => {
issues.push({
type: 'missing_pk',
table: row.table_name,
severity: 'critical',
description: `Table '${row.table_name}' has no primary key`,
suggestion: 'Add a primary key to ensure data integrity and better performance',
sqlFix: `ALTER TABLE ${row.table_name} ADD COLUMN id SERIAL PRIMARY KEY;`
});
scores.foreignKeyIntegrity -= 1;
});
// Check for foreign keys without indexes
const unindexedFkResult = await this.client.query(`
SELECT
tc.table_name,
kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = tc.table_name
AND indexdef LIKE '%' || kcu.column_name || '%'
)
`);
unindexedFkResult.rows.forEach(row => {
issues.push({
type: 'missing_fk_index',
table: row.table_name,
column: row.column_name,
severity: 'high',
description: `Foreign key '${row.column_name}' in table '${row.table_name}' is not indexed`,
suggestion: 'Create an index on foreign key columns for better JOIN performance',
sqlFix: `CREATE INDEX idx_${row.table_name}_${row.column_name} ON ${row.table_name}(${row.column_name});`
});
scores.indexEfficiency -= 0.5;
});
// Check for inefficient data types
const inefficientTypesResult = await this.client.query(`
SELECT table_name, column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'public'
AND (
(data_type = 'character varying' AND character_maximum_length > 255)
OR data_type = 'text'
)
`);
inefficientTypesResult.rows.forEach(row => {
if (row.data_type === 'text' || row.character_maximum_length > 1000) {
issues.push({
type: 'data_type_inefficiency',
table: row.table_name,
column: row.column_name,
severity: 'medium',
description: `Column '${row.column_name}' uses inefficient data type '${row.data_type}'`,
suggestion: 'Consider using more specific data types for better performance'
});
scores.dataTypes -= 0.3;
}
});
const overall = Math.round((scores.normalization + scores.indexEfficiency + scores.foreignKeyIntegrity + scores.dataTypes + scores.naming) / 5);
return {
overall: Math.max(overall, 0),
normalization: Math.max(scores.normalization, 0),
indexEfficiency: Math.max(scores.indexEfficiency, 0),
foreignKeyIntegrity: Math.max(scores.foreignKeyIntegrity, 0),
dataTypes: Math.max(scores.dataTypes, 0),
naming: Math.max(scores.naming, 0),
issues
};
}
/**
* Analyze indexes for efficiency and issues
*/
async analyzeIndexes() {
console.log('📈 Analyzing index efficiency...');
const totalIndexesResult = await this.client.query(`
SELECT COUNT(*) as count FROM pg_indexes WHERE schemaname = 'public'
`);
// Find unused indexes (simplified check)
const unusedIndexesResult = await this.client.query(`
SELECT
indexrelname as name,
relname as table,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public'
`);
const unusedIndexes = unusedIndexesResult.rows.map(row => ({
name: row.name,
table: row.table,
size: row.size,
lastUsed: null,
impact: 'medium'
}));
// Generate missing index suggestions
const missingIndexes = [];
// Check for tables with high sequential scan ratios
const seqScanResult = await this.client.query(`
SELECT
relname as table,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan * 2 AND seq_scan > 100
`);
seqScanResult.rows.forEach(row => {
missingIndexes.push({
table: row.table,
columns: ['id'], // Simplified
reason: 'High sequential scan ratio detected',
estimatedImpact: 'high',
suggestedSql: `-- Consider adding indexes on frequently queried columns for table ${row.table}`
});
});
return {
totalIndexes: parseInt(totalIndexesResult.rows[0].count),
unusedIndexes,
missingIndexes,
duplicateIndexes: [], // Would need more complex logic
oversizedIndexes: [], // Would need more complex logic
indexEfficiencyScore: Math.max(8 - unusedIndexes.length * 0.5, 0)
};
}
/**
* Detect performance issues
*/
async detectPerformanceIssues() {
console.log('⚡ Detecting performance issues...');
const issues = [];
// Check for table bloat
const bloatResult = await this.client.query(`
SELECT
schemaname,
relname as tablename,
pg_size_pretty(pg_total_relation_size(relid)) as size
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 5
`);
bloatResult.rows.forEach(row => {
if (row.size && !row.size.includes('kB')) { // Larger than KB
issues.push({
type: 'table_bloat',
severity: 'medium',
description: `Table '${row.tablename}' is large (${row.size}) and may benefit from optimization`,
impact: 'Query performance may be degraded',
solution: 'Consider VACUUM, REINDEX, or partitioning',
sqlFix: `VACUUM ANALYZE ${row.tablename};`
});
}
});
// Check statistics freshness
const statsResult = await this.client.query(`
SELECT
schemaname,
relname as tablename,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE last_analyze IS NULL OR last_analyze < NOW() - INTERVAL '7 days'
`);
statsResult.rows.forEach(row => {
issues.push({
type: 'poor_statistics',
severity: 'medium',
description: `Table '${row.tablename}' has outdated statistics`,
impact: 'Query planner may make suboptimal decisions',
solution: 'Run ANALYZE to update table statistics',
sqlFix: `ANALYZE ${row.tablename};`
});
});
return issues;
}
/**
* Detect security issues
*/
async detectSecurityIssues() {
console.log('🔒 Checking security issues...');
const issues = [];
// Check for tables without proper permissions
try {
const permissionsResult = await this.client.query(`
SELECT
table_name,
privilege_type,
grantee
FROM information_schema.table_privileges
WHERE table_schema = 'public'
AND grantee = 'public'
AND privilege_type IN ('INSERT', 'UPDATE', 'DELETE')
`);
if (permissionsResult.rows.length > 0) {
issues.push({
type: 'weak_permissions',
severity: 'high',
description: 'Some tables have overly permissive access rights',
recommendation: 'Review and restrict table permissions to necessary users only'
});
}
}
catch (error) {
// Permission check failed, skip
}
return issues;
}
/**
* Analyze database costs and usage
*/
async analyzeCosts() {
console.log('💰 Analyzing database costs...');
const sizeResult = await this.client.query(`
SELECT
pg_size_pretty(pg_database_size(current_database())) as total_size,
pg_database_size(current_database()) as total_bytes
`);
const indexSizeResult = await this.client.query(`
SELECT
pg_size_pretty(SUM(pg_relation_size(indexrelid))) as index_size,
SUM(pg_relation_size(indexrelid)) as index_bytes
FROM pg_stat_user_indexes
`);
const totalBytes = parseInt(sizeResult.rows[0].total_bytes) || 0;
const indexBytes = parseInt(indexSizeResult.rows[0].index_bytes) || 0;
const dataBytes = totalBytes - indexBytes;
return {
storageUsage: {
totalSize: sizeResult.rows[0].total_size,
dataSize: this.formatBytes(dataBytes),
indexSize: indexSizeResult.rows[0].index_size || '0 bytes',
wastedSpace: '0 bytes' // Would need more complex calculation
},
estimatedCosts: {
storage: totalBytes / (1024 * 1024 * 1024) * 0.10, // $0.10 per GB rough estimate
compute: 50, // Base compute cost
maintenance: 20 // Base maintenance cost
},
optimizationSavings: {
storage: 0,
performance: 0
}
};
}
/**
* Generate optimization recommendations
*/
generateOptimizationRecommendations(schemaHealth, indexAnalysis, performanceIssues, securityIssues) {
const recommendations = [];
// Schema recommendations
if (schemaHealth.overall < 7) {
recommendations.push({
category: 'performance',
priority: 'high',
title: 'Improve Schema Design',
description: 'Schema health score is below recommended threshold',
estimatedImpact: 'High - Better query performance and data integrity',
implementation: 'Address missing primary keys and foreign key indexes',
sqlCommands: schemaHealth.issues.filter(i => i.sqlFix).map(i => i.sqlFix)
});
}
// Index recommendations
if (indexAnalysis.unusedIndexes.length > 0) {
recommendations.push({
category: 'storage',
priority: 'medium',
title: 'Remove Unused Indexes',
description: `Found ${indexAnalysis.unusedIndexes.length} unused indexes`,
estimatedImpact: 'Medium - Reduced storage costs and faster writes',
implementation: 'Drop unused indexes after confirming they are not needed',
sqlCommands: indexAnalysis.unusedIndexes.map(idx => `DROP INDEX ${idx.name};`)
});
}
// Performance recommendations
if (performanceIssues.length > 0) {
recommendations.push({
category: 'performance',
priority: 'high',
title: 'Address Performance Issues',
description: `Found ${performanceIssues.length} performance issues`,
estimatedImpact: 'High - Improved query response times',
implementation: 'Run maintenance commands and optimize queries',
sqlCommands: performanceIssues.filter(i => i.sqlFix).map(i => i.sqlFix)
});
}
return recommendations;
}
/**
* Generate maintenance recommendations
*/
generateMaintenanceRecommendations() {
return [
{
task: 'VACUUM ANALYZE',
frequency: 'weekly',
importance: 'high',
description: 'Update table statistics and reclaim space',
command: 'VACUUM ANALYZE;'
},
{
task: 'REINDEX',
frequency: 'monthly',
importance: 'medium',
description: 'Rebuild indexes to maintain performance',
command: 'REINDEX DATABASE your_database;'
},
{
task: 'Check slow queries',
frequency: 'weekly',
importance: 'high',
description: 'Monitor and optimize slow-running queries'
},
{
task: 'Backup verification',
frequency: 'daily',
importance: 'critical',
description: 'Ensure database backups are working correctly'
}
];
}
/**
* Format bytes to human readable string
*/
formatBytes(bytes) {
const units = ['bytes', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}
exports.DatabaseHealthAuditor = DatabaseHealthAuditor;
//# sourceMappingURL=database-health-auditor.js.map