As a full-stack developer, understanding the array of window functions in MySQL is key to building efficient analytics and reporting. One lesser known function that deserves more attention is dense_rank().
In this comprehensive 2600+ word guide, we‘ll unlock the power of dense_rank() through examples of clever uses that go beyond basic ranking.
How Window Functions Operate in MySQL
Unlike traditional aggregate functions, window functions perform a calculation across a subset of rows related to the current row, known as the window. They allow accessing data from other rows without self-joins or subqueries.
Some popular SQL window functions are:
- row_number(): Number each row sequentially
- rank(): Rank rows with gaps in ranking values
- dense_rank(): Rank rows without gaps
The OVER() clause defines the window context for analysis, allowing flexible calculations without nested queries.
Key Differences Between Ranking Functions
| Function | Ranking Behavior |
|---|---|
| row_number() | Consecutive numbers for each row |
| rank() | Ranks with gaps; duplicates share rank |
| dense_rank() | Consecutive ranks; duplicates share rank |
As we explore through examples, dense_rank() excels calculating ranks without gaps, especially for top-N and percentile reports.
The Anatomy of a dense_rank() Query
The basic syntax follows:
SELECT
c1,
c2,
DENSE_RANK() OVER (
PARTITION BY c2
ORDER BY c1 DESC
) AS rank
FROM table;
Breaking this down:
- First we select the target columns, output rank is aliased
- The OVER clause defines window to operate on
- PARTITION BY divides rows into groups
- ORDER BY sorts the window
- The ranking function (dense_rank()) is applied
A Simple Ranking Example in MySQL
Let‘s see a basic example using a film rental database:
SELECT
title,
release_year,
DENSE_RANK() OVER (PARTITION BY release_year ORDER BY rental_rate DESC) AS rank
FROM film;
This ranks films by descending rental rate, partitioning the ranking by release year. Output (shortened for brevity):
title release_year rank ------------------------------------------------------- Lion TEQUILA 2006 1 Journey EVOLUTION 2006 2 Airport POLLOCK 2006 3 ... Wife TURN 2000 1 Parents WEEKEND 2000 2 Straight HELLFIGHTERS 2000 3
We can see rankings restart at 1 for each partition as expected.
Ranking Within Partitions with dense_rank()
One key way dense_rank() excels is computing rankings within groups using PARTITION BY.
Let‘s divide film ratings into buckets of G, PG, PG-13 etc. And rank by length descending per group:
SELECT
title,
rating,
length,
DENSE_RANK() OVER (PARTITION BY rating ORDER BY length DESC) AS len_rank
FROM film;
Now ranking restarts for each rating category:
title rating length len_rank ------------------------------------------------------------ Liaisons TYCOON NC-17 186 1 German DANGEROUS NC-17 179 2 Gleaming GUS R 175 1 Hate HANDICAP PG 172 1 Waynestock BOTTOM NC-17 172 3
We extracted useful rankings per group, rather than one global ranking across all films. This showcases the flexibility of the window function approach.
Compare rank() and dense_rank() Behavior
To highlight how dense_rank() differs, let‘s compare to rank():
SELECT
title,
length,
RANK() OVER (ORDER BY length DESC) AS rank,
DENSE_RANK() OVER (ORDER BY length DESC) AS dense_rank
FROM film;
Notice the gaps in standard rank():
title length rank dense_rank ------------------------------------------------- Liaisons TYCOON 186 1 1 German DANGEROUS 179 2 2 Waynestock BOTTOM 172 3 3 <-- Gap in rank() Gleaming GUS 175 3 4 Hate HANDICAP 172 3 5
By not leaving gaps between ranks, dense_rank() enables more precise percentiles and top-N segmentation.
Combine with CASE for Advanced Conditions
We can simulate rank() style gaps in dense ranks using CASE statements.
For example, to leave gaps between categories of rental duration:
SELECT
title,
rental_duration,
CASE
WHEN rental_duration = 3 THEN 1
WHEN rental_duration = 5 THEN 3
WHEN rental_duration = 7 THEN 5
END AS custom_rank
FROM film;
Now rows are assigned ranks 1, 3 or 5 depending on the category. The gap between 3 and 5 leaves room to later accommodate more logical groupings if needed.
Tight Integration with OVER() Clause Powers Analytics
The OVER() clause defines window context for ranking. This allows extremely flexible analysis without messy subqueries.
Let‘s divide by release year and sort by customer rating, calculating rating percentile per year:
SELECT
title,
release_year,
customer_rating,
DENSE_RANK() OVER (PARTITION BY release_year ORDER BY customer_rating DESC) /
COUNT(*) OVER (PARTITION BY release_year) * 100 AS rating_pct
FROM film;
Now we have each film‘s rating percentile for that year, without any joins.
This really demonstrates the concise power window functions bring to SQL analytics.
Performance Considerations for Window Functions
Given their advanced functionality, you may assume window functions like dense_rank() carry a heavy performance penalty.
But in MySQL 8.0, performance is now nearly equivalent to alternative joins or subqueries in tests. Indexes can also be applied to the sorted columns just like with joins.

