Acumatica · Customization

Acumatica GI Cross-Table Joins — A Deep Dive

How joins work in Acumatica Generic Inquiries — left, right, inner, full, and the performance characteristics of each. With a worked example joining ARInvoices, Customers, and Branches.

John Kihiu12 min read

Every GI beyond a single-table lookup is a join, and the GI Designer's Tables tab makes adding a join dangerously easy — drag a related table in, pick a relation, done. What it does not make easy is understanding what that join does to your row count, and that gap is where most "the GI shows duplicate rows" and "the total is double what it should be" tickets come from.

Relation type controls both correctness and row count

The Tables tab offers Left Join, Inner Join, and a couple of Acumatica-specific relation shortcuts derived from the DAC's declared foreign keys. The choice affects two independent things people conflate: whether parent rows without a matching child survive, and how many rows a matching parent produces.

JOIN BEHAVIOR
SOOrder  InnerJoin  SOLine  On SOLine.OrderNbr = SOOrder.OrderNbr
-- orders with zero lines vanish entirely (rare, but possible on drafts)
-- orders with 5 lines produce 5 output rows, each repeating the order header

SOOrder  LeftJoin   SOLine  On SOLine.OrderNbr = SOOrder.OrderNbr
-- orders with zero lines survive, with SOLine columns null
-- orders with 5 lines still produce 5 output rows

Neither join type avoids the fan-out — that is inherent to joining a one-to-many relationship at all. If someone asks for "one row per order" and your GI joins to line detail, you either accept multiple rows per order (fine for a line-level export) or you need an aggregate (SUM the lines) instead of a raw join, which changes the grain back to one-per-order at the cost of losing line-level detail in that same GI.

The double fan-out: two children joined to one parent

This is the join mistake I see most often in inherited GIs. Someone wants an order's lines and its shipments on one inquiry, joins both children to the order header, and gets a cross-product: 5 lines × 3 shipments = 15 output rows, none of which correspond to anything real, and any SUM on the order total is now inflated 3x. There is no join-type setting that fixes this — it is structural. The fix is one of:

Test row counts against a known order

Before shipping a multi-join GI, pick one real record with a known number of children on each side and verify the output row count matches your mental model exactly. A GI that "looks right" scrolling through a grid of a thousand rows can still be silently wrong — fan-out errors are easy to miss visually and easy to catch with one arithmetic check.

Joins across unrelated modules need an explicit key

The designer's relation shortcuts only exist for foreign keys Acumatica's DACs already declare. Joining, say, a custom GL cost-center dimension to InventoryItem has no built-in relation, so you add the table manually and write the On condition by hand, referencing the actual key fields. Get the key wrong (a common one: joining on a code field that is not actually unique per the intended grain) and you get a fan-out that looks like a legitimate one-to-many join but is really a data-quality bug in your join key.

Wrapping up

Every join in a GI is either narrowing (inner) or preserving (left) on the parent side, and separately, every one-to-many join fans rows out — that part is unavoidable and not a setting. Watch for the double fan-out when two child tables join to the same parent, verify row counts against a known record before shipping, and hand-write join conditions carefully when there is no built-in relation to lean on.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.