package pgmonitor import ( "context" "time" ) // Recommendations builds heuristic items from live stats and snapshots. func (s *Service) Recommendations(ctx context.Context) (RecommendationsResponse, error) { now := time.Now().UTC() var items []RecommendationItem ov, err := s.Overview(ctx) if err == nil { if ov.Database.CacheHitPct > 0 && ov.Database.CacheHitPct < 90 { items = append(items, RecommendationItem{ Severity: "warn", Code: "low_cache_hit", Title: "Низкий cache hit ratio", Detail: "Buffer cache hit ниже 90%; проверьте shared_buffers и горячие seq scan.", }) } if ov.Database.Deadlocks > 0 { items = append(items, RecommendationItem{ Severity: "warn", Code: "deadlocks", Title: "Зафиксированы deadlocks", Detail: "Проверьте конкурирующие транзакции и порядок блокировок.", }) } if ov.Connections.MaxConnections > 0 && float64(ov.Connections.Total)/float64(ov.Connections.MaxConnections) > 0.8 { items = append(items, RecommendationItem{ Severity: "critical", Code: "connections_high", Title: "Много подключений к PostgreSQL", Detail: "Использование max_connections выше 80%; увеличьте pool tuning или лимит.", }) } } tables, err := s.Tables(ctx, 30) if err == nil { for _, t := range tables { if t.SeqScan > 1000 && t.IdxScan < t.SeqScan/10 { items = append(items, RecommendationItem{ Severity: "warn", Code: "missing_index", Title: "Высокий seq_scan", Detail: "Таблица часто сканируется последовательно; рассмотрите индекс.", Refs: []string{t.Relname}, }) } if t.BloatRatio > 0.2 && t.DeadTuples > 5000 { items = append(items, RecommendationItem{ Severity: "info", Code: "autovacuum_lag", Title: "Возможный bloat / мёртвые строки", Detail: "Высокая доля n_dead_tup; запланируйте VACUUM.", Refs: []string{t.Relname}, }) } } } if snap, ok, _ := s.loadSnapshot(ctx, "unused_indexes", 30*time.Minute); ok { type unused struct { Index string `json:"index"` SizeBytes int64 `json:"size_bytes"` } var list []unused if decodePayload(snap.Payload, &list) == nil { for _, u := range list { if u.SizeBytes < 1024*1024 { continue } items = append(items, RecommendationItem{ Severity: "info", Code: "unused_index", Title: "Неиспользуемый индекс", Detail: "idx_scan=0; проверьте перед удалением.", Refs: []string{u.Index}, }) } } } q, err := s.TopQueries(ctx, 5) if err == nil { for _, qs := range q.Items { if qs.MeanExecMs > 500 { items = append(items, RecommendationItem{ Severity: "warn", Code: "slow_query", Title: "Медленный запрос", Detail: "Среднее время выполнения выше 500ms.", Refs: []string{qs.Query}, }) } } } return RecommendationsResponse{CollectedAt: now, Items: items}, nil }