← Back to SQL Tutorial

MySQL 8: ROW_NUMBER() OVER(PARTITION BY ... ORDER BY SUM(...))

This query usually means: rank rows inside each group by an aggregated value. The reliable pattern is to calculate SUM() first, then apply ROW_NUMBER().

Short Answer

In MySQL 8, use ROW_NUMBER() OVER (PARTITION BY group_column ORDER BY aggregate_value DESC) to rank grouped results. The clearest production form is a CTE:

WITH product_sales AS (
    SELECT
        category_id,
        product_id,
        SUM(amount) AS total_amount
    FROM order_items
    GROUP BY category_id, product_id
),
ranked_products AS (
    SELECT
        category_id,
        product_id,
        total_amount,
        ROW_NUMBER() OVER (
            PARTITION BY category_id
            ORDER BY total_amount DESC, product_id
        ) AS rn
    FROM product_sales
)
SELECT category_id, product_id, total_amount
FROM ranked_products
WHERE rn = 1;

This gets total sales for each product in each category, numbers products inside each category by total sales, and returns the top product per category.

Can it be written in one SELECT?

If you only need to display the rank, a grouped MySQL 8 query can put SUM(amount) directly inside the window ORDER BY:

SELECT
    category_id,
    product_id,
    SUM(amount) AS total_amount,
    ROW_NUMBER() OVER (
        PARTITION BY category_id
        ORDER BY SUM(amount) DESC, product_id
    ) AS rn
FROM order_items
GROUP BY category_id, product_id;

Practical advice

Use the CTE form when you need WHERE rn <= 3, want to reuse total_amount, or want the execution order to be obvious to future readers.

Top N per group

WITH customer_product_spend AS (
    SELECT
        customer_id,
        product_id,
        SUM(quantity * unit_price) AS spend
    FROM order_items
    GROUP BY customer_id, product_id
),
ranked AS (
    SELECT
        customer_id,
        product_id,
        spend,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY spend DESC, product_id
        ) AS rn
    FROM customer_product_spend
)
SELECT customer_id, product_id, spend, rn
FROM ranked
WHERE rn <= 3
ORDER BY customer_id, rn;

The extra product_id sort key makes ties deterministic, so repeated runs return the same row order.

ROW_NUMBER vs RANK vs DENSE_RANK

Function Ties Use case
ROW_NUMBER()Always assigns unique numbersReturn exactly one row, such as one top product per category
RANK()Same rank for ties, then skips numbersCompetition ranking, such as 1, 1, 3
DENSE_RANK()Same rank for ties, no skipped numbersReport ranking, such as 1, 1, 2

Common mistakes

Mistake 1: filtering the window alias in WHERE

-- Wrong: WHERE cannot see the SELECT alias rn
SELECT
    category_id,
    product_id,
    ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY SUM(amount) DESC) AS rn
FROM order_items
GROUP BY category_id, product_id
WHERE rn = 1;

Fix it by wrapping the query in a CTE or subquery, then filter rn outside.

Mistake 2: using a window function instead of GROUP BY

A window function does not collapse rows. SUM(amount) OVER (...) returns a window result for each row. If you need one row per product or category, you still need GROUP BY.

Mistake 3: ignoring ties

If two rows have the same total_amount, add a second sort key such as a primary key or date. Otherwise the row numbered first may not match your business rule.

FAQ

Does MySQL 8 support ROW_NUMBER?

Yes. MySQL 8.0 supports window functions including ROW_NUMBER(), RANK(), DENSE_RANK(), and aggregate window functions with OVER.

How should the search query order by sum( be implemented?

Name the aggregate value as total_amount in a CTE, then use ORDER BY total_amount DESC in the outer window function.

What about MySQL 5.7?

MySQL 5.7 does not have window functions. You can simulate ranking with a self-join or correlated subquery, but for production code MySQL 8 is the cleaner path.

Related lessons