import React, { useEffect, useRef, useState } from "react"; import { apiFetch } from "../api"; import { useAuth } from "../state"; const PAGE_SIZE = 10; function scoreQuery(row) { const mean = Number(row.mean_time || 0); const calls = Number(row.calls || 0); const total = Number(row.total_time || 0); const rows = Number(row.rows || 0); return mean * 1.4 + total * 0.9 + calls * 0.2 + Math.min(rows / 50, 25); } function classifyQuery(row) { if ((row.mean_time || 0) > 100) return { label: "Very Slow", kind: "danger" }; if ((row.total_time || 0) > 250) return { label: "Heavy", kind: "warn" }; if ((row.calls || 0) > 500) return { label: "Frequent", kind: "info" }; return { label: "Normal", kind: "ok" }; } function compactSql(sql) { if (!sql) return "-"; return sql.replace(/\s+/g, " ").trim(); } function buildQueryTips(row) { if (!row) return []; const tips = []; const sql = (row.query_text || "").toLowerCase(); if ((row.mean_time || 0) > 100) { tips.push("High latency per call: inspect execution plan with EXPLAIN (ANALYZE, BUFFERS)."); } if ((row.total_time || 0) > 500) { tips.push("High cumulative runtime: optimize this query first for fastest overall impact."); } if ((row.calls || 0) > 500) { tips.push("Very frequent query: consider caching or reducing call frequency in application code."); } if ((row.rows || 0) > 100000) { tips.push("Large row output: return fewer columns/rows or add pagination at query/API layer."); } if (/select\s+\*/.test(sql)) { tips.push("Avoid SELECT *: request only required columns to reduce IO and transfer cost."); } if (/order\s+by/.test(sql) && !/limit\s+\d+/.test(sql)) { tips.push("ORDER BY without LIMIT can be expensive: add LIMIT where possible."); } if (/where/.test(sql) && / from /.test(sql)) { tips.push("Filter query detected: verify indexes exist on WHERE / JOIN columns."); } if (/like\s+'%/.test(sql)) { tips.push("Leading wildcard LIKE can bypass indexes: consider trigram index (pg_trgm)."); } if (tips.length === 0) { tips.push("No obvious anti-pattern detected. Validate with EXPLAIN and index usage statistics."); } return tips.slice(0, 5); } export function QueryInsightsPage() { const { tokens, refresh } = useAuth(); const refreshRef = useRef(refresh); const [targets, setTargets] = useState([]); const [targetId, setTargetId] = useState(""); const [rows, setRows] = useState([]); const [selectedQuery, setSelectedQuery] = useState(null); const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); useEffect(() => { refreshRef.current = refresh; }, [refresh]); useEffect(() => { (async () => { try { const t = await apiFetch("/targets", {}, tokens, refresh); const supported = t.filter((item) => item.use_pg_stat_statements !== false); setTargets(supported); if (supported.length > 0) setTargetId(String(supported[0].id)); else setTargetId(""); } catch (e) { setError(String(e.message || e)); } finally { setLoading(false); } })(); }, []); useEffect(() => { if (!targetId) return; let active = true; (async () => { try { const data = await apiFetch(`/targets/${targetId}/top-queries`, {}, tokens, refreshRef.current); if (!active) return; setRows(data); setSelectedQuery((prev) => { if (!prev) return data[0] || null; const keep = data.find((row) => row.queryid === prev.queryid); return keep || data[0] || null; }); setPage((prev) => (prev === 1 ? prev : 1)); } catch (e) { if (active) setError(String(e.message || e)); } })(); return () => { active = false; }; }, [targetId, tokens?.accessToken, tokens?.refreshToken]); const dedupedByQueryId = [...rows].reduce((acc, row) => { if (!row?.queryid) return acc; if (!acc[row.queryid]) acc[row.queryid] = row; return acc; }, {}); const uniqueRows = Object.values(dedupedByQueryId); const sorted = [...uniqueRows].sort((a, b) => (b.total_time || 0) - (a.total_time || 0)); const byMean = [...uniqueRows].sort((a, b) => (b.mean_time || 0) - (a.mean_time || 0)); const byCalls = [...uniqueRows].sort((a, b) => (b.calls || 0) - (a.calls || 0)); const byRows = [...uniqueRows].sort((a, b) => (b.rows || 0) - (a.rows || 0)); const byPriority = [...uniqueRows].sort((a, b) => scoreQuery(b) - scoreQuery(a)); const filtered = byPriority.filter((r) => { if (!search.trim()) return true; return compactSql(r.query_text).toLowerCase().includes(search.toLowerCase()); }); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE); const selectedTips = buildQueryTips(selectedQuery); const categories = [ { key: "priority", title: "Optimization Priority", row: byPriority[0], subtitle: "Best first candidate to optimize" }, { key: "total", title: "Longest Total Time", row: sorted[0], subtitle: "Biggest cumulative runtime impact" }, { key: "mean", title: "Highest Mean Time", row: byMean[0], subtitle: "Slowest single-call latency" }, { key: "calls", title: "Most Frequent", row: byCalls[0], subtitle: "Executed very often" }, { key: "rows", title: "Most Rows Returned", row: byRows[0], subtitle: "Potentially heavy scans" }, ]; return (
Note: This section requires the pg_stat_statements extension on the monitored target.
pg_stat_statements are available.
Enable it in Targets Management for a target to use Query Insights.
No data
)}| Priority | Calls | Total ms | Mean ms | Rows | Query Preview |
|---|---|---|---|---|---|
| {state.label} | {r.calls} | {Number(r.total_time || 0).toFixed(2)} | {Number(r.mean_time || 0).toFixed(2)} | {r.rows} |
{selectedQuery.query_text || "-- no query text available --"}
Tip: focus first on queries with high Total Time (overall impact) and high Mean Time (latency hotspots).
> ) : (No query selected.
)}