mirror of
https://github.com/supabase/supabase.git
synced 2026-05-23 01:39:34 +08:00
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
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Tabs_Shadcn_
|
||||
value={preset}
|
||||
defaultValue={preset}
|
||||
onValueChange={(value) => setSearchParams({ preset: value })}
|
||||
>
|
||||
<TabsList_Shadcn_ className={cn('flex gap-0 border-0 items-end z-10')}>
|
||||
{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 (
|
||||
<TabsTrigger_Shadcn_
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className={cn(
|
||||
'group relative',
|
||||
'px-6 py-3 border-b-0 flex flex-col items-start !shadow-none border-default border-t',
|
||||
'even:border-x last:border-r even:!border-x-strong last:!border-r-strong',
|
||||
tab.id === preset ? '!bg-surface-200' : '!bg-surface-200/[33%]',
|
||||
'hover:!bg-surface-100',
|
||||
'data-[state=active]:!bg-surface-200',
|
||||
'hover:text-foreground-light',
|
||||
'transition'
|
||||
)}
|
||||
>
|
||||
{tab.id === preset && (
|
||||
<div className="absolute top-0 left-0 w-full h-[1px] bg-foreground" />
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span className="">{tab.label}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InformationCircleIcon className="transition text-foreground-muted w-3 h-3 data-[state=delayed-open]:text-foreground-light" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{tab.description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{tab.isLoading ? (
|
||||
<ShimmeringLoader className="w-32 pt-1" />
|
||||
) : tab.max === undefined ? (
|
||||
<span className="text-xs text-foreground-muted group-hover:text-foreground-lighter group-data-[state=active]:text-foreground-lighter transition">
|
||||
No data yet
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-foreground-muted group-hover:text-foreground-lighter group-data-[state=active]:text-foreground-lighter transition">
|
||||
{maxValue}
|
||||
{tab.id !== QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT
|
||||
? tabMax > 1000
|
||||
? 's'
|
||||
: 'ms'
|
||||
: ' calls'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{tab.id === preset && (
|
||||
<div className="absolute bottom-0 left-0 w-full h-[1px] bg-surface-200"></div>
|
||||
)}
|
||||
</TabsTrigger_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</TabsList_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
|
||||
<QueryPerformanceFilterBar
|
||||
queryPerformanceQuery={queryPerformanceQuery}
|
||||
onResetReportClick={() => setShowResetgPgStatStatements(true)}
|
||||
|
||||
@@ -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<DataGridHandle>(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<number>()
|
||||
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<any> = {
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]: {
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user