Initial commit - PromptTech
This commit is contained in:
305
backend/check_database_health.py
Normal file
305
backend/check_database_health.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
Database Health Check Script
|
||||
Verifies schema correctness, relationship integrity, and performance
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from sqlalchemy import text, inspect
|
||||
from database import async_engine
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def check_schema():
|
||||
"""Verify database schema matches models"""
|
||||
|
||||
async with async_engine.connect() as conn:
|
||||
logger.info("="*70)
|
||||
logger.info("DATABASE HEALTH CHECK")
|
||||
logger.info("="*70)
|
||||
|
||||
# 1. Table Count
|
||||
logger.info("\n📊 1. SCHEMA VERIFICATION")
|
||||
logger.info("-" * 70)
|
||||
|
||||
result = await conn.execute(text("""
|
||||
SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
"""))
|
||||
table_count = result.scalar()
|
||||
|
||||
expected_tables = 13 # Based on models.py
|
||||
status = "✅" if table_count == expected_tables else "⚠️ "
|
||||
logger.info(f" Tables: {table_count}/{expected_tables} {status}")
|
||||
|
||||
# 2. Foreign Keys
|
||||
logger.info("\n🔗 2. RELATIONSHIPS & CONSTRAINTS")
|
||||
logger.info("-" * 70)
|
||||
|
||||
result = await conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.table_constraints
|
||||
WHERE constraint_type = 'FOREIGN KEY' AND table_schema = 'public'
|
||||
"""))
|
||||
fk_count = result.scalar()
|
||||
logger.info(f" Foreign Keys: {fk_count} ✅")
|
||||
|
||||
# 3. Indexes
|
||||
result = await conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public' AND indexname NOT LIKE '%_pkey'
|
||||
"""))
|
||||
index_count = result.scalar()
|
||||
logger.info(f" Indexes (non-PK): {index_count} ✅")
|
||||
|
||||
# 4. Check Constraints
|
||||
result = await conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM pg_constraint
|
||||
WHERE contype = 'c' AND connamespace::regnamespace::text = 'public'
|
||||
"""))
|
||||
check_count = result.scalar()
|
||||
logger.info(f" Check Constraints: {check_count} ✅")
|
||||
|
||||
# 5. Verify all foreign keys have indexes
|
||||
logger.info("\n⚡ 3. PERFORMANCE VALIDATION")
|
||||
logger.info("-" * 70)
|
||||
|
||||
result = await conn.execute(text("""
|
||||
SELECT
|
||||
c.conrelid::regclass AS table_name,
|
||||
a.attname AS column_name
|
||||
FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
|
||||
LEFT JOIN pg_index i ON i.indrelid = c.conrelid
|
||||
AND a.attnum = ANY(i.indkey)
|
||||
WHERE c.contype = 'f' AND i.indexrelid IS NULL
|
||||
"""))
|
||||
missing_indexes = result.fetchall()
|
||||
|
||||
if missing_indexes:
|
||||
logger.info(f" ⚠️ {len(missing_indexes)} foreign keys without indexes:")
|
||||
for table, column in missing_indexes:
|
||||
logger.info(f" {table}.{column}")
|
||||
else:
|
||||
logger.info(" ✅ All foreign keys are indexed")
|
||||
|
||||
# 6. Check for duplicate indexes
|
||||
result = await conn.execute(text("""
|
||||
SELECT
|
||||
t.relname AS table_name,
|
||||
i.relname AS index_name,
|
||||
array_agg(a.attname ORDER BY a.attnum) AS columns
|
||||
FROM pg_class t
|
||||
JOIN pg_index ix ON t.oid = ix.indrelid
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid
|
||||
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
||||
WHERE t.relkind = 'r'
|
||||
AND t.relnamespace::regnamespace::text = 'public'
|
||||
GROUP BY t.relname, i.relname
|
||||
HAVING COUNT(*) > 0
|
||||
"""))
|
||||
indexes = result.fetchall()
|
||||
|
||||
# Group by columns to find duplicates
|
||||
index_map = {}
|
||||
for table, idx_name, columns in indexes:
|
||||
key = (table, tuple(columns))
|
||||
if key not in index_map:
|
||||
index_map[key] = []
|
||||
index_map[key].append(idx_name)
|
||||
|
||||
duplicates = [(k, v) for k, v in index_map.items() if len(v) > 1]
|
||||
if duplicates:
|
||||
logger.info(f" ⚠️ {len(duplicates)} duplicate index patterns found")
|
||||
for (table, cols), names in duplicates[:3]:
|
||||
logger.info(f" {table}{list(cols)}: {', '.join(names)}")
|
||||
else:
|
||||
logger.info(" ✅ No duplicate indexes found")
|
||||
|
||||
# 7. Table sizes and row counts
|
||||
logger.info("\n📦 4. TABLE STATISTICS")
|
||||
logger.info("-" * 70)
|
||||
|
||||
result = await conn.execute(text("""
|
||||
SELECT
|
||||
c.relname AS table_name,
|
||||
pg_size_pretty(pg_total_relation_size(c.oid)) AS size,
|
||||
c.reltuples::bigint AS estimated_rows
|
||||
FROM pg_class c
|
||||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE relkind = 'r' AND nspname = 'public'
|
||||
ORDER BY pg_total_relation_size(c.oid) DESC
|
||||
LIMIT 10
|
||||
"""))
|
||||
sizes = result.fetchall()
|
||||
|
||||
logger.info(f" {'Table':<25} {'Size':>10} {'Rows':>10}")
|
||||
logger.info(" " + "-" * 48)
|
||||
for table, size, rows in sizes:
|
||||
logger.info(f" {table:<25} {size:>10} {rows:>10}")
|
||||
|
||||
# 8. Check column data types alignment
|
||||
logger.info("\n🔍 5. DATA TYPE VERIFICATION")
|
||||
logger.info("-" * 70)
|
||||
|
||||
critical_columns = [
|
||||
('users', 'email', 'character varying'),
|
||||
('products', 'price', 'double precision'),
|
||||
('services', 'price', 'double precision'),
|
||||
('orders', 'total', 'double precision'),
|
||||
('reviews', 'rating', 'integer'),
|
||||
]
|
||||
|
||||
issues = []
|
||||
for table, column, expected_type in critical_columns:
|
||||
result = await conn.execute(text(f"""
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = '{table}' AND column_name = '{column}'
|
||||
"""))
|
||||
actual_type = result.scalar()
|
||||
|
||||
if actual_type != expected_type:
|
||||
issues.append(f"{table}.{column}: expected {expected_type}, got {actual_type}")
|
||||
|
||||
if issues:
|
||||
logger.info(" ⚠️ Data type mismatches:")
|
||||
for issue in issues:
|
||||
logger.info(f" {issue}")
|
||||
else:
|
||||
logger.info(" ✅ All critical columns have correct types")
|
||||
|
||||
# 9. Check for missing NOT NULL constraints
|
||||
logger.info("\n🛡️ 6. DATA INTEGRITY")
|
||||
logger.info("-" * 70)
|
||||
|
||||
result = await conn.execute(text("""
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND column_name IN ('id', 'user_id', 'product_id', 'service_id', 'order_id')
|
||||
AND is_nullable = 'YES'
|
||||
ORDER BY table_name, column_name
|
||||
"""))
|
||||
nullable_fks = result.fetchall()
|
||||
|
||||
if nullable_fks:
|
||||
logger.info(f" ℹ️ {len(nullable_fks)} nullable foreign/primary keys (by design):")
|
||||
for table, column in nullable_fks[:5]:
|
||||
logger.info(f" {table}.{column}")
|
||||
else:
|
||||
logger.info(" ✅ All IDs have appropriate nullability")
|
||||
|
||||
# 10. Enum types verification
|
||||
logger.info("\n📋 7. ENUM TYPES")
|
||||
logger.info("-" * 70)
|
||||
|
||||
result = await conn.execute(text("""
|
||||
SELECT
|
||||
t.typname AS enum_name,
|
||||
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
|
||||
FROM pg_type t
|
||||
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||
WHERE t.typnamespace::regnamespace::text = 'public'
|
||||
GROUP BY t.typname
|
||||
"""))
|
||||
enums = result.fetchall()
|
||||
|
||||
if enums:
|
||||
for enum_name, values in enums:
|
||||
logger.info(f" ✅ {enum_name}: {len(values)} values")
|
||||
else:
|
||||
logger.info(" ⚠️ No enum types found")
|
||||
|
||||
# 11. Index usage statistics
|
||||
logger.info("\n📈 8. INDEX USAGE")
|
||||
logger.info("-" * 70)
|
||||
|
||||
try:
|
||||
result = await conn.execute(text("""
|
||||
SELECT
|
||||
schemaname,
|
||||
relname,
|
||||
indexrelname,
|
||||
idx_scan,
|
||||
idx_tup_read
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY idx_scan DESC
|
||||
LIMIT 10
|
||||
"""))
|
||||
index_stats = result.fetchall()
|
||||
|
||||
logger.info(f" {'Table':<20} {'Index':<30} {'Scans':>10}")
|
||||
logger.info(" " + "-" * 62)
|
||||
for schema, table, idx_name, scans, reads in index_stats:
|
||||
logger.info(f" {table:<20} {idx_name:<30} {scans:>10}")
|
||||
except Exception as e:
|
||||
logger.info(f" ℹ️ Index statistics not available (requires pg_stat_statements)")
|
||||
|
||||
# 12. Connection settings
|
||||
logger.info("\n⚙️ 9. DATABASE CONFIGURATION")
|
||||
logger.info("-" * 70)
|
||||
|
||||
settings_to_check = [
|
||||
'max_connections',
|
||||
'shared_buffers',
|
||||
'effective_cache_size',
|
||||
'work_mem',
|
||||
'maintenance_work_mem'
|
||||
]
|
||||
|
||||
for setting in settings_to_check:
|
||||
try:
|
||||
result = await conn.execute(text(f"SHOW {setting}"))
|
||||
value = result.scalar()
|
||||
logger.info(f" {setting:<25} {value}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Summary
|
||||
logger.info("\n" + "="*70)
|
||||
logger.info("HEALTH CHECK SUMMARY")
|
||||
logger.info("="*70)
|
||||
|
||||
checks = [
|
||||
(table_count == expected_tables, "Schema structure"),
|
||||
(fk_count > 0, "Foreign key relationships"),
|
||||
(index_count > 30, "Performance indexes"),
|
||||
(check_count > 0, "Data validation constraints"),
|
||||
(len(missing_indexes) == 0, "Foreign key indexing"),
|
||||
(len(issues) == 0, "Data type correctness"),
|
||||
]
|
||||
|
||||
passed = sum(1 for check, _ in checks if check)
|
||||
total = len(checks)
|
||||
|
||||
logger.info(f"\n Passed: {passed}/{total} checks")
|
||||
logger.info("")
|
||||
|
||||
for check, description in checks:
|
||||
status = "✅" if check else "⚠️ "
|
||||
logger.info(f" {status} {description}")
|
||||
|
||||
if passed == total:
|
||||
logger.info("\n🎉 Database is healthy and optimized!")
|
||||
elif passed >= total * 0.8:
|
||||
logger.info("\n✅ Database is mostly healthy with minor issues")
|
||||
else:
|
||||
logger.info("\n⚠️ Database needs attention")
|
||||
|
||||
logger.info("\n" + "="*70)
|
||||
|
||||
async def main():
|
||||
try:
|
||||
await check_schema()
|
||||
except Exception as e:
|
||||
logger.error(f"\n❌ Health check failed: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user