mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
chore: clean up query insights query files
This handles a little bit of concern separation and isolating our hooks and sql queries so we can modify them.
This commit is contained in:
1 parent
c4676cb5a2
commit
1837644c7e
9 files changed
+559
-598
No files matched your search
@@ -2,7 +2,7 @@ import { ArrowDown, ArrowUp } from 'lucide-react'
|
||||
import { Column } from 'react-data-grid'
|
||||
|
||||
import { Badge } from 'ui'
|
||||
import { type QueryInsightsQuery } from 'data/query-insights/query-metrics-query'
|
||||
import { type InsightsQuery } from 'data/query-insights/insights-queries-query'
|
||||
import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants'
|
||||
|
||||
export const formatNumberWithCommas = (num: number | string | undefined | null): string => {
|
||||
@@ -29,7 +29,7 @@ export const formatQueryInsightsColumns = ({
|
||||
|
||||
let gridColumns = QUERY_INSIGHTS_TABLE_COLUMNS.map((col) => {
|
||||
const savedConfig = config.find((c) => c.id === col.id)
|
||||
const res: Column<QueryInsightsQuery> = {
|
||||
const res: Column<InsightsQuery> = {
|
||||
key: col.id,
|
||||
name: col.name,
|
||||
resizable: col.resizable ?? true,
|
||||
@@ -53,8 +53,8 @@ export const formatQueryInsightsColumns = ({
|
||||
)
|
||||
},
|
||||
renderCell: (props) => {
|
||||
const value = props.row?.[col.id as keyof QueryInsightsQuery]
|
||||
|
||||
const value = props.row?.[col.id as keyof InsightsQuery]
|
||||
|
||||
if (col.id === 'query') {
|
||||
return (
|
||||
<div className="w-full flex items-center gap-x-2">
|
||||
@@ -81,7 +81,10 @@ export const formatQueryInsightsColumns = ({
|
||||
return (
|
||||
<div className="w-full flex-col justify-center font-mono text-xs inline-flex">
|
||||
<span>
|
||||
<Badge variant={getBadgeVariant(value as string)} className="text-xs !text-center !inline-flex">
|
||||
<Badge
|
||||
variant={getBadgeVariant(value as string)}
|
||||
className="text-xs !text-center !inline-flex"
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
</span>
|
||||
@@ -111,7 +114,9 @@ export const formatQueryInsightsColumns = ({
|
||||
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>}
|
||||
{value && (
|
||||
<p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -121,7 +126,9 @@ export const formatQueryInsightsColumns = ({
|
||||
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>}
|
||||
{value && (
|
||||
<p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
} from 'ui'
|
||||
import { QueryRowExplorer } from './QueryRowExplorer'
|
||||
import {
|
||||
useQueryInsightsMetrics,
|
||||
usePreFetchQueryInsightsData,
|
||||
} from 'data/query-insights/query-metrics-query'
|
||||
useInsightsMetricsQuery,
|
||||
useInsightsPrefetchQuery,
|
||||
} from 'data/query-insights/insights-metrics-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import ComposedChart from 'components/ui/Charts/ComposedChart'
|
||||
import { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
||||
@@ -59,7 +59,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
}, [startTime, endTime])
|
||||
|
||||
// Pre-fetch query data for the QueryRowExplorer
|
||||
usePreFetchQueryInsightsData(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
useInsightsPrefetchQuery(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging
|
||||
console.log('QueryMetricExplorer Debug:', {
|
||||
@@ -73,7 +73,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
data: metricsData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQueryInsightsMetrics(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime)
|
||||
} = useInsightsMetricsQuery(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging for query results
|
||||
console.log('QueryMetricExplorer Query Results:', {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQueryInsightsGlance } from 'data/query-insights/query-glance-query'
|
||||
import { useInsightsGlanceQuery } from 'data/query-insights/insights-glance-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Card, CardContent } from 'ui'
|
||||
import { Database, Clock, AlertCircle } from 'lucide-react'
|
||||
@@ -36,7 +36,7 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps)
|
||||
data: glanceData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQueryInsightsGlance(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
} = useInsightsGlanceQuery(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging for glance results
|
||||
console.log('QueryQuickGlance Results:', {
|
||||
|
||||
@@ -21,15 +21,15 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FilterPopover } from 'components/ui/FilterPopover'
|
||||
import {
|
||||
useQueryInsightsQueries,
|
||||
type QueryInsightsQuery,
|
||||
} from 'data/query-insights/query-metrics-query'
|
||||
useInsightsQueriesQuery,
|
||||
type InsightsQuery,
|
||||
} from 'data/query-insights/insights-queries-query'
|
||||
import {
|
||||
useSingleQueryLatency,
|
||||
useSingleQueryCalls,
|
||||
useSingleQueryRows,
|
||||
useSingleQueryRowsWritten,
|
||||
} from 'data/query-insights/single-query-latency-query'
|
||||
useInsightsSingleQueryLatency,
|
||||
useInsightsSingleQueryCalls,
|
||||
useInsightsSingleQueryRows,
|
||||
useInsightsSingleQueryRowsWritten,
|
||||
} from 'data/query-insights/insights-single-ql-query'
|
||||
import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants'
|
||||
import { formatQueryInsightsColumns } from './QueryInsights.utils'
|
||||
|
||||
@@ -44,7 +44,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const { data: queries, isLoading, error } = useQueryInsightsQueries(ref, startTime, endTime)
|
||||
const { data: queries, isLoading, error } = useInsightsQueriesQuery(ref, startTime, endTime)
|
||||
|
||||
// Debug logging
|
||||
console.log('QueryRowExplorer Debug:', {
|
||||
@@ -128,15 +128,15 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
total_cost_after: 0,
|
||||
index_statements: [],
|
||||
errors: [],
|
||||
} as QueryInsightsQuery,
|
||||
} as InsightsQuery,
|
||||
]
|
||||
|
||||
// Sort data based on current sort state
|
||||
const sortedData = [...reportData].sort((a, b) => {
|
||||
if (!sort) return 0
|
||||
|
||||
const aValue = a[sort.column as keyof QueryInsightsQuery]
|
||||
const bValue = b[sort.column as keyof QueryInsightsQuery]
|
||||
const aValue = a[sort.column as keyof InsightsQuery]
|
||||
const bValue = b[sort.column as keyof InsightsQuery]
|
||||
|
||||
if (aValue === undefined || bValue === undefined) return 0
|
||||
|
||||
@@ -159,28 +159,28 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
const selectedQuery = selectedRow !== undefined ? filteredData[selectedRow] : undefined
|
||||
|
||||
// Single query metrics hooks
|
||||
const { data: singleQueryLatency } = useSingleQueryLatency(
|
||||
const { data: singleQueryLatency } = useInsightsSingleQueryLatency(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryCalls } = useSingleQueryCalls(
|
||||
const { data: singleQueryCalls } = useInsightsSingleQueryCalls(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryRows } = useSingleQueryRows(
|
||||
const { data: singleQueryRows } = useInsightsSingleQueryRows(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryRowsWritten } = useSingleQueryRowsWritten(
|
||||
const { data: singleQueryRowsWritten } = useInsightsSingleQueryRowsWritten(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
|
||||
+15
-15
@@ -2,7 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
|
||||
import { queryInsightsKeys } from './keys'
|
||||
|
||||
export type QueryInsightsGlance = {
|
||||
export type InsightsGlance = {
|
||||
unique_queries: number
|
||||
total_queries: number
|
||||
avg_query_time: number
|
||||
@@ -11,7 +11,7 @@ export type QueryInsightsGlance = {
|
||||
avg_error_rate: number
|
||||
}
|
||||
|
||||
export const getQueriesGlanceMetrics = (startTime: string, endTime: string) =>
|
||||
export const getInsightsGlanceMetrics = (startTime: string, endTime: string) =>
|
||||
/* SQL */ `
|
||||
SELECT
|
||||
COUNT(DISTINCT query) as unique_queries,
|
||||
@@ -30,18 +30,18 @@ export const getQueriesGlanceMetrics = (startTime: string, endTime: string) =>
|
||||
AND bucket_done = true
|
||||
`.trim()
|
||||
|
||||
export type QueryInsightsGlanceVariables = {
|
||||
export type InsightsGlanceVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string | null
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
|
||||
export async function getQueryInsightsGlance(
|
||||
{ projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables,
|
||||
export async function getInsightsGlance(
|
||||
{ projectRef, connectionString, startTime, endTime }: InsightsGlanceVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const sql = getQueriesGlanceMetrics(startTime, endTime)
|
||||
const sql = getInsightsGlanceMetrics(startTime, endTime)
|
||||
|
||||
const { result } = await executeSql(
|
||||
{
|
||||
@@ -53,33 +53,33 @@ export async function getQueryInsightsGlance(
|
||||
signal
|
||||
)
|
||||
|
||||
return result[0] as QueryInsightsGlance
|
||||
return result[0] as InsightsGlance
|
||||
}
|
||||
|
||||
export type QueryInsightsGlanceData = Awaited<ReturnType<typeof getQueryInsightsGlance>>
|
||||
export type QueryInsightsGlanceError = ExecuteSqlError
|
||||
export type InsightsGlanceData = Awaited<ReturnType<typeof getInsightsGlance>>
|
||||
export type InsightsGlanceError = ExecuteSqlError
|
||||
|
||||
export function useQueryInsightsGlance(
|
||||
export function useInsightsGlanceQuery(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsGlanceData>
|
||||
options?: UseQueryOptions<InsightsGlanceData>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const sql = getQueriesGlanceMetrics(startTime, endTime)
|
||||
console.log(`[useQueryInsightsGlance] Executing SQL:`, sql)
|
||||
const sql = getInsightsGlanceMetrics(startTime, endTime)
|
||||
console.log(`[useInsightsGlanceQuery] Executing SQL:`, sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log(`[useQueryInsightsGlance] Result:`, result)
|
||||
return result[0] as QueryInsightsGlance
|
||||
console.log(`[useInsightsGlanceQuery] Result:`, result)
|
||||
return result[0] as InsightsGlance
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
@@ -0,0 +1,261 @@
|
||||
import { useQuery, UseQueryOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { executeSql } from '../sql/execute-sql-query'
|
||||
import { queryInsightsKeys } from './keys'
|
||||
import { useEffect, useCallback } from 'react'
|
||||
|
||||
export type InsightsMetric = {
|
||||
timestamp: string
|
||||
value?: number
|
||||
rows?: number
|
||||
cmd_type_text?: string
|
||||
database: string
|
||||
shared_blks_read?: number
|
||||
shared_blks_hit?: number
|
||||
shared_blks_dirtied?: number
|
||||
shared_blks_written?: number
|
||||
mean_exec_time?: number
|
||||
total_exec_time?: number
|
||||
p50?: number
|
||||
p95?: number
|
||||
p99?: number
|
||||
p99_9?: number
|
||||
}
|
||||
|
||||
export type InsightsMetrics = {
|
||||
queries: {
|
||||
total: number
|
||||
total_time: number
|
||||
avg_time: number
|
||||
total_rows: number
|
||||
}
|
||||
cache: {
|
||||
hit_ratio: number
|
||||
hits: number
|
||||
misses: number
|
||||
}
|
||||
errors: {
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
|
||||
switch (metric) {
|
||||
case 'rows_read':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
AND cmd_type = 1
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
case 'query_latency':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50,
|
||||
percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95,
|
||||
percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99,
|
||||
percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
case 'queries_per_second':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
COUNT(*) / 300.0 as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
case 'calls':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(calls) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
case 'cache_hits':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(shared_blks_hit) as shared_blks_hit,
|
||||
SUM(shared_blks_read) as shared_blks_read,
|
||||
SUM(shared_blks_dirtied) as shared_blks_dirtied,
|
||||
SUM(shared_blks_written) as shared_blks_written,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
case 'issues':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
default:
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) / 300 as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
export function useInsightsMetricsQuery(
|
||||
projectRef: string | undefined,
|
||||
metric: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<InsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const sql = getMetricsSql(metric, startTime, endTime)
|
||||
console.log(`[useInsightsMetricsQuery] Executing SQL for ${metric}:`, sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log(`[useInsightsMetricsQuery] Result for ${metric}:`, result)
|
||||
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
// Hook to pre-fetch all metrics data for the current time range
|
||||
export function useInsightsPrefetchQuery(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectRef) return
|
||||
|
||||
// Pre-fetch all metric types
|
||||
const metricTypes: Array<{ id: string; label: string }> = [
|
||||
{ id: 'query_latency', label: 'Query latency' },
|
||||
{ id: 'rows_read', label: 'Rows read' },
|
||||
{ id: 'calls', label: 'Calls' },
|
||||
{ id: 'cache_hits', label: 'Cache hits' },
|
||||
{ id: 'issues', label: 'Issues' },
|
||||
]
|
||||
|
||||
// Pre-fetch metrics data
|
||||
metricTypes.forEach((metric) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric.id, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
const sql = getMetricsSql(metric.id, startTime, endTime)
|
||||
console.log(`[useInsightsPrefetchQuery] Executing SQL for ${metric.id}:`, sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log(`[useInsightsPrefetchQuery] Result for ${metric.id}:`, result)
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
})
|
||||
})
|
||||
}, [projectRef, startTime, endTime, queryClient])
|
||||
}
|
||||
|
||||
// Hook for better cache management and manual refresh control
|
||||
export function useInsightsCacheManager(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const refreshAllData = useCallback(async () => {
|
||||
if (!projectRef) return
|
||||
|
||||
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
|
||||
|
||||
// Invalidate and refetch all metrics
|
||||
await Promise.all([
|
||||
...metricTypes.map((metric) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
|
||||
})
|
||||
),
|
||||
])
|
||||
}, [projectRef, startTime, endTime, queryClient])
|
||||
|
||||
const isAnyDataStale = useCallback(() => {
|
||||
if (!projectRef) return false
|
||||
|
||||
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
|
||||
|
||||
return metricTypes.some((metric) => {
|
||||
const query = queryClient.getQueryData(
|
||||
queryInsightsKeys.metrics(projectRef, metric, startTime, endTime)
|
||||
)
|
||||
return !query
|
||||
})
|
||||
}, [projectRef, startTime, endTime, queryClient])
|
||||
|
||||
return {
|
||||
refreshAllData,
|
||||
isAnyDataStale,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql } from '../sql/execute-sql-query'
|
||||
import { queryInsightsKeys } from './keys'
|
||||
|
||||
export type InsightsQuery = {
|
||||
query_id: number
|
||||
query: string
|
||||
total_time: number
|
||||
calls: number
|
||||
rows_read: number
|
||||
rows_insert: number
|
||||
rows_update: number
|
||||
rows_delete: number
|
||||
shared_blks_read: number
|
||||
shared_blks_hit: number
|
||||
mean_exec_time: number
|
||||
database: string
|
||||
timestamp: string
|
||||
cmd_type_text: string
|
||||
application_name: string
|
||||
badness_score: number
|
||||
slowness_rating: string
|
||||
// index_advisor results
|
||||
startup_cost_before?: number
|
||||
startup_cost_after?: number
|
||||
total_cost_before?: number
|
||||
total_cost_after?: number
|
||||
index_statements?: string[]
|
||||
errors?: string[]
|
||||
// Error tracking fields
|
||||
error_count?: number
|
||||
}
|
||||
|
||||
const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
|
||||
WITH base_queries AS (
|
||||
SELECT
|
||||
queryid AS query_id,
|
||||
COALESCE(MAX(top_query), MAX(query)) AS query,
|
||||
SUM(total_exec_time) AS total_time,
|
||||
SUM(calls) AS calls,
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read,
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert,
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update,
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete,
|
||||
SUM(shared_blks_read) AS shared_blks_read,
|
||||
SUM(shared_blks_hit) AS shared_blks_hit,
|
||||
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
|
||||
STRING_AGG(DISTINCT datname, ', ') AS database,
|
||||
MAX(bucket_start_time) AS timestamp,
|
||||
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
|
||||
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
|
||||
(
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
|
||||
) AS rows_total,
|
||||
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
|
||||
(
|
||||
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) *
|
||||
LOG(GREATEST(SUM(calls), 1)) /
|
||||
GREATEST(
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
|
||||
1)
|
||||
) AS badness_score,
|
||||
CASE
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
|
||||
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
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'
|
||||
AND bucket_start_time <= '${endTime}'
|
||||
AND bucket_done = true
|
||||
GROUP BY queryid
|
||||
)
|
||||
SELECT
|
||||
q.*,
|
||||
ia.*
|
||||
FROM
|
||||
base_queries q
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM index_advisor(q.query)
|
||||
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
|
||||
ORDER BY badness_score DESC
|
||||
LIMIT 9999
|
||||
`
|
||||
|
||||
const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL */ `
|
||||
WITH base_queries AS (
|
||||
SELECT
|
||||
queryid AS query_id,
|
||||
COALESCE(MAX(top_query), MAX(query)) AS query,
|
||||
SUM(total_exec_time) AS total_time,
|
||||
SUM(calls) AS calls,
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read,
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert,
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update,
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete,
|
||||
SUM(shared_blks_read) AS shared_blks_read,
|
||||
SUM(shared_blks_hit) AS shared_blks_hit,
|
||||
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
|
||||
STRING_AGG(DISTINCT datname, ', ') AS database,
|
||||
MAX(bucket_start_time) AS timestamp,
|
||||
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
|
||||
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
|
||||
(
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
|
||||
) AS rows_total,
|
||||
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
|
||||
(
|
||||
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) *
|
||||
LOG(GREATEST(SUM(calls), 1)) /
|
||||
GREATEST(
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
|
||||
1)
|
||||
) AS badness_score,
|
||||
CASE
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
|
||||
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
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'
|
||||
AND bucket_start_time <= '${endTime}'
|
||||
AND bucket_done = true
|
||||
AND mean_exec_time > 1000
|
||||
AND calls > 1
|
||||
GROUP BY queryid
|
||||
)
|
||||
SELECT
|
||||
q.*,
|
||||
ia.*
|
||||
FROM
|
||||
base_queries q
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM index_advisor(q.query)
|
||||
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
|
||||
ORDER BY error_count DESC, badness_score DESC
|
||||
LIMIT 9999
|
||||
`
|
||||
|
||||
export function useInsightsQueriesQuery(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<InsightsQuery[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getQueriesSql(startTime, endTime),
|
||||
})
|
||||
|
||||
return result as InsightsQuery[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useInsightsQueriesWithErrorsQuery(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<InsightsQuery[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getQueriesWithErrorsSql(startTime, endTime),
|
||||
})
|
||||
|
||||
return result as InsightsQuery[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
+44
-36
@@ -1,25 +1,26 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql } from '../sql/execute-sql-query'
|
||||
import { QueryInsightsMetric } from './query-metrics-query'
|
||||
import { InsightsMetric } from './insights-metrics-query'
|
||||
|
||||
const singleQueryLatencyKeys = {
|
||||
base: ['single-query-latency'] as const,
|
||||
base: ['projects', 'query-insights', 'single-query'] as const,
|
||||
latency: (
|
||||
projectRef: string | undefined,
|
||||
queryId: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) => [...singleQueryLatencyKeys.base, projectRef, queryId, startTime, endTime] as const,
|
||||
) =>
|
||||
[...singleQueryLatencyKeys.base, 'latency', projectRef, queryId, startTime, endTime] as const,
|
||||
}
|
||||
|
||||
const singleQueryCallsKeys = {
|
||||
base: ['single-query-calls'] as const,
|
||||
base: ['projects', 'query-insights', 'single-query-calls'] as const,
|
||||
calls: (
|
||||
projectRef: string | undefined,
|
||||
queryId: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) => [...singleQueryCallsKeys.base, projectRef, queryId, startTime, endTime] as const,
|
||||
) => [...singleQueryCallsKeys.base, 'calls', projectRef, queryId, startTime, endTime] as const,
|
||||
}
|
||||
|
||||
const getSingleQueryLatencySql = (
|
||||
@@ -30,42 +31,43 @@ const getSingleQueryLatencySql = (
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
mean_exec_time as value,
|
||||
calls,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
AND queryid = ${queryId}::bigint
|
||||
AND bucket_done = true
|
||||
AND queryid = ${queryId}
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
const getSingleQueryRowsSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) as value,
|
||||
rows as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
AND queryid = ${queryId}::bigint
|
||||
AND cmd_type = 1
|
||||
GROUP BY bucket_start_time, datname
|
||||
AND queryid = ${queryId}
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
const getSingleQueryRowsWrittenSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ `
|
||||
const getSingleQueryRowsWrittenSql = (
|
||||
queryId: string,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) => /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(CASE WHEN cmd_type IN (2, 3, 4) THEN rows ELSE 0 END) as value,
|
||||
rows as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
AND queryid = ${queryId}::bigint
|
||||
GROUP BY bucket_start_time, datname
|
||||
AND queryid = ${queryId}
|
||||
AND cmd_type IN (2, 3, 4) -- INSERT, UPDATE, DELETE
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
@@ -78,17 +80,16 @@ const getSingleQueryCallsSql = (queryId: string, startTime: string, endTime: str
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
AND queryid = ${queryId}::bigint
|
||||
GROUP BY bucket_start_time, calls, datname
|
||||
AND queryid = ${queryId}
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
export function useSingleQueryLatency(
|
||||
export function useInsightsSingleQueryLatency(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
options?: UseQueryOptions<InsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: singleQueryLatencyKeys.latency(projectRef, queryId, startTime, endTime),
|
||||
@@ -102,19 +103,19 @@ export function useSingleQueryLatency(
|
||||
})
|
||||
|
||||
console.log('Chart data points:', result)
|
||||
return result as QueryInsightsMetric[]
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryRows(
|
||||
export function useInsightsSingleQueryRows(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
options?: UseQueryOptions<InsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...singleQueryLatencyKeys.base, 'rows', projectRef, queryId, startTime, endTime],
|
||||
@@ -123,56 +124,63 @@ export function useSingleQueryRows(
|
||||
if (!queryId) return []
|
||||
|
||||
const sql = getSingleQueryRowsSql(queryId, startTime, endTime)
|
||||
console.log('[useSingleQueryRows] Executing SQL:', sql)
|
||||
console.log('[useInsightsSingleQueryRows] Executing SQL:', sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('[useSingleQueryRows] Result:', result)
|
||||
return result as QueryInsightsMetric[]
|
||||
console.log('[useInsightsSingleQueryRows] Result:', result)
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryRowsWritten(
|
||||
export function useInsightsSingleQueryRowsWritten(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
options?: UseQueryOptions<InsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...singleQueryLatencyKeys.base, 'rows-written', projectRef, queryId, startTime, endTime],
|
||||
queryKey: [
|
||||
...singleQueryLatencyKeys.base,
|
||||
'rows-written',
|
||||
projectRef,
|
||||
queryId,
|
||||
startTime,
|
||||
endTime,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
if (!queryId) return []
|
||||
|
||||
const sql = getSingleQueryRowsWrittenSql(queryId, startTime, endTime)
|
||||
console.log('[useSingleQueryRowsWritten] Executing SQL:', sql)
|
||||
console.log('[useInsightsSingleQueryRowsWritten] Executing SQL:', sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('[useSingleQueryRowsWritten] Result:', result)
|
||||
return result as QueryInsightsMetric[]
|
||||
console.log('[useInsightsSingleQueryRowsWritten] Result:', result)
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryCalls(
|
||||
export function useInsightsSingleQueryCalls(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
options?: UseQueryOptions<InsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: singleQueryCallsKeys.calls(projectRef, queryId, startTime, endTime),
|
||||
@@ -185,9 +193,9 @@ export function useSingleQueryCalls(
|
||||
sql: getSingleQueryCallsSql(queryId, startTime, endTime),
|
||||
})
|
||||
|
||||
return result as QueryInsightsMetric[]
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,517 +0,0 @@
|
||||
import { useQuery, UseQueryOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { executeSql } from '../sql/execute-sql-query'
|
||||
import { queryInsightsKeys } from './keys'
|
||||
import { useEffect, useCallback } from 'react'
|
||||
|
||||
export type QueryInsightsMetric = {
|
||||
timestamp: string
|
||||
value?: number
|
||||
rows?: number
|
||||
cmd_type_text?: string
|
||||
database: string
|
||||
shared_blks_read?: number
|
||||
shared_blks_hit?: number
|
||||
shared_blks_dirtied?: number
|
||||
shared_blks_written?: number
|
||||
mean_exec_time?: number
|
||||
total_exec_time?: number
|
||||
p50?: number
|
||||
p95?: number
|
||||
p99?: number
|
||||
p99_9?: number
|
||||
}
|
||||
|
||||
export type QueryInsightsQuery = {
|
||||
query_id: number
|
||||
query: string
|
||||
total_time: number
|
||||
calls: number
|
||||
rows_read: number
|
||||
rows_insert: number
|
||||
rows_update: number
|
||||
rows_delete: number
|
||||
shared_blks_read: number
|
||||
shared_blks_hit: number
|
||||
mean_exec_time: number
|
||||
database: string
|
||||
timestamp: string
|
||||
cmd_type_text: string
|
||||
application_name: string
|
||||
badness_score: number
|
||||
slowness_rating: string
|
||||
// index_advisor results
|
||||
startup_cost_before?: number
|
||||
startup_cost_after?: number
|
||||
total_cost_before?: number
|
||||
total_cost_after?: number
|
||||
index_statements?: string[]
|
||||
errors?: string[]
|
||||
// Error tracking fields
|
||||
error_count?: number
|
||||
}
|
||||
|
||||
export type QueryInsightsMetrics = {
|
||||
queries: {
|
||||
total: number
|
||||
total_time: number
|
||||
avg_time: number
|
||||
total_rows: number
|
||||
}
|
||||
cache: {
|
||||
hit_ratio: number
|
||||
hits: number
|
||||
misses: number
|
||||
}
|
||||
errors: {
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
|
||||
switch (metric) {
|
||||
case 'rows_read':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
AND cmd_type = 1
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
case 'query_latency':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50,
|
||||
percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95,
|
||||
percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99,
|
||||
percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
case 'queries_per_second':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
COUNT(*) / 300.0 as value, -- Convert to per-second rate (5 mins = 300 seconds)
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
case 'calls':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(calls) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
case 'cache_hits':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(shared_blks_hit) as shared_blks_hit,
|
||||
SUM(shared_blks_read) as shared_blks_read,
|
||||
SUM(shared_blks_dirtied) as shared_blks_dirtied,
|
||||
SUM(shared_blks_written) as shared_blks_written,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
case 'issues':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
default:
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) / 300 as value, -- Convert to per-second rate (5 mins = 300 seconds)
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
|
||||
WITH base_queries AS (
|
||||
SELECT
|
||||
queryid AS query_id,
|
||||
-- Prefer top-level query if available, else fallback to normalized form
|
||||
COALESCE(MAX(top_query), MAX(query)) AS query,
|
||||
-- Total time spent executing this query across all calls
|
||||
SUM(total_exec_time) AS total_time,
|
||||
-- Number of times this query was executed
|
||||
SUM(calls) AS calls,
|
||||
-- Total rows affected or returned depending on command type
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, -- SELECT
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, -- INSERT
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, -- UPDATE
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, -- DELETE
|
||||
-- Buffer activity: disk reads vs cache hits
|
||||
SUM(shared_blks_read) AS shared_blks_read,
|
||||
SUM(shared_blks_hit) AS shared_blks_hit,
|
||||
-- Mean execution time per call
|
||||
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
|
||||
-- Metadata
|
||||
STRING_AGG(DISTINCT datname, ', ') AS database,
|
||||
MAX(bucket_start_time) AS timestamp,
|
||||
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
|
||||
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
|
||||
-- Total number of rows affected or returned across all executions
|
||||
(
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
|
||||
) AS rows_total,
|
||||
-- Error information (using available columns)
|
||||
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
|
||||
-- Heuristic "badness score":
|
||||
-- Penalizes queries that are slow, frequent, and touch few rows
|
||||
(
|
||||
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * -- mean execution time
|
||||
LOG(GREATEST(SUM(calls), 1)) / -- scaled by frequency
|
||||
GREATEST( -- penalize low-row queries
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
|
||||
1)
|
||||
) AS badness_score,
|
||||
-- Slowness rating based on mean execution time
|
||||
CASE
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
|
||||
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
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'
|
||||
AND bucket_start_time <= '${endTime}'
|
||||
AND bucket_done = true
|
||||
-- AND cmd_type IN (1, 2, 3, 4) -- Only consider SELECT, INSERT, UPDATE, DELETE
|
||||
GROUP BY queryid
|
||||
)
|
||||
SELECT
|
||||
q.*,
|
||||
ia.*
|
||||
FROM
|
||||
base_queries q
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM index_advisor(q.query)
|
||||
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
|
||||
ORDER BY badness_score DESC
|
||||
LIMIT 9999;
|
||||
`
|
||||
|
||||
const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL */ `
|
||||
WITH base_queries AS (
|
||||
SELECT
|
||||
queryid AS query_id,
|
||||
-- Prefer top-level query if available, else fallback to normalized form
|
||||
COALESCE(MAX(top_query), MAX(query)) AS query,
|
||||
-- Total time spent executing this query across all calls
|
||||
SUM(total_exec_time) AS total_time,
|
||||
-- Number of times this query was executed
|
||||
SUM(calls) AS calls,
|
||||
-- Total rows affected or returned depending on command type
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, -- SELECT
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, -- INSERT
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, -- UPDATE
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, -- DELETE
|
||||
-- Buffer activity: disk reads vs cache hits
|
||||
SUM(shared_blks_read) AS shared_blks_read,
|
||||
SUM(shared_blks_hit) AS shared_blks_hit,
|
||||
-- Mean execution time per call
|
||||
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
|
||||
-- Metadata
|
||||
STRING_AGG(DISTINCT datname, ', ') AS database,
|
||||
MAX(bucket_start_time) AS timestamp,
|
||||
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
|
||||
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
|
||||
-- Total number of rows affected or returned across all executions
|
||||
(
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
|
||||
) AS rows_total,
|
||||
-- Error information (using available columns)
|
||||
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
|
||||
-- Heuristic "badness score":
|
||||
-- Penalizes queries that are slow, frequent, and touch few rows
|
||||
(
|
||||
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * -- mean execution time
|
||||
LOG(GREATEST(SUM(calls), 1)) / -- scaled by frequency
|
||||
GREATEST( -- penalize low-row queries
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
|
||||
1)
|
||||
) AS badness_score,
|
||||
-- Slowness rating based on mean execution time
|
||||
CASE
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
|
||||
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
|
||||
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
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'
|
||||
AND bucket_start_time <= '${endTime}'
|
||||
AND bucket_done = true
|
||||
AND mean_exec_time > 1000
|
||||
AND calls > 1 -- Only include queries that are slow and called multiple times
|
||||
GROUP BY queryid
|
||||
)
|
||||
SELECT
|
||||
q.*,
|
||||
ia.*
|
||||
FROM
|
||||
base_queries q
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM index_advisor(q.query)
|
||||
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
|
||||
ORDER BY error_count DESC, badness_score DESC
|
||||
LIMIT 9999;
|
||||
`
|
||||
|
||||
export function useQueryInsightsMetrics(
|
||||
projectRef: string | undefined,
|
||||
metric: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const sql = getMetricsSql(metric, startTime, endTime)
|
||||
console.log(`[useQueryInsightsMetrics] Executing SQL for ${metric}:`, sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log(`[useQueryInsightsMetrics] Result for ${metric}:`, result)
|
||||
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useQueryInsightsQueries(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsQuery[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getQueriesSql(startTime, endTime),
|
||||
})
|
||||
|
||||
return result as QueryInsightsQuery[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useQueryInsightsQueriesWithErrors(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsQuery[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getQueriesWithErrorsSql(startTime, endTime),
|
||||
})
|
||||
|
||||
return result as QueryInsightsQuery[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
// Hook to pre-fetch all metrics data for the current time range
|
||||
export function usePreFetchQueryInsightsData(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectRef) return
|
||||
|
||||
// Pre-fetch all metric types
|
||||
const metricTypes: Array<{ id: string; label: string }> = [
|
||||
{ id: 'query_latency', label: 'Query latency' },
|
||||
{ id: 'rows_read', label: 'Rows read' },
|
||||
|
||||
{ id: 'calls', label: 'Calls' },
|
||||
{ id: 'cache_hits', label: 'Cache hits' },
|
||||
{ id: 'issues', label: 'Issues' },
|
||||
]
|
||||
|
||||
// Pre-fetch metrics data
|
||||
metricTypes.forEach((metric) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric.id, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
const sql = getMetricsSql(metric.id, startTime, endTime)
|
||||
console.log(`[usePreFetchQueryInsightsData] Executing SQL for ${metric.id}:`, sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log(`[usePreFetchQueryInsightsData] Result for ${metric.id}:`, result)
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
})
|
||||
})
|
||||
|
||||
// Pre-fetch queries data
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getQueriesSql(startTime, endTime),
|
||||
})
|
||||
return result as QueryInsightsQuery[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
})
|
||||
|
||||
// Pre-fetch queries with errors data
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getQueriesWithErrorsSql(startTime, endTime),
|
||||
})
|
||||
return result as QueryInsightsQuery[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
})
|
||||
}, [projectRef, startTime, endTime, queryClient])
|
||||
}
|
||||
|
||||
// Hook for better cache management and manual refresh control
|
||||
export function useQueryInsightsCacheManager(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const refreshAllData = useCallback(async () => {
|
||||
if (!projectRef) return
|
||||
|
||||
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
|
||||
|
||||
// Invalidate and refetch all metrics
|
||||
await Promise.all([
|
||||
...metricTypes.map((metric) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
|
||||
})
|
||||
),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
|
||||
}),
|
||||
])
|
||||
}, [projectRef, startTime, endTime, queryClient])
|
||||
|
||||
const isAnyDataStale = useCallback(() => {
|
||||
if (!projectRef) return false
|
||||
|
||||
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
|
||||
|
||||
return (
|
||||
metricTypes.some((metric) => {
|
||||
const query = queryClient.getQueryData(
|
||||
queryInsightsKeys.metrics(projectRef, metric, startTime, endTime)
|
||||
)
|
||||
return !query
|
||||
}) ||
|
||||
!queryClient.getQueryData(queryInsightsKeys.queries(projectRef, startTime, endTime)) ||
|
||||
!queryClient.getQueryData(queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime))
|
||||
)
|
||||
}, [projectRef, startTime, endTime, queryClient])
|
||||
|
||||
return {
|
||||
refreshAllData,
|
||||
isAnyDataStale,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user