mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
perf(unified-logs): single-scan arrayJoin facet count query
Replace the OTEL facet count query's 13x UNION ALL with a single-pass arrayJoin "explode" over `logs`. Each row is fanned out into one row per dimension and `multiIf` picks that dimension's value, so all facet counts plus the total are computed in one scan instead of 13. The output (dimension, value, count) shape is unchanged, so the consumer in unified-logs-count-query.ts needs no changes. Reuses the existing LOG_TYPE_EXPR / LEVEL_EXPR / STATUS_EXPR expressions. Based on the single-scan SQL shape Chase verified against the logs.all.otel endpoint (no CTEs / window fns / tuples / subqueries). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
2450f38ce4
commit
aaa6b770f9
2 files changed
+62
-66
No files matched your search
@@ -124,29 +124,29 @@ describe('UnifiedLogs.queries (OTEL flat)', () => {
|
||||
})
|
||||
|
||||
describe('getLogsCountQuery', () => {
|
||||
it('emits one UNION ALL branch per log_type bucket and per level', () => {
|
||||
it('emits a single-scan arrayJoin explode over all dimensions (no UNION ALL)', () => {
|
||||
const sql = getLogsCountQuery(baseSearch)
|
||||
// Per-log-type counts
|
||||
for (const lt of ['edge', 'postgrest', 'storage', 'postgres', 'edge function', 'auth']) {
|
||||
expect(sql).toContain(`'${lt}'`)
|
||||
}
|
||||
// Per-level counts
|
||||
for (const lvl of ['success', 'warning', 'error']) {
|
||||
expect(sql).toContain(`'${lvl}'`)
|
||||
}
|
||||
// Bundled via UNION ALL — multiple occurrences expected
|
||||
expect(sql.match(/UNION ALL/g)?.length ?? 0).toBeGreaterThan(5)
|
||||
// One multi-dimensional pass, not a stack of UNION ALL branches.
|
||||
expect(sql).not.toContain('UNION ALL')
|
||||
expect(sql).toContain(
|
||||
`arrayJoin(['total','log_type','level','method','status','pathname'])`
|
||||
)
|
||||
// multiIf picks each dimension's value off the exploded dimension alias.
|
||||
expect(sql).toContain('multiIf(')
|
||||
expect(sql).toContain(`dimension = 'total', 'all'`)
|
||||
// Single FROM logs scan with a top-level GROUP BY and empty-bucket drop.
|
||||
expect(sql.match(/FROM logs/g)?.length ?? 0).toBe(1)
|
||||
expect(sql).toContain('GROUP BY dimension, value')
|
||||
expect(sql).toContain(`HAVING value != ''`)
|
||||
})
|
||||
|
||||
it('honours an active log_type filter in the total count branch', () => {
|
||||
it('honours an active log_type filter in the shared WHERE', () => {
|
||||
const sql = getLogsCountQuery(withFilters('log_type:eq:edge'))
|
||||
// The first branch is the total — its WHERE must include the edge
|
||||
// log_type predicate, otherwise the total badge would over-count
|
||||
// when a log_type filter is active.
|
||||
const totalBranch = sql.split(/\bUNION ALL\b/)[0]
|
||||
expect(totalBranch).toContain(`'total'`)
|
||||
expect(totalBranch).toContain(`source = 'edge_logs'`)
|
||||
expect(totalBranch).not.toContain(`source = 'postgres_logs'`)
|
||||
// The single WHERE backs every dimension (including the total badge), so
|
||||
// it must carry the edge log_type predicate and exclude postgres.
|
||||
const where = sql.split(/\bWHERE\b/)[1] ?? ''
|
||||
expect(where).toContain(`source = 'edge_logs'`)
|
||||
expect(where).not.toContain(`source = 'postgres_logs'`)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -358,58 +358,54 @@ LIMIT ${lit(MAX_FACETS_QUANTITY)}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundled count query — UNION ALL of (dimension, value, count) rows so the
|
||||
* frontend can render facet counts and total in one round trip.
|
||||
* Bundled count query — single-scan `arrayJoin` "explode".
|
||||
*
|
||||
* Replaces the previous 13× UNION ALL with one pass over `logs`: `arrayJoin`
|
||||
* fans each row out into one row per dimension, and `multiIf` picks that
|
||||
* dimension's value (or '' to drop it via HAVING). Emits the same
|
||||
* (dimension, value, count) shape the UNION ALL version did, so the consumer
|
||||
* in `unified-logs-count-query.ts` is unchanged.
|
||||
*
|
||||
* Shape is dictated by what the Logflare OTEL endpoint tolerates (empirically
|
||||
* verified): no CTEs (a `WITH` collides with the endpoint's own `WITH`), no
|
||||
* GROUPING SETS, no window functions / `LIMIT BY`, no tuples / array indexing,
|
||||
* no outer aggregation over a `FROM logs` subquery. A flat single-level
|
||||
* arrayJoin + top-level GROUP BY is the only construct that yields a single
|
||||
* multi-dimensional pass.
|
||||
*
|
||||
* Tradeoffs vs. the UNION ALL version (both inherent to a single scan):
|
||||
* - One shared WHERE means we cannot exclude each facet's own filter from
|
||||
* its own counts. Counts are conjunctive — an active filter on a dimension
|
||||
* also narrows that dimension's buckets.
|
||||
* - Per-dimension top-N can't be enforced (no `LIMIT BY` / window fns), so we
|
||||
* bound the whole payload with a global ORDER BY + LIMIT instead. The
|
||||
* `total` row is exempt so the total badge is never dropped.
|
||||
*/
|
||||
export const getLogsCountQuery = (search: QuerySearchParamsType): SafeLogSqlFragment => {
|
||||
// When no predicates remain, fall back to `1` so we emit a valid
|
||||
// tautology rather than a bare `WHERE`.
|
||||
const baseFiltersFor = (excludeField?: string): SafeLogSqlFragment => {
|
||||
const predicates = buildBaseWhere(search, excludeField)
|
||||
return predicates.length > 0 ? joinSqlFragments(predicates, ' AND ') : safeSql`1`
|
||||
}
|
||||
const predicates = buildBaseWhere(search)
|
||||
|
||||
// The "total" badge should reflect the user's *current* filter set,
|
||||
// including any active log_type filter. Pass no excludeField so the
|
||||
// log_type predicate is included.
|
||||
const totalSql = safeSql`
|
||||
SELECT 'total' AS dimension, 'all' AS value, count() AS count
|
||||
const MAX_ROWS = 200
|
||||
|
||||
return safeSql`
|
||||
SELECT
|
||||
arrayJoin(['total','log_type','level','method','status','pathname']) AS dimension,
|
||||
multiIf(
|
||||
dimension = 'total', 'all',
|
||||
dimension = 'log_type', (${LOG_TYPE_EXPR}),
|
||||
dimension = 'level', (${LEVEL_EXPR}),
|
||||
dimension = 'method', ${ATTR.method},
|
||||
dimension = 'status', (${STATUS_EXPR}),
|
||||
dimension = 'pathname', ${ATTR.path},
|
||||
''
|
||||
) AS value,
|
||||
count() AS count
|
||||
FROM logs
|
||||
WHERE ${baseFiltersFor()}
|
||||
${whereClause(predicates)}
|
||||
GROUP BY dimension, value
|
||||
HAVING value != ''
|
||||
ORDER BY dimension = 'total' DESC, count DESC
|
||||
LIMIT ${lit(MAX_ROWS)}
|
||||
`
|
||||
|
||||
const logTypeBranches = joinSqlFragments(
|
||||
Object.entries(LOG_TYPE_PREDICATE).map(
|
||||
([logType, predicate]) =>
|
||||
safeSql`
|
||||
SELECT 'log_type' AS dimension, ${lit(logType)} AS value, countIf(${predicate}) AS count
|
||||
FROM logs
|
||||
WHERE ${baseFiltersFor('log_type')}
|
||||
`
|
||||
),
|
||||
' UNION ALL '
|
||||
)
|
||||
|
||||
const levelBranches = joinSqlFragments(
|
||||
(['success', 'warning', 'error'] as const).map(
|
||||
(lvl) =>
|
||||
safeSql`
|
||||
SELECT 'level' AS dimension, ${lit(lvl)} AS value, countIf((${LEVEL_EXPR}) = ${lit(lvl)}) AS count
|
||||
FROM logs
|
||||
WHERE ${baseFiltersFor('level')}
|
||||
`
|
||||
),
|
||||
' UNION ALL '
|
||||
)
|
||||
|
||||
const facetBranches = joinSqlFragments(
|
||||
(['method', 'status', 'pathname'] as const).map(
|
||||
(facet) => safeSql`(${getFacetCountQuery({ search, facet })})`
|
||||
),
|
||||
' UNION ALL '
|
||||
)
|
||||
|
||||
return joinSqlFragments([totalSql, logTypeBranches, levelBranches, facetBranches], ' UNION ALL ')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user