CMD Guide
HomeDatabasesSQL Practice Problems

Dynamic Pivoting of a Table

Because SQL fixes its column list at parse time, you cannot turn unknown row values (store names) into columns in a single static query — so you run two queries: the first reads the distinct store names and assembles a second query as a string, then PREPARE/EXECUTE compiles and runs that string. The pivot is metaprogramming: SQL writing SQL.

Problem

Table Products has columns product_id INT, store VARCHAR, price INT, with (product_id, store) as the primary key — so each product appears at most once per store. There are at most 30 distinct stores. Write a procedure PivotProducts that returns one row per product, with one column per store (the price there, or NULL if not sold), and the store columns sorted lexicographically.

The trap: you do not know the store names — or how many there are — until you query the data. A normal SELECT needs its columns spelled out in the source text, so the column list has to be generated before the pivot query is parsed.

The two-phase mechanism on real data

Take this concrete input (the same three products as the expected output):

Products
+------------+----------+-------+
| product_id | store    | price |
+------------+----------+-------+
| 1          | LC_Store | 100   |
| 1          | Shop     | 110   |
| 2          | Nozama   | 200   |
| 2          | Souq     | 190   |
| 3          | Shop     | 1000  |
| 3          | Souq     | 1900  |
+------------+----------+-------+

Phase 1 — read the schema-to-be. The distinct stores are LC_Store, Nozama, Shop, Souq. For each one, CONCAT stamps out a fragment of column SQL, and GROUP_CONCAT(... ORDER BY store ASC) glues the fragments into a single comma-separated string, alphabetised:

  1. LC_StoreSUM(IF(store = "LC_Store", price, null)) AS LC_Store
  2. NozamaSUM(IF(store = "Nozama", price, null)) AS Nozama
  3. ShopSUM(IF(store = "Shop", price, null)) AS Shop
  4. SouqSUM(IF(store = "Souq", price, null)) AS Souq

That string is stored in @sql. Phase 2 wraps it with SELECT product_id, … FROM Products GROUP BY product_id and runs it. Now trace what the executed query does to product_id = 1: GROUP BY product_id collapses its two rows (LC_Store,100 and Shop,110) into one group, and each SUM(IF(...)) scans that group:

group product_id=1 = { (LC_Store,100), (Shop,110) }

SUM(IF(store="LC_Store", price, null))  = SUM(100, null) = 100
SUM(IF(store="Nozama",   price, null))  = SUM(null, null) = NULL
SUM(IF(store="Shop",     price, null))  = SUM(null, 110)  = 110
SUM(IF(store="Souq",     price, null))  = SUM(null, null) = NULL
  ->  | 1 | 100 | NULL | 110 | NULL |

The IF turns every row into either its price (matching store) or NULL (everything else); inside one product group at most one row matches a given store column, so SUM is just picking that single non-null value. SUM also ignores NULLs, so a store where the product is absent yields NULL, exactly as required.

diagram
diagram

The procedure

CREATE PROCEDURE PivotProducts()
BEGIN
    -- Default GROUP_CONCAT_MAX_LEN is only 1024 bytes; the
    -- generated query is far longer, so raise the cap first.
    SET group_concat_max_len = 1000000;

    SET @sql = NULL;

    -- PHASE 1: build the column list as a string
    SELECT GROUP_CONCAT(DISTINCT
             CONCAT('SUM(IF(store = "', store,
                    '", price, null)) AS ', store)
             ORDER BY store ASC)
    INTO @sql
    FROM Products;

    -- Wrap it into a full statement
    SET @sql = CONCAT('SELECT product_id, ', @sql,
                      ' FROM Products GROUP BY product_id');

    -- PHASE 2: compile and run the generated string
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END

For this input, the string in @sql after the wrap is exactly:

SELECT product_id,
  SUM(IF(store = "LC_Store", price, null)) AS LC_Store,
  SUM(IF(store = "Nozama",   price, null)) AS Nozama,
  SUM(IF(store = "Shop",     price, null)) AS Shop,
  SUM(IF(store = "Souq",     price, null)) AS Souq
FROM Products
GROUP BY product_id

Output

+------------+----------+--------+------+------+
| product_id | LC_Store | Nozama | Shop | Souq |
+------------+----------+--------+------+------+
| 1          | 100      | NULL   | 110  | NULL |
| 2          | NULL     | 200    | NULL | 190  |
| 3          | NULL     | NULL   | 1000 | 1900 |
+------------+----------+--------+------+------+

Product 1 sells at LC_Store (100) and Shop (110); product 2 at Nozama (200) and Souq (190); product 3 at Shop (1000) and Souq (1900). Every other cell is NULL because that product is absent from that store — which is precisely what SUM over an all-null group returns.

Why the naive version is wrong

The instinct is to write the pivot directly: SELECT product_id, SUM(IF(store="Shop",price,null)) AS Shop, ... FROM Products GROUP BY product_id. That works only if you hard-code every store name into the query text — but the spec allows any of up to 30 unknown stores, and the column list is frozen when MySQL parses the query. There is no syntax for “one column per distinct value of store” in a static statement; the value list must be discovered by a query and re-emitted as new query text. That is the entire reason the dynamic-SQL machinery (GROUP_CONCATCONCATPREPARE) exists here.

A second naive slip: replacing SUM(IF(...)) with MAX(IF(...)) looks equivalent here, and it is — only because (product_id, store) is unique so each group has exactly one candidate. If that uniqueness did not hold, SUM would total all matching prices while MAX would keep the largest; they are different aggregations that happen to coincide under the key constraint.

Pitfalls

Takeaways


Sources: MySQL 8.0 Reference Manual — GROUP_CONCAT() and group_concat_max_len, IF()/aggregate-function semantics, and the PREPARE/EXECUTE/DEALLOCATE PREPARE prepared-statement chapter; LeetCode 1543/3061 “Dynamic Pivoting of a Table” problem statement and editorial discussion. Re-authored and deepened for this guide: added a value-traced GROUP BY walkthrough and mechanism diagram, a “why the naive version is wrong” note (static column binding; SUM vs MAX under the unique-key constraint), and concrete failure modes (truncation, ANSI_QUOTES, injection, NULL @sql, column-count limits). Corrected a factual error in the prior version, whose “Interpretation” described a non-existent product_id = 4 (Shop 200 / Souq 300) that never appears in the three-row output.

🤖 Don't fully get this? Learn it with Claude

Stuck on Dynamic Pivoting of a Table? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Dynamic Pivoting of a Table** (Databases) and want to truly understand it. Explain Dynamic Pivoting of a Table from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Dynamic Pivoting of a Table** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Dynamic Pivoting of a Table** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Dynamic Pivoting of a Table** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes