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;
When the row count is not divisible by N, the remainder is distributed one extra row at a time starting with the first group. For example, NTILE(4) over six rows gives two rows each to groups 1 and 2, and one row each to groups 3 and 4.
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 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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) )
Common frame specifications:
• 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 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
PERCENT_RANK() returns a value from 0.0 to 1.0 that represents a row’s relative rank within its group.
Formula: (rank - 1) ÷ (number of rows in the group - 1)
• The lowest-ranked row is always 0.00. A unique highest-ranked row reaches 1.00; tied highest scores can produce a value below 1.00 because they share a rank.
• A one-row group returns 0.00, as databases define the result as zero when the formula’s denominator would be zero.
• The related CUME_DIST() calculates “rows at or below the current value ÷ total rows,” so it never returns zero.
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 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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 month, revenue, LAG(revenue) OVER(ORDER BY month) AS prev_revenue -- Evaluate LAG once in the CTE and reuse it in the outer query FROM monthly_revenue ) SELECT *, (revenue - prev_revenue) * 100.0 / NULLIF(prev_revenue, 0) AS growth_rate FROM base;
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 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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;
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 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free