mirror of
https://github.com/supabase/supabase.git
synced 2026-06-20 15:26:07 +08:00
feat(studio): add columns and update labels for query performance (#38744)
* feat: change calls label to count This changes the column title for calls to count for clarity * feat: add cache hit rate col and number formatting Adds a cache hit rate column for each query as well as tidies up some number formatting. * feat: add styling for 0 numbers This makes anything marked as 0 feint in the table for easier parsing. * chore: remove debug console log * fix: silly next-env again * nit: remove avg rows col * nit: add toFixed to cache hit rate * feat: add tooltip for role column
This commit is contained in:
@@ -16,10 +16,51 @@ export const QUERY_PERFORMANCE_COLUMNS = [
|
||||
{ id: 'query', name: 'Query', description: undefined, minWidth: 500 },
|
||||
{ id: 'prop_total_time', name: 'Time consumed', description: undefined, minWidth: 130 },
|
||||
{ id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 },
|
||||
{ id: 'calls', name: 'Calls', description: undefined, minWidth: 100 },
|
||||
{ id: 'calls', name: 'Count', description: undefined, minWidth: 100 },
|
||||
{ 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 },
|
||||
{ id: 'rows_read', name: 'Rows read', description: undefined, minWidth: 100 },
|
||||
{ id: 'cache_hit_rate', name: 'Cache hit rate', description: undefined, minWidth: 130 },
|
||||
{ id: 'rolname', name: 'Role', description: undefined, minWidth: 160 },
|
||||
] as const
|
||||
|
||||
export const QUERY_PERFORMANCE_ROLE_DESCRIPTION = [
|
||||
{ name: 'postgres', description: 'The default Postgres role. This has admin privileges.' },
|
||||
{
|
||||
name: 'anon',
|
||||
description:
|
||||
'For unauthenticated, public access. This is the role which the API (PostgREST) will use when a user is not logged in.',
|
||||
},
|
||||
{
|
||||
name: 'authenticator',
|
||||
description:
|
||||
'A special role for the API (PostgREST). It has very limited access, and is used to validate a JWT and then "change into" another role determined by the JWT verification.',
|
||||
},
|
||||
{
|
||||
name: 'authenticated',
|
||||
description:
|
||||
'For "authenticated access." This is the role which the API (PostgREST) will use when a user is logged in.',
|
||||
},
|
||||
{
|
||||
name: 'service_role',
|
||||
description:
|
||||
'For elevated access. This role is used by the API (PostgREST) to bypass Row Level Security.',
|
||||
},
|
||||
{
|
||||
name: 'supabase_auth_admin',
|
||||
description:
|
||||
'Used by the Auth middleware to connect to the database and run migration. Access is scoped to the auth schema.',
|
||||
},
|
||||
{
|
||||
name: 'supabase_storage_admin',
|
||||
description:
|
||||
'Used by the Auth middleware to connect to the database and run migration. Access is scoped to the storage schema.',
|
||||
},
|
||||
{ name: 'dashboard_user', description: 'For running commands via the Supabase UI.' },
|
||||
{
|
||||
name: 'supabase_admin',
|
||||
description:
|
||||
'An internal role Supabase uses for administrative tasks, such as running upgrades and automations.',
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
cn,
|
||||
CodeBlock,
|
||||
} from 'ui'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { hasIndexRecommendations } from './index-advisor.utils'
|
||||
import { IndexSuggestionIcon } from './IndexSuggestionIcon'
|
||||
@@ -28,6 +29,7 @@ import { QueryIndexes } from './QueryIndexes'
|
||||
import {
|
||||
QUERY_PERFORMANCE_COLUMNS,
|
||||
QUERY_PERFORMANCE_REPORT_TYPES,
|
||||
QUERY_PERFORMANCE_ROLE_DESCRIPTION,
|
||||
} from './QueryPerformance.constants'
|
||||
import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort'
|
||||
|
||||
@@ -138,14 +140,6 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'rolname') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs">
|
||||
<p>{value || 'n/a'}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'prop_total_time') {
|
||||
const percentage = props.row.prop_total_time || 0
|
||||
const fillWidth = Math.min(percentage, 100)
|
||||
@@ -159,7 +153,13 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
|
||||
opacity: 0.04,
|
||||
}}
|
||||
/>
|
||||
<p>{value ? `${value.toFixed(1)}%` : 'n/a'}</p>
|
||||
{value ? (
|
||||
<p className={cn(value.toFixed(1) === '0.0' && 'text-foreground-lighter')}>
|
||||
{value.toFixed(1)}%
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -175,8 +175,99 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
|
||||
if (col.id === 'total_time') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center text-xs">
|
||||
{isTime && typeof value === 'number' && !isNaN(value) && isFinite(value) && (
|
||||
<p>{(value / 1000).toFixed(2) + 's' || 'n/a'}</p>
|
||||
{isTime && typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||||
<p
|
||||
className={cn((value / 1000).toFixed(2) === '0.00' && 'text-foreground-lighter')}
|
||||
>
|
||||
{(value / 1000).toFixed(2) + 's'}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'calls') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center text-xs">
|
||||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||||
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
||||
{value.toLocaleString()}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'max_time' || col.id === 'mean_time' || col.id === 'min_time') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center text-xs">
|
||||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||||
<p className={cn(value.toFixed(0) === '0' && 'text-foreground-lighter')}>
|
||||
{value.toFixed(0)}ms
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'rows_read') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center text-xs">
|
||||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||||
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
||||
{value.toLocaleString()}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const cacheHitRateToNumber = (value: number | string) => {
|
||||
if (typeof value === 'number') return value
|
||||
return parseFloat(value.toString().replace('%', '')) || 0
|
||||
}
|
||||
|
||||
if (col.id === 'cache_hit_rate') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center text-xs">
|
||||
{typeof value === 'string' ? (
|
||||
<p
|
||||
className={cn(
|
||||
cacheHitRateToNumber(value).toFixed(2) === '0.00' && 'text-foreground-lighter'
|
||||
)}
|
||||
>
|
||||
{cacheHitRateToNumber(value).toFixed(2)}%
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'rolname') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center">
|
||||
{value ? (
|
||||
<span className="flex items-center gap-x-1">
|
||||
<p className="font-mono text-xs">{value}</p>
|
||||
<InfoTooltip align="end" alignOffset={-12} className="w-56">
|
||||
{
|
||||
QUERY_PERFORMANCE_ROLE_DESCRIPTION.find((role) => role.name === value)
|
||||
?.description
|
||||
}
|
||||
</InfoTooltip>
|
||||
</span>
|
||||
) : (
|
||||
<p className="text-muted">–</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -369,7 +369,17 @@ select
|
||||
-- min_time,
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
statements.rows / statements.calls as avg_rows${
|
||||
statements.rows / statements.calls as avg_rows,
|
||||
statements.rows as rows_read,
|
||||
case
|
||||
when (statements.shared_blks_hit + statements.shared_blks_read) > 0
|
||||
then round(
|
||||
(statements.shared_blks_hit * 100.0) /
|
||||
(statements.shared_blks_hit + statements.shared_blks_read),
|
||||
2
|
||||
)
|
||||
else 0
|
||||
end as cache_hit_rate${
|
||||
runIndexAdvisor
|
||||
? `,
|
||||
case
|
||||
@@ -513,6 +523,15 @@ select
|
||||
-- max_time,
|
||||
-- mean_time,
|
||||
statements.rows / statements.calls as avg_rows,
|
||||
statements.rows as rows_read,
|
||||
statements.shared_blks_hit as debug_hit,
|
||||
statements.shared_blks_read as debug_read,
|
||||
case
|
||||
when (statements.shared_blks_hit + statements.shared_blks_read) > 0
|
||||
then (statements.shared_blks_hit::numeric * 100.0) /
|
||||
(statements.shared_blks_hit + statements.shared_blks_read)
|
||||
else 0
|
||||
end as cache_hit_rate,
|
||||
((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100 as prop_total_time${
|
||||
runIndexAdvisor
|
||||
? `,
|
||||
|
||||
Reference in New Issue
Block a user