diff --git a/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx b/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx index 8ae7478965f..781245e29f2 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx @@ -2,7 +2,7 @@ import { ArrowDown, ArrowUp } from 'lucide-react' import { Column } from 'react-data-grid' import { Badge } from 'ui' -import { type QueryInsightsQuery } from 'data/query-insights/query-metrics-query' +import { type InsightsQuery } from 'data/query-insights/insights-queries-query' import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants' export const formatNumberWithCommas = (num: number | string | undefined | null): string => { @@ -29,7 +29,7 @@ export const formatQueryInsightsColumns = ({ let gridColumns = QUERY_INSIGHTS_TABLE_COLUMNS.map((col) => { const savedConfig = config.find((c) => c.id === col.id) - const res: Column = { + const res: Column = { key: col.id, name: col.name, resizable: col.resizable ?? true, @@ -53,8 +53,8 @@ export const formatQueryInsightsColumns = ({ ) }, renderCell: (props) => { - const value = props.row?.[col.id as keyof QueryInsightsQuery] - + const value = props.row?.[col.id as keyof InsightsQuery] + if (col.id === 'query') { return (
@@ -81,7 +81,10 @@ export const formatQueryInsightsColumns = ({ return (
- + {value} @@ -111,7 +114,9 @@ export const formatQueryInsightsColumns = ({ return (

{formattedValue}

- {value &&

{((value as number) / 1000).toFixed(2)}s

} + {value && ( +

{((value as number) / 1000).toFixed(2)}s

+ )}
) } @@ -121,7 +126,9 @@ export const formatQueryInsightsColumns = ({ return (

{formattedValue}

- {value &&

{((value as number) / 1000).toFixed(2)}s

} + {value && ( +

{((value as number) / 1000).toFixed(2)}s

+ )}
) } diff --git a/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx b/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx index 6ba3d6434b8..98a3ee7a7e2 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx @@ -10,9 +10,9 @@ import { } from 'ui' import { QueryRowExplorer } from './QueryRowExplorer' import { - useQueryInsightsMetrics, - usePreFetchQueryInsightsData, -} from 'data/query-insights/query-metrics-query' + useInsightsMetricsQuery, + useInsightsPrefetchQuery, +} from 'data/query-insights/insights-metrics-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import ComposedChart from 'components/ui/Charts/ComposedChart' import { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils' @@ -59,7 +59,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP }, [startTime, endTime]) // Pre-fetch query data for the QueryRowExplorer - usePreFetchQueryInsightsData(project?.ref, effectiveStartTime, effectiveEndTime) + useInsightsPrefetchQuery(project?.ref, effectiveStartTime, effectiveEndTime) // Debug logging console.log('QueryMetricExplorer Debug:', { @@ -73,7 +73,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP data: metricsData, isLoading, error, - } = useQueryInsightsMetrics(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime) + } = useInsightsMetricsQuery(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime) // Debug logging for query results console.log('QueryMetricExplorer Query Results:', { diff --git a/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx b/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx index aa9b189cb1b..1873eead9ce 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { useQueryInsightsGlance } from 'data/query-insights/query-glance-query' +import { useInsightsGlanceQuery } from 'data/query-insights/insights-glance-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Card, CardContent } from 'ui' import { Database, Clock, AlertCircle } from 'lucide-react' @@ -36,7 +36,7 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps) data: glanceData, isLoading, error, - } = useQueryInsightsGlance(project?.ref, effectiveStartTime, effectiveEndTime) + } = useInsightsGlanceQuery(project?.ref, effectiveStartTime, effectiveEndTime) // Debug logging for glance results console.log('QueryQuickGlance Results:', { diff --git a/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx b/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx index 7d01767d672..4f9e2d4307b 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx @@ -21,15 +21,15 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { Input } from 'ui-patterns/DataInputs/Input' import { FilterPopover } from 'components/ui/FilterPopover' import { - useQueryInsightsQueries, - type QueryInsightsQuery, -} from 'data/query-insights/query-metrics-query' + useInsightsQueriesQuery, + type InsightsQuery, +} from 'data/query-insights/insights-queries-query' import { - useSingleQueryLatency, - useSingleQueryCalls, - useSingleQueryRows, - useSingleQueryRowsWritten, -} from 'data/query-insights/single-query-latency-query' + useInsightsSingleQueryLatency, + useInsightsSingleQueryCalls, + useInsightsSingleQueryRows, + useInsightsSingleQueryRowsWritten, +} from 'data/query-insights/insights-single-ql-query' import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants' import { formatQueryInsightsColumns } from './QueryInsights.utils' @@ -44,7 +44,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) const { ref } = useParams() const { data: project } = useSelectedProjectQuery() - const { data: queries, isLoading, error } = useQueryInsightsQueries(ref, startTime, endTime) + const { data: queries, isLoading, error } = useInsightsQueriesQuery(ref, startTime, endTime) // Debug logging console.log('QueryRowExplorer Debug:', { @@ -128,15 +128,15 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) total_cost_after: 0, index_statements: [], errors: [], - } as QueryInsightsQuery, + } as InsightsQuery, ] // Sort data based on current sort state const sortedData = [...reportData].sort((a, b) => { if (!sort) return 0 - const aValue = a[sort.column as keyof QueryInsightsQuery] - const bValue = b[sort.column as keyof QueryInsightsQuery] + const aValue = a[sort.column as keyof InsightsQuery] + const bValue = b[sort.column as keyof InsightsQuery] if (aValue === undefined || bValue === undefined) return 0 @@ -159,28 +159,28 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) const selectedQuery = selectedRow !== undefined ? filteredData[selectedRow] : undefined // Single query metrics hooks - const { data: singleQueryLatency } = useSingleQueryLatency( + const { data: singleQueryLatency } = useInsightsSingleQueryLatency( ref, startTime, endTime, selectedQuery?.query_id?.toString() ) - const { data: singleQueryCalls } = useSingleQueryCalls( + const { data: singleQueryCalls } = useInsightsSingleQueryCalls( ref, startTime, endTime, selectedQuery?.query_id?.toString() ) - const { data: singleQueryRows } = useSingleQueryRows( + const { data: singleQueryRows } = useInsightsSingleQueryRows( ref, startTime, endTime, selectedQuery?.query_id?.toString() ) - const { data: singleQueryRowsWritten } = useSingleQueryRowsWritten( + const { data: singleQueryRowsWritten } = useInsightsSingleQueryRowsWritten( ref, startTime, endTime, diff --git a/apps/studio/data/query-insights/query-glance-query.ts b/apps/studio/data/query-insights/insights-glance-query.ts similarity index 66% rename from apps/studio/data/query-insights/query-glance-query.ts rename to apps/studio/data/query-insights/insights-glance-query.ts index b18b22fb570..502fc8cc445 100644 --- a/apps/studio/data/query-insights/query-glance-query.ts +++ b/apps/studio/data/query-insights/insights-glance-query.ts @@ -2,7 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' import { queryInsightsKeys } from './keys' -export type QueryInsightsGlance = { +export type InsightsGlance = { unique_queries: number total_queries: number avg_query_time: number @@ -11,7 +11,7 @@ export type QueryInsightsGlance = { avg_error_rate: number } -export const getQueriesGlanceMetrics = (startTime: string, endTime: string) => +export const getInsightsGlanceMetrics = (startTime: string, endTime: string) => /* SQL */ ` SELECT COUNT(DISTINCT query) as unique_queries, @@ -30,18 +30,18 @@ export const getQueriesGlanceMetrics = (startTime: string, endTime: string) => AND bucket_done = true `.trim() -export type QueryInsightsGlanceVariables = { +export type InsightsGlanceVariables = { projectRef?: string connectionString?: string | null startTime: string endTime: string } -export async function getQueryInsightsGlance( - { projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables, +export async function getInsightsGlance( + { projectRef, connectionString, startTime, endTime }: InsightsGlanceVariables, signal?: AbortSignal ) { - const sql = getQueriesGlanceMetrics(startTime, endTime) + const sql = getInsightsGlanceMetrics(startTime, endTime) const { result } = await executeSql( { @@ -53,33 +53,33 @@ export async function getQueryInsightsGlance( signal ) - return result[0] as QueryInsightsGlance + return result[0] as InsightsGlance } -export type QueryInsightsGlanceData = Awaited> -export type QueryInsightsGlanceError = ExecuteSqlError +export type InsightsGlanceData = Awaited> +export type InsightsGlanceError = ExecuteSqlError -export function useQueryInsightsGlance( +export function useInsightsGlanceQuery( projectRef: string | undefined, startTime: string, endTime: string, - options?: UseQueryOptions + options?: UseQueryOptions ) { return useQuery({ queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime), queryFn: async () => { if (!projectRef) throw new Error('Project ref is required') - const sql = getQueriesGlanceMetrics(startTime, endTime) - console.log(`[useQueryInsightsGlance] Executing SQL:`, sql) + const sql = getInsightsGlanceMetrics(startTime, endTime) + console.log(`[useInsightsGlanceQuery] Executing SQL:`, sql) const { result } = await executeSql({ projectRef, sql, }) - console.log(`[useQueryInsightsGlance] Result:`, result) - return result[0] as QueryInsightsGlance + console.log(`[useInsightsGlanceQuery] Result:`, result) + return result[0] as InsightsGlance }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes ...options, diff --git a/apps/studio/data/query-insights/insights-metrics-query.ts b/apps/studio/data/query-insights/insights-metrics-query.ts new file mode 100644 index 00000000000..034ef7707ef --- /dev/null +++ b/apps/studio/data/query-insights/insights-metrics-query.ts @@ -0,0 +1,261 @@ +import { useQuery, UseQueryOptions, useQueryClient } from '@tanstack/react-query' +import { executeSql } from '../sql/execute-sql-query' +import { queryInsightsKeys } from './keys' +import { useEffect, useCallback } from 'react' + +export type InsightsMetric = { + timestamp: string + value?: number + rows?: number + cmd_type_text?: string + database: string + shared_blks_read?: number + shared_blks_hit?: number + shared_blks_dirtied?: number + shared_blks_written?: number + mean_exec_time?: number + total_exec_time?: number + p50?: number + p95?: number + p99?: number + p99_9?: number +} + +export type InsightsMetrics = { + queries: { + total: number + total_time: number + avg_time: number + total_rows: number + } + cache: { + hit_ratio: number + hits: number + misses: number + } + errors: { + total: number + } +} + +const getMetricsSql = (metric: string, startTime: string, endTime: string) => { + switch (metric) { + case 'rows_read': + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + SUM(rows) as value, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + AND cmd_type = 1 + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + + case 'query_latency': + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50, + percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95, + percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99, + percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + + case 'queries_per_second': + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + COUNT(*) / 300.0 as value, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + + case 'calls': + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + SUM(calls) as value, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + + case 'cache_hits': + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + SUM(shared_blks_hit) as shared_blks_hit, + SUM(shared_blks_read) as shared_blks_read, + SUM(shared_blks_dirtied) as shared_blks_dirtied, + SUM(shared_blks_written) as shared_blks_written, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + + case 'issues': + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as value, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + + default: + return /* SQL */ ` + SELECT + bucket_start_time as timestamp, + SUM(rows) / 300 as value, + datname as database + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}'::timestamptz + AND bucket_start_time <= '${endTime}'::timestamptz + AND bucket_done = true + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC + ` + } +} + +export function useInsightsMetricsQuery( + projectRef: string | undefined, + metric: string, + startTime: string, + endTime: string, + options?: UseQueryOptions +) { + return useQuery({ + queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime), + queryFn: async () => { + if (!projectRef) throw new Error('Project ref is required') + + const sql = getMetricsSql(metric, startTime, endTime) + console.log(`[useInsightsMetricsQuery] Executing SQL for ${metric}:`, sql) + + const { result } = await executeSql({ + projectRef, + sql, + }) + + console.log(`[useInsightsMetricsQuery] Result for ${metric}:`, result) + + return result as InsightsMetric[] + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + ...options, + }) +} + +// Hook to pre-fetch all metrics data for the current time range +export function useInsightsPrefetchQuery( + projectRef: string | undefined, + startTime: string, + endTime: string +) { + const queryClient = useQueryClient() + + useEffect(() => { + if (!projectRef) return + + // Pre-fetch all metric types + const metricTypes: Array<{ id: string; label: string }> = [ + { id: 'query_latency', label: 'Query latency' }, + { id: 'rows_read', label: 'Rows read' }, + { id: 'calls', label: 'Calls' }, + { id: 'cache_hits', label: 'Cache hits' }, + { id: 'issues', label: 'Issues' }, + ] + + // Pre-fetch metrics data + metricTypes.forEach((metric) => { + queryClient.prefetchQuery({ + queryKey: queryInsightsKeys.metrics(projectRef, metric.id, startTime, endTime), + queryFn: async () => { + const sql = getMetricsSql(metric.id, startTime, endTime) + console.log(`[useInsightsPrefetchQuery] Executing SQL for ${metric.id}:`, sql) + + const { result } = await executeSql({ + projectRef, + sql, + }) + + console.log(`[useInsightsPrefetchQuery] Result for ${metric.id}:`, result) + return result as InsightsMetric[] + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }) + }) + }, [projectRef, startTime, endTime, queryClient]) +} + +// Hook for better cache management and manual refresh control +export function useInsightsCacheManager( + projectRef: string | undefined, + startTime: string, + endTime: string +) { + const queryClient = useQueryClient() + + const refreshAllData = useCallback(async () => { + if (!projectRef) return + + const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues'] + + // Invalidate and refetch all metrics + await Promise.all([ + ...metricTypes.map((metric) => + queryClient.invalidateQueries({ + queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime), + }) + ), + ]) + }, [projectRef, startTime, endTime, queryClient]) + + const isAnyDataStale = useCallback(() => { + if (!projectRef) return false + + const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues'] + + return metricTypes.some((metric) => { + const query = queryClient.getQueryData( + queryInsightsKeys.metrics(projectRef, metric, startTime, endTime) + ) + return !query + }) + }, [projectRef, startTime, endTime, queryClient]) + + return { + refreshAllData, + isAnyDataStale, + } +} diff --git a/apps/studio/data/query-insights/insights-queries-query.ts b/apps/studio/data/query-insights/insights-queries-query.ts new file mode 100644 index 00000000000..c9dfb263b92 --- /dev/null +++ b/apps/studio/data/query-insights/insights-queries-query.ts @@ -0,0 +1,202 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql } from '../sql/execute-sql-query' +import { queryInsightsKeys } from './keys' + +export type InsightsQuery = { + query_id: number + query: string + total_time: number + calls: number + rows_read: number + rows_insert: number + rows_update: number + rows_delete: number + shared_blks_read: number + shared_blks_hit: number + mean_exec_time: number + database: string + timestamp: string + cmd_type_text: string + application_name: string + badness_score: number + slowness_rating: string + // index_advisor results + startup_cost_before?: number + startup_cost_after?: number + total_cost_before?: number + total_cost_after?: number + index_statements?: string[] + errors?: string[] + // Error tracking fields + error_count?: number +} + +const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ ` + WITH base_queries AS ( + SELECT + queryid AS query_id, + COALESCE(MAX(top_query), MAX(query)) AS query, + SUM(total_exec_time) AS total_time, + SUM(calls) AS calls, + SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, + SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, + SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, + SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, + SUM(shared_blks_read) AS shared_blks_read, + SUM(shared_blks_hit) AS shared_blks_hit, + SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time, + STRING_AGG(DISTINCT datname, ', ') AS database, + MAX(bucket_start_time) AS timestamp, + MAX(get_cmd_type(cmd_type)) AS cmd_type_text, + STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name, + ( + SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) + ) AS rows_total, + COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count, + ( + (SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * + LOG(GREATEST(SUM(calls), 1)) / + GREATEST( + SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END), + 1) + ) AS badness_score, + CASE + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT' + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE' + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE' + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW' + ELSE 'CRITICAL' + END AS slowness_rating + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}' + AND bucket_start_time <= '${endTime}' + AND bucket_done = true + GROUP BY queryid + ) + SELECT + q.*, + ia.* + FROM + base_queries q + LEFT JOIN LATERAL ( + SELECT * + FROM index_advisor(q.query) + ) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%' + ORDER BY badness_score DESC + LIMIT 9999 +` + +const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL */ ` + WITH base_queries AS ( + SELECT + queryid AS query_id, + COALESCE(MAX(top_query), MAX(query)) AS query, + SUM(total_exec_time) AS total_time, + SUM(calls) AS calls, + SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, + SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, + SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, + SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, + SUM(shared_blks_read) AS shared_blks_read, + SUM(shared_blks_hit) AS shared_blks_hit, + SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time, + STRING_AGG(DISTINCT datname, ', ') AS database, + MAX(bucket_start_time) AS timestamp, + MAX(get_cmd_type(cmd_type)) AS cmd_type_text, + STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name, + ( + SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) + ) AS rows_total, + COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count, + ( + (SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * + LOG(GREATEST(SUM(calls), 1)) / + GREATEST( + SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + + SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END), + 1) + ) AS badness_score, + CASE + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT' + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE' + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE' + WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW' + ELSE 'CRITICAL' + END AS slowness_rating + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}' + AND bucket_start_time <= '${endTime}' + AND bucket_done = true + AND mean_exec_time > 1000 + AND calls > 1 + GROUP BY queryid + ) + SELECT + q.*, + ia.* + FROM + base_queries q + LEFT JOIN LATERAL ( + SELECT * + FROM index_advisor(q.query) + ) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%' + ORDER BY error_count DESC, badness_score DESC + LIMIT 9999 +` + +export function useInsightsQueriesQuery( + projectRef: string | undefined, + startTime: string, + endTime: string, + options?: UseQueryOptions +) { + return useQuery({ + queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime), + queryFn: async () => { + if (!projectRef) throw new Error('Project ref is required') + + const { result } = await executeSql({ + projectRef, + sql: getQueriesSql(startTime, endTime), + }) + + return result as InsightsQuery[] + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + ...options, + }) +} + +export function useInsightsQueriesWithErrorsQuery( + projectRef: string | undefined, + startTime: string, + endTime: string, + options?: UseQueryOptions +) { + return useQuery({ + queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime), + queryFn: async () => { + if (!projectRef) throw new Error('Project ref is required') + + const { result } = await executeSql({ + projectRef, + sql: getQueriesWithErrorsSql(startTime, endTime), + }) + + return result as InsightsQuery[] + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + ...options, + }) +} diff --git a/apps/studio/data/query-insights/single-query-latency-query.ts b/apps/studio/data/query-insights/insights-single-ql-query.ts similarity index 69% rename from apps/studio/data/query-insights/single-query-latency-query.ts rename to apps/studio/data/query-insights/insights-single-ql-query.ts index 859f53df833..1fb31f4e3c5 100644 --- a/apps/studio/data/query-insights/single-query-latency-query.ts +++ b/apps/studio/data/query-insights/insights-single-ql-query.ts @@ -1,25 +1,26 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { executeSql } from '../sql/execute-sql-query' -import { QueryInsightsMetric } from './query-metrics-query' +import { InsightsMetric } from './insights-metrics-query' const singleQueryLatencyKeys = { - base: ['single-query-latency'] as const, + base: ['projects', 'query-insights', 'single-query'] as const, latency: ( projectRef: string | undefined, queryId: string | undefined, startTime: string, endTime: string - ) => [...singleQueryLatencyKeys.base, projectRef, queryId, startTime, endTime] as const, + ) => + [...singleQueryLatencyKeys.base, 'latency', projectRef, queryId, startTime, endTime] as const, } const singleQueryCallsKeys = { - base: ['single-query-calls'] as const, + base: ['projects', 'query-insights', 'single-query-calls'] as const, calls: ( projectRef: string | undefined, queryId: string | undefined, startTime: string, endTime: string - ) => [...singleQueryCallsKeys.base, projectRef, queryId, startTime, endTime] as const, + ) => [...singleQueryCallsKeys.base, 'calls', projectRef, queryId, startTime, endTime] as const, } const getSingleQueryLatencySql = ( @@ -30,42 +31,43 @@ const getSingleQueryLatencySql = ( SELECT bucket_start_time as timestamp, mean_exec_time as value, - calls, datname as database FROM pg_stat_monitor WHERE bucket_start_time >= '${startTime}'::timestamptz AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - AND queryid = ${queryId}::bigint + AND bucket_done = true + AND queryid = ${queryId} ORDER BY timestamp ASC ` const getSingleQueryRowsSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ ` SELECT bucket_start_time as timestamp, - SUM(rows) as value, + rows as value, datname as database FROM pg_stat_monitor WHERE bucket_start_time >= '${startTime}'::timestamptz AND bucket_start_time <= '${endTime}'::timestamptz AND bucket_done = true - AND queryid = ${queryId}::bigint - AND cmd_type = 1 - GROUP BY bucket_start_time, datname + AND queryid = ${queryId} ORDER BY timestamp ASC ` -const getSingleQueryRowsWrittenSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ ` +const getSingleQueryRowsWrittenSql = ( + queryId: string, + startTime: string, + endTime: string +) => /* SQL */ ` SELECT bucket_start_time as timestamp, - SUM(CASE WHEN cmd_type IN (2, 3, 4) THEN rows ELSE 0 END) as value, + rows as value, datname as database FROM pg_stat_monitor WHERE bucket_start_time >= '${startTime}'::timestamptz AND bucket_start_time <= '${endTime}'::timestamptz AND bucket_done = true - AND queryid = ${queryId}::bigint - GROUP BY bucket_start_time, datname + AND queryid = ${queryId} + AND cmd_type IN (2, 3, 4) -- INSERT, UPDATE, DELETE ORDER BY timestamp ASC ` @@ -78,17 +80,16 @@ const getSingleQueryCallsSql = (queryId: string, startTime: string, endTime: str WHERE bucket_start_time >= '${startTime}'::timestamptz AND bucket_start_time <= '${endTime}'::timestamptz AND bucket_done = true - AND queryid = ${queryId}::bigint - GROUP BY bucket_start_time, calls, datname + AND queryid = ${queryId} ORDER BY timestamp ASC ` -export function useSingleQueryLatency( +export function useInsightsSingleQueryLatency( projectRef: string | undefined, startTime: string, endTime: string, queryId: string | undefined, - options?: UseQueryOptions + options?: UseQueryOptions ) { return useQuery({ queryKey: singleQueryLatencyKeys.latency(projectRef, queryId, startTime, endTime), @@ -102,19 +103,19 @@ export function useSingleQueryLatency( }) console.log('Chart data points:', result) - return result as QueryInsightsMetric[] + return result as InsightsMetric[] }, enabled: !!queryId && !!projectRef, ...options, }) } -export function useSingleQueryRows( +export function useInsightsSingleQueryRows( projectRef: string | undefined, startTime: string, endTime: string, queryId: string | undefined, - options?: UseQueryOptions + options?: UseQueryOptions ) { return useQuery({ queryKey: [...singleQueryLatencyKeys.base, 'rows', projectRef, queryId, startTime, endTime], @@ -123,56 +124,63 @@ export function useSingleQueryRows( if (!queryId) return [] const sql = getSingleQueryRowsSql(queryId, startTime, endTime) - console.log('[useSingleQueryRows] Executing SQL:', sql) + console.log('[useInsightsSingleQueryRows] Executing SQL:', sql) const { result } = await executeSql({ projectRef, sql, }) - console.log('[useSingleQueryRows] Result:', result) - return result as QueryInsightsMetric[] + console.log('[useInsightsSingleQueryRows] Result:', result) + return result as InsightsMetric[] }, enabled: !!queryId && !!projectRef, ...options, }) } -export function useSingleQueryRowsWritten( +export function useInsightsSingleQueryRowsWritten( projectRef: string | undefined, startTime: string, endTime: string, queryId: string | undefined, - options?: UseQueryOptions + options?: UseQueryOptions ) { return useQuery({ - queryKey: [...singleQueryLatencyKeys.base, 'rows-written', projectRef, queryId, startTime, endTime], + queryKey: [ + ...singleQueryLatencyKeys.base, + 'rows-written', + projectRef, + queryId, + startTime, + endTime, + ], queryFn: async () => { if (!projectRef) throw new Error('Project ref is required') if (!queryId) return [] const sql = getSingleQueryRowsWrittenSql(queryId, startTime, endTime) - console.log('[useSingleQueryRowsWritten] Executing SQL:', sql) + console.log('[useInsightsSingleQueryRowsWritten] Executing SQL:', sql) const { result } = await executeSql({ projectRef, sql, }) - console.log('[useSingleQueryRowsWritten] Result:', result) - return result as QueryInsightsMetric[] + console.log('[useInsightsSingleQueryRowsWritten] Result:', result) + return result as InsightsMetric[] }, enabled: !!queryId && !!projectRef, ...options, }) } -export function useSingleQueryCalls( +export function useInsightsSingleQueryCalls( projectRef: string | undefined, startTime: string, endTime: string, queryId: string | undefined, - options?: UseQueryOptions + options?: UseQueryOptions ) { return useQuery({ queryKey: singleQueryCallsKeys.calls(projectRef, queryId, startTime, endTime), @@ -185,9 +193,9 @@ export function useSingleQueryCalls( sql: getSingleQueryCallsSql(queryId, startTime, endTime), }) - return result as QueryInsightsMetric[] + return result as InsightsMetric[] }, enabled: !!queryId && !!projectRef, ...options, }) -} \ No newline at end of file +} diff --git a/apps/studio/data/query-insights/query-metrics-query.ts b/apps/studio/data/query-insights/query-metrics-query.ts deleted file mode 100644 index 156b35745f9..00000000000 --- a/apps/studio/data/query-insights/query-metrics-query.ts +++ /dev/null @@ -1,517 +0,0 @@ -import { useQuery, UseQueryOptions, useQueryClient } from '@tanstack/react-query' -import { executeSql } from '../sql/execute-sql-query' -import { queryInsightsKeys } from './keys' -import { useEffect, useCallback } from 'react' - -export type QueryInsightsMetric = { - timestamp: string - value?: number - rows?: number - cmd_type_text?: string - database: string - shared_blks_read?: number - shared_blks_hit?: number - shared_blks_dirtied?: number - shared_blks_written?: number - mean_exec_time?: number - total_exec_time?: number - p50?: number - p95?: number - p99?: number - p99_9?: number -} - -export type QueryInsightsQuery = { - query_id: number - query: string - total_time: number - calls: number - rows_read: number - rows_insert: number - rows_update: number - rows_delete: number - shared_blks_read: number - shared_blks_hit: number - mean_exec_time: number - database: string - timestamp: string - cmd_type_text: string - application_name: string - badness_score: number - slowness_rating: string - // index_advisor results - startup_cost_before?: number - startup_cost_after?: number - total_cost_before?: number - total_cost_after?: number - index_statements?: string[] - errors?: string[] - // Error tracking fields - error_count?: number -} - -export type QueryInsightsMetrics = { - queries: { - total: number - total_time: number - avg_time: number - total_rows: number - } - cache: { - hit_ratio: number - hits: number - misses: number - } - errors: { - total: number - } -} - -const getMetricsSql = (metric: string, startTime: string, endTime: string) => { - switch (metric) { - case 'rows_read': - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - SUM(rows) as value, - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true - AND cmd_type = 1 - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - - case 'query_latency': - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50, - percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95, - percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99, - percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9, - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - case 'queries_per_second': - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - COUNT(*) / 300.0 as value, -- Convert to per-second rate (5 mins = 300 seconds) - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - case 'calls': - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - SUM(calls) as value, - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - case 'cache_hits': - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - SUM(shared_blks_hit) as shared_blks_hit, - SUM(shared_blks_read) as shared_blks_read, - SUM(shared_blks_dirtied) as shared_blks_dirtied, - SUM(shared_blks_written) as shared_blks_written, - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - case 'issues': - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as value, - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - default: - return /* SQL */ ` - SELECT - bucket_start_time as timestamp, - SUM(rows) / 300 as value, -- Convert to per-second rate (5 mins = 300 seconds) - datname as database - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}'::timestamptz - AND bucket_start_time <= '${endTime}'::timestamptz - AND bucket_done = true -- Only include completed buckets - GROUP BY bucket_start_time, datname - ORDER BY timestamp ASC - ` - } -} - -const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ ` - WITH base_queries AS ( - SELECT - queryid AS query_id, - -- Prefer top-level query if available, else fallback to normalized form - COALESCE(MAX(top_query), MAX(query)) AS query, - -- Total time spent executing this query across all calls - SUM(total_exec_time) AS total_time, - -- Number of times this query was executed - SUM(calls) AS calls, - -- Total rows affected or returned depending on command type - SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, -- SELECT - SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, -- INSERT - SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, -- UPDATE - SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, -- DELETE - -- Buffer activity: disk reads vs cache hits - SUM(shared_blks_read) AS shared_blks_read, - SUM(shared_blks_hit) AS shared_blks_hit, - -- Mean execution time per call - SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time, - -- Metadata - STRING_AGG(DISTINCT datname, ', ') AS database, - MAX(bucket_start_time) AS timestamp, - MAX(get_cmd_type(cmd_type)) AS cmd_type_text, - STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name, - -- Total number of rows affected or returned across all executions - ( - SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) - ) AS rows_total, - -- Error information (using available columns) - COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count, - -- Heuristic "badness score": - -- Penalizes queries that are slow, frequent, and touch few rows - ( - (SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * -- mean execution time - LOG(GREATEST(SUM(calls), 1)) / -- scaled by frequency - GREATEST( -- penalize low-row queries - SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END), - 1) - ) AS badness_score, - -- Slowness rating based on mean execution time - CASE - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT' - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE' - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE' - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW' - ELSE 'CRITICAL' - END AS slowness_rating - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}' - AND bucket_start_time <= '${endTime}' - AND bucket_done = true - -- AND cmd_type IN (1, 2, 3, 4) -- Only consider SELECT, INSERT, UPDATE, DELETE - GROUP BY queryid - ) - SELECT - q.*, - ia.* - FROM - base_queries q - LEFT JOIN LATERAL ( - SELECT * - FROM index_advisor(q.query) - ) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%' - ORDER BY badness_score DESC - LIMIT 9999; -` - -const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL */ ` - WITH base_queries AS ( - SELECT - queryid AS query_id, - -- Prefer top-level query if available, else fallback to normalized form - COALESCE(MAX(top_query), MAX(query)) AS query, - -- Total time spent executing this query across all calls - SUM(total_exec_time) AS total_time, - -- Number of times this query was executed - SUM(calls) AS calls, - -- Total rows affected or returned depending on command type - SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, -- SELECT - SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, -- INSERT - SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, -- UPDATE - SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, -- DELETE - -- Buffer activity: disk reads vs cache hits - SUM(shared_blks_read) AS shared_blks_read, - SUM(shared_blks_hit) AS shared_blks_hit, - -- Mean execution time per call - SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time, - -- Metadata - STRING_AGG(DISTINCT datname, ', ') AS database, - MAX(bucket_start_time) AS timestamp, - MAX(get_cmd_type(cmd_type)) AS cmd_type_text, - STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name, - -- Total number of rows affected or returned across all executions - ( - SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) - ) AS rows_total, - -- Error information (using available columns) - COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count, - -- Heuristic "badness score": - -- Penalizes queries that are slow, frequent, and touch few rows - ( - (SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * -- mean execution time - LOG(GREATEST(SUM(calls), 1)) / -- scaled by frequency - GREATEST( -- penalize low-row queries - SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) + - SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END), - 1) - ) AS badness_score, - -- Slowness rating based on mean execution time - CASE - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT' - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE' - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE' - WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW' - ELSE 'CRITICAL' - END AS slowness_rating - FROM pg_stat_monitor - WHERE bucket_start_time >= '${startTime}' - AND bucket_start_time <= '${endTime}' - AND bucket_done = true - AND mean_exec_time > 1000 - AND calls > 1 -- Only include queries that are slow and called multiple times - GROUP BY queryid - ) - SELECT - q.*, - ia.* - FROM - base_queries q - LEFT JOIN LATERAL ( - SELECT * - FROM index_advisor(q.query) - ) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%' - ORDER BY error_count DESC, badness_score DESC - LIMIT 9999; -` - -export function useQueryInsightsMetrics( - projectRef: string | undefined, - metric: string, - startTime: string, - endTime: string, - options?: UseQueryOptions -) { - return useQuery({ - queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime), - queryFn: async () => { - if (!projectRef) throw new Error('Project ref is required') - - const sql = getMetricsSql(metric, startTime, endTime) - console.log(`[useQueryInsightsMetrics] Executing SQL for ${metric}:`, sql) - - const { result } = await executeSql({ - projectRef, - sql, - }) - - console.log(`[useQueryInsightsMetrics] Result for ${metric}:`, result) - - return result as QueryInsightsMetric[] - }, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - ...options, - }) -} - -export function useQueryInsightsQueries( - projectRef: string | undefined, - startTime: string, - endTime: string, - options?: UseQueryOptions -) { - return useQuery({ - queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime), - queryFn: async () => { - if (!projectRef) throw new Error('Project ref is required') - - const { result } = await executeSql({ - projectRef, - sql: getQueriesSql(startTime, endTime), - }) - - return result as QueryInsightsQuery[] - }, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - ...options, - }) -} - -export function useQueryInsightsQueriesWithErrors( - projectRef: string | undefined, - startTime: string, - endTime: string, - options?: UseQueryOptions -) { - return useQuery({ - queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime), - queryFn: async () => { - if (!projectRef) throw new Error('Project ref is required') - - const { result } = await executeSql({ - projectRef, - sql: getQueriesWithErrorsSql(startTime, endTime), - }) - - return result as QueryInsightsQuery[] - }, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - ...options, - }) -} - -// Hook to pre-fetch all metrics data for the current time range -export function usePreFetchQueryInsightsData( - projectRef: string | undefined, - startTime: string, - endTime: string -) { - const queryClient = useQueryClient() - - useEffect(() => { - if (!projectRef) return - - // Pre-fetch all metric types - const metricTypes: Array<{ id: string; label: string }> = [ - { id: 'query_latency', label: 'Query latency' }, - { id: 'rows_read', label: 'Rows read' }, - - { id: 'calls', label: 'Calls' }, - { id: 'cache_hits', label: 'Cache hits' }, - { id: 'issues', label: 'Issues' }, - ] - - // Pre-fetch metrics data - metricTypes.forEach((metric) => { - queryClient.prefetchQuery({ - queryKey: queryInsightsKeys.metrics(projectRef, metric.id, startTime, endTime), - queryFn: async () => { - const sql = getMetricsSql(metric.id, startTime, endTime) - console.log(`[usePreFetchQueryInsightsData] Executing SQL for ${metric.id}:`, sql) - - const { result } = await executeSql({ - projectRef, - sql, - }) - - console.log(`[usePreFetchQueryInsightsData] Result for ${metric.id}:`, result) - return result as QueryInsightsMetric[] - }, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - }) - }) - - // Pre-fetch queries data - queryClient.prefetchQuery({ - queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime), - queryFn: async () => { - const { result } = await executeSql({ - projectRef, - sql: getQueriesSql(startTime, endTime), - }) - return result as QueryInsightsQuery[] - }, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - }) - - // Pre-fetch queries with errors data - queryClient.prefetchQuery({ - queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime), - queryFn: async () => { - const { result } = await executeSql({ - projectRef, - sql: getQueriesWithErrorsSql(startTime, endTime), - }) - return result as QueryInsightsQuery[] - }, - staleTime: 5 * 60 * 1000, // Cache for 5 minutes - }) - }, [projectRef, startTime, endTime, queryClient]) -} - -// Hook for better cache management and manual refresh control -export function useQueryInsightsCacheManager( - projectRef: string | undefined, - startTime: string, - endTime: string -) { - const queryClient = useQueryClient() - - const refreshAllData = useCallback(async () => { - if (!projectRef) return - - const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues'] - - // Invalidate and refetch all metrics - await Promise.all([ - ...metricTypes.map((metric) => - queryClient.invalidateQueries({ - queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime), - }) - ), - queryClient.invalidateQueries({ - queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime), - }), - queryClient.invalidateQueries({ - queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime), - }), - ]) - }, [projectRef, startTime, endTime, queryClient]) - - const isAnyDataStale = useCallback(() => { - if (!projectRef) return false - - const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues'] - - return ( - metricTypes.some((metric) => { - const query = queryClient.getQueryData( - queryInsightsKeys.metrics(projectRef, metric, startTime, endTime) - ) - return !query - }) || - !queryClient.getQueryData(queryInsightsKeys.queries(projectRef, startTime, endTime)) || - !queryClient.getQueryData(queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime)) - ) - }, [projectRef, startTime, endTime, queryClient]) - - return { - refreshAllData, - isAnyDataStale, - } -}