Knowledge Guide
HomeDatabasesSQL Practice Problems

medium Retail Expansion Analysis

The whole query is a set-intersection done with two GROUP BY rollups: one pass groups rows by rev_2020 and keeps the groups of size > 1 (revenue shared with someone), a second pass groups rows by the coordinate pair and keeps the groups of size = 1 (location belongs to exactly one store), and a store survives only if it is in both result sets — so you join the base table to both and SUM the survivors' rev_2021.

Problem

Table StorePerformance(store_id PK, rev_2020 float, rev_2021 float, lat float, lon float). Return the total rev_2021 across stores that (a) share their rev_2020 with at least one other store, and (b) sit at a coordinate pair no other store occupies.

store_id | rev_2020 | rev_2021 | lat  | lon
   1     |   100    |   150    | 10.0 | 10.0
   2     |   200    |   250    | 20.0 | 20.0
   3     |   100    |   300    | 20.0 | 20.0
   4     |   100    |   400    | 40.0 | 40.0

Expected output: a single value 550.00.

The two conditions are independent filters

Notice the trap in the data: rev_2020 = 100 is shared by three stores (1, 3, 4), so all three pass the revenue test. But store 3 sits at (20.0, 20.0) — the same pair as store 2 — so it fails the location test. Each condition slices a different axis; only stores that survive both count. The diagram traces which stores fall out at each gate.

diagram
diagram

The query

Each gate is its own GROUP BY … HAVING derived table; the base table is inner-joined to both, which keeps only the rows present in both survivor sets, then summed.

SELECT FORMAT(SUM(sp.rev_2021), 2) AS rev_2021
FROM StorePerformance sp
JOIN (                              -- Gate A: revenue shared with another store
    SELECT rev_2020
    FROM StorePerformance
    GROUP BY rev_2020
    HAVING COUNT(*) > 1
) dup_rev   ON sp.rev_2020 = dup_rev.rev_2020
JOIN (                              -- Gate B: this store owns its coordinate alone
    SELECT lat, lon
    FROM StorePerformance
    GROUP BY lat, lon
    HAVING COUNT(*) = 1
) uniq_loc  ON sp.lat = uniq_loc.lat AND sp.lon = uniq_loc.lon;

Step-by-step trace

store_idrev_2020(lat, lon)in dup_rev?in uniq_loc?kept?rev_2021
1100(10.0, 10.0)yes (100×3)yes (1 store)yes150
2200(20.0, 20.0)no (200×1)no (2&3 here)no
3100(20.0, 20.0)yes (100×3)no (2&3 here)no
4100(40.0, 40.0)yes (100×3)yes (1 store)yes400

Survivors are stores 1 and 4. SUM(rev_2021) = 150 + 400 = 550, and FORMAT(550, 2) renders the string "550.00".

Watch store 3 specifically: it passes Gate A on revenue but is killed by Gate B because its coordinate group (20.0, 20.0) has COUNT(*) = 2. That row is exactly why the two filters cannot be collapsed into one condition.

Pitfalls

1. FORMAT returns a string, not a number

FORMAT(x, 2) in MySQL produces a locale-formatted string like "550.00" — and for large values it inserts grouping commas ("1,234,567.00"). The instant you wrap a number in FORMAT you have left numeric land. Consequences that bite in real reports:

2. Float equality is the real trap in this problem

Both gates hinge on equality of float columns: GROUP BY rev_2020, sp.rev_2020 = dup_rev.rev_2020, and sp.lat = uniq_loc.lat. FLOAT/DOUBLE are binary IEEE-754 — they cannot represent most decimal fractions exactly. Two stores you think have the same revenue may differ in the last bit, so GROUP BY places them in separate groups and HAVING COUNT(*) > 1 never fires. The toy data uses clean integers stored as floats (100, 10.0) which are exact, so the bug stays hidden here — but on real data rev_2020 = 19.99 entered from two sources can compare unequal. Group/equate on an exact type: store money as DECIMAL(p,s), or compare with a tolerance (ABS(a - b) < 1e-9) when you are stuck with floats. Coordinates have the same hazard, made worse by rounding at different precisions on ingest.

3. Same-store self-match and NULLs

Gate A's COUNT(*) > 1 counts rows, which is what you want — but if you ever rewrite it as a self-join (a.rev_2020 = b.rev_2020 AND a.store_id <> b.store_id) remember to exclude the row matching itself. Also, GROUP BY collapses all NULL coordinates into one group, so a pile of stores with unknown lat/lon would look like a single shared location and all fail Gate B; filter or treat NULLs deliberately if they exist.

Takeaways


Re-authored and deepened for this guide. Mechanism (GROUP BY/HAVING as set membership, then join-as-intersection), the float-equality and FORMAT-returns-a-string traps, NULL grouping, and the IEEE-754 reasoning draw on the MySQL Reference Manual (sections on FORMAT(), the FLOAT/DECIMAL types, and "Problems with Floating-Point Values"), the SQL standard's treatment of GROUP BY/HAVING, and the LeetCode-style "Retail Expansion Analysis" problem this lesson is built on. Worked example verified: stores 1 + 4 = 150 + 400 = 550.00.

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

Stuck on Retail Expansion Analysis? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🪜 Hint ladder (no spoilers)

Progressively stronger hints — you still solve it.

I'm working on the problem **Retail Expansion Analysis** (Databases). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🎨 Explain the approach visually

See the technique, not just code.

Explain the optimal approach to **Retail Expansion Analysis** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔍 Review my solution

Catch bugs, edge cases, sub-optimality.

I'll paste my solution to **Retail Expansion Analysis**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔁 Drill the pattern

Lock in recognition with look-alikes.

Give me 2 problems that use the SAME underlying pattern as **Retail Expansion Analysis**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes