P15 · SQL & Querying
SQL pivot and unpivot — reshaping without loss
How SQL pivot and unpivot reshape data — and the trap where folding columns into rows without a label silently collapses two cells into one.
Start with the basics
Reshaping moves data between wide and long: wide has one column per measure (a column each for act 1 and act 2), long has one row per measure with a label saying which.
Pivot turns rows into columns; unpivot turns columns into rows. You unpivot by stacking each column as its own labelled row, and pivot with a conditional sum per target column.
It reads like rearranging a spreadsheet, and mostly it is. The one rule that keeps it honest: every cell must carry a label so nothing collapses.
One that works
| name | math | eng |
|---|---|---|
| Ada | 90 | 80 |
SELECT name, 'math' AS subject, math AS score FROM scores
UNION ALL
SELECT name, 'eng', eng FROM scores
ORDER BY subject;| name | subject | score |
|---|---|---|
| Ada | eng | 80 |
| Ada | math | 90 |
Each column becomes its own labelled row — the subject label keeps the two cells distinct.
Pivot turns rows into columns and unpivot turns columns into rows — and it's only lossless if every cell keeps a label.
When to use it
- Reach for a pivot to turn categories into columns (a column per month), and unpivot to turn columns back into labelled rows.
- Keep a label column for every measure so no two cells merge.
- For a compute-across-columns task, unpivot, transform, then re-pivot.
That is exactly where reshaping loses data. Fold columns into rows without keeping a label for each, and two cells that happen to share a value collapse into one. Here is the day that bites.
See it happen
The unpivot that swallows a cell — UNION without a label
A puppet build has two acts, both 8 minutes. You unpivot the two minute columns into rows. Watch UNION merge the two identical 8s into one.
| puppet | act1_mins | act2_mins |
|---|---|---|
| alpha | 8 | 8 |
| beta | 5 | 9 |
UNION, no label — merges the twins
SELECT puppet, act1_mins AS mins FROM builds
UNION
SELECT puppet, act2_mins FROM builds
ORDER BY puppet, mins;| puppet | mins |
|---|---|
| alpha | 8 |
| beta | 5 |
| beta | 9 |
UNION ALL + label — keeps every cell
SELECT puppet, 'act1' AS act, act1_mins AS mins FROM builds
UNION ALL
SELECT puppet, 'act2', act2_mins FROM builds
ORDER BY puppet, act;| puppet | act | mins |
|---|---|---|
| alpha | act1 | 8 |
| alpha | act2 | 8 |
| beta | act1 | 5 |
| beta | act2 | 9 |
The unpivot used UNION and dropped the act label, so alpha's two identical 8-minute acts collapsed into one row — UNION ALL with a label keeps every cell as its own row.
Walk the full example in the unit →The rest of the family
More ways reshaping loses data
A portable pivot is a sum of a CASE per target column; dedicated PIVOT syntax is just a shorthand over the same idea.
SQL can't take column names as parameters, so a truly dynamic pivot needs generated SQL or the application layer.
Going wide to long and back is safe only when each cell keeps a label; unpivot, transform, re-pivot is the honest path.
Self-check
How to spot it in your own SQL
- After unpivoting, the row count is lower than columns times rows.
- You stacked columns with UNION instead of UNION ALL.
- You need a pivot with a variable number of columns.
- A wide-to-long-to-wide round-trip lost some values.
What people miss
Reshaping looks like moving cells around, but the trap is quiet data loss — a fold that drops a label, a round-trip that loses the grain. The shape looks right; a row just went missing.
Go deeper
Ask Colearn
Instant answers, grounded in the same verified material the diagnostic grades against.
Why did my unpivot lose rows?
You likely stacked the columns with UNION, which removes duplicates — so two cells with the same value merged into one row.
Use UNION ALL and add a label column naming which source column each value came from.
from the unit →How do I pivot rows into columns portably?
Use a sum of a CASE expression per target column — one aggregate per column you want to create.
Dedicated PIVOT syntax is just a convenience over the same conditional-aggregation idea.
from the unit →Can I pivot on values I don't know ahead of time?
Not in plain SQL — column names can't be parameters, so the set of output columns must be fixed in the query text.
A truly dynamic pivot needs generated SQL or logic in the application layer.
from the unit →Is going wide to long and back safe?
Only if each cell keeps a label through the trip — otherwise the grain is lost and cells can merge.
Unpivot, transform, then re-pivot, keeping the label the whole way.
from the unit →That’s the free taste