A trial balance report should be one of the fastest reports in the system — it's summing pre-aggregated balances by account and subaccount, which is exactly what Acumatica's GL balance tables exist to make cheap. When a trial balance is slow, it's almost always because the report was built to read GLTran directly instead of the summarized balance tables, and that one modeling choice is worth chasing down before anything else.
Reading GLTran instead of the balance tables is the single most common cause
Acumatica maintains running period balances specifically so that "what's the balance of account X, subaccount Y, as of period Z" doesn't require summing every transaction since the account was created. A trial balance report built by joining and summing raw GLTran rows per account, per period, is redoing that summarization from scratch on every run — on an instance with years of transaction history, that's the difference between a sub-second report and one that takes real time. The report should read from the balance history tables (the GLHistory-family tables that store period-end running balances) and only fall back to transaction-level detail for something the balance tables genuinely can't answer, like a same-period drill filtered by a specific reference number pattern.
Base: GLHistory WHERE FinPeriodID = @Period AND LedgerID = @Ledger
Group: AccountID, SubID
Sum: FinYtdBalance / period movement, already maintained by the platform
-- avoid: SUM(GLTran.TranAmt) grouped from raw transaction rows across the same range
Subaccount-level detail multiplies row count fast on segmented COAs
A trial balance at account level might be a few hundred rows. The same trial balance broken out by every combination of account and subaccount segment (department, location, project) on a heavily segmented chart of accounts can be tens of thousands of rows, and if the report defaults to full subaccount detail when most requests actually want account-level totals with subaccount as a drill-down, you're paying for detail nobody asked for on the common case. Default to account-level summary with subaccount detail as an optional parameter or a separate drill-through report, not baked into the standard render.
Most trial balance requests implicitly want only accounts with non-zero activity or non-zero ending balance — filtering those out in the schema (rather than fetching everything and suppressing zero rows in the layout) is a real and easy win. But some audit and compliance use cases specifically want to see zero-balance accounts to confirm they're genuinely zero, not missing. Make the zero-balance filter a parameter with a sensible default, not a silent hardcoded exclusion — an auditor who can't get the zero rows will not be happy to learn the report quietly dropped them.
Multi-ledger and multi-currency trial balances: know which ledger the balance table row belongs to
Instances running statutory plus reporting ledgers, or budget ledgers alongside actual, have multiple balance rows per account/period — one per ledger. A trial balance that doesn't filter or group explicitly by LedgerID will either double-count by summing across ledgers that shouldn't be combined, or silently pick up rows from a budget ledger mixed in with actual. Always make ledger an explicit, required schema filter, never an afterthought.
Wrapping up
If a trial balance report is slow, check first whether it's reading raw GLTran instead of the maintained balance history tables — that's the fix that matters most, before any indexing conversation. Default to account-level summary rather than full subaccount detail, make zero-balance suppression an explicit parameter rather than a silent default, and always filter by ledger explicitly on any multi-ledger instance.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.