diff --git a/apps/studio/components/interfaces/QueryInsights/QueryInsights.constants.ts b/apps/studio/components/interfaces/QueryInsights/QueryInsights.constants.ts index bb0272fbfe7..f57278dd613 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryInsights.constants.ts +++ b/apps/studio/components/interfaces/QueryInsights/QueryInsights.constants.ts @@ -11,10 +11,14 @@ export type ColumnConfiguration = { id: string; width?: number } export const QUERY_INSIGHTS_TABLE_COLUMNS: QueryInsightsTableColumn[] = [ { id: 'query', name: 'Query', minWidth: 600, width: 600, resizable: true }, { id: 'slowness_rating', name: 'Slowness Rating', minWidth: 140, width: 120, resizable: true }, - { id: 'database', name: 'Database', minWidth: 120, width: 120, resizable: true }, - { id: 'calls', name: 'Calls', minWidth: 100, width: 100, resizable: true }, - { id: 'total_time', name: 'Total time', minWidth: 180, width: 180, resizable: true }, + { id: 'total_time', name: 'Total time (latency)', minWidth: 180, width: 180, resizable: true }, { id: 'mean_exec_time', name: 'Mean time', minWidth: 150, width: 150, resizable: true }, + { id: 'calls', name: 'Calls', minWidth: 100, width: 100, resizable: true }, { id: 'rows_read', name: 'Rows read', minWidth: 120, width: 120, resizable: true }, - { id: 'badness_score', name: 'Badness score', minWidth: 150, width: 150, resizable: true }, + { id: 'avg_p90', name: 'Avg p90', minWidth: 120, width: 120, resizable: true }, + { id: 'avg_p95', name: 'Avg p95', minWidth: 120, width: 120, resizable: true }, + { id: 'badness_score', name: 'Badness Score', minWidth: 120, width: 120, resizable: true }, + { id: 'total_cost_before', name: 'Total Cost', minWidth: 120, width: 120, resizable: true }, + { id: 'index_advisor', name: 'Index advisor', minWidth: 150, width: 150, resizable: true }, + { id: 'last_run', name: 'Last run', minWidth: 200, width: 200, resizable: true }, ] diff --git a/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx b/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx index 30fa2ecc218..6ad1b732183 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryInsights.utils.tsx @@ -48,7 +48,6 @@ export const formatQueryInsightsColumns = ({ >
{col.name}
- {col.id === 'total_time' &&latency
}{value}
-{formattedValue}
+ {value && ( +{((value as number) / 1000).toFixed(2)}s
+ )} +{formattedValue}
+ {value && ( +{((value as number) / 1000).toFixed(2)}s
+ )} +-
+{formattedValue}
+{value}
diff --git a/apps/studio/data/query-insights/insights-queries-query.ts b/apps/studio/data/query-insights/insights-queries-query.ts index c9dfb263b92..1b4a7e96f39 100644 --- a/apps/studio/data/query-insights/insights-queries-query.ts +++ b/apps/studio/data/query-insights/insights-queries-query.ts @@ -20,6 +20,9 @@ export type InsightsQuery = { application_name: string badness_score: number slowness_rating: string + avg_p90: number + avg_p95: number + last_run: string // index_advisor results startup_cost_before?: number startup_cost_after?: number @@ -27,6 +30,7 @@ export type InsightsQuery = { total_cost_after?: number index_statements?: string[] errors?: string[] + is_optimized?: boolean // Error tracking fields error_count?: number } @@ -72,18 +76,47 @@ const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ ` 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 + END AS slowness_rating, + MAX(bucket_start_time) AS last_run FROM pg_stat_monitor WHERE bucket_start_time >= '${startTime}' AND bucket_start_time <= '${endTime}' AND bucket_done = true GROUP BY queryid + ), + percentile_data AS ( + SELECT + queryid, + percentile_cont(0.90) WITHIN GROUP (ORDER BY mean_exec_time) AS p90_time, + percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) AS p95_time + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}' + AND bucket_start_time <= '${endTime}' + AND bucket_done = true + AND mean_exec_time IS NOT NULL + GROUP BY queryid ) SELECT q.*, - ia.* + COALESCE(p.p90_time, 0) AS avg_p90, + COALESCE(p.p95_time, 0) AS avg_p95, + COALESCE((ia.startup_cost_before->>0)::numeric, 0) AS startup_cost_before, + COALESCE((ia.startup_cost_after->>0)::numeric, 0) AS startup_cost_after, + COALESCE((ia.total_cost_before->>0)::numeric, 0) AS total_cost_before, + COALESCE((ia.total_cost_after->>0)::numeric, 0) AS total_cost_after, + ia.index_statements, + ia.errors, + -- Check if query is optimized: if no index statements are returned (meaning query is already optimized) + -- or if the cost improvement is significant (more than 10% improvement) + CASE + WHEN ia.index_statements IS NULL OR array_length(ia.index_statements, 1) = 0 THEN true + WHEN ia.total_cost_after IS NOT NULL AND ia.total_cost_before IS NOT NULL + AND (ia.total_cost_after->>0)::numeric < (ia.total_cost_before->>0)::numeric * 0.9 THEN true + ELSE false + END AS is_optimized FROM base_queries q + LEFT JOIN percentile_data p ON q.query_id = p.queryid LEFT JOIN LATERAL ( SELECT * FROM index_advisor(q.query) @@ -133,7 +166,8 @@ const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL * 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 + END AS slowness_rating, + MAX(bucket_start_time) AS last_run FROM pg_stat_monitor WHERE bucket_start_time >= '${startTime}' AND bucket_start_time <= '${endTime}' @@ -141,12 +175,42 @@ const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL * AND mean_exec_time > 1000 AND calls > 1 GROUP BY queryid + ), + percentile_data AS ( + SELECT + queryid, + percentile_cont(0.90) WITHIN GROUP (ORDER BY mean_exec_time) AS p90_time, + percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) AS p95_time + FROM pg_stat_monitor + WHERE bucket_start_time >= '${startTime}' + AND bucket_start_time <= '${endTime}' + AND bucket_done = true + AND mean_exec_time IS NOT NULL + AND mean_exec_time > 1000 + AND calls > 1 + GROUP BY queryid ) SELECT q.*, - ia.* + COALESCE(p.p90_time, 0) AS avg_p90, + COALESCE(p.p95_time, 0) AS avg_p95, + COALESCE((ia.startup_cost_before->>0)::numeric, 0) AS startup_cost_before, + COALESCE((ia.startup_cost_after->>0)::numeric, 0) AS startup_cost_after, + COALESCE((ia.total_cost_before->>0)::numeric, 0) AS total_cost_before, + COALESCE((ia.total_cost_after->>0)::numeric, 0) AS total_cost_after, + ia.index_statements, + ia.errors, + -- Check if query is optimized: if no index statements are returned (meaning query is already optimized) + -- or if the cost improvement is significant (more than 10% improvement) + CASE + WHEN ia.index_statements IS NULL OR array_length(ia.index_statements, 1) = 0 THEN true + WHEN ia.total_cost_after IS NOT NULL AND ia.total_cost_before IS NOT NULL + AND (ia.total_cost_after->>0)::numeric < (ia.total_cost_before->>0)::numeric * 0.9 THEN true + ELSE false + END AS is_optimized FROM base_queries q + LEFT JOIN percentile_data p ON q.query_id = p.queryid LEFT JOIN LATERAL ( SELECT * FROM index_advisor(q.query) diff --git a/apps/studio/pages/project/[ref]/reports/query-insights.tsx b/apps/studio/pages/project/[ref]/reports/query-insights.tsx index 47dee8303cf..b675012fde0 100644 --- a/apps/studio/pages/project/[ref]/reports/query-insights.tsx +++ b/apps/studio/pages/project/[ref]/reports/query-insights.tsx @@ -46,8 +46,12 @@ const QueryInsights = () => { } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES) // Convert selectedDateRange to startTime and endTime for QueryMetricExplorer - const startTime = selectedDateRange?.period_start?.date ? dayjs(selectedDateRange.period_start.date).toISOString() : undefined - const endTime = selectedDateRange?.period_end?.date ? dayjs(selectedDateRange.period_end.date).toISOString() : undefined + const startTime = selectedDateRange?.period_start?.date + ? dayjs(selectedDateRange.period_start.date).toISOString() + : dayjs().subtract(1, 'hour').toISOString() + const endTime = selectedDateRange?.period_end?.date + ? dayjs(selectedDateRange.period_end.date).toISOString() + : dayjs().toISOString() return ( <>