166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
||
const { pool, query } = require("./config/database");
|
||
|
||
async function analyzeQueryPatterns() {
|
||
console.log("🔍 Analyzing Query Patterns...\n");
|
||
|
||
try {
|
||
// 1. Check for missing indexes on frequently queried columns
|
||
console.log("1️⃣ Checking Query Performance:");
|
||
|
||
// Test products query (most common)
|
||
const productsExplain = await query(`
|
||
EXPLAIN ANALYZE
|
||
SELECT p.id, p.name, p.slug, p.price, p.category, p.createdat
|
||
FROM products p
|
||
WHERE p.isactive = true
|
||
ORDER BY p.createdat DESC
|
||
LIMIT 20
|
||
`);
|
||
console.log(" Products listing:");
|
||
productsExplain.rows.forEach((row) => {
|
||
if (
|
||
row["QUERY PLAN"].includes("Index") ||
|
||
row["QUERY PLAN"].includes("Seq Scan")
|
||
) {
|
||
console.log(` ${row["QUERY PLAN"]}`);
|
||
}
|
||
});
|
||
|
||
// Test portfolio query
|
||
const portfolioExplain = await query(`
|
||
EXPLAIN ANALYZE
|
||
SELECT id, title, category, displayorder, createdat
|
||
FROM portfolioprojects
|
||
WHERE isactive = true
|
||
ORDER BY displayorder ASC, createdat DESC
|
||
`);
|
||
console.log("\n Portfolio listing:");
|
||
portfolioExplain.rows.slice(0, 3).forEach((row) => {
|
||
console.log(` ${row["QUERY PLAN"]}`);
|
||
});
|
||
|
||
// Test product with images (JOIN query)
|
||
const productWithImagesExplain = await query(`
|
||
EXPLAIN ANALYZE
|
||
SELECT p.*, pi.image_url, pi.color_variant
|
||
FROM products p
|
||
LEFT JOIN product_images pi ON pi.product_id = p.id
|
||
WHERE p.isactive = true
|
||
LIMIT 10
|
||
`);
|
||
console.log("\n Products with images (JOIN):");
|
||
productWithImagesExplain.rows.slice(0, 5).forEach((row) => {
|
||
console.log(` ${row["QUERY PLAN"]}`);
|
||
});
|
||
|
||
// 2. Check for slow queries
|
||
console.log("\n2️⃣ Checking Table Statistics:");
|
||
const stats = await query(`
|
||
SELECT
|
||
schemaname,
|
||
relname as tablename,
|
||
n_live_tup as row_count,
|
||
n_dead_tup as dead_rows,
|
||
CASE
|
||
WHEN n_live_tup > 0 THEN round(100.0 * n_dead_tup / n_live_tup, 2)
|
||
ELSE 0
|
||
END as bloat_pct,
|
||
last_vacuum,
|
||
last_analyze
|
||
FROM pg_stat_user_tables
|
||
WHERE schemaname = 'public'
|
||
AND relname IN ('products', 'product_images', 'portfolioprojects', 'blogposts', 'pages')
|
||
ORDER BY n_live_tup DESC
|
||
`);
|
||
|
||
console.log(" Table health:");
|
||
stats.rows.forEach((row) => {
|
||
console.log(
|
||
` ${row.tablename.padEnd(20)} ${String(row.row_count).padStart(
|
||
6
|
||
)} rows, ${String(row.dead_rows).padStart(4)} dead (${String(
|
||
row.bloat_pct
|
||
).padStart(5)}% bloat)`
|
||
);
|
||
});
|
||
|
||
// 3. Check index usage
|
||
console.log("\n3️⃣ Index Usage Statistics:");
|
||
const indexUsage = await query(`
|
||
SELECT
|
||
schemaname,
|
||
relname as tablename,
|
||
indexrelname as indexname,
|
||
idx_scan as scans,
|
||
idx_tup_read as rows_read,
|
||
idx_tup_fetch as rows_fetched
|
||
FROM pg_stat_user_indexes
|
||
WHERE schemaname = 'public'
|
||
AND relname IN ('products', 'product_images', 'portfolioprojects', 'blogposts', 'pages')
|
||
AND idx_scan > 0
|
||
ORDER BY idx_scan DESC
|
||
LIMIT 15
|
||
`);
|
||
|
||
console.log(" Most used indexes:");
|
||
indexUsage.rows.forEach((row) => {
|
||
console.log(
|
||
` ${row.indexname.padEnd(40)} ${String(row.scans).padStart(
|
||
6
|
||
)} scans`
|
||
);
|
||
});
|
||
|
||
// 4. Check for unused indexes
|
||
const unusedIndexes = await query(`
|
||
SELECT
|
||
schemaname,
|
||
relname as tablename,
|
||
indexrelname as indexname
|
||
FROM pg_stat_user_indexes
|
||
WHERE schemaname = 'public'
|
||
AND relname IN ('products', 'product_images', 'portfolioprojects', 'blogposts', 'pages')
|
||
AND idx_scan = 0
|
||
AND indexrelname NOT LIKE '%_pkey'
|
||
ORDER BY relname, indexrelname
|
||
`);
|
||
|
||
if (unusedIndexes.rows.length > 0) {
|
||
console.log("\n4️⃣ Unused Indexes (consider removing):");
|
||
unusedIndexes.rows.forEach((row) => {
|
||
console.log(` ${row.tablename}.${row.indexname}`);
|
||
});
|
||
} else {
|
||
console.log("\n4️⃣ ✅ All indexes are being used");
|
||
}
|
||
|
||
// 5. Check cache hit ratio
|
||
console.log("\n5️⃣ Cache Hit Ratio:");
|
||
const cacheHit = await query(`
|
||
SELECT
|
||
sum(heap_blks_read) as heap_read,
|
||
sum(heap_blks_hit) as heap_hit,
|
||
CASE
|
||
WHEN sum(heap_blks_hit) + sum(heap_blks_read) > 0 THEN
|
||
round(100.0 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)), 2)
|
||
ELSE 0
|
||
END as cache_hit_ratio
|
||
FROM pg_statio_user_tables
|
||
WHERE schemaname = 'public'
|
||
`);
|
||
|
||
const ratio = cacheHit.rows[0].cache_hit_ratio;
|
||
const status = ratio > 99 ? "✅" : ratio > 95 ? "⚠️" : "❌";
|
||
console.log(` ${status} ${ratio}% (target: >99%)`);
|
||
|
||
console.log("\n✅ Analysis complete!");
|
||
} catch (error) {
|
||
console.error("❌ Error:", error.message);
|
||
} finally {
|
||
await pool.end();
|
||
}
|
||
}
|
||
|
||
analyzeQueryPatterns();
|