From b481f77b75bc37a5be5f6a77c844a50c3023cea8 Mon Sep 17 00:00:00 2001 From: David Li Date: Wed, 10 Dec 2025 15:46:01 -0500 Subject: [PATCH] [ENG-1509] Fix /v4/fills/parentSubaccount query performance (#3279) (cherry picked from commit c275378bf214efb2fc74e574e213d2b1740a08e1) --- .../postgres/src/stores/fill-table.ts | 11 +++--- .../postgres/src/stores/subaccount-table.ts | 34 ++++++++++++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/indexer/packages/postgres/src/stores/fill-table.ts b/indexer/packages/postgres/src/stores/fill-table.ts index 536000ea64..82eed007c4 100644 --- a/indexer/packages/postgres/src/stores/fill-table.ts +++ b/indexer/packages/postgres/src/stores/fill-table.ts @@ -11,7 +11,6 @@ import { knexReadReplica } from '../helpers/knex'; import { setupBaseQuery, verifyAllRequiredFields } from '../helpers/stores-helpers'; import Transaction from '../helpers/transaction'; import { getUuid } from '../helpers/uuid'; -import { getSubaccountQueryForParent } from '../lib/parent-subaccount-helpers'; import FillModel from '../models/fill-model'; import { FillColumns, @@ -31,6 +30,7 @@ import { QueryableField, QueryConfig, } from '../types'; +import { findIdsForParentSubaccount } from './subaccount-table'; export function uuid(eventId: Buffer, liquidity: Liquidity): string { // TODO(IND-483): Fix all uuid string substitutions to use Array.join. @@ -147,10 +147,11 @@ export async function findAll( if (subaccountId !== undefined) { baseQuery = baseQuery.whereIn(FillColumns.subaccountId, subaccountId); } else if (parentSubaccount !== undefined) { - baseQuery = baseQuery.whereIn( - FillColumns.subaccountId, - getSubaccountQueryForParent(parentSubaccount), - ); + // PERFORMANCE CRITICAL: Resolve subaccountIds to concrete UUIDs before querying. + // Using IN (subquery) causes Postgres to misestimate cardinality and scan millions + // of rows. With explicit UUIDs, Postgres uses optimal index scans per subaccount. + const subaccountIds = await findIdsForParentSubaccount(parentSubaccount); + baseQuery = baseQuery.whereIn(FillColumns.subaccountId, subaccountIds); } if (side !== undefined) { diff --git a/indexer/packages/postgres/src/stores/subaccount-table.ts b/indexer/packages/postgres/src/stores/subaccount-table.ts index 5c3ea231fd..9f06e166c8 100644 --- a/indexer/packages/postgres/src/stores/subaccount-table.ts +++ b/indexer/packages/postgres/src/stores/subaccount-table.ts @@ -2,7 +2,7 @@ import { IndexerSubaccountId } from '@dydxprotocol-indexer/v4-protos'; import { PartialModelObject, QueryBuilder } from 'objection'; import config from '../config'; -import { BUFFER_ENCODING_UTF_8, DEFAULT_POSTGRES_OPTIONS } from '../constants'; +import { BUFFER_ENCODING_UTF_8, DEFAULT_POSTGRES_OPTIONS, MAX_PARENT_SUBACCOUNTS } from '../constants'; import { verifyAllRequiredFields, setupBaseQuery, @@ -201,3 +201,35 @@ export async function deleteById( Transaction.get(options.txId), ).deleteById(id); } + +/** + * Retrieves all subaccount IDs associated with a parent subaccount. + * A subaccount is considered a child of the parent if it has the same address + * and its subaccount number follows the modulo relationship with the parent. + * + * @param parentSubaccount The parent subaccount object with address and subaccountNumber + * @param options Query options including transaction ID + * @returns A promise that resolves to an array of subaccount ID strings + */ +export async function findIdsForParentSubaccount( + parentSubaccount: { + address: string, + subaccountNumber: number, + }, + options: Options = DEFAULT_POSTGRES_OPTIONS, +): Promise { + // Get all subaccounts for the address + const subaccounts = await findAll( + { address: parentSubaccount.address }, + [], + options, + ); + + // Filter for subaccounts that match the parent relationship + // (subaccountNumber - parentSubaccountNumber) % MAX_PARENT_SUBACCOUNTS = 0 + return subaccounts + .filter((subaccount) => (subaccount.subaccountNumber - parentSubaccount.subaccountNumber) % + MAX_PARENT_SUBACCOUNTS === 0, + ) + .map((subaccount) => subaccount.id); +}