Joins and reconciliation
Why does my transactionAccountingLine join return far more rows than expected?
Short answer
You joined on transaction alone. transactionAccountingLine must match transactionLine on both transaction and line id. The transaction-only join matches every accounting line against every line of the same document, silently multiplying rows and any sum built on them.
You joined on transaction alone. transactionAccountingLine has to match transactionLine on both the transaction and the line id. With only the transaction predicate, every accounting line of a document matches every line of the same document, and nothing warns you.
Minimal reproduction
Two counts over the same two tables, differing by one predicate. One statement per call:
-- wrong: transaction only
SELECT COUNT(*) AS n
FROM transactionAccountingLine tal
JOIN transactionLine tl ON tl.transaction = tal.transaction
-- right: transaction plus line id
SELECT COUNT(*) AS n
FROM transactionAccountingLine tal
JOIN transactionLine tl ON tl.transaction = tal.transaction AND tl.id = tal.transactionline
The first returned many times as many rows as the second. The multiplier is not a constant you can look up: the loose join emits one row per accounting line per line of the same document, so it is roughly the average number of lines on your documents, and it grows with them. Both statements succeeded. Neither warned about anything.
The fix
SELECT SUM(tal.amount) AS posted
FROM transactionAccountingLine tal
JOIN transactionLine tl ON tl.transaction = tal.transaction
AND tl.id = tal.transactionline
The join key is a compound one. tal.transactionline holds the line id and pairs with tl.id; tal.transaction pairs with tl.transaction. Both transactionline and transaction are on transactionAccountingLine, and both id and transaction are on transactionLine.
One more predicate may be needed. transactionAccountingLine also carries an accountingbook column. In the account tested, the compound-key join returned exactly the transactionLine row count, one accounting line per line, so a single book was in play. Where multi-book accounting is enabled the compound key is no longer unique and a book predicate is needed as well. That case was not measured here.
Why this happens
A document with N lines and N accounting lines produces N by N rows instead of N. That is a cartesian product inside each document, and it is why the ratio you measure is not the typical document's line count: squaring makes the largest documents dominate. The measured multiplier sits at or above the average lines per document, and further above it the more the account's volume is concentrated in large documents.
The multiplier is a property of your data, not of NetSuite. An account of mostly single-line documents would barely fan out at all, which is exactly what makes this dangerous to test on small sample data. That reading is inference from the mechanism, since only one account was measured.
COUNT(*) is the visible symptom. SUM(amount) is the expensive one: each accounting line is counted once per line on its document, so any total built on the loose join is wrong while still looking plausible. The row ratio is not the money ratio, and no sum was measured on the loose join in this run, so do not use the row ratio to correct a total. Rerun the total on the compound key instead.
Scope: one production account on NetSuite 2026.1. Every probe behind this page ran entirely through N/query.runSuiteQL; the finding was not tested on the REST endpoint.
How to tell if you are affected
Run the two counts as two separate calls and compare them. Do not combine them into one statement: the loose join is the heavy half, the compound-key join on its own already hit the runner's transport timeout on a first attempt, and a timeout reads as "test inconclusive".
SELECT COUNT(*) AS n
FROM transactionAccountingLine tal
JOIN transactionLine tl ON tl.transaction = tal.transaction
SELECT COUNT(*) AS n
FROM transactionAccountingLine tal
JOIN transactionLine tl ON tl.transaction = tal.transaction AND tl.id = tal.transactionline
Equal counts mean the loose join happens to be harmless on your data. Any ratio above 1 means it is duplicating rows, and every query using that join is counting the same accounting line more than once.
How this was established
4 probe runs against one live NetSuite 2026.1 account on the Administrator role, through N/query.runSuiteQL. Where this page draws a boundary around a finding, that boundary is the edge of what was actually run.
Related
- Why does my SuiteQL RIGHT JOIN between transaction and transactionLine return 0 rows?
RIGHT JOIN between transaction and transactionLine runs as an inner join and returns zero rows. FULL OUTER is downgraded to a LEFT join from the header. No error. LEFT JOIN from the header and Oracle (+) both work.
- Why can't I select subsidiary from transactionAccountingLine in SuiteQL?
There is no subsidiary column on transactionAccountingLine, and selecting it fails with a bare "Unexpected Error" that never names the column. Join transactionLine on both transaction and line id, then read subsidiary from there.
- Why does totalResults in the SuiteQL REST response not match SELECT COUNT(*)?
totalResults is not a row count. It reports min(rows your query returns, limit x 1000), so on a larger result set it reports the cap and calls it the total, HTTP 200, no flag. Run SELECT COUNT(*) separately.
From the blog
- Why Your NetSuite AP Aging Never Ties to the General Ledger
The AP Aging report and the accounts payable balance on the balance sheet disagree, and the difference is not a missing bill. Three specific traps in how NetSuite stores payables, and the SuiteQL that finds each one.