From 5fe3383c390e100b05abd4800e2e23dfc2f154ab Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:20:52 +0100 Subject: [PATCH] feat(studio): remove query performance tabs (#38606) * feat: remove tabs and unify columns This removes the tabs from Query Performance with unified columns in the table. * chore: remove unused imports * chore: small adjustment to min max and mean time col size * chore: remove unused prop --- .../QueryPerformance/QueryDetail.tsx | 10 +- .../QueryPerformance.constants.ts | 43 ++---- .../QueryPerformance/QueryPerformance.tsx | 146 +----------------- .../QueryPerformance/QueryPerformanceGrid.tsx | 12 +- .../hooks/useIndexInvalidation.ts | 2 +- .../interfaces/Reports/Reports.constants.ts | 47 ++++++ .../interfaces/Reports/Reports.queries.ts | 7 +- .../[ref]/advisors/query-performance.tsx | 7 +- 8 files changed, 84 insertions(+), 190 deletions(-) diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx index 9f1531829c..768d10a996 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx @@ -6,7 +6,7 @@ import { formatSql } from 'lib/formatSql' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' import { QueryPanelContainer, QueryPanelSection } from './QueryPanel' import { - QUERY_PERFORMANCE_REPORTS, + QUERY_PERFORMANCE_COLUMNS, QUERY_PERFORMANCE_REPORT_TYPES, } from './QueryPerformance.constants' @@ -24,14 +24,10 @@ const SqlMonacoBlock = dynamic( } ) -export const QueryDetail = ({ - reportType, - selectedRow, - onClickViewSuggestion, -}: QueryDetailProps) => { +export const QueryDetail = ({ selectedRow, onClickViewSuggestion }: QueryDetailProps) => { // [Joshen] TODO implement this logic once the linter rules are in const isLinterWarning = false - const report = QUERY_PERFORMANCE_REPORTS[reportType] + const report = QUERY_PERFORMANCE_COLUMNS const [query, setQuery] = useState(selectedRow?.['query']) useEffect(() => { diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts index 6dc2007951..0575834829 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts @@ -2,41 +2,24 @@ export enum QUERY_PERFORMANCE_REPORT_TYPES { MOST_TIME_CONSUMING = 'most_time_consuming', MOST_FREQUENT = 'most_frequent', SLOWEST_EXECUTION = 'slowest_execution', + UNIFIED = 'unified', } export const QUERY_PERFORMANCE_PRESET_MAP = { [QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING]: 'mostTimeConsuming', [QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT]: 'mostFrequentlyInvoked', [QUERY_PERFORMANCE_REPORT_TYPES.SLOWEST_EXECUTION]: 'slowestExecutionTime', + [QUERY_PERFORMANCE_REPORT_TYPES.UNIFIED]: 'unified', } as const -export const QUERY_PERFORMANCE_REPORTS = { - [QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING]: [ - { id: 'query', name: 'Query', description: undefined, minWidth: 500 }, - { id: 'calls', name: 'Calls', description: undefined, minWidth: 100 }, - { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 }, - { id: 'prop_total_time', name: 'Time consumed', description: undefined, minWidth: 150 }, - { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: 150 }, - { id: 'rolname', name: 'Role', description: undefined, minWidth: 120 }, - ], - [QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT]: [ - { id: 'query', name: 'Query', description: undefined, minWidth: 500 }, - { id: 'avg_rows', name: 'Avg. Rows', description: undefined, minWidth: 100 }, - { id: 'calls', name: 'Calls', description: undefined, minWidth: 100 }, - { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 }, - { id: 'max_time', name: 'Max time', description: undefined, minWidth: 150 }, - { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: 150 }, - { id: 'min_time', name: 'Min time', description: undefined, minWidth: 150 }, - { id: 'rolname', name: 'Role', description: undefined, minWidth: 120 }, - ], - [QUERY_PERFORMANCE_REPORT_TYPES.SLOWEST_EXECUTION]: [ - { id: 'query', name: 'Query', description: undefined, minWidth: 500 }, - { id: 'avg_rows', name: 'Avg. Rows', description: undefined, minWidth: 100 }, - { id: 'calls', name: 'Calls', description: undefined, minWidth: 100 }, - { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 }, - { id: 'max_time', name: 'Max time', description: undefined, minWidth: 150 }, - { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: 150 }, - { id: 'min_time', name: 'Min time', description: undefined, minWidth: 150 }, - { id: 'rolname', name: 'Role', description: undefined, minWidth: 120 }, - ], -} as const +export const QUERY_PERFORMANCE_COLUMNS = [ + { id: 'query', name: 'Query', description: undefined, minWidth: 500 }, + { id: 'calls', name: 'Calls', description: undefined, minWidth: 100 }, + { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 }, + { id: 'prop_total_time', name: 'Time consumed', description: undefined, minWidth: 150 }, + { id: 'max_time', name: 'Max time', description: undefined, minWidth: 100 }, + { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: 100 }, + { id: 'min_time', name: 'Min time', description: undefined, minWidth: 100 }, + { id: 'avg_rows', name: 'Avg. Rows', description: undefined, minWidth: 100 }, + { id: 'rolname', name: 'Role', description: undefined, minWidth: 120 }, +] as const diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx index a7272f278e..9df7a22eda 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx @@ -1,7 +1,6 @@ -import { InformationCircleIcon } from '@heroicons/react/16/solid' import { X } from 'lucide-react' -import { parseAsString, useQueryStates } from 'nuqs' -import { useEffect, useMemo, useState } from 'react' +import { parseAsArrayOf, parseAsString, useQueryStates } from 'nuqs' +import { useEffect, useState } from 'react' import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS, useParams } from 'common' @@ -13,23 +12,10 @@ import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM } from 'lib/constants' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' -import { - Button, - LoadingLine, - TabsList_Shadcn_, - TabsTrigger_Shadcn_, - Tabs_Shadcn_, - Tooltip, - TooltipContent, - TooltipTrigger, - cn, -} from 'ui' +import { Button, LoadingLine, cn } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { Markdown } from '../Markdown' -import { useQueryPerformanceQuery } from '../Reports/Reports.queries' import { PresetHookResult } from '../Reports/Reports.utils' -import { QUERY_PERFORMANCE_REPORT_TYPES } from './QueryPerformance.constants' import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar' import { QueryPerformanceGrid } from './QueryPerformanceGrid' @@ -46,11 +32,11 @@ export const QueryPerformance = ({ const { data: project } = useSelectedProjectQuery() const state = useDatabaseSelectorStateSnapshot() - const [{ preset }, setSearchParams] = useQueryStates({ + const [{ search: searchQuery, roles }] = useQueryStates({ sort: parseAsString, search: parseAsString, order: parseAsString, - preset: parseAsString.withDefault(QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING), + roles: parseAsArrayOf(parseAsString).withDefault([]), }) const { isLoading, isRefetching } = queryPerformanceQuery @@ -71,133 +57,13 @@ export const QueryPerformance = ({ const { data: databases } = useReadReplicasQuery({ projectRef: ref }) - const { data: mostTimeConsumingQueries, isLoading: isLoadingMTC } = useQueryPerformanceQuery({ - preset: 'mostTimeConsuming', - }) - const { data: mostFrequentlyInvoked, isLoading: isLoadingMFI } = useQueryPerformanceQuery({ - preset: 'mostFrequentlyInvoked', - }) - const { data: slowestExecutionTime, isLoading: isLoadingMMF } = useQueryPerformanceQuery({ - preset: 'slowestExecutionTime', - }) - - const QUERY_PERFORMANCE_TABS = useMemo(() => { - return [ - { - id: QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING, - label: 'Most time consuming', - description: 'Lists queries ordered by their cumulative total execution time.', - isLoading: isLoadingMTC, - max: - (mostTimeConsumingQueries ?? []).length > 0 - ? Math.max(...(mostTimeConsumingQueries ?? []).map((x: any) => x.total_time)).toFixed(2) - : undefined, - }, - { - id: QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT, - label: 'Most frequent', - description: 'Lists queries in order of their execution count', - isLoading: isLoadingMFI, - max: - (mostFrequentlyInvoked ?? []).length > 0 - ? Math.max(...(mostFrequentlyInvoked ?? []).map((x: any) => x.calls)).toFixed(2) - : undefined, - }, - { - id: QUERY_PERFORMANCE_REPORT_TYPES.SLOWEST_EXECUTION, - label: 'Slowest execution', - description: 'Lists queries ordered by their maximum execution time', - isLoading: isLoadingMMF, - max: - (slowestExecutionTime ?? []).length > 0 - ? Math.max(...(slowestExecutionTime ?? []).map((x: any) => x.max_time)).toFixed(2) - : undefined, - }, - ] - }, [ - isLoadingMFI, - isLoadingMMF, - isLoadingMTC, - mostFrequentlyInvoked, - mostTimeConsumingQueries, - slowestExecutionTime, - ]) - useEffect(() => { state.setSelectedDatabaseId(ref) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ref]) return ( <> - setSearchParams({ preset: value })} - > - - {QUERY_PERFORMANCE_TABS.map((tab) => { - const tabMax = Number(tab.max) - const maxValue = - tab.id !== QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT - ? tabMax > 1000 - ? (tabMax / 1000).toFixed(2) - : tabMax.toFixed(0) - : tabMax.toLocaleString() - - return ( - - {tab.id === preset && ( -
- )} - -
- {tab.label} - - - - - {tab.description} - -
- {tab.isLoading ? ( - - ) : tab.max === undefined ? ( - - No data yet - - ) : ( - - {maxValue} - {tab.id !== QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT - ? tabMax > 1000 - ? 's' - : 'ms' - : ' calls'} - - )} - - {tab.id === preset && ( -
- )} - - ) - })} - - - setShowResetgPgStatStatements(true)} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx index db7336f9ab..afbe23df0d 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx @@ -26,7 +26,7 @@ import { IndexSuggestionIcon } from './IndexSuggestionIcon' import { QueryDetail } from './QueryDetail' import { QueryIndexes } from './QueryIndexes' import { - QUERY_PERFORMANCE_REPORTS, + QUERY_PERFORMANCE_COLUMNS, QUERY_PERFORMANCE_REPORT_TYPES, } from './QueryPerformance.constants' import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' @@ -38,15 +38,14 @@ interface QueryPerformanceGridProps { export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformanceGridProps) => { const { sort, setSortConfig } = useQueryPerformanceSort() const gridRef = useRef(null) - const { preset, sort: urlSort, order, roles, search } = useParams() + const { sort: urlSort, order, roles, search } = useParams() const { isLoading, data } = queryPerformanceQuery const [view, setView] = useState<'details' | 'suggestion'>('details') const [selectedRow, setSelectedRow] = useState() - const reportType = - (preset as QUERY_PERFORMANCE_REPORT_TYPES) ?? QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING + const reportType = QUERY_PERFORMANCE_REPORT_TYPES.UNIFIED - const columns = QUERY_PERFORMANCE_REPORTS[reportType].map((col) => { + const columns = QUERY_PERFORMANCE_COLUMNS.map((col) => { const nonSortableColumns = ['query'] const result: Column = { @@ -214,7 +213,8 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance useEffect(() => { setSelectedRow(undefined) - }, [preset, search, roles, urlSort, order]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [search, roles, urlSort, order]) const handleKeyDown = useCallback( (event: KeyboardEvent) => { diff --git a/apps/studio/components/interfaces/QueryPerformance/hooks/useIndexInvalidation.ts b/apps/studio/components/interfaces/QueryPerformance/hooks/useIndexInvalidation.ts index edea843039..2a94bbb3f1 100644 --- a/apps/studio/components/interfaces/QueryPerformance/hooks/useIndexInvalidation.ts +++ b/apps/studio/components/interfaces/QueryPerformance/hooks/useIndexInvalidation.ts @@ -26,7 +26,7 @@ export function useIndexInvalidation() { sort: parseAsString, search: parseAsString.withDefault(''), order: parseAsString, - preset: parseAsString.withDefault(QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING), + preset: parseAsString.withDefault('unified'), }) const preset = QUERY_PERFORMANCE_PRESET_MAP[urlPreset as QUERY_PERFORMANCE_REPORT_TYPES] diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts index af8866eae5..28e9067953 100644 --- a/apps/studio/components/interfaces/Reports/Reports.constants.ts +++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts @@ -492,6 +492,53 @@ select sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read),0) as ratio from pg_statio_user_tables;`, }, + unified: { + queryType: 'db', + sql: (_params, where, orderBy, runIndexAdvisor = false) => ` + -- reports-query-performance-unified + set search_path to public, extensions; + + select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows, + ((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100 as prop_total_time${ + runIndexAdvisor + ? `, + case + when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%') + then ( + select json_build_object( + 'has_suggestion', array_length(index_statements, 1) > 0, + 'startup_cost_before', startup_cost_before, + 'startup_cost_after', startup_cost_after, + 'total_cost_before', total_cost_before, + 'total_cost_after', total_cost_after, + 'index_statements', index_statements + ) + from index_advisor(statements.query) + ) + else null + end as index_advisor_result` + : '' + } + from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid + ${where || ''} + ${orderBy || 'order by statements.total_exec_time + statements.total_plan_time desc'} + limit 20`, + }, }, }, [Presets.DATABASE]: { diff --git a/apps/studio/components/interfaces/Reports/Reports.queries.ts b/apps/studio/components/interfaces/Reports/Reports.queries.ts index 8de1c7bde5..1ceae34789 100644 --- a/apps/studio/components/interfaces/Reports/Reports.queries.ts +++ b/apps/studio/components/interfaces/Reports/Reports.queries.ts @@ -17,7 +17,12 @@ export type QueryPerformanceSort = { } export type QueryPerformanceQueryOpts = { - preset: 'mostFrequentlyInvoked' | 'mostTimeConsuming' | 'slowestExecutionTime' | 'queryHitRate' + preset: + | 'mostFrequentlyInvoked' + | 'mostTimeConsuming' + | 'slowestExecutionTime' + | 'queryHitRate' + | 'unified' searchQuery?: string orderBy?: QueryPerformanceSort roles?: string[] diff --git a/apps/studio/pages/project/[ref]/advisors/query-performance.tsx b/apps/studio/pages/project/[ref]/advisors/query-performance.tsx index f35dc61ed3..fccb44e206 100644 --- a/apps/studio/pages/project/[ref]/advisors/query-performance.tsx +++ b/apps/studio/pages/project/[ref]/advisors/query-performance.tsx @@ -25,11 +25,10 @@ const QueryPerformanceReport: NextPageWithLayout = () => { const { isIndexAdvisorEnabled } = useIndexAdvisorStatus() const { sort: sortConfig } = useQueryPerformanceSort() - const [{ preset: urlPreset, search: searchQuery, roles }] = useQueryStates({ + const [{ search: searchQuery, roles }] = useQueryStates({ sort: parseAsString, order: parseAsString, search: parseAsString.withDefault(''), - preset: parseAsString.withDefault(QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING), roles: parseAsArrayOf(parseAsString).withDefault([]), }) @@ -37,12 +36,10 @@ const QueryPerformanceReport: NextPageWithLayout = () => { const hooks = queriesFactory(config.queries, ref ?? 'default') const queryHitRate = hooks.queryHitRate() - const preset = QUERY_PERFORMANCE_PRESET_MAP[urlPreset as QUERY_PERFORMANCE_REPORT_TYPES] - const queryPerformanceQuery = useQueryPerformanceQuery({ searchQuery, orderBy: sortConfig || undefined, - preset, + preset: 'unified', roles, runIndexAdvisor: isIndexAdvisorEnabled, })