Chapter 10: Joining and Summarizing Data in SQL

Accounting Analytics: An Integrated Approach

In this chapter

Part III of V: Accounting Analytics with SQL

Chapter 2 of 4 in this Part

  • 10.1 Why Join Tables
  • 10.2 Inner Joins
  • 10.3 Following the Keys
  • 10.4 Keeping Unmatched Rows with LEFT JOIN
  • 10.5 Summarizing with Aggregate Functions
  • 10.6 Filtering Groups with HAVING
  • 10.7 Profiling a Table Before Using It
  • Guided Tutorial 10.1
  • 10.9 Fan-Out and Double Counting
  • 10.10 Subqueries
  • 10.11 The Order in Which a Query Runs
  • Guided Tutorial 10.2
  • 10.13 Measuring the Plant Against Its Standards
  • Guided Tutorial 10.3
  • 10.15 Looking Ahead

Learning objectives

  1. Join tables with INNER JOIN on their logical keys, qualifying every column, and test that the keys match before relying on the result.
  2. Use LEFT JOIN to keep rows that have no match, and find the records that have none with anti-joins and NOT EXISTS.
  3. Summarize rows with aggregate functions, GROUP BY, and HAVING, grouping by key rather than by name, use them to profile a table before relying on it, and explain the order in which a query’s clauses are evaluated.

Learning objectives (continued)

  1. Recognize fan-out when a join repeats a header’s amount on every line, and avoid the double counting it causes.
  2. Write subqueries in the SELECT, WHERE, and FROM clauses, including correlated subqueries, to answer questions that take more than one step.
  3. Build a trial balance from the general ledger, tie a summary to the ledger, and compare the hours a plant records with the hours its standards allow.

Which products, which years, volume or cost?

  • Chapter 9 found the variance recorded at each work order’s close, mostly overhead
  • Which products carry it? How much arose in each year?
  • Is it volume, fewer units to spread costs over, or the cost itself?
  • Together they need summaries and joins: product, hours, output and amounts live apart

10.1 Why Join Tables

10.2 Inner Joins

An inner join keeps the matched pairs

SELECT woc.WorkOrderCloseID, i.ItemGroup,
    woc.TotalVarianceAmount
FROM WorkOrderClose AS woc
    INNER JOIN WorkOrder AS wo
        ON wo.WorkOrderID = woc.WorkOrderID
    INNER JOIN Item AS i ON i.ItemID = wo.ItemID;
  • Aliases such as woc keep a query short; qualify every column in a join
  • Every close has one work order and every work order one item, so the rows must equal the closes

Only the work orders that have a close

Detail of: Four Charles River work orders on the left and the work order closes on the right, three of the work orders with a close and one without. The upper result, an inner join on WorkOrderID, holds three rows, one for each work order with a close. The next result, a left join from WorkOrder, holds all four work orders, and the one without a close shows NULL in the close columns. The last result, an anti-join, keeps only that work order.
Figure 10.1 · How Inner and Left Joins Match Rows (detail)

10.3 Following the Keys

Join paths through purchasing

Table 10.1 · Join paths through the purchasing, manufacturing, payroll, and ledger tables

From To Join on
PurchaseOrder Supplier SupplierID
PurchaseOrder Employee, as creator CreatedByEmployeeID = EmployeeID
PurchaseOrder Employee, as approver ApprovedByEmployeeID = EmployeeID
PurchaseOrderLine PurchaseOrder PurchaseOrderID
GoodsReceiptLine PurchaseOrderLine POLineID
PurchaseInvoiceLine PurchaseInvoice PurchaseInvoiceID
PurchaseInvoiceLine PurchaseOrderLine POLineID
PurchaseInvoiceLine GoodsReceiptLine GoodsReceiptLineID
PurchaseInvoice Supplier SupplierID
DisbursementPayment PurchaseInvoice PurchaseInvoiceID
DisbursementPayment Supplier SupplierID

Join paths through manufacturing

Table 10.1 · Join paths through the purchasing, manufacturing, payroll, and ledger tables

From To Join on
WorkOrder Item ItemID
WorkOrderOperation WorkOrder WorkOrderID
WorkOrderOperation WorkCenter WorkCenterID
WorkOrderClose WorkOrder WorkOrderID
ProductionCompletionLine ProductionCompletion ProductionCompletionID
ProductionCompletionLine Item ItemID
LaborTimeEntry WorkOrderOperation WorkOrderOperationID

Join paths through payroll and the ledger

Table 10.1 · Join paths through the purchasing, manufacturing, payroll, and ledger tables

