How can window functions be used to reshape rows into columns?
The combination that does it is a window function to compute the value, then conditional aggregation to spread it across columns. The window function gives every row its group's value; the CASE expression inside an aggregate decides which column each row's value belongs in.
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
placed_on TEXT NOT NULL
);
INSERT INTO orders (id, customer_id, amount, placed_on) VALUES
(1, 1, 120.00, '2026-01-05'),
(2, 1, 340.50, '2026-01-20'),
(3, 2, 75.25, '2026-02-03'),
(4, 1, 90.00, '2026-02-14');
SELECT id,
customer_id,
amount,
MAX(amount) OVER (PARTITION BY customer_id) AS largest_order,
MIN(amount) OVER (PARTITION BY customer_id) AS smallest_order,
FIRST_VALUE(amount) OVER (
PARTITION BY customer_id
ORDER BY amount DESC
) AS top_order
FROM orders
ORDER BY customer_id, id;Four rows come back. Ada's three orders each carry her largest order of 340.50 and her smallest of 90.00, and every one of them carries 340.50 as the top order. Grace's single order carries 75.25 for all three.
Each of the three window functions does the same job by a different route. MAX and MIN over the partition compute the extremes directly. FIRST_VALUE with an ORDER BY inside the window takes the value from the first row of the ordered partition, which is the largest order because the ordering is by amount descending. That one is the most useful of the three, because it can pull any field from the leading row of a group — the customer's name, the first order date, the latest status.
Two things to keep in mind. The partition determines the row's group, and it is entirely independent of the ORDER BY on the final output — so you can sort the report however you like without disturbing the numbers. And the window functions do not reduce the row count, so this is an annotation, not a reshape. To actually reshape into fewer, wider rows, you need the conditional aggregation in the next answer.