Back to Window Functions: ROW_NUMBER, RANK, LEAD, LAG
Q4Question 4 of 8

Top 5 Employees Earning Above the Average Salary: LIMIT and RANK Versions

Key Concepts
  • The overall average is computed as a scalar subquery: `(SELECT AVG(salary) FROM employees)`, because you cannot compare a row's salary to an aggregate in `WHERE` directly.
  • The LIMIT version filters above-average employees, orders by salary descending, and caps to 5 rows with `LIMIT 5` or `SELECT TOP 5`.
  • The RANK version applies `RANK() OVER (ORDER BY salary DESC)` to the above-average employees and keeps ranks up to 5.
  • LIMIT 5 arbitrarily includes one of two employees tied at the fifth salary, while RANK keeps all tied employees together.
  • Use `DENSE_RANK` if you want the top 5 distinct salary tiers including everyone at each tier.
  • `AVG` ignores NULL salaries, so decide whether to coalesce NULLs before computing the average.

Definition

Filter `salary > (SELECT AVG(salary) ...)`, then `ORDER BY salary DESC LIMIT 5`, or use `RANK()`/`DENSE_RANK()` and keep ranks `<= 5` to handle ties.
  • Average filter – a scalar subquery (SELECT AVG(salary) FROM employees) computes the overall average, and the outer query keeps only rows above it.
  • LIMIT version – ORDER BY salary DESC LIMIT 5 (or SELECT TOP 5 on SQL Server) caps the result to exactly 5 rows, breaking ties at the boundary arbitrarily.
  • RANK version – RANK() OVER (ORDER BY salary DESC) assigns a competition-style rank to the above-average employees, and WHERE rnk <= 5 keeps all employees in the top 5 ranks, including ties.
  • DENSE_RANK alternative – DENSE_RANK() gives the top 5 distinct salary tiers with no gaps, potentially returning more than 5 rows when ties exist.
  • NULL handling – AVG() ignores NULLs; if NULL salaries should be treated as zero, use COALESCE(salary, 0).

National Bank of India wants to reward its best-paid high performers. First find the average salary of everyone. Then keep only people earning more than that average. Then from those, take the top five. The "take top five" can be a simple cutoff of five rows, or a ranking where everyone tied at fifth place is also included. The LIMIT approach is a strict head-count; the RANK approach is a fair tier-based selection.

Understanding the Concepts

What is the problem statement?

The goal is to find the top 5 employees whose salary is greater than the overall average salary. There are two approaches: the LIMIT/TOP approach for a simple row cutoff, and the RANK approach for fair tie handling.

How do you compute the average for filtering?

The overall average must be computed as a scalar subquery because you cannot compare a row's salary to an aggregate of the whole table directly in WHERE.

SQL
-- Scalar subquery returns one number
SELECT AVG(salary) FROM employees;
-- Result: 185000.00

This subquery runs once and returns a single number. The outer query keeps only rows above it.

How does the LIMIT version work?

SQL
-- MySQL / PostgreSQL
SELECT emp_name, branch_id, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC
LIMIT 5;

-- SQL Server
SELECT TOP 5 emp_name, branch_id, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
DIAGRAM
emp_name  | branch_id | salary
----------+-----------+----------
Pankaj    | BR001     | 350000.00
Rajesh    | BR002     | 300000.00
Meena     | BR001     | 280000.00
Anita     | BR003     | 250000.00
Suresh    | BR002     | 250000.00   ← tied with Anita, arbitrarily included
-- Deepak with 250000 is excluded despite same salary as #5

The subtle behavior: if the fifth and sixth employees earn exactly the same salary, LIMIT 5 arbitrarily includes one and excludes the other.

How does the RANK version work?

SQL
SELECT emp_name, branch_id, salary, rnk
FROM (
    SELECT emp_name, branch_id, salary,
           RANK() OVER (ORDER BY salary DESC) AS rnk
    FROM employees
    WHERE salary > (SELECT AVG(salary) FROM employees)
) ranked
WHERE rnk <= 5
ORDER BY rnk;
DIAGRAM
emp_name  | branch_id | salary     | rnk
----------+-----------+------------+-----
Pankaj    | BR001     | 350000.00  |   1
Rajesh    | BR002     | 300000.00  |   2
Meena     | BR001     | 280000.00  |   3
Anita     | BR003     | 250000.00  |   4
Suresh    | BR002     | 250000.00  |   4   ← same rank, both included
Deepak    | BR001     | 250000.00  |   4   ← same rank, also included

RANK gives tied employees the same rank. Since Anita, Suresh, and Deepak all earn Rs.250,000, they all get rank 4, and the next distinct salary would be rank 7. Filtering rnk <= 5 includes all of them.

What is the difference between RANK and DENSE_RANK here?

With DENSE_RANK, tied employees still share a rank but the next rank does not skip. So three employees tied at rank 4 are followed by rank 5, not rank 7.

| Employees (salary DESC) | RANK | DENSE_RANK |
|-------------------------|------|------------|
| Pankaj (350,000) | 1 | 1 |
| Rajesh (300,000) | 2 | 2 |
| Meena (280,000) | 3 | 3 |
| Anita (250,000) | 4 | 4 |
| Suresh (250,000) | 4 | 4 |
| Deepak (250,000) | 4 | 4 |
| Vikram (220,000) | 7 | 5 |

