Knowledge Guide
HomeDatabasesSQL Practice Problems

medium Order Fulfillment Rate

The engine fans every order out to its delivery rows with a LEFT JOIN, collapses each order back to one row with GROUP BY so that SUM(delivered)/COUNT(*) becomes that order's rate, then averages those per-order rates per customer — which means every order counts equally toward its customer's score no matter how many delivery attempts it had.

That last clause is the whole subtlety of this problem, and it is the thing the grader flagged as never stated. The query is correct for the metric as specified ("average of per-order rates"), but that metric is deliberately not the same number as "total delivered / total attempts" for the customer. Hold onto that distinction — the Pitfalls section returns to it.

The mechanism, in three moves

  1. Preserve every order. Orders LEFT JOIN Deliveries keeps orders that have zero delivery rows; the right side comes back all-NULL for them. This is what lets the "no attempts → rate 0" rule even be expressible — an INNER JOIN would silently drop those orders.
  2. Reduce each order to a rate. GROUP BY o.order_id turns the post-join row-set for one order into a single number: SUM(CASE WHEN status='delivered' THEN 1 ELSE 0 END) over COUNT(d.order_id).
  3. Average rates per customer. A second CTE does AVG(fulfillment_rate) grouped by customer_id, rounded to 2 dp.

The query (corrected and annotated)

WITH OrderFulfillment AS (
    -- one row per order: its individual fulfillment rate
    SELECT
        o.order_id,
        o.customer_id,
        IFNULL(
            SUM(CASE WHEN d.status = 'delivered' THEN 1 ELSE 0 END)
              / COUNT(d.order_id),   -- COUNT ignores NULLs from the LEFT JOIN
            0                        -- 0/0 is NULL in MySQL -> coerce to 0
        ) AS fulfillment_rate
    FROM Orders o
    LEFT JOIN Deliveries d ON o.order_id = d.order_id
    GROUP BY o.order_id, o.customer_id
),
CustomerFulfillment AS (
    -- average the per-order rates, equal weight per order
    SELECT
        customer_id,
        ROUND(AVG(fulfillment_rate), 2) AS fulfillment_rate
    FROM OrderFulfillment
    GROUP BY customer_id
)
SELECT customer_id, fulfillment_rate
FROM CustomerFulfillment;

The original page's SQL was already correct; what was missing was a trace of why the two NULL-prone spots are safe. COUNT(d.order_id) counts non-NULL values only, so a zero-attempt order yields COUNT = 0; SUM(...) over an all-NULL group is 0; and 0 / 0 evaluates to NULL in MySQL (not an error), which IFNULL(..., 0) then turns into the required 0.00. Three different NULL-handling rules conspire to make the formula land on exactly the value the spec asks for.

A traced example — with a zero-attempt order added

The original example never exercises the headline edge case: every order in it had at least one delivery row (order 4 had one failed attempt, so it hit 0/1=0.00, not the 0/0 path). To actually trace the LEFT JOIN + COUNT mechanism we add order 5 for customer 300 with no rows in Deliveries at all.

Orders

order_idcustomer_id
1100
2200
3100
4300
5300

Deliveries

order_idstatus
1delivered
1failed
2delivered
3delivered
3delivered
4failed

(Order 5 appears nowhere in Deliveries.)

After the LEFT JOIN, before grouping — note order 5 produces a single all-NULL right side:

o.order_ido.customer_idd.order_idd.status
11001delivered
11001failed
22002delivered
31003delivered
31003delivered
43004failed
5300NULLNULL

After GROUP BY o.order_id — apply the formula per group:

order_idcustomer_idSUM(delivered)COUNT(d.order_id)SUM / COUNTIFNULL → rate
1100120.50000.50
2200111.00001.00
3100221.00001.00
4300010.00000.00
5300000/0 = NULL0.00

Row 5 is the one the old page asserted but never showed: COUNT(d.order_id)=0 because every joined value was NULL, the division is 0/0=NULL, and IFNULL rescues it to 0.00.

diagram
diagram

After AVG per customer — order 5 now drags customer 300's average:

customer_idper-order ratesAVGROUND(…,2)
1000.50, 1.000.750.75
2001.001.001.00
3000.00, 0.000.000.00

With only the original four orders, customers 100/200/300 give 0.75/1.00/0.00 — matching the stated expected output. Adding the zero-attempt order 5 keeps 300 at 0.00 but now via the genuine 0/0 path, which is the case the metric was designed to handle.

Pitfalls

Execution & performance notes

The planner materializes OrderFulfillment (or inlines it), driving from Orders and probing Deliveries on order_id. Since order_id is the PK of Orders and the leading column of the composite PK (order_id, time_stamp) on Deliveries, that probe is an index range scan, not a table scan — the existing keys already cover the join. The first GROUP BY aggregates on the join's output (one pass), the second aggregates the small per-order result. For a customer-facing dashboard you would not recompute this live over the full Deliveries history; you would maintain per-order rates incrementally (or a rollup table keyed by customer) since delivery rows are append-only and an order's rate only changes when a new attempt lands.

Takeaways


Re-authored / deepened for this guide. Based on the LeetCode-style "Order Fulfillment Rate" problem and its reference two-CTE solution. NULL-handling semantics (COUNT ignoring NULLs, 0/0 → NULL, IFNULL) verified against the MySQL 8.0 Reference Manual (Aggregate Functions; Arithmetic Operators; Flow Control Functions). PostgreSQL integer-division and COALESCE/NULLIF contrasts per the PostgreSQL 16 documentation. The average-of-rates vs. pooled-rate distinction and the zero-attempt trace were added here; the original page asserted the zero-attempt path but never exercised it, and its example data contained no zero-attempt order.

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

Stuck on Order Fulfillment Rate? 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 **Order Fulfillment Rate** (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 **Order Fulfillment Rate** 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 **Order Fulfillment Rate**. 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 **Order Fulfillment Rate**. 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