mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 13:49:09 +03:00
fix: use old sql queries as they were working
This commit is contained in:
1 parent
d54cb2ea61
commit
8ba76455f1
6 files changed
+875
-267
No files matched your search
@@ -9,7 +9,10 @@ import {
|
||||
CardHeader,
|
||||
} from 'ui'
|
||||
import { QueryRowExplorer } from './QueryRowExplorer'
|
||||
import { useQueryInsightsMetricsQuery } from 'data/query-insights/query-metrics-query'
|
||||
import {
|
||||
useQueryInsightsMetrics,
|
||||
usePreFetchQueryInsightsData,
|
||||
} from 'data/query-insights/query-metrics-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import ComposedChart from 'components/ui/Charts/ComposedChart'
|
||||
import { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
||||
@@ -55,6 +58,9 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
return { startTime: fallbackStartTime, endTime: fallbackEndTime }
|
||||
}, [startTime, endTime])
|
||||
|
||||
// Pre-fetch query data for the QueryRowExplorer
|
||||
usePreFetchQueryInsightsData(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging
|
||||
console.log('QueryMetricExplorer Debug:', {
|
||||
props: { startTime, endTime },
|
||||
@@ -67,13 +73,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
data: metricsData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQueryInsightsMetricsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
metric: selectedMetric,
|
||||
startTime: effectiveStartTime,
|
||||
endTime: effectiveEndTime,
|
||||
})
|
||||
} = useQueryInsightsMetrics(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging for query results
|
||||
console.log('QueryMetricExplorer Query Results:', {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQueryInsightsGlanceQuery } from 'data/query-insights/query-glance-query'
|
||||
import { useQueryInsightsGlance } from 'data/query-insights/query-glance-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { Card, CardContent } from 'ui'
|
||||
import { Database, Clock, AlertCircle } from 'lucide-react'
|
||||
@@ -29,25 +29,20 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps)
|
||||
console.log('QueryQuickGlance Debug:', {
|
||||
props: { startTime, endTime },
|
||||
effective: { startTime: effectiveStartTime, endTime: effectiveEndTime },
|
||||
project: { ref: project?.ref, hasConnectionString: !!project?.connectionString }
|
||||
project: { ref: project?.ref, hasConnectionString: !!project?.connectionString },
|
||||
})
|
||||
|
||||
const {
|
||||
data: glanceData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQueryInsightsGlanceQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
startTime: effectiveStartTime,
|
||||
endTime: effectiveEndTime,
|
||||
})
|
||||
} = useQueryInsightsGlance(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging for glance results
|
||||
console.log('QueryQuickGlance Results:', {
|
||||
glanceData,
|
||||
isLoading,
|
||||
error
|
||||
error,
|
||||
})
|
||||
|
||||
const cards = useMemo(() => {
|
||||
@@ -110,7 +105,7 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">Error loading query insights: {error.message}</div>
|
||||
return <div className="text-red-500">Error loading query insights: {String(error)}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowDown, ArrowUp, TextSearch, X } from 'lucide-react'
|
||||
import { ArrowDown, ArrowUp, Search, Settings2, TextSearch, X } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
|
||||
@@ -17,7 +17,18 @@ import {
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { useQueryInsightsQueries, type QueryInsightsQuery } from 'data/query-insights/query-metrics-query'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import {
|
||||
useQueryInsightsQueries,
|
||||
type QueryInsightsQuery,
|
||||
} from 'data/query-insights/query-metrics-query'
|
||||
import {
|
||||
useSingleQueryLatency,
|
||||
useSingleQueryCalls,
|
||||
useSingleQueryRows,
|
||||
useSingleQueryRowsWritten,
|
||||
} from 'data/query-insights/single-query-latency-query'
|
||||
|
||||
interface QueryRowExplorerProps {
|
||||
startTime: string
|
||||
@@ -40,7 +51,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
queries,
|
||||
isLoading,
|
||||
error,
|
||||
queriesLength: queries?.length
|
||||
queriesLength: queries?.length,
|
||||
})
|
||||
|
||||
const [sort, setSort] = useState<{ column: string; order: 'asc' | 'desc' } | undefined>({
|
||||
@@ -48,7 +59,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
order: 'desc',
|
||||
})
|
||||
const [selectedRow, setSelectedRow] = useState<number>()
|
||||
const [view, setView] = useState<'details' | 'indexes'>('details')
|
||||
const [view, setView] = useState<'details' | 'indexes' | 'metrics'>('details')
|
||||
|
||||
// Define columns similar to QueryPerformance
|
||||
const columns: Column<QueryInsightsQuery>[] = [
|
||||
@@ -171,9 +182,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
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 / 1000).toFixed(2)}s</p>
|
||||
)}
|
||||
{value && <p className="text-foreground-lighter">{(value / 1000).toFixed(2)}s</p>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -205,9 +214,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
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 / 1000).toFixed(2)}s</p>
|
||||
)}
|
||||
{value && <p className="text-foreground-lighter">{(value / 1000).toFixed(2)}s</p>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -277,29 +284,66 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
]
|
||||
|
||||
// For debugging - show test data if no queries found
|
||||
const reportData = queries?.length ? queries : [
|
||||
{
|
||||
query_id: 1,
|
||||
query: 'SELECT * FROM test_table WHERE id = 1',
|
||||
total_time: 150.5,
|
||||
calls: 10,
|
||||
rows_read: 1000,
|
||||
rows_insert: 0,
|
||||
rows_update: 0,
|
||||
rows_delete: 0,
|
||||
shared_blks_read: 50,
|
||||
shared_blks_hit: 950,
|
||||
mean_exec_time: 15.05,
|
||||
database: 'postgres',
|
||||
timestamp: new Date().toISOString(),
|
||||
cmd_type_text: 'SELECT',
|
||||
application_name: 'Test App',
|
||||
badness_score: 25.5,
|
||||
error_count: 0
|
||||
} as QueryInsightsQuery
|
||||
]
|
||||
const reportData = queries?.length
|
||||
? queries
|
||||
: [
|
||||
{
|
||||
query_id: 1,
|
||||
query: 'SELECT * FROM users WHERE id = 1',
|
||||
total_time: 150.5,
|
||||
calls: 10,
|
||||
rows_read: 1000,
|
||||
rows_insert: 0,
|
||||
rows_update: 0,
|
||||
rows_delete: 0,
|
||||
shared_blks_read: 50,
|
||||
shared_blks_hit: 950,
|
||||
mean_exec_time: 15.05,
|
||||
database: 'postgres',
|
||||
timestamp: new Date().toISOString(),
|
||||
cmd_type_text: 'SELECT',
|
||||
application_name: 'Test App',
|
||||
badness_score: 25.5,
|
||||
error_count: 0,
|
||||
startup_cost_before: 0,
|
||||
startup_cost_after: 0,
|
||||
total_cost_before: 0,
|
||||
total_cost_after: 0,
|
||||
index_statements: [],
|
||||
errors: [],
|
||||
} as QueryInsightsQuery,
|
||||
]
|
||||
const selectedQuery = selectedRow !== undefined ? reportData[selectedRow] : undefined
|
||||
|
||||
// Single query metrics hooks
|
||||
const { data: singleQueryLatency } = useSingleQueryLatency(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryCalls } = useSingleQueryCalls(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryRows } = useSingleQueryRows(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryRowsWritten } = useSingleQueryRowsWritten(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const onSortChange = (column: string) => {
|
||||
let updatedSort: { column: string; order: 'asc' | 'desc' } | undefined = undefined
|
||||
|
||||
@@ -326,9 +370,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
if (aValue === undefined || bValue === undefined) return 0
|
||||
|
||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||
return sort.order === 'asc'
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue)
|
||||
return sort.order === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue)
|
||||
}
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
@@ -338,17 +380,39 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
return 0
|
||||
})
|
||||
|
||||
// Debug logging for table data
|
||||
console.log('QueryRowExplorer Table Debug:', {
|
||||
queries,
|
||||
reportData,
|
||||
reportDataLength: reportData.length,
|
||||
sortedDataLength: sortedData.length,
|
||||
isLoading,
|
||||
error,
|
||||
firstRow: sortedData[0],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRow(undefined)
|
||||
}, [startTime, endTime])
|
||||
|
||||
return (
|
||||
<div className="border-t bg-surface-100 flex flex-col h-full">
|
||||
<div className="border-t bg-surface-100 flex flex-col h-96 overflow-auto">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<span>Search</span>
|
||||
<span>Filters</span>
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Filter by query name..."
|
||||
icon={<Search size={14} />}
|
||||
className="w-64"
|
||||
/>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Settings2 size={14} />}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Toggle columns' } }}
|
||||
className="!px-1.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="relative flex flex-grow bg-alternative min-h-0"
|
||||
@@ -363,6 +427,9 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
headerRowHeight={36}
|
||||
columns={columns}
|
||||
rows={sortedData}
|
||||
onRowsChange={(newRows) => {
|
||||
console.log('DataGrid rows changed:', newRows.length)
|
||||
}}
|
||||
rowClass={(_, idx) => {
|
||||
const isSelected = idx === selectedRow
|
||||
return [
|
||||
@@ -408,7 +475,12 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
{selectedRow !== undefined && selectedQuery && (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={30} maxSize={45} minSize={30} className="bg-studio border-t">
|
||||
<ResizablePanel
|
||||
defaultSize={30}
|
||||
maxSize={45}
|
||||
minSize={30}
|
||||
className="bg-studio border-t"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
className="absolute top-3 right-3 px-1"
|
||||
@@ -427,6 +499,12 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
>
|
||||
Query details
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_
|
||||
value="metrics"
|
||||
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
|
||||
>
|
||||
Metrics
|
||||
</TabsTrigger_Shadcn_>
|
||||
{selectedQuery.index_statements && selectedQuery.index_statements.length > 0 && (
|
||||
<TabsTrigger_Shadcn_
|
||||
value="indexes"
|
||||
@@ -447,65 +525,184 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
{selectedQuery.query}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Database</h5>
|
||||
<p className="text-sm">{selectedQuery.database}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Command Type</h5>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Command Type
|
||||
</h5>
|
||||
<p className="text-sm">{selectedQuery.cmd_type_text}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Application</h5>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Application
|
||||
</h5>
|
||||
<p className="text-sm">{selectedQuery.application_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Timestamp</h5>
|
||||
<p className="text-sm">{new Date(selectedQuery.timestamp).toLocaleString()}</p>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Timestamp
|
||||
</h5>
|
||||
<p className="text-sm">
|
||||
{new Date(selectedQuery.timestamp).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Performance</h5>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Performance
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs">Total time: {selectedQuery.total_time?.toFixed(2)}ms</p>
|
||||
<p className="text-xs">Mean time: {selectedQuery.mean_exec_time?.toFixed(2)}ms</p>
|
||||
<p className="text-xs">
|
||||
Total time: {selectedQuery.total_time?.toFixed(2)}ms
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Mean time: {selectedQuery.mean_exec_time?.toFixed(2)}ms
|
||||
</p>
|
||||
<p className="text-xs">Calls: {selectedQuery.calls?.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Rows</h5>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs">Read: {selectedQuery.rows_read?.toLocaleString()}</p>
|
||||
<p className="text-xs">Insert: {selectedQuery.rows_insert?.toLocaleString()}</p>
|
||||
<p className="text-xs">Update: {selectedQuery.rows_update?.toLocaleString()}</p>
|
||||
<p className="text-xs">Delete: {selectedQuery.rows_delete?.toLocaleString()}</p>
|
||||
<p className="text-xs">
|
||||
Read: {selectedQuery.rows_read?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Insert: {selectedQuery.rows_insert?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Update: {selectedQuery.rows_update?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Delete: {selectedQuery.rows_delete?.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Cache Performance</h5>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Cache Performance
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs">Blocks read: {selectedQuery.shared_blks_read?.toLocaleString()}</p>
|
||||
<p className="text-xs">Blocks hit: {selectedQuery.shared_blks_hit?.toLocaleString()}</p>
|
||||
<p className="text-xs">Hit ratio: {selectedQuery.shared_blks_hit && selectedQuery.shared_blks_read
|
||||
? ((selectedQuery.shared_blks_hit / (selectedQuery.shared_blks_hit + selectedQuery.shared_blks_read)) * 100).toFixed(1) + '%'
|
||||
: 'N/A'}</p>
|
||||
<p className="text-xs">
|
||||
Blocks read: {selectedQuery.shared_blks_read?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Blocks hit: {selectedQuery.shared_blks_hit?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Hit ratio:{' '}
|
||||
{selectedQuery.shared_blks_hit && selectedQuery.shared_blks_read
|
||||
? (
|
||||
(selectedQuery.shared_blks_hit /
|
||||
(selectedQuery.shared_blks_hit +
|
||||
selectedQuery.shared_blks_read)) *
|
||||
100
|
||||
).toFixed(1) + '%'
|
||||
: 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedQuery.error_count && selectedQuery.error_count > 0 && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-red-500 mb-1">Errors</h5>
|
||||
<p className="text-xs text-red-500">{selectedQuery.error_count} slow queries detected</p>
|
||||
<p className="text-xs text-red-500">
|
||||
{selectedQuery.error_count} slow queries detected
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="metrics"
|
||||
className="mt-0 flex-grow min-h-0 overflow-y-auto"
|
||||
>
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Query Performance Over Time</h4>
|
||||
|
||||
{singleQueryLatency && singleQueryLatency.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">
|
||||
Latency
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryLatency.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toFixed(2)}ms</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{singleQueryCalls && singleQueryCalls.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">Calls</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryCalls.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{singleQueryRows && singleQueryRows.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">
|
||||
Rows Read
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryRows.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{singleQueryRowsWritten && singleQueryRowsWritten.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">
|
||||
Rows Written
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryRowsWritten.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!singleQueryLatency || singleQueryLatency.length === 0) &&
|
||||
(!singleQueryCalls || singleQueryCalls.length === 0) &&
|
||||
(!singleQueryRows || singleQueryRows.length === 0) &&
|
||||
(!singleQueryRowsWritten || singleQueryRowsWritten.length === 0) && (
|
||||
<p className="text-sm text-foreground-light">
|
||||
No metrics data available for this query
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="indexes"
|
||||
className="mt-0 flex-grow min-h-0 overflow-y-auto"
|
||||
@@ -513,28 +710,35 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Index Recommendations</h4>
|
||||
{selectedQuery.index_statements && selectedQuery.index_statements.length > 0 ? (
|
||||
{selectedQuery.index_statements &&
|
||||
selectedQuery.index_statements.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{selectedQuery.index_statements.map((statement: string, index: number) => (
|
||||
<div key={index} className="bg-surface-200 p-3 rounded">
|
||||
<pre className="text-xs overflow-x-auto">{statement}</pre>
|
||||
</div>
|
||||
))}
|
||||
{selectedQuery.index_statements.map(
|
||||
(statement: string, index: number) => (
|
||||
<div key={index} className="bg-surface-200 p-3 rounded">
|
||||
<pre className="text-xs overflow-x-auto">{statement}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">No index recommendations available</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
No index recommendations available
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedQuery.errors && selectedQuery.errors.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-red-500 mb-2">Index Advisor Errors</h4>
|
||||
<h4 className="text-sm font-medium text-red-500 mb-2">
|
||||
Index Advisor Errors
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedQuery.errors.map((error: string, index: number) => (
|
||||
<div key={index} className="text-xs text-red-500 bg-red-50 p-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
{selectedQuery.errors.map((error: string, index: number) => (
|
||||
<div key={index} className="text-xs text-red-500 bg-red-50 p-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,8 @@ export type QueryInsightsGlance = {
|
||||
avg_error_rate: number
|
||||
}
|
||||
|
||||
export const getQueriesGlanceMetrics = (startTime: string, endTime: string) => /* SQL */ `
|
||||
export const getQueriesGlanceMetrics = (startTime: string, endTime: string) =>
|
||||
/* SQL */ `
|
||||
SELECT
|
||||
COUNT(DISTINCT query) as unique_queries,
|
||||
SUM(calls) as total_queries,
|
||||
@@ -43,7 +44,12 @@ export async function getQueryInsightsGlance(
|
||||
const sql = getQueriesGlanceMetrics(startTime, endTime)
|
||||
|
||||
const { result } = await executeSql(
|
||||
{ projectRef, connectionString, sql, queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime) },
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime),
|
||||
},
|
||||
signal
|
||||
)
|
||||
|
||||
@@ -53,21 +59,29 @@ export async function getQueryInsightsGlance(
|
||||
export type QueryInsightsGlanceData = Awaited<ReturnType<typeof getQueryInsightsGlance>>
|
||||
export type QueryInsightsGlanceError = ExecuteSqlError
|
||||
|
||||
export const useQueryInsightsGlanceQuery = <TData = QueryInsightsGlanceData>(
|
||||
{ projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<QueryInsightsGlanceData, QueryInsightsGlanceError, TData> = {}
|
||||
) => {
|
||||
return useQuery<QueryInsightsGlanceData, QueryInsightsGlanceError, TData>(
|
||||
queryInsightsKeys.glance(projectRef, startTime, endTime),
|
||||
({ signal }) => getQueryInsightsGlance({ projectRef, connectionString, startTime, endTime }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
...options
|
||||
}
|
||||
)
|
||||
export function useQueryInsightsGlance(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsGlanceData>
|
||||
) {
|
||||
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 { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log(`[useQueryInsightsGlance] Result:`, result)
|
||||
return result[0] as QueryInsightsGlance
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,17 +1,9 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
|
||||
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
|
||||
p50?: number
|
||||
p95?: number
|
||||
p99?: number
|
||||
p99_9?: number
|
||||
}
|
||||
|
||||
export type QueryInsightsMetricDetail = {
|
||||
timestamp: string
|
||||
value?: number
|
||||
rows?: number
|
||||
@@ -57,27 +49,37 @@ export type QueryInsightsQuery = {
|
||||
error_count?: number
|
||||
}
|
||||
|
||||
export type QueryInsightsMetricsVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string | null
|
||||
metric: 'rows_read' | 'query_latency' | 'queries_per_second' | 'calls' | 'cache_hits'
|
||||
startTime: string
|
||||
endTime: string
|
||||
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 getQueryInsightsMetricsSQL = (metric: string, startTime: string, endTime: string) => {
|
||||
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
|
||||
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
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
@@ -89,205 +91,261 @@ const getQueryInsightsMetricsSQL = (metric: string, startTime: string, endTime:
|
||||
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,
|
||||
AVG(mean_exec_time) 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
|
||||
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
|
||||
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
|
||||
GROUP BY bucket_start_time
|
||||
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
|
||||
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
|
||||
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,
|
||||
CASE
|
||||
WHEN SUM(shared_blks_read) > 0 THEN
|
||||
(SUM(shared_blks_hit)::float / (SUM(shared_blks_hit) + SUM(shared_blks_read))::float) * 100
|
||||
ELSE 0
|
||||
END as value
|
||||
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
|
||||
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,
|
||||
COUNT(*) as value
|
||||
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
|
||||
GROUP BY bucket_start_time
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQueryInsightsMetrics(
|
||||
{ projectRef, connectionString, metric, startTime, endTime }: QueryInsightsMetricsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const sql = getQueryInsightsMetricsSQL(metric, startTime, endTime)
|
||||
|
||||
console.log('getQueryInsightsMetrics Debug:', {
|
||||
projectRef,
|
||||
hasConnectionString: !!connectionString,
|
||||
metric,
|
||||
startTime,
|
||||
endTime,
|
||||
sql
|
||||
})
|
||||
|
||||
const { result } = await executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime)
|
||||
},
|
||||
signal
|
||||
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
|
||||
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;
|
||||
`
|
||||
|
||||
console.log('getQueryInsightsMetrics Result:', {
|
||||
result,
|
||||
resultLength: result?.length,
|
||||
firstRow: result?.[0]
|
||||
})
|
||||
|
||||
return result as QueryInsightsMetric[]
|
||||
}
|
||||
|
||||
export type QueryInsightsMetricsData = Awaited<ReturnType<typeof getQueryInsightsMetrics>>
|
||||
export type QueryInsightsMetricsError = ExecuteSqlError
|
||||
|
||||
export const useQueryInsightsMetricsQuery = <TData = QueryInsightsMetricsData>(
|
||||
{ projectRef, connectionString, metric, startTime, endTime }: QueryInsightsMetricsVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<QueryInsightsMetricsData, QueryInsightsMetricsError, TData> = {}
|
||||
) => {
|
||||
return useQuery<QueryInsightsMetricsData, QueryInsightsMetricsError, TData>(
|
||||
queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
|
||||
({ signal }) => getQueryInsightsMetrics({ projectRef, connectionString, metric, startTime, endTime }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
...options
|
||||
}
|
||||
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
|
||||
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;
|
||||
`
|
||||
|
||||
// Hook to fetch all queries
|
||||
export const useQueryInsightsQueries = (
|
||||
export function useQueryInsightsMetrics(
|
||||
projectRef: string | undefined,
|
||||
metric: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsQuery[]>
|
||||
) => {
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
|
||||
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const sql = /* 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,
|
||||
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
|
||||
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;
|
||||
`
|
||||
|
||||
console.log('useQueryInsightsQueries Debug:', {
|
||||
projectRef,
|
||||
startTime,
|
||||
endTime,
|
||||
sql
|
||||
})
|
||||
const sql = getMetricsSql(metric, startTime, endTime)
|
||||
console.log(`[useQueryInsightsMetrics] Executing SQL for ${metric}:`, sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('useQueryInsightsQueries Result:', {
|
||||
result,
|
||||
resultLength: result?.length,
|
||||
firstRow: result?.[0]
|
||||
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[]
|
||||
@@ -295,4 +353,148 @@ export const useQueryInsightsQueries = (
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql } from '../sql/execute-sql-query'
|
||||
import { QueryInsightsMetric } from './query-metrics-query'
|
||||
|
||||
const singleQueryLatencyKeys = {
|
||||
base: ['single-query-latency'] as const,
|
||||
latency: (
|
||||
projectRef: string | undefined,
|
||||
queryId: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) => [...singleQueryLatencyKeys.base, projectRef, queryId, startTime, endTime] as const,
|
||||
}
|
||||
|
||||
const singleQueryCallsKeys = {
|
||||
base: ['single-query-calls'] as const,
|
||||
calls: (
|
||||
projectRef: string | undefined,
|
||||
queryId: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) => [...singleQueryCallsKeys.base, projectRef, queryId, startTime, endTime] as const,
|
||||
}
|
||||
|
||||
const getSingleQueryLatencySql = (
|
||||
queryId: string,
|
||||
startTime: string,
|
||||
endTime: string
|
||||
) => /* SQL */ `
|
||||
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
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
const getSingleQueryRowsSql = (queryId: string, startTime: string, endTime: string) => /* 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 queryid = ${queryId}::bigint
|
||||
AND cmd_type = 1
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
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,
|
||||
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
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
const getSingleQueryCallsSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
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
|
||||
AND queryid = ${queryId}::bigint
|
||||
GROUP BY bucket_start_time, calls, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
export function useSingleQueryLatency(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: singleQueryLatencyKeys.latency(projectRef, queryId, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
if (!queryId) return []
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getSingleQueryLatencySql(queryId, startTime, endTime),
|
||||
})
|
||||
|
||||
console.log('Chart data points:', result)
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryRows(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...singleQueryLatencyKeys.base, 'rows', projectRef, queryId, startTime, endTime],
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
if (!queryId) return []
|
||||
|
||||
const sql = getSingleQueryRowsSql(queryId, startTime, endTime)
|
||||
console.log('[useSingleQueryRows] Executing SQL:', sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('[useSingleQueryRows] Result:', result)
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryRowsWritten(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
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)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('[useSingleQueryRowsWritten] Result:', result)
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryCalls(
|
||||
projectRef: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
queryId: string | undefined,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: singleQueryCallsKeys.calls(projectRef, queryId, startTime, endTime),
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
if (!queryId) return []
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getSingleQueryCallsSql(queryId, startTime, endTime),
|
||||
})
|
||||
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user