With DENSE_RANK() <= 5, Vikram at rank 5 is also included. With RANK() <= 5, only ranks 1 through 4 match because the next rank is 7.

How do you handle NULL salaries?

AVG() ignores NULL values by default. If employees with NULL salary should be treated as having zero salary:

SQL
-- Without COALESCE: NULLs are ignored in AVG
SELECT AVG(salary) FROM employees;
-- Might return 185000.00 (computed over non-NULL rows only)

-- With COALESCE: NULLs treated as zero, lowering the average
SELECT AVG(COALESCE(salary, 0)) FROM employees;
-- Returns a lower average, so more employees qualify

How do you implement this in Java?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class TopEarnerService {

    private final Connection connection;

    public TopEarnerService(Connection connection) {
        this.connection = connection;
    }

    public List<String> getTopEarnersAboveAverage(int topN) throws SQLException {
        String sql = """
            SELECT emp_name, salary, rnk FROM (
                SELECT emp_name, salary,
                       RANK() OVER (ORDER BY salary DESC) AS rnk
                FROM employees
                WHERE salary > (SELECT AVG(salary) FROM employees)
            ) ranked
            WHERE rnk <= ?
            ORDER BY rnk
            """;
        List<String> results = new ArrayList<>();
        try (PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setInt(1, topN);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    String name = rs.getString("emp_name");
                    BigDecimal salary = rs.getBigDecimal("salary");
                    int rank = rs.getInt("rnk");
                    results.add(String.format("Rank %d: %s – Rs.%s", rank, name, salary));
                }
            }
        }
        return results;
    }
}

What about performance?

An index on salary helps both the ordering and the average scan.

SQL
CREATE INDEX idx_emp_salary ON employees (salary DESC);

The scalar subquery for AVG runs once and is cached. The outer query then uses the index for both the filter and the sort.

What are common mistakes?

The most common mistake is using the average without a subquery and getting a syntax error.

SQL
-- Wrong: cannot use aggregate directly in WHERE
SELECT emp_name, salary FROM employees
WHERE salary > AVG(salary);
-- ERROR: aggregate functions are not allowed in WHERE

-- Correct: use a scalar subquery
SELECT emp_name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

Another mistake is confusing HAVING (which filters groups after aggregation) with WHERE (which filters rows before aggregation).

What are common interview follow-up questions?

  • "What if the average should be per department?" Use a correlated subquery or a window function: AVG(salary) OVER (PARTITION BY dept_id).
  • "How do you get the bottom 5 below the average?" Filter salary < AVG(...) and order ASC.
  • "What if you want the top 5 per department above that department's average?" Combine PARTITION BY for both the average and the rank.

Visual Overview

DIAGRAM
Step 1: Compute overall average
┌──────────────────────────┐
│ AVG(salary) = 185,000    │
└──────────────────────────┘
              │
Step 2: Filter above average
              ▼
┌──────────────────────────┐
│ Pankaj   350,000  ✓     │
│ Rajesh   300,000  ✓     │
│ Meena    280,000  ✓     │
│ Anita    250,000  ✓     │
│ Suresh   250,000  ✓     │
│ Deepak   250,000  ✓     │
│ Vikram   220,000  ✓     │
│ Priya    150,000  ✗     │  ← below average
└──────────────────────────┘
              │
Step 3: Take top 5
              ▼
┌────────────────────────────────────────┐
│ LIMIT 5: exactly 5 rows (arbitrary    │
│   tie-break at boundary)              │
│                                        │
│ RANK <= 5: includes all ties at rank  │
│   boundary (may return > 5 rows)      │
└────────────────────────────────────────┘

Key Takeaway

To find the top five employees earning above the average salary, first compute the overall average with a scalar subquery and compare each row to it, then take the top five. The LIMIT version is straightforward: WHERE salary > (SELECT AVG(salary) FROM employees) ORDER BY salary DESC LIMIT 5. The RANK version wraps the above-average filter in a subquery, applies RANK() OVER (ORDER BY salary DESC), and keeps ranks up to five.

The difference matters at the boundary. LIMIT 5 arbitrarily includes one of two people tied at the fifth salary, while RANK keeps all tied employees together. At National Bank of India's annual bonus, which rewards the top five earners who are also above the company average, a plain LIMIT 5 once split two employees tied at the fifth-highest salary so one got a bonus and the other did not, triggering a fairness complaint. Rajesh moved the selection to the RANK version so ties at the cutoff are handled consistently.

The senior distinctions are that ROW_NUMBER gives exactly one row per position and is arbitrary on ties, RANK gives tied rows the same number and then skips the next numbers, and DENSE_RANK gives tied rows the same number without skipping. AVG ignores NULL salaries, and salary should be a DECIMAL type for precision. An index on salary helps both the filter and the sort.

Remember one sentence: filter to above the average, then LIMIT 5 for speed or RANK up to 5 for fair tie handling.

---

Share
Use ← → arrow keys to navigate