"The invoice prints fine, but the month-end register takes eleven minutes" is a message I get from Acumatica clients often enough that I have a standard diagnostic sequence for it. Report Designer reports are usually slow for boring, fixable reasons — the schema pulls too much, filters run on the wrong side, or a subreport turned the render into an N+1 storm. Here is the sequence, in the order that finds the problem fastest.
Step 0: measure before touching anything
Run the report from the browser with the Request Profiler enabled (SM205070). You care about two numbers: total SQL time versus total request time, and the count of SQL queries. The split tells you which world you are debugging:
- High SQL time, few queries — the main dataset query is expensive. Database-side problem.
- Many queries — hundreds or thousands of statements means subreports or per-row lookups. Report-structure problem.
- Low SQL time, high total — rendering and layout cost. Usually images, or an enormous page count.
Push filters into the schema, not the layout
The single most common defect I find: the report's schema selects a broad dataset and then the layout suppresses rows with visibility expressions, or groups filter with expressions like =IIf([ARInvoice.DocDate] >= [@FromDate], ...). The database happily returns every invoice since go-live; the report engine then throws most of them away in memory.
Every parameter should appear as a condition on the Filters tab of the Schema Builder so it becomes a SQL WHERE clause. Check this by profiling: the query text in the profiler should contain your date range. If it does not, the filter is running client-side.
Field: ARInvoice.DocDate Condition: Greater or Equal Value: =[@FromDate]
Field: ARInvoice.DocDate Condition: Less or Equal Value: =[@ToDate]
Field: ARInvoice.Released Condition: Equal Value: True
Trim relations and fields
The Schema Builder makes it painless to join eight tables and select every field "in case". Every joined table widens the row and can change the query plan; every selected long text field (descriptions, note text) inflates the data transferred. Two rules:
- Delete relations the layout does not reference. The designer will not warn you about unused joins.
- Watch join types. A child table joined with an inner join when some parents lack children silently drops rows (a correctness bug), while a left join to a large child table for one display field may be better fetched differently — or accepted, but consciously.
Also check for the fan-out trap: parent joined to two child tables multiplies rows, and your aggregates go wrong and your row count explodes. That is a case for a subreport or a pre-aggregated join, not a wider flat query.
Subreports: the N+1 you can see in the profiler
If the profiler shows one query repeated with different parameters hundreds of times, a subreport (or an embedded per-row report control) is executing per detail row. Fixes in order of cheapness: move the subreport from the detail band to a group footer; replace an aggregate-only subreport with a joined SQL aggregate; pre-flatten the child data into a PXProjection the parent schema can join once.
Layout-level aggregate functions — Sum(), Count() in group footers — are computed by the report engine over the fetched rows. That is fine. What is not fine is fetching detail rows only to compute a total you never display line-by-line. If the report shows totals without details, aggregate in the schema (GROUP BY via a projection or GI-backed source) and fetch the totals directly.
Database-side: indexes and the big history tables
When the profiler shows one slow query, take its text and look at the plan (on SaaS, work with Acumatica support; on private cloud, do it yourself). The usual suspects in my experience:
- Date-range scans on
GLTran,ARTran,INTranwithout a supporting index. Acumatica's stock indexes cover the screens' access patterns, not necessarily your report's. On private cloud, an index on the filtered columns (respectingCompanyIDas the leading column convention) can turn minutes into seconds. Keep a script of custom indexes — upgrades can drop them. - Functions on columns in filters — expressions applied to the field side of a condition prevent index use.
- FinPeriodID vs date confusion — filtering GL data by
FinPeriodID(a string, indexed, aligned with how balances are stored) is dramatically cheaper than by date on big ledgers.
Rendering costs: images and page count
Two rendering-side offenders worth checking when SQL is fast but the report is not: high-resolution logos placed in the detail band (rendered per row — put them in the page header, and resize the source image to roughly its printed size), and reports that paginate into thousands of pages nobody will read. For the latter, question the requirement: a 4,000-page register exists to be exported, and a GI with Export to Excel serves that need in a tenth of the time, because it skips pagination entirely.
When it is still slow: schedule it
Some reports are legitimately heavy — a full-year consolidated pack is going to cost real compute. Move those to the Automation Schedules with output to email or a file share, run them off-hours, and keep interactive rendering for interactive-sized questions. Users perceive a report that arrives in their inbox at 6 a.m. as fast, regardless of how long it ran.
Wrapping up
Tune in this order: profile, push filters into SQL, trim joins and fields, kill per-row subreports, then and only then look at indexes. Ninety percent of slow .rpx reports I have been handed were fixed in the first three steps without touching the database. The profiler is your ground truth — never tune a report you have not measured.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.