NTILE(N) divides all rows into up to N groups whose sizes differ by at most one row (buckets) and assigns each row a group number in the range 1 through N.
It belongs to the same family of numbering functions as ROW_NUMBER, RANK, and DENSE_RANK.
NTILE(4) OVER(ORDER BY total_purchase DESC) -- Sort by purchase amount descending and divide into four groups -- 1 = highest-value group (top 25%) -- 4 = lowest-value group (bottom 25%)
Using the customers table below, classify every user into four tiers in descending order of total purchase amount (total_purchase).
Divide the rows so tier 1 is the highest-value group and tier 4 is the lowest-value group.
| user_id | name | total_purchase |
|---|---|---|
| U1 | Tanaka | 48000 |
| U2 | Suzuki | 12000 |
| U3 | Sato | 95000 |
| U4 | Takahashi | 33000 |
| U5 | Ito | 71000 |
| U6 | Watanabe | 8000 |
| user_id | name | total_purchase | tier |
|---|---|---|---|
| U3 | Sato | 95000 | 1 |
| U5 | Ito | 71000 | 1 |
| U1 | Tanaka | 48000 | 2 |
| U4 | Takahashi | 33000 | 2 |
| U2 | Suzuki | 12000 | 3 |
| U6 | Watanabe | 8000 | 4 |
Six rows divided into four groups gives a quotient of 1 and a remainder of 2. The first two groups (tiers 1 and 2) receive two rows each; the remaining groups (tiers 3 and 4) receive one row each.
- 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 each row's relative position within the full set as a decimal from 0.0 to 1.0.
For a set with multiple rows, the minimum row is 0.0 (0th percentile) and the maximum row is 1.0 (100th percentile). A single-row set returns 0.0.
PERCENT_RANK() OVER(ORDER BY score) -- Formula: (rank - 1) / (total_rows - 1) -- Example: rank 3 of 5 → (3-1)/(5-1) = 0.5 = 50th percentile
CUME_DIST() returns the proportion of rows whose value is less than or equal to the current row, producing a value greater than 0 and at most 1. PERCENT_RANK starts at 0 by normalizing rank as (rank - 1) / (total_rows - 1). Their values differ even without ties; when ties occur, the difference in how ranks are handled becomes even more important.Using the exam_scores table below, calculate the percentile position of each employee's test score within the full set.
Display the result as a percentage with one decimal place and order the output by score ascending.
| employee_id | name | score |
|---|---|---|
| E1 | Tanaka | 70 |
| E2 | Suzuki | 90 |
| E3 | Sato | 60 |
| E4 | Takahashi | 85 |
| E5 | Ito | 75 |
| name | score | percentile |
|---|---|---|
| Sato | 60 | 0.0% |
| Tanaka | 70 | 25.0% |
| Ito | 75 | 50.0% |
| Takahashi | 85 | 75.0% |
| Suzuki | 90 | 100.0% |
PERCENT_RANK = (rank-1)/(n-1). With five rows, the denominator is 4. Tanaka has rank 2, so (2-1)/4 = 0.25 → 25.0%.
- 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
Writing a long query containing window functions as nested subqueries can make it difficult to read. A WITH clause (CTE: Common Table Expression) provides a modern top-to-bottom structure: first build an intermediate table with window functions, then use it for further calculations.
WITH cte_name AS ( -- ① Build an intermediate table with a window function SELECT ..., SUM(col) OVER(...) AS running_total FROM tbl ) -- ② Reference the intermediate table for further calculations SELECT ..., running_total / total * 100 AS pct FROM cte_name;
SUM(revenue) OVER(), the function aggregates across all rows, producing the table-wide total. This lets one CTE calculate both the grand total and the running total.Using the monthly_revenue table below and a WITH clause, output each month's revenue together with the following two calculated values.
1. running_total — cumulative revenue from January
2. monthly_pct — that month's share of total revenue (percent, one decimal place).
| month | revenue |
|---|---|
| Jan | 200 |
| Feb | 250 |
| Mar | 180 |
| Apr | 320 |
| May | 290 |
| month | revenue | running_total | monthly_pct |
|---|---|---|---|
| Jan | 200 | 200 | 16.1% |
| Feb | 250 | 450 | 20.2% |
| Mar | 180 | 630 | 14.5% |
| Apr | 320 | 950 | 25.8% |
| May | 290 | 1240 | 23.4% |
Grand total = 200 + 250 + 180 + 320 + 290 = 1240. Jan: 200 ÷ 1240 × 100 = 16.1%.
- 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
Using LAG (the previous row) and LEAD (the next row) together enables a three-point comparison with adjacent values. This is a common time-series pattern for detecting peaks (local maxima), valleys (local minima), and sudden changes.
LAG(amount) OVER(ORDER BY date) AS prev_amount -- Value from one row earlier LEAD(amount) OVER(ORDER BY date) AS next_amount -- Value from one row later -- Compare three points with CASE WHEN → detect a peak CASE WHEN amount > prev_amount AND amount > next_amount THEN 'PEAK' END
comparison with NULL (> NULL) is UNKNOWN; because CASE WHEN does not treat UNKNOWN as true, the first and last rows are automatically excluded from peak detection. This is the intended behavior.Using the daily_sales table below, output the previous day's sales as prev_amount and the next day's sales as next_amount beside each date. In the is_peak column, output 'PEAK' when sales exceed both adjacent days; otherwise output the empty string ''.
| date | amount |
|---|---|
| 04-01 | 80 |
| 04-02 | 160 |
| 04-03 | 110 |
| 04-04 | 190 |
| 04-05 | 130 |
| 04-06 | 175 |
| date | amount | prev_amount | next_amount | is_peak |
|---|---|---|---|---|
| 04-01 | 80 | NULL | 160 | |
| 04-02 | 160 | 80 | 110 | PEAK |
| 04-03 | 110 | 160 | 190 | |
| 04-04 | 190 | 110 | 130 | PEAK |
| 04-05 | 130 | 190 | 175 | |
| 04-06 | 175 | 130 | NULL |
Apr-02: 160 > 80 and 160 > 110 → PEAK. Apr-06: because LEAD is NULL, the comparison is UNKNOWN and does not satisfy CASE WHEN → not PEAK.
- 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 PARTITION BY (group partitioning) with ROWS BETWEEN (frame specification) creates a window that moves only within each group. This is one of the most practical window-function patterns.
AVG(amount) OVER( PARTITION BY store_id -- Separate the window by store ORDER BY date -- Order by date within each partition ROWS BETWEEN 1 PRECEDING AND CURRENT ROW -- One preceding row + current row = two-day average )
Using the store_daily_sales table below, calculate an independent moving average over the latest two rows (moving_avg_2d) for each store (store_id).
The window must reset whenever the store changes.
| store_id | date | amount |
|---|---|---|
| S1 | 04-01 | 100 |
| S1 | 04-02 | 140 |
| S1 | 04-03 | 120 |
| S1 | 04-04 | 160 |
| S2 | 04-01 | 200 |
| S2 | 04-02 | 180 |
| S2 | 04-03 | 220 |
| S2 | 04-04 | 190 |
| store_id | date | amount | moving_avg_2d |
|---|---|---|---|
| S1 | 04-01 | 100 | 100.0 |
| S1 | 04-02 | 140 | 120.0 |
| S1 | 04-03 | 120 | 130.0 |
| S1 | 04-04 | 160 | 140.0 |
| S2 | 04-01 | 200 | 200.0 |
| S2 | 04-02 | 180 | 190.0 |
| S2 | 04-03 | 220 | 200.0 |
| S2 | 04-04 | 190 | 205.0 |
S2 on Apr-01 is the first row in the S2 partition, so its average uses only one row and equals 200.0. The value from S1 on Apr-04 is not carried over.
- 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