perf(api): pre-aggregate ETH balances into eth_user_balances#886
Open
rickyrombo wants to merge 1 commit into
Open
perf(api): pre-aggregate ETH balances into eth_user_balances#886rickyrombo wants to merge 1 commit into
rickyrombo wants to merge 1 commit into
Conversation
GetUsers reads balances through v_user_balances. The sol side was already pre-aggregated (sol_user_balances, one PK lookup) but the eth side was still computed live via a correlated LEFT JOIN LATERAL summing eth_wallet_balances per user. That shape defeats predicate pushdown into the view, so the planner often materializes it across the whole user base on every /v1/users call. Add eth_user_balances (the ETH analog of sol_user_balances): a trigger- maintained, backfilled table, and rewrite the view's eth side to a single PK join. The per-user correlated aggregate is gone; the view is now three flat PK joins, fully inlinable/parameterizable. - 0213_add_eth_user_balances.sql: table (balance NUMERIC, wei) + backfill - update_eth_user_balance(user_id): single-user recompute - handle_eth_wallet_balance_change: trigger on eth_wallet_balances writes - handle_associated_wallet.sql: also recompute eth on chain=eth link/unlink - v_user_balances: LEFT JOIN eth_user_balances, LATERAL removed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
GetUsersreads balances throughv_user_balances. The sol side was already pre-aggregated intosol_user_balances(one PK lookup), but the eth side was still computed live inside the view via a correlatedLEFT JOIN LATERALthat, per user, built an IN-list of the user's wallets and summedeth_wallet_balances.Two problems:
FROM users u ... WHERE u.is_currentand wraps eth in a correlatedLATERALwith anIN (subquery UNION ALL subquery)aggregate. That shape frequently defeats the planner's ability to pushuser_id = ANY(@ids)into the view and parameterize it — so Postgres computes the eth balance for every current user and hash-joins down to the handful requested. Every/v1/userscall ends up scanning the whole user base.associated_walletsscan + Neth_wallet_balancesprobes + an aggregate per user, versus the sol side's single PK fetch.What
Add
eth_user_balances— the ETH analog ofsol_user_balances— a pre-aggregated, trigger-maintained, backfilled table, and rewrite the view's eth side to a single PK join. The view becomes three flat PK joins, fully inlinable/parameterizable; the per-user correlated aggregate disappears.0213_add_eth_user_balances.sql—eth_user_balances(user_id PK, balance NUMERIC, updated_at, created_at)+ backfill.balanceisNUMERIC(eth is wei, 1e18 scale) rather thanBIGINTlike the sol side.update_eth_user_balance(user_id)— single-user recompute (analog ofupdate_sol_user_balance_mint). NoGROUP BY, so unlinking the last wallet drives the balance to 0 instead of leaving a stale value.handle_eth_wallet_balance_change—AFTER INSERT OR UPDATE ON eth_wallet_balancestrigger; maps the changed wallet → affected users (primary + linked) and recomputes. Skips no-op balance updates.handle_associated_wallet.sql— the existingon_associated_walletstrigger now also recomputeseth_user_balancesonchain='eth'link/unlink, alongside the unchanged sol logic.v_user_balances— eth is nowLEFT JOIN eth_user_balances eub ON eub.user_id = u.user_id; theLATERALis gone.get_users.sqlis unchanged (sameeth_balance/sol_balancecolumns).Verification
EXPLAIN (ANALYZE, BUFFERS)of a GetUsers-shaped balance query against local Postgres:SubPlan 1(Aggregate → Nested Loop over the wallet IN-list) executed once per user row.(Plan shape verified on an empty local test DB, which is sufficient to confirm the structural change; with data +
ANALYZEtheuser_id = ANY(...)restriction driveseth_user_balances_pkeyper id.)