mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
feat: total cost of query column
This commit is contained in:
1 parent
9d34413797
commit
70e193180a
4 files changed
+181
-19
No files matched your search
@@ -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 },
|
||||
]
|
||||
@@ -48,7 +48,6 @@ export const formatQueryInsightsColumns = ({
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="!text-foreground">{col.name}</p>
|
||||
{col.id === 'total_time' && <p className="text-foreground-lighter">latency</p>}
|
||||
</div>
|
||||
{sort?.column === col.id && (
|
||||
<>{sort.order === 'desc' ? <ArrowDown size={14} /> : <ArrowUp size={14} />}</>
|
||||
@@ -104,14 +103,6 @@ export const formatQueryInsightsColumns = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'database') {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs">
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'calls') {
|
||||
const formattedValue = (value as number)?.toLocaleString() || ''
|
||||
return (
|
||||
@@ -154,6 +145,30 @@ export const formatQueryInsightsColumns = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'avg_p90') {
|
||||
const formattedValue = value ? `${(value as number).toFixed(0)}ms` : ''
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs text-right">
|
||||
<p>{formattedValue}</p>
|
||||
{value && (
|
||||
<p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'avg_p95') {
|
||||
const formattedValue = value ? `${(value as number).toFixed(0)}ms` : ''
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs text-right">
|
||||
<p>{formattedValue}</p>
|
||||
{value && (
|
||||
<p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'badness_score') {
|
||||
const formattedValue = value ? (value as number).toFixed(2) : ''
|
||||
return (
|
||||
@@ -163,6 +178,81 @@ export const formatQueryInsightsColumns = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'total_cost_before') {
|
||||
// Ensure costValue is a number, handle string conversion from jsonb
|
||||
const costValue = typeof value === 'string' ? parseFloat(value) : (value as number)
|
||||
const numericValue = isNaN(costValue) ? 0 : costValue
|
||||
const formattedValue = numericValue.toFixed(2)
|
||||
|
||||
const getCostBadgeVariant = (cost: number) => {
|
||||
if (cost <= 10) return 'success' // green
|
||||
if (cost <= 1000) return 'warning' // yellow
|
||||
return 'destructive' // red
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex-col justify-center font-mono text-xs inline-flex">
|
||||
<span>
|
||||
<Badge
|
||||
variant={getCostBadgeVariant(numericValue)}
|
||||
className="text-xs !text-center !inline-flex items-center"
|
||||
>
|
||||
{formattedValue}
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'last_run') {
|
||||
if (!value)
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs text-center">
|
||||
<p className="text-foreground-lighter">-</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const date = new Date(value as string)
|
||||
const formattedValue = date.toLocaleString()
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs text-right">
|
||||
<p>{formattedValue}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (col.id === 'index_advisor') {
|
||||
const isOptimized = props.row?.is_optimized
|
||||
|
||||
if (isOptimized) {
|
||||
return (
|
||||
<div className="w-full flex-col justify-center font-mono text-xs inline-flex">
|
||||
<span>
|
||||
<Badge
|
||||
variant="success"
|
||||
className="text-xs !text-center !inline-flex items-center"
|
||||
>
|
||||
Optimized
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="w-full flex-col justify-center font-mono text-xs inline-flex">
|
||||
<span>
|
||||
<Badge
|
||||
variant="warning"
|
||||
className="text-xs !text-center !inline-flex items-center"
|
||||
>
|
||||
Not optimized
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center font-mono text-xs">
|
||||
<p>{value}</p>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user