Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title = "Running EXPLAIN ANALYZE on functions"
topics = ["database", "functions"]
keywords = [] # any strings (topics are automatically added so no need to duplicate)

[api]
sdk = ["rpc"]
---

Sometimes it can help to look at Postgres query plans inside a function. The problem is that running [`EXPLAIN ANALYZE`](https://www.depesz.com/2013/04/16/explaining-the-unexplainable/) on a function usually just shows a [function scan](https://pganalyze.com/docs/explain/scan-nodes/function-scan) or result node, which gives little insight into how the queries actually perform.

[`auto_explain`](https://www.postgresql.org/docs/current/auto-explain.html) is a pre-installed module that is able to log query plans for queries within functions.

`auto_explain` has a few settings that you still need to configure:

- `auto_explain.log_nested_statements`: log the plans of queries within functions
- `auto_explain.log_analyze`: capture the `explain analyze` results instead of `explain`
- `auto_explain.log_min_duration`: if a query is expected to run for longer than the setting's threshold, log the plan

Changing these settings at a broad scale can lead to excessive logging. Instead, you can change the configs within a `begin/rollback` block with the `set local` command. This ensures the changes are isolated to the transaction, and any writes made during testing are undone.

```sql
begin;

set local auto_explain.log_min_duration = '0'; -- log all query plans
set local auto_explain.log_analyze = true; -- use explain analyze
set local auto_explain.log_buffers = true; -- use explain (buffers)
set local auto_explain.log_nested_statements = true; -- log query plans in functions

select example_func(); ---<--ADD YOUR FUNCTION HERE

rollback;
```

If needed, you can change these settings for specific roles, but we don't recommend configuring the value below `1s` for extended periods, as it may degrade performance.

For instance, you could change the value for the authenticator role (powers the Data API).

```sql
ALTER ROLE postgres SET auto_explain.log_min_duration = '.5s';
```

After running your test, you should be able to find the plan in the [Postgres logs](/dashboard/project/_/logs/postgres-logs?s=duration:). The auto_explain module always starts logs with the term "duration:", which can be used as a filter keyword.

You can also filter for the specific function in the [log explorer](/dashboard/project/_/logs/explorer?q=select%0A++cast%28postgres_logs.timestamp+as+datetime%29+as+timestamp%2C%0A++event_message+AS+query_and_plan%2C%0A++parsed.user_name%2C%0A++parsed.context%0Afrom%0A++postgres_logs%0A++cross+join+unnest%28metadata%29+as+metadata%0A++cross+join+unnest%28metadata.parsed%29+as+parsed%0Awhere%0A++regexp_contains%28event_message%2C+%27duration%3A%27%29%0A++AND%0A++regexp_contains%28context%2C+%27example_func%27%29+--%3C----ADD+FUNCTION+NAME+HERE.+IS+CASE+SENSITIVE%0Aorder+by+timestamp+desc%0Alimit+100%3B) with the below query:

```sql
select
cast(postgres_logs.timestamp as datetime) as timestamp,
event_message as query_and_plan,
parsed.user_name,
parsed.context
from
postgres_logs
cross join unnest(metadata) as metadata
cross join unnest(metadata.parsed) as parsed
where regexp_contains(event_message, 'duration:') and regexp_contains(context, '(?i)FUNCTION_NAME')
order by timestamp desc
limit 100;
```
2 changes: 1 addition & 1 deletion apps/docs/spec/supabase_dart_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5513,7 +5513,7 @@ functions:
(2, 'viola'),
(3, 'cello');
```
reponse: |
response: |
```json
[
{
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/spec/supabase_js_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6083,7 +6083,7 @@ functions:
(2, 'Leia'),
(3, 'Han');
```
reponse: |
response: |
```json
{
"data": [
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/spec/supabase_py_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6109,7 +6109,7 @@ functions:
(2, 'Earth'),
(3, 'Mars');
```
reponse: |
response: |
```json
{
"data": [
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/spec/supabase_swift_v1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2070,7 +2070,7 @@ functions:
(2, 'viola'),
(3, 'cello');
```
reponse: |
response: |
```json
{
"data": [
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/spec/supabase_swift_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3119,7 +3119,7 @@ functions:
(2, 'viola'),
(3, 'cello');
```
reponse: |
response: |
```json
{
"data": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const ExportDialog = ({

const outputName = `${table?.name}_rows`
const queryChains = !table ? undefined : getAllTableRowsSql({ table, sorts, filters })
const query = !!queryChains
const queryWithSemicolon = !!queryChains
? ignoreRoleImpersonation
? queryChains.sql.toSql()
: wrapWithRoleImpersonation(
Expand All @@ -75,6 +75,8 @@ export const ExportDialog = ({
)
: ''

const query = queryWithSemicolon.replace(/;\s*$/, '')

const csvExportCommand = `
${connectionStrings.direct.psql} -c "COPY (${query}) TO STDOUT WITH CSV HEADER DELIMITER ',';" > ${outputName}.csv`.trim()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,15 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro

const onSubmit = (data: z.infer<typeof FormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
if (!ref) return console.error('Branch ref is required')
if (!branch?.project_ref) return console.error('Branch ref is required')

const payload: {
branchRef: string
projectRef: string
branchName: string
gitBranch?: string
} = {
branchRef: ref,
branchRef: branch.project_ref,
projectRef,
branchName: data.branchName,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const HIDDEN_EXTENSIONS = [
'intagg',
'xml2',
'pg_tle',
'pg_stat_monitor',
]

export const SEARCH_TERMS: Record<string, string[]> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { useDebounce } from '@uidotdev/usehooks'
import { Lightbulb, Search, X } from 'lucide-react'
import { parseAsArrayOf, parseAsJson, parseAsString, useQueryStates } from 'nuqs'
import { ChangeEvent, ReactNode, useEffect, useState } from 'react'
import { ReactNode, useEffect, useState } from 'react'

import {
NumericFilter,
ReportsNumericFilter,
} from 'components/interfaces/Reports/v2/ReportsNumericFilter'
import { FilterPopover } from 'components/ui/FilterPopover'
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Button, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FilterInput } from './components/FilterInput'
import { IndexAdvisorFilter } from './components/IndexAdvisorFilter'
import { RolesFilterDropdown } from './components/RolesFilterDropdown'
import { SortIndicator } from './components/SortIndicator'
import { useIndexAdvisorStatus } from './hooks/useIsIndexAdvisorStatus'
import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort'
import { useDebouncedValue } from 'hooks/misc/useDebouncedValue'

export const QueryPerformanceFilterBar = ({
actions,
Expand All @@ -22,7 +20,6 @@ export const QueryPerformanceFilterBar = ({
actions?: ReactNode
showRolesFilter?: boolean
}) => {
const { data: project } = useSelectedProjectQuery()
const { sort, clearSort } = useQueryPerformanceSort()
const { isIndexAdvisorEnabled } = useIndexAdvisorStatus()

Expand All @@ -38,19 +35,11 @@ export const QueryPerformanceFilterBar = ({
} as NumericFilter),
indexAdvisor: parseAsString.withDefault('false'),
})
const { data, isPending: isLoadingRoles } = useDatabaseRolesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const roles = (data ?? []).sort((a, b) => a.name.localeCompare(b.name))

const [filters, setFilters] = useState<{ roles: string[] }>({
roles: defaultFilterRoles,
})
const [inputValue, setInputValue] = useState(searchQuery)
const debouncedInputValue = useDebounce(inputValue, 500)
// const debouncedMinCalls = useDebounce(minCallsInput, 300)
const searchValue = inputValue.length === 0 ? inputValue : debouncedInputValue

const onSearchQueryChange = (value: string) => {
setSearchParams({ search: value || '' })
Expand All @@ -61,47 +50,22 @@ export const QueryPerformanceFilterBar = ({
setSearchParams({ roles })
}

const onIndexAdvisorChange = (options: string[]) => {
setSearchParams({ indexAdvisor: options.includes('true') ? 'true' : 'false' })
}
const debouncedInputValue = useDebouncedValue(inputValue, 300)

const onIndexAdvisorToggle = () => {
setSearchParams({ indexAdvisor: indexAdvisor === 'true' ? 'false' : 'true' })
}

useEffect(() => {
onSearchQueryChange(searchValue)
onSearchQueryChange(debouncedInputValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue])

const indexAdvisorOptions = [{ value: 'true', label: 'Index Advisor' }]
}, [debouncedInputValue])

return (
<div className="px-4 py-1.5 bg-surface-200 border-t -mt-px flex justify-between items-center overflow-x-auto overflow-y-hidden w-full flex-shrink-0">
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-2">
<Input
size="tiny"
autoComplete="off"
icon={<Search />}
value={inputValue}
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
name="keyword"
id="keyword"
placeholder="Filter by query"
className="w-56"
actions={[
inputValue && (
<Button
size="tiny"
type="text"
icon={<X />}
onClick={() => setInputValue('')}
className="p-0 h-5 w-5"
/>
),
]}
/>
<FilterInput value={inputValue} onChange={setInputValue} />

<ReportsNumericFilter
label="Calls"
Expand All @@ -115,48 +79,20 @@ export const QueryPerformanceFilterBar = ({
/>

{showRolesFilter && (
<FilterPopover
name="Roles"
options={roles}
labelKey="name"
valueKey="name"
activeOptions={isLoadingRoles ? [] : filters.roles}
<RolesFilterDropdown
activeOptions={filters.roles}
onSaveFilters={onFilterRolesChange}
className="w-56"
/>
)}

{isIndexAdvisorEnabled && (
<Button
type={indexAdvisor === 'true' ? 'default' : 'outline'}
size="tiny"
className={cn(indexAdvisor === 'true' ? 'bg-surface-300' : 'border-dashed')}
onClick={onIndexAdvisorToggle}
iconRight={indexAdvisor === 'true' ? <X size={14} /> : undefined}
>
<span className="flex items-center gap-x-2">
<Lightbulb
size={12}
className={indexAdvisor === 'true' ? 'text-warning' : 'text-foreground-lighter'}
/>
<span>Index Advisor</span>
</span>
</Button>
<IndexAdvisorFilter
isActive={indexAdvisor === 'true'}
onToggle={onIndexAdvisorToggle}
/>
)}

{sort && (
<div className="text-xs border rounded-md px-1.5 md:px-2.5 py-1 h-[26px] flex items-center gap-x-2">
<p className="md:inline-flex gap-x-1 hidden truncate">
Sort: {sort.column} <span className="text-foreground-lighter">{sort.order}</span>
</p>
<Tooltip>
<TooltipTrigger onClick={clearSort}>
<X size={14} className="text-foreground-light hover:text-foreground" />
</TooltipTrigger>
<TooltipContent side="bottom">Clear sort</TooltipContent>
</Tooltip>
</div>
)}
{sort && <SortIndicator sort={sort} onClearSort={clearSort} />}
</div>
</div>
<div className="flex gap-2 items-center pl-2">{actions}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Search, X } from 'lucide-react'

import { Button } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'

interface FilterInputProps {
value: string
onChange: (value: string) => void
placeholder?: string
className?: string
}

export const FilterInput = ({ value, onChange, placeholder, className }: FilterInputProps) => {
return (
<Input
size="tiny"
autoComplete="off"
icon={<Search />}
value={value}
onChange={(e) => onChange(e.target.value)}
name="keyword"
id="keyword"
placeholder={placeholder || 'Filter by query'}
className={className || 'w-56'}
actions={[
value && (
<Button
size="tiny"
type="text"
icon={<X />}
onClick={() => onChange('')}
className="p-0 h-5 w-5"
/>
),
]}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Lightbulb, X } from 'lucide-react'
import { Button, cn } from 'ui'

interface IndexAdvisorFilterProps {
isActive: boolean
onToggle: () => void
}

export const IndexAdvisorFilter = ({ isActive, onToggle }: IndexAdvisorFilterProps) => {
return (
<Button
type={isActive ? 'default' : 'outline'}
size="tiny"
className={cn(isActive ? 'bg-surface-300' : 'border-dashed')}
onClick={onToggle}
iconRight={isActive ? <X size={14} /> : undefined}
>
<span className="flex items-center gap-x-2">
<Lightbulb size={12} className={isActive ? 'text-warning' : 'text-foreground-lighter'} />
<span>Index Advisor</span>
</span>
</Button>
)
}
Loading
Loading