From To Join on
TimeClockEntry, LaborTimeEntry Employee EmployeeID
PayrollRegisterLine PayrollRegister PayrollRegisterID
PayrollRegister PayrollPeriod PayrollPeriodID
PayrollRegister Employee, CostCenter EmployeeID, CostCenterID
PayrollPayment PayrollRegister PayrollRegisterID
GLEntry Account, CostCenter AccountID, CostCenterID
GLEntry A source document table SourceDocumentType and SourceDocumentID

A join to the ledger names the document type too

  • SourceDocumentType names the kind of document, usually a table, and SourceDocumentID its key
  • Each document table numbers its own rows, so one ID belongs to many kinds of document
  • So the join’s condition includes the type, such as gl.SourceDocumentType = 'JournalEntry'
  • Without it, ledger rows pair with whichever document happens to share their number

10.4 Keeping Unmatched Rows with LEFT JOIN

A left join keeps every row of the first table

Detail of: Four Charles River work orders on the left and the work order closes on the right, three of the work orders with a close and one without. The upper result, an inner join on WorkOrderID, holds three rows, one for each work order with a close. The next result, a left join from WorkOrder, holds all four work orders, and the one without a close shows NULL in the close columns. The last result, an anti-join, keeps only that work order.
Figure 10.1 · How Inner and Left Joins Match Rows (detail)

Anti-joins find what is missing; NOT IN can fail

  • An anti-join keeps the left join’s rows whose matched key IS NULL
  • NOT EXISTS asks the same thing with a correlated subquery
  • NOT IN keeps no row when its list holds a NULL, because none can be shown to be absent
  • So use NOT EXISTS or the anti-join when the column can be NULL

Watch out: WHERE can undo a left join

  • A condition on the second table in WHERE removes the unmatched rows
  • A NULL Status never equals Closed, so items without work orders vanish
  • Move such a condition into the ON clause, where it limits the matches
  • The IS NULL test of an anti-join is the exception: it stays in WHERE

Check your understanding

Which join answers each question?

  1. Every manufactured item, with its work orders if it has any
  2. The work orders that have never closed
  3. Each close with its item’s group
  4. The employees who never approved a requisition, when some requisitions have no approver

Each question has its join

  1. A left join from Item
  2. An anti-join: a left join keeping rows whose close IS NULL
  3. Inner joins from the close through the work order to the item
  4. NOT EXISTS or an anti-join, never NOT IN

10.5 Summarizing with Aggregate Functions

Aggregates and how they treat missing values

Table 10.2 · Aggregate functions and how they treat missing values

Function Returns Treatment of NULL
COUNT(*) The number of rows Counts every row
COUNT(column) The number of values in a column Skips NULLs
COUNT(DISTINCT column) The number of different values Skips NULLs
SUM(column) The total Skips NULLs
AVG(column) The average Skips NULLs, so they do not count as zero
MIN(column), MAX(column) The smallest and largest values Skip NULLs
GROUP_CONCAT(column, sep) The values joined into one text, separated by sep, or by commas if sep is left out Skips NULLs

GROUP BY returns one row per group

On the left, several Charles River work order closes of fiscal 2026 with their item group and total variance, some from Furniture and some from Lighting. Arrows gather the rows of each group into one row of the result on the right, which shows each item group with the number of closes and the sum of their variances.
Figure 10.2 · GROUP BY Collapses Rows into One Row per Group

Watch out: group by the key, not the name

  • Two different Charles River customers share one name
  • Several items share their names with other items
  • Grouping by name merges them into one row, and nothing in the result says so
  • Group by the ID, and add the name to GROUP BY if you want to show it

10.6 Filtering Groups with HAVING

WHERE filters rows; HAVING filters groups

  • WHERE runs before any group exists, so it cannot use an aggregate
  • An aggregate in WHERE stops with “misuse of aggregate”
  • HAVING runs after the aggregates, so its condition can use them
  • Filter rows in WHERE where you can, so fewer rows reach the grouping step

Check your understanding

WHERE or HAVING?

  1. Keep the closes of one year
  2. Keep the items whose total variance passes a threshold
  3. Keep the accounts whose balance is not zero
  4. Keep the ledger rows of one account

Rows in WHERE, groups in HAVING

  1. WHERE: a date is a property of each row
  2. HAVING: a total exists only after grouping
  3. HAVING: a balance is a sum of rows
  4. WHERE: an account is a property of each row

10.7 Profiling a Table Before Using It

Data quality tests in SQL

Table 10.3 · Data quality tests in SQL

