Recipe Tableau and SQL
Why does my percent of total change when I filter?
SUM([x]) / TOTAL(SUM([x])) divides by whatever is left after the filter, so the visible rows always add up to 100%. To divide by the whole dataset, use MIN({ FIXED : SUM([x]) }) in Tableau, or compute SUM(x) OVER () in a CTE before the WHERE in SQL. Filter this library system to the East Side and Riverside reads 47% of checkouts, more than the main Central library's real 32%, though Riverside is only 13.5% of the city.
The data
Ashford Public Library, checkouts by branch, month and format, 2025
A city library system with eight branches in three areas. The big Central library on the West Side does about a third of all checkouts; Riverside leads the East Side.
month | branch | area | format | checkouts |
|---|---|---|---|---|
| 2025-01-01 | Central | West Side | 6293 | |
| 2025-01-01 | Central | West Side | E-book | 2155 |
| 2025-01-01 | Central | West Side | Audiobook | 1192 |
| 2025-01-01 | Harbor View | West Side | 1776 |
Why does a branch jump from 13.5% to 47%?
Because the denominator changed, not the branch. Filter to the East Side and the formula divides Riverside by the East Side's checkouts, not the city's. Turn the filter and watch every bar grow.
Step 2
Filter to one area and watch TOTAL() move
Same formula, new denominator. The filter removes rows first, then TOTAL() adds up what is left, so the visible bars sum to 100% again. Every bar grows, and a mid-sized branch can look like the biggest in the city.
Tableau
SUM([checkouts]) / TOTAL(SUM([checkouts]))
// Filters: area = {knob.area}SQL
WITH b AS (
SELECT branch, SUM(checkouts) AS checkouts
FROM input
WHERE ('{knob.area}' = 'All areas' OR area = '{knob.area}')
GROUP BY branch
)
SELECT
branch,
checkouts,
checkouts / SUM(checkouts) OVER () AS share_of_visible
FROM b
ORDER BY checkouts DESCRows
| Branch | Checkouts | Share of visible |
|---|---|---|
| Riverside | 50,978 | 47% |
| Elm Park | 29,301 | 27% |
| Foundry | 28,227 | 26% |
Common mistake
Calling the share of the visible rows 'share of total'
Filtered to the East Side, Riverside reads 47% under a 'share of total' title, more than Central's real 32%. Riverside is 13.5% of the city. If the title says total, the denominator has to be the total.
Tableau
SUM([checkouts]) / TOTAL(SUM([checkouts]))
// titled 'Share of all checkouts', area filter onSQL
WITH b AS (
SELECT branch, SUM(checkouts) AS checkouts
FROM input
WHERE area = 'East Side'
GROUP BY branch
)
SELECT
branch,
checkouts / SUM(checkouts) OVER () AS share_of_total
FROM b
ORDER BY share_of_total DESCTableau
SUM([checkouts]) / MIN({ FIXED : SUM([checkouts]) })SQL
WITH b AS (
SELECT branch, area, SUM(checkouts) AS checkouts
FROM input
GROUP BY branch, area
), s AS (
SELECT
branch,
area,
checkouts / SUM(checkouts) OVER () AS share_of_total
FROM b
)
SELECT branch, share_of_total
FROM s
WHERE area = 'East Side'
ORDER BY share_of_total DESCHow do I keep the denominator fixed?
Compute the total before the filter runs. In Tableau that is a FIXED expression; in SQL, a window in a CTE with the WHERE outside it.
Step 3
Pin the denominator to the whole city
{ FIXED : SUM([checkouts]) } is computed before ordinary filters, so it stays the city total. MIN() turns the repeated per-row value into one number per mark. In SQL the window runs in the CTE over all rows and the WHERE comes after it. Now the bars no longer sum to 100%, and that is the point.
Tableau
SUM([checkouts]) / MIN({ FIXED : SUM([checkouts]) })
// Filters: area = {knob.area}SQL
WITH b AS (
SELECT branch, area, SUM(checkouts) AS checkouts
FROM input
GROUP BY branch, area
), s AS (
SELECT
branch,
area,
checkouts,
checkouts / SUM(checkouts) OVER () AS share_of_city
FROM b
)
SELECT branch, checkouts, share_of_city
FROM s
WHERE ('{knob.area}' = 'All areas' OR area = '{knob.area}')
ORDER BY checkouts DESCRows
| Branch | Checkouts | Share of city |
|---|---|---|
| Riverside | 50,978 | 13.5% |
| Elm Park | 29,301 | 7.7% |
| Foundry | 28,227 | 7.5% |
Step 4
Show both, side by side
Both numbers are correct; they answer different questions. The gap between the bars is the part of the city the filter hid. Pick All areas and the two bars match.
Tableau
Share of visible: SUM([checkouts]) / TOTAL(SUM([checkouts]))
// Share of city: SUM([checkouts]) / MIN({ FIXED : SUM([checkouts]) })SQL
WITH b AS (
SELECT branch, area, SUM(checkouts) AS checkouts
FROM input
GROUP BY branch, area
), s AS (
SELECT
branch,
area,
checkouts / SUM(checkouts) OVER () AS share_of_city
FROM b
), f AS (
SELECT
branch,
share_of_city,
share_of_city / SUM(share_of_city) OVER () AS share_of_visible
FROM s
WHERE ('{knob.area}' = 'All areas' OR area = '{knob.area}')
)
SELECT
branch,
'Share of visible branches' AS denominator,
share_of_visible AS share
FROM f
UNION ALL
SELECT branch, 'Share of all branches', share_of_city
FROM f
ORDER BY branch, denominatorRows
| Branch | Denominator | Share |
|---|---|---|
| Elm Park | Share of all branches | 7.7% |
| Elm Park | Share of visible branches | 27% |
| Foundry | Share of all branches | 7.5% |
| Foundry | Share of visible branches | 26% |
| Riverside | Share of all branches | 13.5% |
| Riverside | Share of visible branches | 47% |