From 8ba76455f1d4db44aea5f121e05e2769dbf4fb18 Mon Sep 17 00:00:00 2001 From: kemal Date: Wed, 27 Aug 2025 20:06:39 +0100 Subject: [PATCH] fix: use old sql queries as they were working --- .../QueryInsights/QueryMetricExplorer.tsx | 16 +- .../QueryInsights/QueryQuickGlance.tsx | 15 +- .../QueryInsights/QueryRowExplorer.tsx | 346 +++++++++--- .../data/query-insights/query-glance-query.ts | 52 +- .../query-insights/query-metrics-query.ts | 520 ++++++++++++------ .../single-query-latency-query.ts | 193 +++++++ 6 files changed, 875 insertions(+), 267 deletions(-) create mode 100644 apps/studio/data/query-insights/single-query-latency-query.ts diff --git a/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx b/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx index 85fe286e5c9..6ba3d6434b8 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryMetricExplorer.tsx @@ -9,7 +9,10 @@ import { CardHeader, } from 'ui' import { QueryRowExplorer } from './QueryRowExplorer' -import { useQueryInsightsMetricsQuery } from 'data/query-insights/query-metrics-query' +import { + useQueryInsightsMetrics, + usePreFetchQueryInsightsData, +} from 'data/query-insights/query-metrics-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import ComposedChart from 'components/ui/Charts/ComposedChart' import { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils' @@ -55,6 +58,9 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP return { startTime: fallbackStartTime, endTime: fallbackEndTime } }, [startTime, endTime]) + // Pre-fetch query data for the QueryRowExplorer + usePreFetchQueryInsightsData(project?.ref, effectiveStartTime, effectiveEndTime) + // Debug logging console.log('QueryMetricExplorer Debug:', { props: { startTime, endTime }, @@ -67,13 +73,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP data: metricsData, isLoading, error, - } = useQueryInsightsMetricsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - metric: selectedMetric, - startTime: effectiveStartTime, - endTime: effectiveEndTime, - }) + } = useQueryInsightsMetrics(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 387fbc262a3..aa9b189cb1b 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 { useQueryInsightsGlanceQuery } from 'data/query-insights/query-glance-query' +import { useQueryInsightsGlance } from 'data/query-insights/query-glance-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Card, CardContent } from 'ui' import { Database, Clock, AlertCircle } from 'lucide-react' @@ -29,25 +29,20 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps) console.log('QueryQuickGlance Debug:', { props: { startTime, endTime }, effective: { startTime: effectiveStartTime, endTime: effectiveEndTime }, - project: { ref: project?.ref, hasConnectionString: !!project?.connectionString } + project: { ref: project?.ref, hasConnectionString: !!project?.connectionString }, }) const { data: glanceData, isLoading, error, - } = useQueryInsightsGlanceQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - startTime: effectiveStartTime, - endTime: effectiveEndTime, - }) + } = useQueryInsightsGlance(project?.ref, effectiveStartTime, effectiveEndTime) // Debug logging for glance results console.log('QueryQuickGlance Results:', { glanceData, isLoading, - error + error, }) const cards = useMemo(() => { @@ -110,7 +105,7 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps) } if (error) { - return
Error loading query insights: {error.message}
+ return
Error loading query insights: {String(error)}
} return ( diff --git a/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx b/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx index e18ae80db17..2445590e49d 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryRowExplorer.tsx @@ -1,4 +1,4 @@ -import { ArrowDown, ArrowUp, TextSearch, X } from 'lucide-react' +import { ArrowDown, ArrowUp, Search, Settings2, TextSearch, X } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' @@ -17,7 +17,18 @@ import { cn, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { useQueryInsightsQueries, type QueryInsightsQuery } from 'data/query-insights/query-metrics-query' +import { Input } from 'ui-patterns/DataInputs/Input' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { + useQueryInsightsQueries, + type QueryInsightsQuery, +} from 'data/query-insights/query-metrics-query' +import { + useSingleQueryLatency, + useSingleQueryCalls, + useSingleQueryRows, + useSingleQueryRowsWritten, +} from 'data/query-insights/single-query-latency-query' interface QueryRowExplorerProps { startTime: string @@ -40,7 +51,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) queries, isLoading, error, - queriesLength: queries?.length + queriesLength: queries?.length, }) const [sort, setSort] = useState<{ column: string; order: 'asc' | 'desc' } | undefined>({ @@ -48,7 +59,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) order: 'desc', }) const [selectedRow, setSelectedRow] = useState() - const [view, setView] = useState<'details' | 'indexes'>('details') + const [view, setView] = useState<'details' | 'indexes' | 'metrics'>('details') // Define columns similar to QueryPerformance const columns: Column[] = [ @@ -171,9 +182,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) return (

{formattedValue}

- {value && ( -

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

- )} + {value &&

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

}
) }, @@ -205,9 +214,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) return (

{formattedValue}

- {value && ( -

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

- )} + {value &&

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

}
) }, @@ -277,29 +284,66 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) ] // For debugging - show test data if no queries found - const reportData = queries?.length ? queries : [ - { - query_id: 1, - query: 'SELECT * FROM test_table WHERE id = 1', - total_time: 150.5, - calls: 10, - rows_read: 1000, - rows_insert: 0, - rows_update: 0, - rows_delete: 0, - shared_blks_read: 50, - shared_blks_hit: 950, - mean_exec_time: 15.05, - database: 'postgres', - timestamp: new Date().toISOString(), - cmd_type_text: 'SELECT', - application_name: 'Test App', - badness_score: 25.5, - error_count: 0 - } as QueryInsightsQuery - ] + const reportData = queries?.length + ? queries + : [ + { + query_id: 1, + query: 'SELECT * FROM users WHERE id = 1', + total_time: 150.5, + calls: 10, + rows_read: 1000, + rows_insert: 0, + rows_update: 0, + rows_delete: 0, + shared_blks_read: 50, + shared_blks_hit: 950, + mean_exec_time: 15.05, + database: 'postgres', + timestamp: new Date().toISOString(), + cmd_type_text: 'SELECT', + application_name: 'Test App', + badness_score: 25.5, + error_count: 0, + startup_cost_before: 0, + startup_cost_after: 0, + total_cost_before: 0, + total_cost_after: 0, + index_statements: [], + errors: [], + } as QueryInsightsQuery, + ] const selectedQuery = selectedRow !== undefined ? reportData[selectedRow] : undefined + // Single query metrics hooks + const { data: singleQueryLatency } = useSingleQueryLatency( + ref, + startTime, + endTime, + selectedQuery?.query_id?.toString() + ) + + const { data: singleQueryCalls } = useSingleQueryCalls( + ref, + startTime, + endTime, + selectedQuery?.query_id?.toString() + ) + + const { data: singleQueryRows } = useSingleQueryRows( + ref, + startTime, + endTime, + selectedQuery?.query_id?.toString() + ) + + const { data: singleQueryRowsWritten } = useSingleQueryRowsWritten( + ref, + startTime, + endTime, + selectedQuery?.query_id?.toString() + ) + const onSortChange = (column: string) => { let updatedSort: { column: string; order: 'asc' | 'desc' } | undefined = undefined @@ -326,9 +370,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) if (aValue === undefined || bValue === undefined) return 0 if (typeof aValue === 'string' && typeof bValue === 'string') { - return sort.order === 'asc' - ? aValue.localeCompare(bValue) - : bValue.localeCompare(aValue) + return sort.order === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue) } if (typeof aValue === 'number' && typeof bValue === 'number') { @@ -338,17 +380,39 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) return 0 }) + // Debug logging for table data + console.log('QueryRowExplorer Table Debug:', { + queries, + reportData, + reportDataLength: reportData.length, + sortedDataLength: sortedData.length, + isLoading, + error, + firstRow: sortedData[0], + }) + useEffect(() => { setSelectedRow(undefined) }, [startTime, endTime]) return ( -
+
- Search - Filters + } + className="w-64" + /> + } + tooltip={{ content: { side: 'bottom', text: 'Toggle columns' } }} + className="!px-1.5" + />
- + { + console.log('DataGrid rows changed:', newRows.length) + }} rowClass={(_, idx) => { const isSelected = idx === selectedRow return [ @@ -408,7 +475,12 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) {selectedRow !== undefined && selectedQuery && ( <> - +
- +
Database

{selectedQuery.database}

-
Command Type
+
+ Command Type +

{selectedQuery.cmd_type_text}

-
Application
+
+ Application +

{selectedQuery.application_name}

-
Timestamp
-

{new Date(selectedQuery.timestamp).toLocaleString()}

+
+ Timestamp +
+

+ {new Date(selectedQuery.timestamp).toLocaleString()} +

-
Performance
+
+ Performance +
-

Total time: {selectedQuery.total_time?.toFixed(2)}ms

-

Mean time: {selectedQuery.mean_exec_time?.toFixed(2)}ms

+

+ Total time: {selectedQuery.total_time?.toFixed(2)}ms +

+

+ Mean time: {selectedQuery.mean_exec_time?.toFixed(2)}ms +

Calls: {selectedQuery.calls?.toLocaleString()}

Rows
-

Read: {selectedQuery.rows_read?.toLocaleString()}

-

Insert: {selectedQuery.rows_insert?.toLocaleString()}

-

Update: {selectedQuery.rows_update?.toLocaleString()}

-

Delete: {selectedQuery.rows_delete?.toLocaleString()}

+

+ Read: {selectedQuery.rows_read?.toLocaleString()} +

+

+ Insert: {selectedQuery.rows_insert?.toLocaleString()} +

+

+ Update: {selectedQuery.rows_update?.toLocaleString()} +

+

+ Delete: {selectedQuery.rows_delete?.toLocaleString()} +

-
Cache Performance
+
+ Cache Performance +
-

Blocks read: {selectedQuery.shared_blks_read?.toLocaleString()}

-

Blocks hit: {selectedQuery.shared_blks_hit?.toLocaleString()}

-

Hit ratio: {selectedQuery.shared_blks_hit && selectedQuery.shared_blks_read - ? ((selectedQuery.shared_blks_hit / (selectedQuery.shared_blks_hit + selectedQuery.shared_blks_read)) * 100).toFixed(1) + '%' - : 'N/A'}

+

+ Blocks read: {selectedQuery.shared_blks_read?.toLocaleString()} +

+

+ Blocks hit: {selectedQuery.shared_blks_hit?.toLocaleString()} +

+

+ Hit ratio:{' '} + {selectedQuery.shared_blks_hit && selectedQuery.shared_blks_read + ? ( + (selectedQuery.shared_blks_hit / + (selectedQuery.shared_blks_hit + + selectedQuery.shared_blks_read)) * + 100 + ).toFixed(1) + '%' + : 'N/A'} +

{selectedQuery.error_count && selectedQuery.error_count > 0 && (
Errors
-

{selectedQuery.error_count} slow queries detected

+

+ {selectedQuery.error_count} slow queries detected +

)}
+ +
+
+

Query Performance Over Time

+ + {singleQueryLatency && singleQueryLatency.length > 0 && ( +
+
+ Latency +
+
+ {singleQueryLatency.slice(-5).map((point, index) => ( +
+ {new Date(point.timestamp).toLocaleTimeString()} + {point.value?.toFixed(2)}ms +
+ ))} +
+
+ )} + + {singleQueryCalls && singleQueryCalls.length > 0 && ( +
+
Calls
+
+ {singleQueryCalls.slice(-5).map((point, index) => ( +
+ {new Date(point.timestamp).toLocaleTimeString()} + {point.value?.toLocaleString()} +
+ ))} +
+
+ )} + + {singleQueryRows && singleQueryRows.length > 0 && ( +
+
+ Rows Read +
+
+ {singleQueryRows.slice(-5).map((point, index) => ( +
+ {new Date(point.timestamp).toLocaleTimeString()} + {point.value?.toLocaleString()} +
+ ))} +
+
+ )} + + {singleQueryRowsWritten && singleQueryRowsWritten.length > 0 && ( +
+
+ Rows Written +
+
+ {singleQueryRowsWritten.slice(-5).map((point, index) => ( +
+ {new Date(point.timestamp).toLocaleTimeString()} + {point.value?.toLocaleString()} +
+ ))} +
+
+ )} + + {(!singleQueryLatency || singleQueryLatency.length === 0) && + (!singleQueryCalls || singleQueryCalls.length === 0) && + (!singleQueryRows || singleQueryRows.length === 0) && + (!singleQueryRowsWritten || singleQueryRowsWritten.length === 0) && ( +

+ No metrics data available for this query +

+ )} +
+
+

Index Recommendations

- {selectedQuery.index_statements && selectedQuery.index_statements.length > 0 ? ( + {selectedQuery.index_statements && + selectedQuery.index_statements.length > 0 ? (
- {selectedQuery.index_statements.map((statement: string, index: number) => ( -
-
{statement}
-
- ))} + {selectedQuery.index_statements.map( + (statement: string, index: number) => ( +
+
{statement}
+
+ ) + )}
) : ( -

No index recommendations available

+

+ No index recommendations available +

)}
{selectedQuery.errors && selectedQuery.errors.length > 0 && (
-

Index Advisor Errors

+

+ Index Advisor Errors +

- {selectedQuery.errors.map((error: string, index: number) => ( -
- {error} -
- ))} + {selectedQuery.errors.map((error: string, index: number) => ( +
+ {error} +
+ ))}
)} diff --git a/apps/studio/data/query-insights/query-glance-query.ts b/apps/studio/data/query-insights/query-glance-query.ts index bdb301b0bd6..b18b22fb570 100644 --- a/apps/studio/data/query-insights/query-glance-query.ts +++ b/apps/studio/data/query-insights/query-glance-query.ts @@ -11,7 +11,8 @@ export type QueryInsightsGlance = { avg_error_rate: number } -export const getQueriesGlanceMetrics = (startTime: string, endTime: string) => /* SQL */ ` +export const getQueriesGlanceMetrics = (startTime: string, endTime: string) => + /* SQL */ ` SELECT COUNT(DISTINCT query) as unique_queries, SUM(calls) as total_queries, @@ -43,7 +44,12 @@ export async function getQueryInsightsGlance( const sql = getQueriesGlanceMetrics(startTime, endTime) const { result } = await executeSql( - { projectRef, connectionString, sql, queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime) }, + { + projectRef, + connectionString, + sql, + queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime), + }, signal ) @@ -53,21 +59,29 @@ export async function getQueryInsightsGlance( export type QueryInsightsGlanceData = Awaited> export type QueryInsightsGlanceError = ExecuteSqlError -export const useQueryInsightsGlanceQuery = ( - { projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables, - { enabled = true, ...options }: UseQueryOptions = {} -) => { - return useQuery( - queryInsightsKeys.glance(projectRef, startTime, endTime), - ({ signal }) => getQueryInsightsGlance({ projectRef, connectionString, startTime, endTime }, signal), - { - enabled: enabled && typeof projectRef !== 'undefined', - staleTime: 5 * 60 * 1000, // 5 minutes - cacheTime: 10 * 60 * 1000, // 10 minutes - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - ...options - } - ) +export function useQueryInsightsGlance( + projectRef: string | undefined, + startTime: string, + endTime: string, + 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 { result } = await executeSql({ + projectRef, + sql, + }) + + console.log(`[useQueryInsightsGlance] Result:`, result) + return result[0] as QueryInsightsGlance + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + ...options, + }) } diff --git a/apps/studio/data/query-insights/query-metrics-query.ts b/apps/studio/data/query-insights/query-metrics-query.ts index 6e39ed38b71..1141493d068 100644 --- a/apps/studio/data/query-insights/query-metrics-query.ts +++ b/apps/studio/data/query-insights/query-metrics-query.ts @@ -1,17 +1,9 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' +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 - p50?: number - p95?: number - p99?: number - p99_9?: number -} - -export type QueryInsightsMetricDetail = { timestamp: string value?: number rows?: number @@ -57,27 +49,37 @@ export type QueryInsightsQuery = { error_count?: number } -export type QueryInsightsMetricsVariables = { - projectRef?: string - connectionString?: string | null - metric: 'rows_read' | 'query_latency' | 'queries_per_second' | 'calls' | 'cache_hits' - startTime: string - endTime: string +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 getQueryInsightsMetricsSQL = (metric: string, startTime: string, endTime: string) => { +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 + 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 + GROUP BY bucket_start_time, datname ORDER BY timestamp ASC ` @@ -89,205 +91,261 @@ const getQueryInsightsMetricsSQL = (metric: string, startTime: string, endTime: 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, - AVG(mean_exec_time) 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 + 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 + 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 - GROUP BY bucket_start_time + 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 + 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 + 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, - CASE - WHEN SUM(shared_blks_read) > 0 THEN - (SUM(shared_blks_hit)::float / (SUM(shared_blks_hit) + SUM(shared_blks_read))::float) * 100 - ELSE 0 - END as value + 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 + 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, - COUNT(*) as value + 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 - GROUP BY bucket_start_time + AND bucket_done = true -- Only include completed buckets + GROUP BY bucket_start_time, datname ORDER BY timestamp ASC ` } } -export async function getQueryInsightsMetrics( - { projectRef, connectionString, metric, startTime, endTime }: QueryInsightsMetricsVariables, - signal?: AbortSignal -) { - const sql = getQueryInsightsMetricsSQL(metric, startTime, endTime) - - console.log('getQueryInsightsMetrics Debug:', { - projectRef, - hasConnectionString: !!connectionString, - metric, - startTime, - endTime, - sql - }) - - const { result } = await executeSql( - { - projectRef, - connectionString, - sql, - queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime) - }, - signal +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 + 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; +` - console.log('getQueryInsightsMetrics Result:', { - result, - resultLength: result?.length, - firstRow: result?.[0] - }) - - return result as QueryInsightsMetric[] -} - -export type QueryInsightsMetricsData = Awaited> -export type QueryInsightsMetricsError = ExecuteSqlError - -export const useQueryInsightsMetricsQuery = ( - { projectRef, connectionString, metric, startTime, endTime }: QueryInsightsMetricsVariables, - { enabled = true, ...options }: UseQueryOptions = {} -) => { - return useQuery( - queryInsightsKeys.metrics(projectRef, metric, startTime, endTime), - ({ signal }) => getQueryInsightsMetrics({ projectRef, connectionString, metric, startTime, endTime }, signal), - { - enabled: enabled && typeof projectRef !== 'undefined', - staleTime: 5 * 60 * 1000, // 5 minutes - cacheTime: 10 * 60 * 1000, // 10 minutes - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - ...options - } +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 + 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; +` -// Hook to fetch all queries -export const useQueryInsightsQueries = ( +export function useQueryInsightsMetrics( projectRef: string | undefined, + metric: string, startTime: string, endTime: string, - options?: UseQueryOptions -) => { + options?: UseQueryOptions +) { return useQuery({ - queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime), + queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime), queryFn: async () => { if (!projectRef) throw new Error('Project ref is required') - const sql = /* 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, - 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 - 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; - ` - - console.log('useQueryInsightsQueries Debug:', { - projectRef, - startTime, - endTime, - sql - }) + const sql = getMetricsSql(metric, startTime, endTime) + console.log(`[useQueryInsightsMetrics] Executing SQL for ${metric}:`, sql) const { result } = await executeSql({ projectRef, sql, }) - console.log('useQueryInsightsQueries Result:', { - result, - resultLength: result?.length, - firstRow: result?.[0] + 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[] @@ -295,4 +353,148 @@ export const useQueryInsightsQueries = ( staleTime: 5 * 60 * 1000, // Cache for 5 minutes ...options, }) -} \ No newline at end of file +} + +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, + } +} diff --git a/apps/studio/data/query-insights/single-query-latency-query.ts b/apps/studio/data/query-insights/single-query-latency-query.ts new file mode 100644 index 00000000000..859f53df833 --- /dev/null +++ b/apps/studio/data/query-insights/single-query-latency-query.ts @@ -0,0 +1,193 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql } from '../sql/execute-sql-query' +import { QueryInsightsMetric } from './query-metrics-query' + +const singleQueryLatencyKeys = { + base: ['single-query-latency'] as const, + latency: ( + projectRef: string | undefined, + queryId: string | undefined, + startTime: string, + endTime: string + ) => [...singleQueryLatencyKeys.base, projectRef, queryId, startTime, endTime] as const, +} + +const singleQueryCallsKeys = { + base: ['single-query-calls'] as const, + calls: ( + projectRef: string | undefined, + queryId: string | undefined, + startTime: string, + endTime: string + ) => [...singleQueryCallsKeys.base, projectRef, queryId, startTime, endTime] as const, +} + +const getSingleQueryLatencySql = ( + queryId: string, + startTime: string, + endTime: string +) => /* SQL */ ` + 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 + ORDER BY timestamp ASC +` + +const getSingleQueryRowsSql = (queryId: string, startTime: string, endTime: string) => /* 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 queryid = ${queryId}::bigint + AND cmd_type = 1 + GROUP BY bucket_start_time, datname + ORDER BY timestamp ASC +` + +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, + 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 + ORDER BY timestamp ASC +` + +const getSingleQueryCallsSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ ` + SELECT + bucket_start_time as timestamp, + 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 + AND queryid = ${queryId}::bigint + GROUP BY bucket_start_time, calls, datname + ORDER BY timestamp ASC +` + +export function useSingleQueryLatency( + projectRef: string | undefined, + startTime: string, + endTime: string, + queryId: string | undefined, + options?: UseQueryOptions +) { + return useQuery({ + queryKey: singleQueryLatencyKeys.latency(projectRef, queryId, startTime, endTime), + queryFn: async () => { + if (!projectRef) throw new Error('Project ref is required') + if (!queryId) return [] + + const { result } = await executeSql({ + projectRef, + sql: getSingleQueryLatencySql(queryId, startTime, endTime), + }) + + console.log('Chart data points:', result) + return result as QueryInsightsMetric[] + }, + enabled: !!queryId && !!projectRef, + ...options, + }) +} + +export function useSingleQueryRows( + projectRef: string | undefined, + startTime: string, + endTime: string, + queryId: string | undefined, + options?: UseQueryOptions +) { + return useQuery({ + queryKey: [...singleQueryLatencyKeys.base, 'rows', projectRef, queryId, startTime, endTime], + queryFn: async () => { + if (!projectRef) throw new Error('Project ref is required') + if (!queryId) return [] + + const sql = getSingleQueryRowsSql(queryId, startTime, endTime) + console.log('[useSingleQueryRows] Executing SQL:', sql) + + const { result } = await executeSql({ + projectRef, + sql, + }) + + console.log('[useSingleQueryRows] Result:', result) + return result as QueryInsightsMetric[] + }, + enabled: !!queryId && !!projectRef, + ...options, + }) +} + +export function useSingleQueryRowsWritten( + projectRef: string | undefined, + startTime: string, + endTime: string, + queryId: string | undefined, + options?: UseQueryOptions +) { + return useQuery({ + 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) + + const { result } = await executeSql({ + projectRef, + sql, + }) + + console.log('[useSingleQueryRowsWritten] Result:', result) + return result as QueryInsightsMetric[] + }, + enabled: !!queryId && !!projectRef, + ...options, + }) +} + +export function useSingleQueryCalls( + projectRef: string | undefined, + startTime: string, + endTime: string, + queryId: string | undefined, + options?: UseQueryOptions +) { + return useQuery({ + queryKey: singleQueryCallsKeys.calls(projectRef, queryId, startTime, endTime), + queryFn: async () => { + if (!projectRef) throw new Error('Project ref is required') + if (!queryId) return [] + + const { result } = await executeSql({ + projectRef, + sql: getSingleQueryCallsSql(queryId, startTime, endTime), + }) + + return result as QueryInsightsMetric[] + }, + enabled: !!queryId && !!projectRef, + ...options, + }) +} \ No newline at end of file