NTILE() — Divide Rows into N Rank Groups
NTILE(N) divides all rows into N groups as evenly as possible and assigns each row a group number from 1 to N. It is useful for quantile-based groups such as “customers in the top 25%” or “products in the bottom 10% by score.”
SELECT name, score, NTILE(4) OVER(ORDER BY score DESC) AS quartile -- Sort by score descending and divide all rows into four groups; the top 25% has quartile=1 FROM students;
From the sales_scores table below, return each sales representative’s name, score, and group number (tier) after dividing the rows into three groups by descending score.
The highest-scoring group is tier=1, and the lowest-scoring group is tier=3.
| sales_rep | score |
|---|---|
| Alex | 95 |
| Blair | 80 |
| Casey | 65 |
| Drew | 50 |
| Emery | 35 |
| Flynn | 20 |
| sales_rep | score | tier |
|---|---|---|
| Alex | 95 | 1 |
| Blair | 80 | 1 |
| Casey | 65 | 2 |
| Drew | 50 | 2 |
| Emery | 35 | 3 |
| Flynn | 20 | 3 |
ROWS BETWEEN — Calculate a Moving Average with a Window Frame
A frame clause inside a window function’s OVER() defines which range of rows contributes to the calculation for each current row.
AVG(revenue) OVER( ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- Calculate over the current row and the two preceding rows (up to three rows total) )
•
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: first row through current row (running aggregate)•
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW: two preceding rows plus current row (three-row moving average)• With ORDER BY but no frame clause, the default is
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (running aggregate)From the daily_sales table below, return each date’s revenue and its three-day moving average (moving_avg_3d).
Use at most three rows—the current row and two preceding rows—and display the average to one decimal place.
For the opening rows with fewer than three days of data, average only the rows that exist.
| sale_date | revenue |
|---|---|
| 04-01 | 100 |
| 04-02 | 200 |
| 04-03 | 150 |
| 04-04 | 300 |
| 04-05 | 250 |
| sale_date | revenue | moving_avg_3d |
|---|---|---|
| 04-01 | 100 | 100.0 |
| 04-02 | 200 | 150.0 |
| 04-03 | 150 | 150.0 |
| 04-04 | 300 | 216.7 |
| 04-05 | 250 | 233.3 |
PERCENT_RANK() — Calculate Percentile Rank Within a Group
PERCENT_RANK() returns a value from 0.0 to 1.0 that represents a row’s relative rank within its group. The similar CUME_DIST() instead returns the number of rows at or below the current value divided by the total row count, so its minimum is not zero.
PERCENT_RANK() OVER( PARTITION BY subject ORDER BY score ASC ) AS pct_rank
(rank - 1) / (row count - 1). With ascending order, the lowest score is 0.00 and a unique highest score is 1.00. Tied highest scores can remain below 1.00 because they share a rank. A one-row group returns 0.00.From the test_scores table below, return each student’s name, subject, score, and percentile rank within the subject (pct_rank).
Calculate each subject independently in ascending score order and display the 0.0–1.0 result to two decimal places.
| student_name | subject | score |
|---|---|---|
| Alex | Mathematics | 90 |
| Blair | Mathematics | 75 |
| Casey | Mathematics | 60 |
| Drew | English | 85 |
| Emery | English | 70 |
| student_name | subject | score | pct_rank |
|---|---|---|---|
| Casey | Mathematics | 60 | 0.00 |
| Blair | Mathematics | 75 | 0.50 |
| Alex | Mathematics | 90 | 1.00 |
| Emery | English | 70 | 0.00 |
| Drew | English | 85 | 1.00 |
LAG() + CTE — Calculate Month-over-Month Growth
Combining the preceding row returned by LAG() with the current value lets you calculate rates of change such as month-over-month growth.
Growth-rate formula: (current month − previous month) ÷ previous month × 100
Repeating LAG() several times in the SELECT list can duplicate window-expression work and can hurt performance. Computing LAG once in a CTE (WITH clause) and calculating the growth rate in the outer query keeps the expression reusable and readable.
WITH base AS ( SELECT sort_col, num_col, LAG(num_col) OVER(ORDER BY sort_col) AS prev_val -- Evaluate LAG once in the CTE and reuse it in the outer query FROM table_name ) SELECT *, (num_col - prev_val) * 100.0 / NULLIF(prev_val, 0) AS rate_pct FROM base;
NULL with NULLIF. On the first row there is no preceding row, so LAG returns NULL and anything computed from it is NULL too.From the monthly_revenue table below, return each month, revenue, previous month’s revenue (prev_revenue), and month-over-month growth percentage (growth_rate_pct).
Display the growth rate to one decimal place. NULL is expected for the first row, which has no previous month.
| month | revenue |
|---|---|
| 2024-01 | 1000 |
| 2024-02 | 1200 |
| 2024-03 | 1100 |
| 2024-04 | 1500 |
| month | revenue | prev_revenue | growth_rate_pct |
|---|---|---|---|
| 2024-01 | 1000 | NULL | NULL |
| 2024-02 | 1200 | 1000 | 20.0 |
| 2024-03 | 1100 | 1200 | -8.3 |
| 2024-04 | 1500 | 1100 | 36.4 |
ROW_NUMBER() + Subquery — Return the Top N Rows per Category
A window-function result such as a row number cannot be filtered in WHERE at the same query level. WHERE is evaluated before window functions, so use a subquery or CTE first.
-- ✗ Error: a window-function result cannot be used directly in WHERE SELECT * FROM t WHERE ROW_NUMBER() OVER(...) <= 2; -- SQL Error! -- ✓ Wrap the calculation in a subquery, then filter in WHERE SELECT * FROM ( SELECT *, ROW_NUMBER() OVER(...) AS rn FROM t ) ranked WHERE rn <= 2;
ROW_NUMBER() always assigns distinct numbers to tied rows, but which of them comes first is undefined while the ordering stays equal. Append a unique column to ORDER BY so that the rows selected are deterministic.From the product_sales table below, return only the top two products by sales amount in each category.
Use ROW_NUMBER(), not DENSE_RANK, to assign a unique row number when amounts tie within a category. Break ties by product name so the selected rows are deterministic.
| category | product | amount |
|---|---|---|
| Beverages | Coffee | 4000 |
| Beverages | Tea | 2800 |
| Beverages | Juice | 1500 |
| Food | Apple | 3000 |
| Food | Banana | 2500 |
| Food | Grapes | 1800 |
| category | product | amount |
|---|---|---|
| Beverages | Coffee | 4000 |
| Beverages | Tea | 2800 |
| Food | Apple | 3000 |
| Food | Banana | 2500 |