mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
* fix: estimate number of users when count is large In the Auth Users table, we always fetch an exact count of users. This can be a problem for projects with many (>50K) users as the count(*) might cause performance issues on the database. We already have logic on the Table Editor to only run automatic count estimates (fetching the exact count only if usr requests it), this change ports the same logic over to Auth Users. * Nit refactor --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
import { Query } from '@supabase/pg-meta/src/query'
|
|
import { QueryClient, useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react-query'
|
|
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
|
|
import type { Filter, SupaTable } from 'components/grid/types'
|
|
import { prefetchTableEditor } from 'data/table-editor/table-editor-query'
|
|
import { RoleImpersonationState, wrapWithRoleImpersonation } from 'lib/role-impersonation'
|
|
import { isRoleImpersonationEnabled } from 'state/role-impersonation-state'
|
|
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
|
|
import { tableRowKeys } from './keys'
|
|
import { formatFilterValue } from './utils'
|
|
|
|
type GetTableRowsCountArgs = {
|
|
table?: SupaTable
|
|
filters?: Filter[]
|
|
enforceExactCount?: boolean
|
|
}
|
|
|
|
export const THRESHOLD_COUNT = 50000
|
|
export const COUNT_ESTIMATE_SQL = /* SQL */ `
|
|
CREATE OR REPLACE FUNCTION pg_temp.count_estimate(
|
|
query text
|
|
) RETURNS integer LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
plan jsonb;
|
|
BEGIN
|
|
EXECUTE 'EXPLAIN (FORMAT JSON)' || query INTO plan;
|
|
RETURN plan->0->'Plan'->'Plan Rows';
|
|
END;
|
|
$$;
|
|
`.trim()
|
|
|
|
export const getTableRowsCountSql = ({
|
|
table,
|
|
filters = [],
|
|
enforceExactCount = false,
|
|
}: GetTableRowsCountArgs) => {
|
|
if (!table) return ``
|
|
|
|
if (enforceExactCount) {
|
|
const query = new Query()
|
|
let queryChains = query.from(table.name, table.schema ?? undefined).count()
|
|
filters
|
|
.filter((x) => x.value && x.value !== '')
|
|
.forEach((x) => {
|
|
const value = formatFilterValue(table, x)
|
|
queryChains = queryChains.filter(x.column, x.operator, value)
|
|
})
|
|
return `select (${queryChains.toSql().slice(0, -1)}), false as is_estimate;`
|
|
} else {
|
|
const selectQuery = new Query()
|
|
let selectQueryChains = selectQuery.from(table.name, table.schema ?? undefined).select('*')
|
|
filters
|
|
.filter((x) => x.value && x.value != '')
|
|
.forEach((x) => {
|
|
const value = formatFilterValue(table, x)
|
|
selectQueryChains = selectQueryChains.filter(x.column, x.operator, value)
|
|
})
|
|
const selectBaseSql = selectQueryChains.toSql()
|
|
|
|
const countQuery = new Query()
|
|
let countQueryChains = countQuery.from(table.name, table.schema ?? undefined).count()
|
|
filters
|
|
.filter((x) => x.value && x.value != '')
|
|
.forEach((x) => {
|
|
const value = formatFilterValue(table, x)
|
|
countQueryChains = countQueryChains.filter(x.column, x.operator, value)
|
|
})
|
|
const countBaseSql = countQueryChains.toSql().slice(0, -1)
|
|
|
|
const sql = `
|
|
${COUNT_ESTIMATE_SQL}
|
|
|
|
with approximation as (
|
|
select reltuples as estimate
|
|
from pg_class
|
|
where oid = ${table.id}
|
|
)
|
|
select
|
|
case
|
|
when estimate = -1 then (select pg_temp.count_estimate('${selectBaseSql.replaceAll("'", "''")}'))
|
|
when estimate > ${THRESHOLD_COUNT} then ${filters.length > 0 ? `pg_temp.count_estimate('${selectBaseSql.replaceAll("'", "''")}')` : 'estimate'}
|
|
else (${countBaseSql})
|
|
end as count,
|
|
estimate = -1 or estimate > ${THRESHOLD_COUNT} as is_estimate
|
|
from approximation;
|
|
`.trim()
|
|
|
|
return sql
|
|
}
|
|
}
|
|
|
|
export type TableRowsCount = {
|
|
count: number
|
|
is_estimate?: boolean
|
|
}
|
|
|
|
export type TableRowsCountVariables = Omit<GetTableRowsCountArgs, 'table'> & {
|
|
queryClient: QueryClient
|
|
tableId?: number
|
|
roleImpersonationState?: RoleImpersonationState
|
|
projectRef?: string
|
|
connectionString?: string | null
|
|
}
|
|
|
|
export type TableRowsCountData = TableRowsCount
|
|
export type TableRowsCountError = ExecuteSqlError
|
|
|
|
export async function getTableRowsCount(
|
|
{
|
|
queryClient,
|
|
projectRef,
|
|
connectionString,
|
|
tableId,
|
|
filters,
|
|
roleImpersonationState,
|
|
enforceExactCount,
|
|
}: TableRowsCountVariables,
|
|
signal?: AbortSignal
|
|
) {
|
|
const entity = await prefetchTableEditor(queryClient, {
|
|
projectRef,
|
|
connectionString,
|
|
id: tableId,
|
|
})
|
|
if (!entity) {
|
|
throw new Error('Table not found')
|
|
}
|
|
|
|
const table = parseSupaTable(entity)
|
|
|
|
const sql = wrapWithRoleImpersonation(
|
|
getTableRowsCountSql({ table, filters, enforceExactCount }),
|
|
roleImpersonationState
|
|
)
|
|
const { result } = await executeSql(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
sql,
|
|
queryKey: ['table-rows-count', table.id],
|
|
isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role),
|
|
},
|
|
signal
|
|
)
|
|
|
|
return {
|
|
count: result[0].count,
|
|
is_estimate: result[0].is_estimate ?? false,
|
|
} as TableRowsCount
|
|
}
|
|
|
|
export const useTableRowsCountQuery = <TData = TableRowsCountData>(
|
|
{ projectRef, connectionString, tableId, ...args }: Omit<TableRowsCountVariables, 'queryClient'>,
|
|
{
|
|
enabled = true,
|
|
...options
|
|
}: UseQueryOptions<TableRowsCountData, TableRowsCountError, TData> = {}
|
|
) => {
|
|
const queryClient = useQueryClient()
|
|
return useQuery<TableRowsCountData, TableRowsCountError, TData>(
|
|
tableRowKeys.tableRowsCount(projectRef, { table: { id: tableId }, ...args }),
|
|
({ signal }) =>
|
|
getTableRowsCount({ queryClient, projectRef, connectionString, tableId, ...args }, signal),
|
|
{
|
|
enabled: enabled && typeof projectRef !== 'undefined' && typeof tableId !== 'undefined',
|
|
...options,
|
|
}
|
|
)
|
|
}
|