useQueryPerformanceQuery.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import {
  2. ident,
  3. joinSqlFragments,
  4. keyword,
  5. literal,
  6. safeSql,
  7. type SafeSqlFragment,
  8. } from '@supabase/pg-meta'
  9. import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
  10. import { PRESET_CONFIG } from '../Reports/Reports.constants'
  11. import { Presets } from '../Reports/Reports.types'
  12. import {
  13. QueryPerformanceRow,
  14. QueryPerformanceSort,
  15. QueryPerformanceSQLParams,
  16. } from './QueryPerformance.types'
  17. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  18. import { executeSql } from '@/data/sql/execute-sql-query'
  19. import useDbQuery from '@/hooks/analytics/useDbQuery'
  20. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  21. import { IS_PLATFORM } from '@/lib/constants'
  22. import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
  23. const VALID_SORT_COLUMNS: ReadonlySet<string> = new Set<QueryPerformanceSort['column']>([
  24. 'query',
  25. 'rolname',
  26. 'total_time',
  27. 'prop_total_time',
  28. 'calls',
  29. 'avg_rows',
  30. 'max_time',
  31. 'mean_time',
  32. 'min_time',
  33. ])
  34. export function generateQueryPerformanceSql({
  35. preset,
  36. orderBy,
  37. searchQuery = '',
  38. roles = [],
  39. sources = [],
  40. minCalls = 0,
  41. minTotalTime = 0,
  42. runIndexAdvisor = false,
  43. filterIndexAdvisor = false,
  44. page = 1,
  45. pageSize = 20,
  46. }: QueryPerformanceSQLParams) {
  47. const safePage = Number.isFinite(page) ? Math.max(1, Math.floor(page)) : 1
  48. const safePageSize = Number.isFinite(pageSize)
  49. ? Math.min(Math.max(1, Math.floor(pageSize)), 100)
  50. : 20
  51. const queryPerfQueries = PRESET_CONFIG[Presets.QUERY_PERFORMANCE]
  52. const baseSQL = queryPerfQueries.queries[preset]
  53. const isValidOrderBy =
  54. orderBy != null &&
  55. VALID_SORT_COLUMNS.has(orderBy.column) &&
  56. (orderBy.order === 'asc' || orderBy.order === 'desc')
  57. const orderBySql = isValidOrderBy
  58. ? safeSql`ORDER BY ${ident(orderBy!.column)} ${keyword(orderBy!.order)}`
  59. : undefined
  60. const whereConditions: SafeSqlFragment[] = []
  61. if (roles.length > 0) {
  62. whereConditions.push(
  63. safeSql`auth.rolname in (${joinSqlFragments(
  64. roles.map((r) => literal(r)),
  65. ', '
  66. )})`
  67. )
  68. }
  69. if (searchQuery.length > 0) {
  70. whereConditions.push(safeSql`statements.query ~* ${literal(searchQuery)}`)
  71. }
  72. if (sources.includes('dashboard') && !sources.includes('non-dashboard')) {
  73. whereConditions.push(safeSql`statements.query ~* 'source: dashboard'`)
  74. }
  75. if (sources.includes('non-dashboard') && !sources.includes('dashboard')) {
  76. whereConditions.push(safeSql`statements.query !~* 'source: dashboard'`)
  77. }
  78. if (Number.isFinite(minCalls) && minCalls > 0) {
  79. whereConditions.push(safeSql`statements.calls >= ${literal(minCalls)}`)
  80. }
  81. if (Number.isFinite(minTotalTime) && minTotalTime > 0) {
  82. whereConditions.push(
  83. safeSql`(statements.total_exec_time + statements.total_plan_time) >= ${literal(minTotalTime)}`
  84. )
  85. }
  86. const whereSql = joinSqlFragments(whereConditions, ' AND ')
  87. if (baseSQL.queryType !== 'db') {
  88. throw new Error(
  89. `Query performance presets must be db queries; got ${baseSQL.queryType} for preset ${preset}`
  90. )
  91. }
  92. const sql = baseSQL.safeSql(
  93. [],
  94. whereSql.length > 0 ? safeSql`WHERE ${whereSql}` : undefined,
  95. orderBySql,
  96. runIndexAdvisor,
  97. filterIndexAdvisor,
  98. safePage,
  99. safePageSize
  100. )
  101. return { sql, whereSql, orderBySql }
  102. }
  103. export const useQueryPerformanceQuery = (props: QueryPerformanceSQLParams) => {
  104. const { sql, whereSql, orderBySql } = generateQueryPerformanceSql(props)
  105. return useDbQuery({ sql, params: undefined, where: whereSql, orderBy: orderBySql })
  106. }
  107. export interface QueryPerformanceInfiniteHook {
  108. data: QueryPerformanceRow[] | undefined
  109. isLoading: boolean
  110. isRefetching: boolean
  111. isFetchingNextPage: boolean
  112. hasNextPage: boolean
  113. error: unknown
  114. fetchNextPage: () => void
  115. refetch: () => void
  116. resolvedSql: string
  117. }
  118. export const useQueryPerformanceInfiniteQuery = (
  119. props: Omit<QueryPerformanceSQLParams, 'page'>
  120. ): QueryPerformanceInfiniteHook => {
  121. const queryClient = useQueryClient()
  122. const { data: project } = useSelectedProjectQuery()
  123. const state = useDatabaseSelectorStateSnapshot()
  124. const { data: databases } = useReadReplicasQuery({ projectRef: project?.ref })
  125. const connectionString = (databases || []).find(
  126. (db) => db.identifier === state.selectedDatabaseId
  127. )?.connectionString
  128. // Clamp pageSize the same way generateQueryPerformanceSql does so getNextPageParam
  129. // and the queryKey are always consistent with the SQL actually executed.
  130. const rawPageSize = props.pageSize
  131. const safePageSize = Number.isFinite(rawPageSize)
  132. ? Math.min(Math.max(1, Math.floor(rawPageSize!)), 100)
  133. : 20
  134. const { sql: page1Sql } = generateQueryPerformanceSql({
  135. ...props,
  136. page: 1,
  137. pageSize: safePageSize,
  138. })
  139. // When a read-replica is selected, require its connection string before fetching.
  140. // Falling back to the primary's connection string would silently query the wrong database.
  141. const isPrimarySelected = !state.selectedDatabaseId || state.selectedDatabaseId === project?.ref
  142. const effectiveConnectionString = isPrimarySelected
  143. ? (connectionString ?? project?.connectionString)
  144. : connectionString
  145. const { data, isPending, isRefetching, isFetchingNextPage, hasNextPage, error, fetchNextPage } =
  146. useInfiniteQuery({
  147. queryKey: [
  148. 'projects',
  149. project?.ref,
  150. 'query-performance-infinite',
  151. {
  152. ...props,
  153. pageSize: safePageSize,
  154. identifier: state.selectedDatabaseId,
  155. connectionString: effectiveConnectionString,
  156. },
  157. ],
  158. initialPageParam: 1,
  159. queryFn: ({ pageParam, signal }) => {
  160. const { sql } = generateQueryPerformanceSql({
  161. ...props,
  162. page: pageParam,
  163. pageSize: safePageSize,
  164. })
  165. return executeSql<QueryPerformanceRow[]>(
  166. {
  167. projectRef: project?.ref,
  168. connectionString: effectiveConnectionString,
  169. sql,
  170. },
  171. signal
  172. ).then((res) => res.result)
  173. },
  174. getNextPageParam: (lastPage, allPages) => {
  175. return lastPage.length < safePageSize ? undefined : allPages.length + 1
  176. },
  177. // Don't run until we have a connection string for the selected database.
  178. // For replicas this prevents a silent fallback to the primary before replicas load.
  179. // In self-hosted mode (IS_PLATFORM=false) there is no real connection string, so we
  180. // skip the check — executeSql works fine without one on self-hosted deployments.
  181. enabled: Boolean(project?.ref) && (!IS_PLATFORM || Boolean(effectiveConnectionString)),
  182. refetchOnWindowFocus: false,
  183. refetchOnReconnect: false,
  184. })
  185. return {
  186. data: data?.pages.flatMap((page) => page) ?? undefined,
  187. isLoading: isPending,
  188. isRefetching,
  189. isFetchingNextPage,
  190. hasNextPage: hasNextPage ?? false,
  191. error,
  192. fetchNextPage,
  193. // Reset to page 1 instead of re-fetching all loaded pages, avoiding a burst
  194. // of N requests when the user clicks Refresh after scrolling through multiple pages.
  195. refetch: () =>
  196. queryClient.resetQueries({
  197. queryKey: ['projects', project?.ref, 'query-performance-infinite'],
  198. exact: false,
  199. }),
  200. resolvedSql: page1Sql,
  201. }
  202. }