SQL Window Functions

Summary


1. Aggregate Window Functions
These functions perform calculations across a set of table rows that are somehow related to the current row.

    SUM() OVER(…) – Running total or sum per partition
    AVG() OVER(…) – Average per partition
    COUNT() OVER(…) – Count rows in partition
    MIN() OVER(…) – Minimum value in partition
    MAX() OVER(…) – Maximum value in partition

    2. Ranking Window Functions
    These assign a rank or row number to each row within a partition.

      ROW_NUMBER() OVER(…) – Unique sequential number per row in partition
      RANK() OVER(…) – Rank with gaps for ties
      DENSE_RANK() OVER(…) – Rank without gaps for ties
      NTILE(n) OVER(…) – Divides partition into n buckets and assigns a bucket number

      3. Value Navigation (Analytic) Functions
      These functions return values from other rows in the window frame relative to the current row.

        LAG(expression, offset, default) OVER(…) – Value from a previous row
        LEAD(expression, offset, default) OVER(…) – Value from a following row
        FIRST_VALUE(expression) OVER(…) – First value in the window frame
        LAST_VALUE(expression) OVER(…) – Last value in the window frame
        NTH_VALUE(expression, n) OVER(…) – The nth value in the window frame


        Examples


        1. Aggregate Window Functions

        These perform calculations over a window of rows but return a value for each row.

        NameSalesDate
        Alice1002025-01-01
        Alice1502025-01-03
        Bob2002025-01-01
        Alice502025-01-05
        Bob3002025-01-04


        SUM() OVER()

        SELECT
          Name,
          Sales,
          SUM(Sales) OVER (PARTITION BY Name ORDER BY Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal
        FROM SalesData;


        The window function SUM(Sales) OVER (...) calculates a running total of sales for each Name.

        The window is partitioned by Name (so calculations are done separately for each person).

        The rows are ordered by Date within each partition.

        The frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW means the sum includes all rows from the first date up to the current row’s date.

        Output:

        NameSalesRunningTotal
        Alice100100
        Alice150250
        Alice50300
        Bob200200
        Bob300500


        AVG() OVER()

        Customer_idsales
        101200
        102150
        101300
        103400
        102250
        101100
        SELECT 
        customer_id, 
        sales, 
        AVG(sales) OVER (PARTITION BY customer_id) AS AvgSales 
        FROM orders;
        • The window function AVG(sales) OVER (PARTITION BY customer_id) calculates the average sales for each customer_id.
        • The average is computed over all rows with the same customer_id.
        • The result is shown on every row corresponding to that customer.
        customer_idsalesAvgSales
        101200200
        101300200
        101100200
        102150200
        102250200
        103400400


        COUNT(*) OVER

        departmentemployee_id
        Sales101
        Sales102
        HR201
        HR202
        HR203
        IT301
        SELECT
          department,
          employee_id,
          COUNT(*) OVER (PARTITION BY department) AS DeptCount
        FROM employees;
        • The window function COUNT(*) OVER (PARTITION BY department) counts the total number of employees in each department.
        • This count is repeated on every row for that department.
        • No ordering is required here because the count is the same for all rows in the partition.

        departmentemployee_idDeptCount
        Sales1012
        Sales1022
        HR2013
        HR2023
        HR2033
        IT3011


        2. Ranking Functions

        Assign ranks or row numbers within partitions.

        ROW_NUMBER() — Assigns unique row numbers

        employee_iddepartmentsalary
        101Sales5000
        102Sales7000
        103Sales6000
        201HR4500
        202HR4800
        301IT8000

        SELECT
          employee_id,
          salary,
          ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS RowNum
        FROM employees;

        employee_idsalaryRowNum
        10270001
        10360002
        10150003
        20248001
        20145002
        30180001

        The window function ROW_NUMBER() assigns a unique sequential number to each row within the partition defined by department.

        Rows are ordered by salary in descending order within each department.

        The highest salary in each department gets RowNum = 1, the next highest gets 2, and so on.

        RANK() — Assigns rank, with gaps for ties

        employee_iddepartmentsalary
        101Sales7000
        102Sales7000
        103Sales6000
        201HR4800
        202HR4500
        301IT8000
        302IT8000
        303IT7500
        SELECT
          employee_id,
          salary,
          RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS Rank
        FROM employees;

        The RANK() function assigns a rank to each employee within their department, ordering by salary descending.

        Employees with the same salary get the same rank.

        The next rank after a tie skips the appropriate number of positions (i.e., gaps in ranking).

        employee_idsalaryRank
        10170001
        10270001
        10360003
        20148001
        20245002
        30180001
        30280001
        30375003


        DENSE_RANK() — Like RANK but no gaps

        employee_iddepartmentsalary
        101Sales7000
        102Sales7000
        103Sales6000
        201HR4800
        202HR4500
        301IT8000
        302IT8000
        303IT7500
        SELECT
          employee_id,
          salary,
          DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS DenseRank
        FROM employees;
        • DENSE_RANK() assigns ranks within each department, ordered by salary descending.
        • Employees with the same salary get the same rank.
        • Unlike RANK()DENSE_RANK() does not skip ranks after ties. The next distinct value gets the next consecutive rank.
        employee_idsalaryDenseRank
        10170001
        10270001
        10360002
        20148001
        20245002
        30180001
        30280001
        30375002



        3. Value Navigation Functions

        Access values from other rows relative to the current row.

        LAG() — Value from previous row

        employee_iddepartmentsalary
        101Sales5000
        102Sales6000
        103Sales7000
        201HR4500
        202HR4800
        301IT8000
        SELECT
          employee_id,
          salary,
          LAG(salary, 1) OVER (PARTITION BY department ORDER BY salary) AS PrevSalary
        FROM employees;

        LAG(salary, 1) returns the salary value from the previous row within the same department, based on the ascending order of salary.

        For the first row in each department, there is no previous salary, so the result is NULL.

        employee_idsalaryPrevSalary
        1015000NULL
        10260005000
        10370006000
        2014500NULL
        20248004500
        3018000NULL



        LEAD() — Value from next row

        employee_iddepartmentsalary
        101Sales5000
        102Sales6000
        103Sales7000
        201HR4500
        202HR4800
        301IT8000

        SELECT
          employee_id,
          salary,
          LEAD(salary, 1) OVER (PARTITION BY department ORDER BY salary) AS NextSalary
        FROM employees;


        LEAD(salary, 1) returns the salary value from the next row within the same department, based on the ascending order of salary.

        For the last row in each department, there is no next salary, so the result is NULL


        5000
        6000
        10260007000
        1037000NULL
        20145004800
        2024800NULL
        3018000NULL


        LAST_VALUE() — Last value in the window frame

        employee_iddepartmentsalary
        101Sales5000
        102Sales7000
        103Sales6000
        201HR4500
        202HR4800
        301IT8000

        SELECT
          employee_id,
          salary,
          LAST_VALUE(salary) OVER (PARTITION BY department ORDER BY salary ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS MaxSalary
        FROM employees;
        
        • LAST_VALUE(salary) returns the last salary value in the ordered partition.
        • The window frame ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ensures the entire partition is considered (not just up to the current row).
        • When ordered by salary ascending, the last value is the highest salary in the department.
        employee_idsalaryMaxSalary
        10150007000
        10360007000
        10270007000
        20145004800
        20248004800
        30180008000