Dimension SQL techniques
Accuracy MIN and MAX of amounts; a WHERE condition that compares two dates; GROUP BY the key with HAVING COUNT(*) > 1
Completeness COUNT(column) against COUNT(*); COUNT(*) by month; GROUP BY a type column
Consistency GROUP BY two related columns; an anti-join to the parent table; the distinct values of a code column
Timeliness MIN and MAX of the dates; COUNT(*) by month at each end

A profile tells you what a table holds

  • One query counts the rows, the distinct numbers, the filled columns and the dates
  • Work order numbers are unique, and every closed work order has a close date
  • No other work order has one, so the status and the date tell one story
  • A clean profile is a finding: record it with the script

Guided Tutorial 10.1: Joining the Variance to Its Work Orders and Products

Join the variance to its products in six steps

  1. Join the closes to their work orders, and test the join
  2. Add the items
  3. Total the variance of fiscal 2026 by item group, and check it against the ledger
  4. Compare the three years
  5. Keep the items that carry the most variance
  6. List every manufactured item, including those with no closes

Furniture’s variance rose as its closes fell

Detail of: A mock of the Execute SQL editor and its result. The editor, on the tab Chapter10.sql, is scrolled to a comment and a query that joins WorkOrderClose to WorkOrder and Item and groups the closes by item group and close year, with the number of closes, the total variance, and the overhead variance. The result holds nine rows, three years each for Furniture, Lighting, and Textiles. The Furniture rows, whose total variance rises in each year while the number of closes falls, are outlined.
Figure 10.3 · The Manufacturing Variance by Item Group and Year (detail)

Checkpoint

Check that:

  • the joins in Steps 1 and 2 return as many rows as the WorkOrderClose table holds;
  • the variance of fiscal 2026 by item group adds up to the ledger total of Step 3;
  • Furniture carries most of the variance, and its variance rose in each year while its number of closes fell;

Checkpoint (continued)

  • HAVING keeps only a handful of items above $50,000, spread across several product types;
  • the complete list of Step 6 keeps the items with no closes only while the date condition is in the ON clause;
  • Chapter10.sql holds the header and the queries of this tutorial, each under a comment.

10.9 Fan-Out and Double Counting

A header amount repeats on every line

A Charles River sales invoice of fiscal 2026 with three lines. Joined to its lines, the invoice's GrandTotal appears on all three rows, so summing GrandTotal over the join counts it three times, while summing the LineTotal of the lines counts each line once. A note states that across fiscal 2026 the sum of GrandTotal over the joined rows is nearly twice the true total of the invoices.
Figure 10.4 · Fan-Out: A Header Amount Repeated on Every Line

Add up each amount at its own level

  • Sum GrandTotal over SalesInvoice alone, and LineTotal over its lines
  • Closes joined to their operations repeat each variance three or four times
  • A count of a join’s rows counts lines, not headers: COUNT(DISTINCT) counts headers
  • Summarize the many side first, then join one row per header

Check your understanding

What is wrong with each result?

  1. GrandTotal summed over invoices joined to their lines
  2. The rows of that join reported as the number of invoices
  3. The variance summed over closes joined to their operations
  4. Revenue totals grouped by customer name

Fan-out and grouping errors

  1. Fan-out: each invoice’s total repeats once per line
  2. It counts lines: use COUNT(DISTINCT) on the invoice key
  3. Fan-out: each variance repeats once per operation
  4. Two customers with one name are merged: group by the key

10.10 Subqueries

Subqueries answer two-step questions in one

  • A scalar subquery returns one value, such as the year’s average variance
  • It can stand wherever a value can, including the SELECT list, side by side
  • A subquery after IN supplies a list, such as the work orders of Furniture items
  • No row: NULL. Several rows: SQLite takes the first; most systems stop

A correlated subquery runs once per outer row

  • It refers to a column of the outer query, such as each account’s ID
  • NOT EXISTS is one; a count of each account’s postings is another
  • With an index on the searched column, it is quick
  • Without one, over thousands of rows, it can take minutes, and a join is often faster

Derived tables summarize before they join

  • A subquery in FROM is a table of its own: give it an alias
  • Sum each invoice’s lines in a derived table, one row per invoice
  • Join that to the invoices, and nothing repeats
  • The same pattern combines output by year with hours by year

10.11 The Order in Which a Query Runs

Written in one order, evaluated in another

