SQL for the ISC CPA Exam: Commands, Joins, Aggregates and Completeness Checks
1. What You Need to Know
SQL on ISC is about audit/assurance data work: pulling populations, joining tables correctly, summarizing data, and performing completeness checks (did you get all records, once, and tied to totals?). Expect questions that test whether you can:
- Use core commands (primarily
SELECT, plus basic DDL/DCL concepts) - Choose the right join and join keys to avoid missing/duplicating rows
- Use aggregates (
COUNT,SUM, etc.) withGROUP BY/HAVING - Design completeness tests: missing items, orphan records, duplicates, gaps, tie-outs (record counts and control totals)
Why it matters (ISC context): SQL is a common CAAT for testing completeness and accuracy assertions, evaluating IT-dependent controls, and supporting reconciliations (e.g., subledger-to-GL, order-to-cash, procure-to-pay).
Core idea to internalize:
Most SQL errors on exams come from (1) the wrong join type/key, (2) filters placed in
WHEREthat accidentally turn an outer join into an inner join, and (3) aggregates used without correctGROUP BYlogic.
2. Step-by-Step Breakdown
A. Building a clean audit query (population + logic)
- Define the population (FROM): Identify the “base table” that represents the full population you care about (e.g., all invoices, all POs, all GL entries).
- Select only needed fields (SELECT): Include IDs/keys, dates, amounts, status fields.
- Add filters carefully (WHERE): Put row-level filters here (date range, status). Avoid filtering joined tables in
WHEREif you need to preserve non-matches (outer join issue). - Join supporting tables (JOIN … ON):
- Join on stable keys (e.g.,
invoice_id,vendor_id)—not names/descriptions. - Validate join cardinality (1:1, 1:many) to avoid multiplication of amounts.
- Join on stable keys (e.g.,
- Summarize if needed (GROUP BY): If you use aggregates, every non-aggregated column in
SELECTmust be inGROUP BY(standard SQL rule). - Apply group filters (HAVING): Use
HAVINGfor conditions on aggregates (e.g., duplicates whereCOUNT(*) > 1). - Sanity checks: Run quick checks (counts, totals, min/max dates) and compare to control totals/reports.
B. Completeness check workflow (high-yield)
Use this sequence when asked to “test completeness” of a dataset:
- Row count tie-out: Compare
COUNT(*)to an expected number (report count, interface count, batch header, or control total). - Control total tie-out: Compare
SUM(amount)(or other key measure) to a control total (batch total, GL total, management report total). - Missing/Orphan detection: Use an outer join to find records that should have a match but don’t (e.g., shipments without invoices).
- Duplicate detection: Use
GROUP BY key HAVING COUNT(*) > 1. - Gap detection (sequence completeness): Identify missing document numbers (often via anti-join logic or window functions if supported).
- Exception output: Return the exception list (IDs) for follow-up.
C. Mini worked example (completeness via outer join)
Goal: Find shipments that have no matching invoice (possible revenue completeness issue).
SELECT s.shipment_id, s.ship_date, s.customer_id
FROM shipments s
LEFT JOIN invoices i
ON i.shipment_id = s.shipment_id
WHERE i.invoice_id IS NULL;
Key insight: LEFT JOIN keeps all shipments; WHERE i.invoice_id IS NULL isolates non-matches.
Critical: If you instead put a condition on
iin theWHEREclause (other than theIS NULLtest), you can accidentally drop the null-extended rows and lose exceptions.
3. Key Formulas, Rules & Facts
A. Core SQL command families (know the purpose)
| Category | Commands (high-yield) | What ISC cares about | Notes |
|---|---|---|---|
| DML (data manipulation) | SELECT, INSERT, UPDATE, DELETE | Evidence extraction (SELECT) and understanding risk of unauthorized changes | SELECT is the bread-and-butter for audit analytics |
| DDL (data definition) | CREATE, ALTER, DROP, TRUNCATE | Config/change risk; schema controls | TRUNCATE is typically faster and can bypass row logging/trigger behavior depending on DB |
| DCL (access control) | GRANT, REVOKE | Access controls, least privilege | ISC often frames this as control design/deficiency |
| TCL (transaction control) | COMMIT, ROLLBACK, SAVEPOINT | Data integrity, recoverability | Concepts matter even if syntax isn’t tested deeply |
B. Join types (what you get back)
| Join | Returns | Common ISC use | Exam trap |
|---|---|---|---|
| INNER JOIN | Only matches in both tables | Confirm matched transactions (e.g., invoice has vendor) | Drops non-matching rows (bad for completeness exceptions) |
| LEFT JOIN | All left rows + matches from right | Find missing right-side records | Filtering right table in WHERE can turn it into an inner join |
| RIGHT JOIN | All right rows + matches from left | Same concept; less commonly used | Many candidates misread “right” orientation |
| FULL OUTER JOIN | All rows from both + matches | Reconcile two systems/populations | Not supported in some DBs; emulate with UNION of left+right exceptions |
| CROSS JOIN | Cartesian product | Rare in audit; used for generating combinations | Easy to blow up row counts |
| SELF JOIN | Table joined to itself | Hierarchies, duplicates by “same vendor + same amount” patterns | Must alias carefully |
C. Filter order & clauses (practical rules)
| Clause | Purpose | Common use | Gotcha |
|---|---|---|---|
WHERE | Filter rows before grouping | Date ranges, statuses | Conditions on the “outer side” of an outer join can remove exception rows |
GROUP BY | Define aggregation level | Summaries by vendor/customer | Missing a column can change the grain and distort totals |
HAVING | Filter groups after aggregation | Duplicates, threshold tests | Don’t use WHERE for aggregate conditions |
ORDER BY | Sort final output | Review exception lists | Sorting doesn’t change results, just display |
D. Aggregate functions (completeness & accuracy)
| Function | What it does | When you use it in ISC | Notes |
|---|---|---|---|
COUNT(*) | Counts rows | Population counts, duplicates | Counts rows even if columns are NULL |
COUNT(col) | Counts non-NULL values | Missing field completeness | Nulls are excluded |
SUM(col) | Total | Control total tie-outs | Beware row multiplication from joins |
AVG(col) | Mean | Reasonableness checks | Outliers can mislead; consider median if available |
MIN(col), MAX(col) | Extremes | Date range completeness, outlier spotting | Great for quick sanity checks |
E. High-yield completeness checks (patterns)
| Check | What it detects | Canonical SQL pattern | Notes |
|---|---|---|---|
| Orphans / missing matches | Records with no parent/child | LEFT JOIN ... WHERE right.key IS NULL | Core completeness exception pattern |
| Duplicates | Same key repeated | GROUP BY key HAVING COUNT(*) > 1 | Use business key (invoice number + vendor) when needed |
| Missing values | NULL in required fields | WHERE col IS NULL or COUNT(col) vs COUNT(*) | NULL handling is a classic trap |
| Tie-out totals | Incomplete extraction or mapping issues | COUNT(*), SUM(amount) compare to control totals | Join grain must match (header vs line) |
| Sequence gaps | Missing document numbers | Anti-join / window function approach | Often tested conceptually; syntax varies |
4. Examples & Applications
Example 1: Subledger-to-GL completeness tie-out
Scenario: Tie AP invoice totals (subledger) to GL postings.
SELECT
COUNT(*) AS invoice_count,
SUM(i.invoice_amount) AS invoice_total
FROM ap_invoices i
WHERE i.invoice_date >= DATE '2026-01-01'
AND i.invoice_date < DATE '2027-01-01';
Then compare to GL (by account / period):
SELECT
SUM(g.amount) AS gl_total
FROM gl_entries g
WHERE g.posting_date >= DATE '2026-01-01'
AND g.posting_date < DATE '2027-01-01'
AND g.account_no = '2000';
Key insight: You’re validating completeness of posting (did subledger activity fully hit GL?) and/or completeness of extraction.
Example 2: Find invoices missing required approvals (workflow completeness)
SELECT i.invoice_id, i.vendor_id, i.invoice_amount
FROM ap_invoices i
LEFT JOIN approvals a
ON a.invoice_id = i.invoice_id
WHERE a.approval_id IS NULL
AND i.status = 'POSTED';
Key insight: Posted invoices without approvals indicate a control exception (or data/interface issue).
Example 3: Duplicate invoice detection (common fraud/error analytic)
SELECT i.vendor_id, i.invoice_number, COUNT(*) AS dup_count, SUM(i.invoice_amount) AS dup_total
FROM ap_invoices i
GROUP BY i.vendor_id, i.invoice_number
HAVING COUNT(*) > 1;
Variation: If invoice numbers can repeat across vendors, include vendor in the key (as above). If not, group by invoice number alone.
Example 4: Join grain trap (header-to-line multiplication)
Problem: You want total invoice amount, but you join header to lines and accidentally multiply header amounts.
Bad pattern:
SELECT SUM(h.invoice_amount)
FROM invoice_header h
JOIN invoice_line l
ON l.invoice_id = h.invoice_id;
If each invoice has 5 lines, header amount is counted 5 times.
Fix options:
- Sum line amounts instead:
SELECT SUM(l.line_amount)
FROM invoice_line l;
- Or aggregate lines first then join:
SELECT SUM(h.invoice_amount)
FROM invoice_header h
JOIN (
SELECT invoice_id
FROM invoice_line
GROUP BY invoice_id
) x
ON x.invoice_id = h.invoice_id;
Key insight: Always match your aggregation level to your question.
5. Common Mistakes & Traps
Filtering the outer-joined table in
WHERE- Wrong:
LEFT JOIN ... WHERE right.col = 'X' - Why wrong: Null-extended rows fail the
WHEREpredicate, so you lose the very exceptions you wanted. - Fix: Put right-table predicates in the
ONclause (or keepWHERElimited to null-test exceptions).
- Wrong:
Using
COUNT(col)when you meantCOUNT(*)(or vice versa)- Wrong: Assuming
COUNT(col)counts rows. - Why wrong:
COUNT(col)ignores NULLs. - Fix: Use
COUNT(*)for row counts; useCOUNT(col)to measure field completeness.
- Wrong: Assuming
Join key mismatch (joining on names/descriptions)
- Wrong: Joining
vendor_nametovendor_name. - Why wrong: Non-unique, spelling/case differences, name changes lead to missed/false matches.
- Fix: Join on stable surrogate/business keys (
vendor_id,invoice_id).
- Wrong: Joining
Row multiplication causing overstated totals
- Wrong: Summing header amounts after joining to line items.
- Why wrong: 1-to-many join duplicates header rows.
- Fix: Aggregate at the correct grain (sum lines, or de-duplicate/aggregate before join).
Confusing
WHEREvsHAVING- Wrong:
WHERE COUNT(*) > 1 - Why wrong:
WHEREruns before aggregation. - Fix: Use
HAVING COUNT(*) > 1.
- Wrong:
NOT INwith NULLs producing “no rows” surprises- Wrong:
WHERE key NOT IN (SELECT key FROM t2)whent2.keycan be NULL. - Why wrong: NULL in the subquery can make
NOT INevaluate unpredictably (often returning zero rows). - Fix: Prefer
NOT EXISTSor filter NULLs in the subquery.
- Wrong:
Assuming
DISTINCT“fixes” bad joins- Wrong: Slapping
SELECT DISTINCTon a query with duplicate rows. - Why wrong: It hides symptoms while totals may still be wrong and keys unclear.
- Fix: Fix join logic/grain; use
DISTINCTonly when conceptually correct.
- Wrong: Slapping
Missing date boundary logic (inclusive/exclusive errors)
- Wrong: Using
<=on end dates with timestamps. - Why wrong: You can unintentionally exclude late-day transactions.
- Fix: Use half-open intervals:
>= start_dateand< next_day/next_period.
- Wrong: Using
6. Memory Aids & Quick Tricks
| Trick / mnemonic | Helps you remember | When to use |
|---|---|---|
| “WHERE is for rows, HAVING is for groups” | Clause purpose/order | Any GROUP BY question |
| “LEFT keeps left” | Outer join direction | Orphan/missing match tests |
| “COUNT(*) counts stars, COUNT(col) counts filled cells” | NULL behavior | Field completeness vs row counts |
| “Grain before gain” | Aggregation level must match question | Any SUM/COUNT after joins |
Exception recipe: LEFT JOIN + IS NULL | Standard completeness exception pattern | Missing approvals, missing invoices, missing master data |
Prefer NOT EXISTS for anti-joins | Avoid NOT IN NULL trap | Missing records / set difference |
7. Quick Review Checklist
- You can explain and apply INNER vs LEFT (outer) joins—especially for completeness exceptions.
- You remember: filters on the outer-joined table belong in
ON, notWHERE(unless you’re doingIS NULLto find exceptions). - You can write duplicates logic:
GROUP BY ... HAVING COUNT(*) > 1. - You know when to use
COUNT(*)vsCOUNT(col)(NULL handling). - You sanity-check any audit query with counts, sums, min/max dates and tie-outs to control totals.
- You avoid join multiplication by matching grain (header vs line) before summing.
- You can produce common completeness outputs: orphans, missing required fields, duplicates, tie-out totals.
You’ve got this—if you keep joins and grain straight, most ISC SQL questions become free points.