How do you turn rows into columns in SQL?
You use conditional aggregation: for each column you want in the output, you add one aggregate expression that sums or counts only the rows belonging to that column. Each expression is a CASE that returns the value for its own bucket and something neutral for everything else.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TEXT NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
INSERT INTO orders (id, customer_id, order_date, amount) VALUES
(1, 1, '2026-01-08', 120.00),
(2, 1, '2026-02-05', 200.00),
(3, 1, '2026-03-11', 60.00),
(4, 2, '2026-01-14', 300.00),
(5, 2, '2026-02-19', 150.00),
(6, 2, '2026-03-02', 220.00);
SELECT customer_id,
ROUND(SUM(CASE WHEN strftime('%Y-%m', order_date) = '2026-01' THEN amount ELSE 0 END), 2) AS jan,
ROUND(SUM(CASE WHEN strftime('%Y-%m', order_date) = '2026-02' THEN amount ELSE 0 END), 2) AS feb,
ROUND(SUM(CASE WHEN strftime('%Y-%m', order_date) = '2026-03' THEN amount ELSE 0 END), 2) AS mar
FROM orders
GROUP BY customer_id
ORDER BY customer_id;Two rows come back. Customer 1 shows 120.00 in January, 200.00 in February and 60.00 in March. Customer 2 shows 300.00, 150.00 and 220.00. Six rows of data have become two rows with three measure columns.
Three details make it work. The CASE has an ELSE branch — using 0 for a sum or omitting the row for a count — because a CASE without ELSE returns NULL, and SUM ignores NULLs, so a missing bucket would quietly contribute nothing while still leaving a NULL in the output for a MIN or MAX. The neutral value differs by aggregate: SUM wants 0, COUNT wants to count only the matching rows, and MIN or MAX wants the NULL to be ignored. And the grouping key stays outside the CASE, so each row of the output is one customer.
The dialect note: PostgreSQL has a native PIVOT statement, and SQL Server has PIVOT, both of which take a column-to-column mapping. Neither MySQL nor SQLite has one, so conditional aggregation is the portable approach that works everywhere.