Two columns. The written order lists SELECT with DISTINCT, FROM and JOIN, WHERE, GROUP BY, HAVING, ORDER BY, and LIMIT, each with a numbered badge giving the step at which it is evaluated. The evaluated order lists the same clauses by step: 1 FROM and JOIN, 2 WHERE, 3 GROUP BY, 4 HAVING, 5 SELECT and DISTINCT, 6 ORDER BY, and 7 LIMIT, with a note beside each step on what it does.
Figure 10.5 · The Order in Which a Query Is Written and Evaluated

The order explains the rules

  • WHERE runs before grouping, so it cannot use an aggregate; HAVING can
  • ORDER BY runs after SELECT, so it can sort by an alias
  • By the same logic, GROUP BY should not use an alias, but SQLite allows it
  • PostgreSQL and MySQL also allow it; SQL Server does not

Guided Tutorial 10.2: Building the Trial Balance and Following the Conversion Cost

Follow the cost through the ledger in six steps

  1. Put the closes and the ledger side by side
  2. Build the pre-closing trial balance
  3. Check that debits equal credits
  4. Follow account 1090 by year and source
  5. Split the conversion variance into labor and overhead
  6. Identify the journal entries

The ledger rebuilds the pre-closing trial balance

Detail of: A mock of the Execute SQL editor and its result. The editor, on the tab Chapter10.sql, is scrolled to a comment and a query that joins GLEntry to Account, keeps the postings through 2026-12-31 without the two closing entries of fiscal 2026, and groups them by account. The first rows of the result show account numbers, names, types, and balances, positive for debit balances and negative for credit balances. The message pane reports the number of accounts returned.
Figure 10.6 · The Pre-Closing Trial Balance of Fiscal 2026 (detail)

Conversion cost flows through the clearing account

Detail of: A flow diagram of account 1090 Manufacturing Cost Clearing in fiscal 2026. On the left, the manufacturing payroll, factory overhead, and depreciation of manufacturing equipment flow into the account as debits. On the right, the standard conversion cost of the units completed flows out to finished goods inventory, and the conversion variance flows out through work order closes to account 5080 Manufacturing Variance. The payroll is the largest inflow, and the amount sent to the variance account is a large part of the total. A note gives the year's small net change in the account. Below, a table gives the same postings for fiscal 2024, 2025, and 2026, with the variance split into its direct labor and overhead parts: the payroll rises in each year, the standard cost released stays nearly flat, the favorable labor part shrinks, and the highlighted overhead part grows.
Figure 10.7 · The Flow of Conversion Cost Through Account 1090 (detail)

The overhead part grew while the labor part shrank

Detail of: A flow diagram of account 1090 Manufacturing Cost Clearing in fiscal 2026. On the left, the manufacturing payroll, factory overhead, and depreciation of manufacturing equipment flow into the account as debits. On the right, the standard conversion cost of the units completed flows out to finished goods inventory, and the conversion variance flows out through work order closes to account 5080 Manufacturing Variance. The payroll is the largest inflow, and the amount sent to the variance account is a large part of the total. A note gives the year's small net change in the account. Below, a table gives the same postings for fiscal 2024, 2025, and 2026, with the variance split into its direct labor and overhead parts: the payroll rises in each year, the standard cost released stays nearly flat, the favorable labor part shrinks, and the highlighted overhead part grows.
Figure 10.7 · The Flow of Conversion Cost Through Account 1090 (detail)

Checkpoint

Check that:

  • the scalar subqueries of Step 1 return the same total for the closes and the ledger;
  • the trial balance returns the same accounts and balances as Chapter 6’s, and the net of all its balances is zero;
  • account 1090 receives payroll and journal entries and releases the standard cost of completions;

Checkpoint (continued)

  • the payroll charged to production rose in each year, while the standard cost released stayed nearly flat;
  • the labor part of the variance was favorable and shrank, while the overhead part grew;
  • Chapter10.sql holds this tutorial’s queries under their comments.

10.13 Measuring the Plant Against Its Standards

Standard hours allowed measure direct labor

  • Compare the inputs used with the inputs the standards allow for the output achieved
  • Standard hours allowed: units completed times each unit’s standard labor hours
  • Compare them with the direct labor time recorded
  • It is Chapter 7’s flexible budget applied to hours

Indirect time is overhead

  • Indirect labor is time no single work order carries
  • In Charles River’s records it reaches the variance through overhead
  • Fixed overhead is absorbed only by units made: output below plan leaves some unabsorbed
  • LaborType separates direct, indirect and non-manufacturing time

The Payroll and Time tables