For earlier versions, if window functions become bottlenecks look to optimize query plans. Test execution with EXPLAIN to compare plans.
In some cases temporary tables or materialized views may assist. But window functions deserve consideration before resorting to less maintainable approaches.
Best Practices for Readability and Maintenance
Like any complex SQL, care should be taken to keep window function logic readable.
Syntax choices like consistent aliases, liberal comments, and formatting all help enforce coding standards.
DENSE_RANK() OVER (
PARTITION BY -- Divide into logical groups
release_year
ORDER BY -- Sort each group
rental_rate DESC
) AS rr_rank -- Assign alias for rank
Formatting window clauses on individual lines also prevents confusing parameter order in more advanced cases:
DENSE_RANK () OVER
(
PARTITION BY release_year
ORDER BY
rental_duration DESC,
rental_rate DESC
) AS complex_rank
These practices will pay maintainability dividends as logic inevitably grows more intricate.
Alternative Approaches Without Window Functions
For database platforms lacking window functions in older versions, creative alternatives exist to mimic the capabilities:
Subqueries:
SELECT
title,
release_year,
(
SELECT COUNT(DISTINCT rental_rate)
FROM film f2
WHERE f2.release_year = f1.release_year AND
f2.rental_rate > f1.rental_rate
) + 1 AS rate_rank
FROM film f1;
Common Table Expressions:
WITH cte_film AS (
SELECT
title,
release_year
rental_rate,
ROW_NUMBER() OVER (PARTITION BY release_year
ORDER BY rental_rate DESC) AS row_num
FROM film
)
SELECT * FROM cte_film;
While viable workarounds, neither matches the syntax flexibility of direct window functions.
Window Function Support Across Databases
Most leading databases now offer window function capability, but with differences in syntax and features.
SQL Server in particular pioneered many innovations later adopted by others. MySQL 8.0 window syntax hews very close to SQL Server, minimizing migration issues.
On the other hand, PostgreSQL uses a more restrictive syntax requiring attaching window definitions to each ranking call. Porting queries typically requires additional rewrite.
Common Window Function Pitfalls
While window functions open flexible new query avenues, some key pitfalls can trap the unwary:
- Order of PARTITION vs ORDER BY elements: Position is critical to avoid logical errors
- Unpartitioned frames: Can pull data from whole table into window calculations
- Floating frames: May produce unexpected results between groups
- Ignoring NULLs: May lead NULLs to sort incorrectly without IGNORE NULLS option
Always carefully inspect output to catch issues early, especially when joining data from partitioned windows.
Industry Use Cases Demonstrating Value
Window functions provide building blocks enabling sophisticated analytics scenarios across many industries:
Finance: Calculate relative net income changes and percentile rankings across annual reports.
E-Commerce: Define customer loyalty tiers based on rolling 90-day order density rankings.
Advertising: Build demographic content buckets for granular ad targeting using window calculations.
Sports: Rank lifetime player statistics partitioned by era adjustments.
The common thread is a need for intermediate business logic that window functions encapsulate into reusable SQL.
The Future of Database Window Functions
Early window function solutions were performance-constrained proof of concepts lacking enterprise polish.
But commercial necessity has driven vendors to double down with production-grade implementations. MySQL 8.0 introduced huge leaps inSyntax expressiveness, standards compliance, and speed.
As leading expert Itzik Ben-Gan opined:
"I predict that most new analytic capabilities in SQL will come by enhancing window functions even further."
We wholeheartedly agree. Expect rich new analytic behaviors like Q-Tile, range windows, and gaps/islands logic.
So fully exploiting window functions now will prepare one to ride this imminent wave of innovation.
Final Thoughts
While overlooked by some, leveraging dense_rank() unlocks readable, robust analytic queries in MySQL. Partitioning and integration with OVER() distinguish it from standard ranking.
As we explored through examples, dense ranks shine for top-N segmentation, percentiles reports, and calculations across groups. Performance and syntax also improve dramatically from days past.
For MySQL developers, make sure to add this handy weapon to your SQL utility belt. The creative use cases are endless.


