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

scores
namematheng
Ada9080
SELECT name, 'math' AS subject, math AS score FROM scores
UNION ALL
SELECT name, 'eng', eng FROM scores
ORDER BY subject;
result
namesubjectscore
Adaeng80
Adamath90

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.

builds
puppetact1_minsact2_mins
alpha88
beta59

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;
result
puppetmins
alpha8
beta5
beta9

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;
result
puppetactmins
alphaact18
alphaact28
betaact15
betaact29

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

Pivot with conditional sumsopen the unit →

A portable pivot is a sum of a CASE per target column; dedicated PIVOT syntax is just a shorthand over the same idea.

You can't parameterize column namesopen the unit →

SQL can't take column names as parameters, so a truly dynamic pivot needs generated SQL or the application layer.

Reshaping is lossless only if the grain survivesopen the unit →

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

3 of 3 free questions left

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 →
The P15 pattern pageConditional aggregation with CASEAggregation and GROUP BY