CMD Guide
HomeDatabasesSQL Practice Problems

Product Price on a Specific Date

A product's price is whatever the most recent change said it was as of the cut-off date — so the whole problem reduces to: for each product, find the row with the maximum effective_date <= '2019-08-16' and read its new_price, falling back to 10 when no such row exists.

Problem

Table PriceChanges(product_id, new_price, effective_date); (product_id, effective_date) is the primary key. Each row says "on this date the price became new_price." Report every product's price on 2019-08-16. A product with no change on or before that date is assumed to cost 10. Order by product_id.

Input

PriceChanges
+------------+-----------+----------------+
| product_id | new_price | effective_date |
+------------+-----------+----------------+
|     1      |    20     |   2019-08-14   |
|     2      |    50     |   2019-08-14   |
|     1      |    30     |   2019-08-15   |
|     1      |    35     |   2019-08-16   |
|     2      |    65     |   2019-08-17   |   <- after cutoff
|     3      |    20     |   2019-08-18   |   <- after cutoff
+------------+-----------+----------------+

Expected output

+------------+-------+
| product_id | price |
+------------+-------+
|     1      |  35   |   latest change on/before 08-16
|     2      |  50   |   08-17 change is in the future
|     3      |  10   |   no change on/before 08-16 -> default
+------------+-------+

The mechanism: "latest row per group" + a default

Two sub-jobs are hiding here, and seeing them separately is the whole insight:

  1. Driving set. The answer has one row per product that exists — not per price change. Product 3's only change (08-18) is filtered out by the cut-off, yet product 3 must still appear with price 10. So you cannot derive the row set from the rows that survive the date filter; you need the full universe of products first. That is why SELECT DISTINCT product_id is the outer driver, not the filtered table.
  2. Pick the winning row. Within each product's history, keep only changes with effective_date <= '2019-08-16', then take the one with the largest date. COALESCE(..., 10) patches the hole when that set is empty.
diagram
diagram

Solution A — correlated subquery + COALESCE

This is the most direct transcription of the mechanism: drive off the distinct products, and for each one run a tiny "latest price" lookup.

SELECT p.product_id,
       COALESCE(
         (SELECT pc.new_price
          FROM PriceChanges pc
          WHERE pc.product_id = p.product_id
            AND pc.effective_date <= '2019-08-16'
          ORDER BY pc.effective_date DESC
          LIMIT 1),
         10
       ) AS price
FROM (SELECT DISTINCT product_id FROM PriceChanges) p
ORDER BY p.product_id;

It is correct, but be honest about its cost. The inner SELECT ... ORDER BY ... LIMIT 1 is correlated — it re-runs once per driving row. With N distinct products it fires N times; each execution scans/sorts that product's history. With an index on (product_id, effective_date) each lookup is a cheap index seek + backward scan of one row, so it's fine at small scale. Without that index, each iteration is a full-table scan and the query degrades toward O(N · rows) — the classic correlated-subquery trap.

Solution B — one pass with ROW_NUMBER() (the window alternative)

Instead of N independent lookups, rank every qualifying row once and keep rank 1 per product. One scan, one sort, no correlation.

SELECT pids.product_id,
       COALESCE(latest.new_price, 10) AS price
FROM (SELECT DISTINCT product_id FROM PriceChanges) pids
LEFT JOIN (
    SELECT product_id, new_price
    FROM (
        SELECT product_id, new_price,
               ROW_NUMBER() OVER (
                 PARTITION BY product_id
                 ORDER BY effective_date DESC
               ) AS rn
        FROM PriceChanges
        WHERE effective_date <= '2019-08-16'
    ) ranked
    WHERE rn = 1
) latest ON latest.product_id = pids.product_id
ORDER BY pids.product_id;

Note the structure: the window-ranked table only contains products that have a qualifying row, so it cannot stand alone as the result — product 3 vanishes from it. The LEFT JOIN back onto the full distinct-product set re-introduces product 3 as a NULL, and COALESCE(..., 10) fills it. Same two sub-jobs as Solution A, just reorganized so the database does the "latest per group" work in a single pass instead of per row.

Worked trace (Solution A)

Driving set from SELECT DISTINCT product_id: {1, 2, 3}. For each, the correlated subquery runs:

p.product_idRows where date <= 08-16ORDER BY date DESC, LIMIT 1COALESCE result
1(08-14,20), (08-15,30), (08-16,35)(08-16, 35)35
2(08-14,50) — 08-17 excluded(08-14, 50)50
3none — 08-18 excludedNULLCOALESCE(NULL,10) = 10

Final, ordered by product_id: (1, 35), (2, 50), (3, 10).

Pitfalls

Takeaways


Based on LeetCode 1164 "Product Price at a Given Date" (Database, Medium). Mechanism, cost analysis, and the ROW_NUMBER()/window alternative drawn from the PostgreSQL and MySQL window-function docs and Markus Winand's SQL Performance Explained (correlated-subquery cost, latest-row-per-group patterns). Re-authored and deepened for this guide — added the per-row cost discussion, the window-function solution, the driving-set rationale, a worked trace, and the failure modes.

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

Stuck on Product Price on a Specific Date? 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 **Product Price on a Specific Date** (Databases) and want to truly understand it. Explain Product Price on a Specific Date 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 **Product Price on a Specific Date** 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 **Product Price on a Specific Date** 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 **Product Price on a Specific Date** 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