QueryIndexes.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import { AccordionTrigger } from '@ui/components/shadcn/ui/accordion'
  2. import { Check, Lightbulb, Table2 } from 'lucide-react'
  3. import { useEffect, useState } from 'react'
  4. import {
  5. Accordion,
  6. AccordionContent,
  7. AccordionItem,
  8. Alert,
  9. AlertDescription,
  10. AlertTitle,
  11. Button,
  12. cn,
  13. Collapsible,
  14. CollapsibleContent,
  15. CollapsibleTrigger,
  16. } from 'ui'
  17. import { Admonition } from 'ui-patterns'
  18. import { CodeBlock } from 'ui-patterns/CodeBlock'
  19. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  20. import { useIndexInvalidation } from './hooks/useIndexInvalidation'
  21. import { EnableIndexAdvisorButton } from './IndexAdvisor/EnableIndexAdvisorButton'
  22. import {
  23. calculateImprovement,
  24. createIndexes,
  25. hasIndexRecommendations,
  26. } from './IndexAdvisor/index-advisor.utils'
  27. import { IndexAdvisorDisabledState } from './IndexAdvisor/IndexAdvisorDisabledState'
  28. import { IndexImprovementText } from './IndexAdvisor/IndexImprovementText'
  29. import { QueryPanelContainer, QueryPanelScoreSection, QueryPanelSection } from './QueryPanel'
  30. import { QueryPerformanceRow } from './QueryPerformance.types'
  31. import { useIndexAdvisorStatus } from '@/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
  32. import AlertError from '@/components/ui/AlertError'
  33. import { DocsButton } from '@/components/ui/DocsButton'
  34. import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
  35. import {
  36. GetIndexAdvisorResultResponse,
  37. useGetIndexAdvisorResult,
  38. } from '@/data/database/retrieve-index-advisor-result-query'
  39. import { useGetIndexesFromSelectQuery } from '@/data/database/retrieve-index-from-select-query'
  40. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  41. import { DOCS_URL } from '@/lib/constants'
  42. import { useTrack } from '@/lib/telemetry/track'
  43. interface QueryIndexesProps {
  44. selectedRow: Pick<QueryPerformanceRow, 'query'>
  45. columnName?: string
  46. suggestedSelectQuery?: string
  47. prefetchedIndexAdvisorResult?: GetIndexAdvisorResultResponse | null
  48. onClose?: () => void
  49. }
  50. // [Joshen] There's several more UX things we can do to help ease the learning curve of indexes I think
  51. // e.g understanding "costs", what numbers of "costs" are actually considered insignificant
  52. export const QueryIndexes = ({
  53. selectedRow,
  54. columnName,
  55. suggestedSelectQuery,
  56. prefetchedIndexAdvisorResult,
  57. onClose,
  58. }: QueryIndexesProps) => {
  59. // [Joshen] TODO implement this logic once the linter rules are in
  60. const isLinterWarning = false
  61. const { data: project } = useSelectedProjectQuery()
  62. const [showStartupCosts, setShowStartupCosts] = useState(false)
  63. const [isExecuting, setIsExecuting] = useState(false)
  64. const track = useTrack()
  65. const [hasTrackedTabView, setHasTrackedTabView] = useState(false)
  66. const {
  67. data: usedIndexes,
  68. isSuccess,
  69. isPending: isLoading,
  70. isError,
  71. error,
  72. } = useGetIndexesFromSelectQuery({
  73. projectRef: project?.ref,
  74. connectionString: project?.connectionString,
  75. query: selectedRow?.['query'],
  76. })
  77. const { isPending: isLoadingExtensions } = useDatabaseExtensionsQuery({
  78. projectRef: project?.ref,
  79. connectionString: project?.connectionString,
  80. })
  81. const { isIndexAdvisorEnabled } = useIndexAdvisorStatus()
  82. const hasPrefetchedResult = prefetchedIndexAdvisorResult !== undefined
  83. const {
  84. data: fetchedIndexAdvisorResult,
  85. error: indexAdvisorError,
  86. refetch,
  87. isError: isErrorIndexAdvisorResult,
  88. isSuccess: isFetchSuccessIndexAdvisorResult,
  89. isLoading: isFetchLoadingIndexAdvisorResult,
  90. } = useGetIndexAdvisorResult(
  91. {
  92. projectRef: project?.ref,
  93. connectionString: project?.connectionString,
  94. query: selectedRow?.['query'],
  95. },
  96. { enabled: isIndexAdvisorEnabled && !hasPrefetchedResult }
  97. )
  98. const indexAdvisorResult = hasPrefetchedResult
  99. ? prefetchedIndexAdvisorResult
  100. : fetchedIndexAdvisorResult
  101. const isSuccessIndexAdvisorResult = hasPrefetchedResult || isFetchSuccessIndexAdvisorResult
  102. const isLoadingIndexAdvisorResult = hasPrefetchedResult ? false : isFetchLoadingIndexAdvisorResult
  103. const {
  104. index_statements,
  105. startup_cost_after,
  106. startup_cost_before,
  107. total_cost_after,
  108. total_cost_before,
  109. } = indexAdvisorResult ?? { index_statements: [], total_cost_after: 0, total_cost_before: 0 }
  110. const hasIndexRecommendation = hasIndexRecommendations(
  111. indexAdvisorResult,
  112. isSuccessIndexAdvisorResult
  113. )
  114. const totalImprovement = calculateImprovement(total_cost_before, total_cost_after)
  115. const invalidateQueries = useIndexInvalidation()
  116. useEffect(() => {
  117. if (!isLoadingIndexAdvisorResult && !hasTrackedTabView) {
  118. track('index_advisor_tab_clicked', {
  119. hasRecommendations: hasIndexRecommendation,
  120. isIndexAdvisorEnabled: isIndexAdvisorEnabled,
  121. })
  122. setHasTrackedTabView(true)
  123. }
  124. }, [
  125. isLoadingIndexAdvisorResult,
  126. hasIndexRecommendation,
  127. hasTrackedTabView,
  128. track,
  129. isIndexAdvisorEnabled,
  130. ])
  131. const createIndex = async () => {
  132. if (index_statements.length === 0) return
  133. setIsExecuting(true)
  134. track('index_advisor_create_indexes_button_clicked')
  135. try {
  136. await createIndexes({
  137. projectRef: project?.ref,
  138. connectionString: project?.connectionString,
  139. indexStatements: index_statements,
  140. onSuccess: () => refetch(),
  141. })
  142. // Only invalidate queries if index creation was successful
  143. invalidateQueries()
  144. } catch (error) {
  145. // Error is already handled by createIndexes with a toast notification
  146. // But we could add component-specific error handling here if needed
  147. console.error('Failed to create index:', error)
  148. setIsExecuting(false)
  149. } finally {
  150. setIsExecuting(false)
  151. onClose?.()
  152. }
  153. }
  154. if (!isLoadingExtensions && !isIndexAdvisorEnabled) {
  155. return (
  156. <QueryPanelContainer className="h-full">
  157. <QueryPanelSection className="pt-2">
  158. <div className="border rounded-sm border-dashed flex flex-col items-center justify-center py-4 px-12 gap-y-1 text-center">
  159. <p className="text-sm text-foreground-light">Enable Index Advisor</p>
  160. <p className="text-center text-xs text-foreground-lighter mb-2">
  161. Recommends indexes to improve query performance.
  162. </p>
  163. <div className="flex items-center gap-x-2">
  164. <DocsButton href={`${DOCS_URL}/guides/database/extensions/index_advisor`} />
  165. <EnableIndexAdvisorButton />
  166. </div>
  167. </div>
  168. </QueryPanelSection>
  169. </QueryPanelContainer>
  170. )
  171. }
  172. return (
  173. <QueryPanelContainer className="h-full overflow-y-auto py-0 pt-4">
  174. {(columnName || suggestedSelectQuery) && (
  175. <QueryPanelSection className="pt-2 pb-6 border-b">
  176. <div className="flex flex-col gap-y-3">
  177. <div>
  178. <h4 className="mb-2">Recommendation reason</h4>
  179. {columnName && (
  180. <p className="text-sm text-foreground-light">
  181. Recommendation for column: <span className="font-mono">{columnName}</span>
  182. </p>
  183. )}
  184. </div>
  185. {suggestedSelectQuery && (
  186. <div className="flex flex-col gap-y-4">
  187. <p className="text-sm text-foreground-light">Based on the following query:</p>
  188. <CodeBlock
  189. hideLineNumbers
  190. value={suggestedSelectQuery}
  191. language="sql"
  192. className={cn(
  193. 'max-w-full max-h-[200px]',
  194. 'py-2! px-2.5! prose dark:prose-dark',
  195. '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
  196. )}
  197. />
  198. </div>
  199. )}
  200. </div>
  201. </QueryPanelSection>
  202. )}
  203. <QueryPanelSection
  204. className={cn('mb-6', !suggestedSelectQuery && !columnName ? 'pt-2' : 'pt-6')}
  205. >
  206. <div className="mb-4 flex flex-col gap-y-1">
  207. <h4 className="mb-2">Indexes in use</h4>
  208. <p className="text-sm text-foreground-light">
  209. This query is using the following index{(usedIndexes ?? []).length > 1 ? 's' : ''}:
  210. </p>
  211. </div>
  212. {isLoading && <GenericSkeletonLoader />}
  213. {isError && (
  214. <AlertError
  215. projectRef={project?.ref}
  216. error={error}
  217. subject="Failed to retrieve indexes in use"
  218. />
  219. )}
  220. {isSuccess && (
  221. <div>
  222. {usedIndexes.length === 0 && (
  223. <div className="border rounded-sm border-dashed flex flex-col items-center justify-center py-4 px-12 gap-y-1 text-center">
  224. <p className="text-sm text-foreground-light">
  225. No indexes are involved in this query
  226. </p>
  227. <p className="text-center text-xs text-foreground-lighter">
  228. Indexes may not necessarily be used if they incur a higher cost when executing the
  229. query
  230. </p>
  231. </div>
  232. )}
  233. {usedIndexes.map((index) => {
  234. return (
  235. <div
  236. key={index.name}
  237. className="flex items-center gap-x-4 bg-surface-100 border first:rounded-tl first:rounded-tr border-b-0 last:border-b last:rounded-b px-2 py-2"
  238. >
  239. <div className="flex items-center gap-x-2">
  240. <Table2 size={14} className="text-foreground-light" />
  241. <span className="text-xs font-mono text-foreground-light">
  242. {index.schema}.{index.table}
  243. </span>
  244. </div>
  245. <span className="text-xs font-mono">{index.name}</span>
  246. </div>
  247. )
  248. })}
  249. </div>
  250. )}
  251. </QueryPanelSection>
  252. <QueryPanelSection className="flex flex-col gap-y-6 py-6 border-t">
  253. <div className="flex flex-col gap-y-1">
  254. {(!isSuccessIndexAdvisorResult || indexAdvisorResult !== null) && (
  255. <h4 className="mb-2">New index recommendations</h4>
  256. )}
  257. {isLoadingExtensions ? (
  258. <GenericSkeletonLoader />
  259. ) : !isIndexAdvisorEnabled ? (
  260. <IndexAdvisorDisabledState />
  261. ) : (
  262. <>
  263. {isLoadingIndexAdvisorResult && <GenericSkeletonLoader />}
  264. {isErrorIndexAdvisorResult && (
  265. <AlertError
  266. projectRef={project?.ref}
  267. error={indexAdvisorError}
  268. subject="Failed to retrieve result from index advisor"
  269. />
  270. )}
  271. {isSuccessIndexAdvisorResult && (
  272. <>
  273. {indexAdvisorResult === null ? (
  274. <Admonition
  275. type="default"
  276. showIcon={true}
  277. title="Index recommendations not available"
  278. description="Index advisor could not analyze this query. This can happen if the query references tables, functions, or extensions that no longer exist or were deleted."
  279. />
  280. ) : (index_statements ?? []).length === 0 ? (
  281. <Alert className="[&>svg]:rounded-full">
  282. <Check />
  283. <AlertTitle>This query is optimized</AlertTitle>
  284. <AlertDescription>
  285. Recommendations for indexes will show here
  286. </AlertDescription>
  287. </Alert>
  288. ) : (
  289. <>
  290. {isLinterWarning ? (
  291. <Alert
  292. variant="default"
  293. className="border-brand-400 bg-alternative [&>svg]:p-0.5 [&>svg]:bg-transparent [&>svg]:text-brand my-3"
  294. >
  295. <Lightbulb />
  296. <AlertTitle>
  297. We have {index_statements.length} index recommendation
  298. {index_statements.length > 1 ? 's' : ''}
  299. </AlertTitle>
  300. <AlertDescription>
  301. You can improve this query's performance by{' '}
  302. <span className="text-brand">{totalImprovement.toFixed(2)}%</span> by
  303. adding the following suggested{' '}
  304. {index_statements.length > 1 ? 'indexes' : 'index'}
  305. </AlertDescription>
  306. </Alert>
  307. ) : (
  308. <IndexImprovementText
  309. indexStatements={index_statements}
  310. totalCostBefore={total_cost_before}
  311. totalCostAfter={total_cost_after}
  312. className="text-sm text-foreground-light"
  313. />
  314. )}
  315. <CodeBlock
  316. hideLineNumbers
  317. value={index_statements.join(';\n') + ';'}
  318. language="sql"
  319. className={cn(
  320. 'max-w-full max-h-[310px]',
  321. 'py-3! px-3.5! prose dark:prose-dark transition',
  322. '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
  323. )}
  324. />
  325. <p className="text-sm text-foreground-light mt-3">
  326. This recommendation serves to prevent your queries from slowing down as your
  327. application grows, and hence the index may not be used immediately after
  328. it's created (e.g If your table is still small at this time).
  329. </p>
  330. </>
  331. )}
  332. </>
  333. )}
  334. </>
  335. )}
  336. </div>
  337. </QueryPanelSection>
  338. {isIndexAdvisorEnabled && hasIndexRecommendation && (
  339. <>
  340. <QueryPanelSection className="py-6 border-t">
  341. <div className="flex flex-col gap-y-1">
  342. <h4 className="mb-2">Query costs</h4>
  343. <div className="border rounded-md flex flex-col bg-surface-100">
  344. <QueryPanelScoreSection
  345. name="Total cost of query"
  346. description="An estimate of how long it will take to return all the rows (Includes start up cost)"
  347. before={total_cost_before}
  348. after={total_cost_after}
  349. />
  350. <Collapsible open={showStartupCosts} onOpenChange={setShowStartupCosts}>
  351. <CollapsibleContent asChild className="pb-3">
  352. <QueryPanelScoreSection
  353. hideArrowMarkers
  354. className="border-t"
  355. name="Start up cost"
  356. description="An estimate of how long it will take to fetch the first row"
  357. before={startup_cost_before}
  358. after={startup_cost_after}
  359. />
  360. </CollapsibleContent>
  361. <CollapsibleTrigger className="text-xs py-1.5 border-t text-foreground-light bg-studio w-full rounded-b-md">
  362. View {showStartupCosts ? 'less' : 'more'}
  363. </CollapsibleTrigger>
  364. </Collapsible>
  365. </div>
  366. </div>
  367. </QueryPanelSection>
  368. <QueryPanelSection className="py-6 border-t">
  369. <div className="flex flex-col gap-y-2">
  370. <h4 className="mb-2">FAQ</h4>
  371. <Accordion collapsible type="single" className="border rounded-md">
  372. <AccordionItem value="1">
  373. <AccordionTrigger className="px-4 py-3 text-sm font-normal text-foreground-light hover:text-foreground transition data-open:text-foreground">
  374. What units are cost in?
  375. </AccordionTrigger>
  376. <AccordionContent className="px-4 text-foreground-light">
  377. Costs are in an arbitrary unit, and do not represent a unit of time. The units
  378. are anchored (by default) to a single sequential page read costing 1.0 units.
  379. They do, however, serve as a predictor of higher execution times.
  380. </AccordionContent>
  381. </AccordionItem>
  382. <AccordionItem value="2" className="border-b-0">
  383. <AccordionTrigger className="px-4 py-3 text-sm font-normal text-foreground-light hover:text-foreground transition data-open:text-foreground">
  384. How should I prioritize start up and total cost?
  385. </AccordionTrigger>
  386. <AccordionContent className="px-4 text-foreground-light [&>div]:space-y-2">
  387. <p>This depends on the expected size of the result set from the query.</p>
  388. <p>
  389. For queries that return a small number or rows, the startup cost is more
  390. critical and minimizing startup cost can lead to faster response times,
  391. especially in interactive applications.
  392. </p>
  393. <p>
  394. For queries that return a large number of rows, the total cost becomes more
  395. important, and optimizing it will help in efficiently using resources and
  396. reducing overall query execution time.
  397. </p>
  398. </AccordionContent>
  399. </AccordionItem>
  400. </Accordion>
  401. </div>
  402. </QueryPanelSection>
  403. </>
  404. )}
  405. {isIndexAdvisorEnabled && hasIndexRecommendation && (
  406. <div className="bg-studio sticky bottom-0 border-t py-3 flex items-center justify-between px-5">
  407. <div className="flex flex-col gap-y-0.5 text-xs">
  408. <span>Apply index to database</span>
  409. <span className="text-xs text-foreground-light">
  410. This will run the SQL that is shown above
  411. </span>
  412. </div>
  413. <Button
  414. disabled={isExecuting}
  415. loading={isExecuting}
  416. type="primary"
  417. onClick={() => createIndex()}
  418. >
  419. Create index
  420. </Button>
  421. </div>
  422. )}
  423. </QueryPanelContainer>
  424. )
  425. }