An entity-relationship diagram of the Payroll and Time tables. TimeClockEntry records each employee's day, with clock times and regular and overtime hours, and links to the OvertimeApproval that supports its overtime. LaborTimeEntry splits the day's time by labor type and links to its time clock entry and, for direct labor, to a work order operation from the Manufacturing group. PayrollPeriod groups the PayrollRegister rows; each register has its lines and, once paid, a PayrollPayment, and names its employee and cost center. Employee, CostCenter, and WorkOrderOperation, from other table groups, have gray headers and connect with heavier amber lines.
Figure 10.8 · The Payroll and Time Tables

Guided Tutorial 10.3: Comparing Output with the Hours It Took

Compare output with the hours in eight steps

  1. Measure the output and its standard hours
  2. Measure the hours and pay of the manufacturing payroll
  3. Count the people
  4. Profile the two tables before comparing them
  5. Find why the time records end early
  6. Compare the hours recorded with the standard hours, by kind of labor
  7. Test the time that has no work order operation
  8. Complete and save the script

Two derived tables meet on the year

Detail of: A mock of the Execute SQL editor and its result. The editor, on the tab Chapter10.sql, is scrolled to a comment and a query that joins two derived tables on the year: one with the hours of labor time by year and labor type, without NonManufacturing time, and one with the standard labor hours of the units completed. The result has six rows, Direct Manufacturing and Indirect Manufacturing for 2024, 2025, and 2026. The HoursPerStandardHour column, outlined, stays below one for direct labor in every year and rises for indirect labor.
Figure 10.9 · Hours per Standard Hour by Labor Type and Year (detail)

Direct time stayed below the standard hours

Detail of: A mock of the Execute SQL editor and its result. The editor, on the tab Chapter10.sql, is scrolled to a comment and a query that joins two derived tables on the year: one with the hours of labor time by year and labor type, without NonManufacturing time, and one with the standard labor hours of the units completed. The result has six rows, Direct Manufacturing and Indirect Manufacturing for 2024, 2025, and 2026. The HoursPerStandardHour column, outlined, stays below one for direct labor in every year and rises for indirect labor.
Figure 10.9 · Hours per Standard Hour by Labor Type and Year (detail)

Checkpoint

Check that:

  • output was nearly flat, with a dip in fiscal 2025, and its standard hours moved with it;
  • the payroll paid the same number of people in each year, with overtime hours more than doubling;
  • COUNT(*) counts registers and COUNT(DISTINCT pr.EmployeeID) counts people;
  • the profiles show that the time records end on December 11, 2026, so the output is counted only up to that day, although indirect time ends a week earlier;

Checkpoint (continued)

  • the direct hours recorded stayed below the standard hours in every year, while indirect time grew;
  • a few direct labor entries have no valid operation;
  • Chapter10.sql is saved with a completed header and a comment above each query.

10.15 Looking Ahead

Back to the controller’s questions

  • Products: the variance rises where the plant makes most, while closes fall
  • Where: conversion cost the standard did not absorb, mostly overhead
  • Volume: output was about the same, so the records point to cost
  • But indirect time grew by about two-thirds and overtime more than doubled

Summary

  • Joins follow logical keys, tested by their keys, row counts and totals
  • Left joins keep whole populations; anti-joins and NOT EXISTS find what is missing
  • Aggregates with GROUP BY and HAVING summarize by key, after profiling the table
  • Fan-out repeats header amounts: sum each amount at its own level
  • Subqueries and derived tables answer questions that take more than one step

Key terms

Aggregate function · Anti-join · Correlated subquery · Data profile · Derived table · Fan-out · GROUP BY clause · HAVING · Inner join · Join · Left outer join · Logical evaluation order · NOT EXISTS · Production-volume variance · Scalar subquery · Standard hours allowed · Subquery · Table alias · Trial balance

Practice and next steps

  • Financial Accounting
    • Exercise 10.1: Reconciling Invoice Lines to the Ledger in SQL
    • Exercise 10.2: Tracing a Sale from the Ledger to Its Documents
  • Managerial Accounting
    • Exercise 10.3: Average Order Value by Customer
    • Exercise 10.4: Purchase Price Variance by Supplier
  • Auditing
    • Exercise 10.5: Testing Referential Integrity with Anti-Joins
    • Exercise 10.6: Suppliers That Share an Employee’s Address

Next: Chapter 11: Analytical Queries in SQL

About these slides

These slides accompany Accounting Analytics: An Integrated Approach, licensed under CC BY-SA 4.0. Every figure and table comes from the chapter:

https://aa.accountinganalyticshub.com/chapters/10-joining-and-summarizing/chapter.html