perf(coderd/database): aggregate AI spend before joining user profiles (#29737)

The organization spend query joined user profile fields onto every usage
row before aggregating. On dogfood's September window, this sorted 1.23
million rows and wrote about 1.5 GiB of temporary data per request.
Aggregate by user first, then join profiles and apply ordering and
pagination.

A counterbalanced production-data benchmark (six runs each, AB/BA order,
September 1 through September 22 15:00 UTC cutoff, 1,233,784 usage rows)
measured median database execution time of 6.08 s before and 3.12 s
after this change. Ranges were 5.74 to 8.32 s and 2.52 to 4.06 s,
respectively; median per-pair speedup was 2.10x. Median temporary writes
fell from 1,610 MiB to 329 MiB. Both queries had zero shared-buffer
reads in every run. These are EXPLAIN ANALYZE timings on a busy
database, not API/page-load latency. A same-snapshot, unlimited-page
comparison found zero differing rows in either direction, including row
order, across all 88 users.

Refs #28964. This is a one-layer performance stack; further optimization
experiments are not included here. No schema, API, or frontend changes.

<details>
<summary>Delivery record</summary>

Head: `41a179a17464bb54bbc9b3886e9c3ea6b7bda7ef`. SQLC generation,
targeted database/HTTP/export/authorization tests, and the full
pre-commit hook passed before publication and again before requesting
Codex review. The existing query test now covers usage across two groups
in one organization.

Remote browser UAT: not run. Production SQL evidence covers a prototype
verified identical to the committed query; this head is not deployed.
Frontend checks were not run because no frontend or response type
changed. Local pre-push checks were skipped as requested.

Codex:
[OK](https://github.com/coder/coder/pull/29737#issuecomment-5780120712)
at 2026-09-22 16:27:06 UTC for `41a179a174`, after the
[request](https://github.com/coder/coder/pull/29737#issuecomment-5780086039)
at 16:24:34 UTC. No findings or unresolved threads. Completed
independent review passes: 1/6. CI: all current-head check runs passed
or were skipped, including the required gate. The non-required `Pixel /
Review` status remains pending. Codex reports its current-head review
completed; one trigger, no retriggers, no code changes requested.

</details>

> Xum opened and manages this PR on behalf of @ibetitsmike.
This commit is contained in:
Michael Suchacz
2026-09-22 21:58:18 +02:00
committed by GitHub
parent ccffab23a0
commit 2872e88827
3 changed files with 90 additions and 79 deletions
+2 -1
View File
@@ -19286,6 +19286,7 @@ func TestListOrganizationAISpendUsers(t *testing.T) {
org := dbgen.Organization(t, db, database.Organization{})
group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
secondGroup := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
otherOrg := dbgen.Organization(t, db, database.Organization{})
otherGroup := dbgen.Group(t, db, database.Group{OrganizationID: otherOrg.ID})
@@ -19314,7 +19315,7 @@ func TestListOrganizationAISpendUsers(t *testing.T) {
for _, u := range []usage{
// alice: 1500 priced plus one unpriced usage without a recorded client.
{user: alice, group: inGroup, at: start, providerName: "anthropic-prod", model: "claude", client: vscode, cost: priced(1000)},
{user: alice, group: inGroup, at: start.Add(time.Hour), providerName: "openai-prod", model: "gpt-4", cost: priced(500)},
{user: alice, group: uuid.NullUUID{UUID: secondGroup.ID, Valid: true}, at: start.Add(time.Hour), providerName: "openai-prod", model: "gpt-4", cost: priced(500)},
{user: alice, group: inGroup, at: start.Add(2 * time.Hour), providerName: "openai-prod", model: "gpt-4o"},
// bob: the most expensive user.
{user: bob, group: inGroup, at: start.Add(time.Hour), providerName: "anthropic-prod", model: "claude", client: cursor, cost: priced(3000)},
+47 -42
View File
@@ -3686,59 +3686,64 @@ func (q *sqlQuerier) IncrementUserAIDailySpend(ctx context.Context, arg Incremen
}
const listOrganizationAISpendUsers = `-- name: ListOrganizationAISpendUsers :many
WITH spend AS (
SELECT
ai.initiator_id AS user_id,
COALESCE(SUM(tu.cost_micros), 0)::BIGINT AS cost_micros,
COUNT(*) FILTER (WHERE tu.cost_micros IS NULL)::BIGINT AS unpriced_usage_count,
ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider)::text[] AS providers,
ARRAY_AGG(DISTINCT COALESCE(ai.client, 'Unknown') ORDER BY COALESCE(ai.client, 'Unknown'))::text[] AS clients,
ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model)::text[] AS models
FROM aibridge_token_usages tu
JOIN aibridge_interceptions ai ON ai.id = tu.interception_id
JOIN groups ON groups.id = tu.effective_group_id
WHERE groups.organization_id = $1
AND tu.created_at >= $4::timestamptz
AND tu.created_at < $5::timestamptz
AND CASE
WHEN $6::text != '' THEN ai.provider_name = $6::text
ELSE true
END
AND CASE
WHEN $7::text != '' THEN ai.model = $7::text
ELSE true
END
AND CASE
WHEN $8::text != '' THEN COALESCE(ai.client, 'Unknown') = $8::text
ELSE true
END
GROUP BY ai.initiator_id
)
SELECT
ai.initiator_id AS user_id,
users.username AS username,
users.name AS name,
users.avatar_url AS avatar_url,
groups.organization_id AS organization_id,
COALESCE(SUM(tu.cost_micros), 0)::BIGINT AS cost_micros,
COUNT(*) FILTER (WHERE tu.cost_micros IS NULL)::BIGINT AS unpriced_usage_count,
ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider)::text[] AS providers,
ARRAY_AGG(DISTINCT COALESCE(ai.client, 'Unknown') ORDER BY COALESCE(ai.client, 'Unknown'))::text[] AS clients,
ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model)::text[] AS models,
COUNT(*) OVER ()::BIGINT AS count,
COALESCE(SUM(SUM(tu.cost_micros)) OVER (), 0)::BIGINT AS total_cost_micros,
COALESCE(SUM(COUNT(*) FILTER (WHERE tu.cost_micros IS NULL)) OVER (), 0)::BIGINT AS total_unpriced_usage_count
FROM aibridge_token_usages tu
JOIN aibridge_interceptions ai ON ai.id = tu.interception_id
JOIN users ON users.id = ai.initiator_id
JOIN groups ON groups.id = tu.effective_group_id
WHERE groups.organization_id = $1
AND tu.created_at >= $2::timestamptz
AND tu.created_at < $3::timestamptz
AND CASE
WHEN $4::text != '' THEN ai.provider_name = $4::text
ELSE true
END
AND CASE
WHEN $5::text != '' THEN ai.model = $5::text
ELSE true
END
AND CASE
WHEN $6::text != '' THEN COALESCE(ai.client, 'Unknown') = $6::text
ELSE true
END
GROUP BY
ai.initiator_id,
spend.user_id,
users.username,
users.name,
users.avatar_url,
groups.organization_id
ORDER BY cost_micros DESC, LOWER(users.username), ai.initiator_id
LIMIT NULLIF($8::int, 0)
OFFSET $7::int
$1::uuid AS organization_id,
spend.cost_micros,
spend.unpriced_usage_count,
spend.providers,
spend.clients,
spend.models,
COUNT(*) OVER ()::BIGINT AS count,
COALESCE(SUM(spend.cost_micros) OVER (), 0)::BIGINT AS total_cost_micros,
COALESCE(SUM(spend.unpriced_usage_count) OVER (), 0)::BIGINT AS total_unpriced_usage_count
FROM spend
JOIN users ON users.id = spend.user_id
ORDER BY cost_micros DESC, LOWER(users.username), spend.user_id
LIMIT NULLIF($3::int, 0)
OFFSET $2::int
`
type ListOrganizationAISpendUsersParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
PeriodStart time.Time `db:"period_start" json:"period_start"`
PeriodEnd time.Time `db:"period_end" json:"period_end"`
ProviderName string `db:"provider_name" json:"provider_name"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
OffsetOpt int32 `db:"offset_opt" json:"offset_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type ListOrganizationAISpendUsersRow struct {
@@ -3765,13 +3770,13 @@ type ListOrganizationAISpendUsersRow struct {
func (q *sqlQuerier) ListOrganizationAISpendUsers(ctx context.Context, arg ListOrganizationAISpendUsersParams) ([]ListOrganizationAISpendUsersRow, error) {
rows, err := q.db.QueryContext(ctx, listOrganizationAISpendUsers,
arg.OrganizationID,
arg.OffsetOpt,
arg.LimitOpt,
arg.PeriodStart,
arg.PeriodEnd,
arg.ProviderName,
arg.Model,
arg.Client,
arg.OffsetOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
+41 -36
View File
@@ -501,46 +501,51 @@ LIMIT NULLIF(@limit_opt::int, 0);
-- providers, clients, and models each user spent through and the count and
-- totals over every matching user. It must keep the same joins and predicates as
-- ExportOrganizationAISpend so both report the same token usage.
WITH spend AS (
SELECT
ai.initiator_id AS user_id,
COALESCE(SUM(tu.cost_micros), 0)::BIGINT AS cost_micros,
COUNT(*) FILTER (WHERE tu.cost_micros IS NULL)::BIGINT AS unpriced_usage_count,
ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider)::text[] AS providers,
ARRAY_AGG(DISTINCT COALESCE(ai.client, 'Unknown') ORDER BY COALESCE(ai.client, 'Unknown'))::text[] AS clients,
ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model)::text[] AS models
FROM aibridge_token_usages tu
JOIN aibridge_interceptions ai ON ai.id = tu.interception_id
JOIN groups ON groups.id = tu.effective_group_id
WHERE groups.organization_id = @organization_id
AND tu.created_at >= @period_start::timestamptz
AND tu.created_at < @period_end::timestamptz
AND CASE
WHEN @provider_name::text != '' THEN ai.provider_name = @provider_name::text
ELSE true
END
AND CASE
WHEN @model::text != '' THEN ai.model = @model::text
ELSE true
END
AND CASE
WHEN @client::text != '' THEN COALESCE(ai.client, 'Unknown') = @client::text
ELSE true
END
GROUP BY ai.initiator_id
)
SELECT
ai.initiator_id AS user_id,
users.username AS username,
users.name AS name,
users.avatar_url AS avatar_url,
groups.organization_id AS organization_id,
COALESCE(SUM(tu.cost_micros), 0)::BIGINT AS cost_micros,
COUNT(*) FILTER (WHERE tu.cost_micros IS NULL)::BIGINT AS unpriced_usage_count,
ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider)::text[] AS providers,
ARRAY_AGG(DISTINCT COALESCE(ai.client, 'Unknown') ORDER BY COALESCE(ai.client, 'Unknown'))::text[] AS clients,
ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model)::text[] AS models,
COUNT(*) OVER ()::BIGINT AS count,
COALESCE(SUM(SUM(tu.cost_micros)) OVER (), 0)::BIGINT AS total_cost_micros,
COALESCE(SUM(COUNT(*) FILTER (WHERE tu.cost_micros IS NULL)) OVER (), 0)::BIGINT AS total_unpriced_usage_count
FROM aibridge_token_usages tu
JOIN aibridge_interceptions ai ON ai.id = tu.interception_id
JOIN users ON users.id = ai.initiator_id
JOIN groups ON groups.id = tu.effective_group_id
WHERE groups.organization_id = @organization_id
AND tu.created_at >= @period_start::timestamptz
AND tu.created_at < @period_end::timestamptz
AND CASE
WHEN @provider_name::text != '' THEN ai.provider_name = @provider_name::text
ELSE true
END
AND CASE
WHEN @model::text != '' THEN ai.model = @model::text
ELSE true
END
AND CASE
WHEN @client::text != '' THEN COALESCE(ai.client, 'Unknown') = @client::text
ELSE true
END
GROUP BY
ai.initiator_id,
spend.user_id,
users.username,
users.name,
users.avatar_url,
groups.organization_id
ORDER BY cost_micros DESC, LOWER(users.username), ai.initiator_id
@organization_id::uuid AS organization_id,
spend.cost_micros,
spend.unpriced_usage_count,
spend.providers,
spend.clients,
spend.models,
COUNT(*) OVER ()::BIGINT AS count,
COALESCE(SUM(spend.cost_micros) OVER (), 0)::BIGINT AS total_cost_micros,
COALESCE(SUM(spend.unpriced_usage_count) OVER (), 0)::BIGINT AS total_unpriced_usage_count
FROM spend
JOIN users ON users.id = spend.user_id
ORDER BY cost_micros DESC, LOWER(users.username), spend.user_id
LIMIT NULLIF(@limit_opt::int, 0)
OFFSET @offset_opt::int;