Ranks say "third". These say "top quarter" or "better than 89% of the field":
| Function | Returns | For the best of 19 batters | For the worst |
|---|---|---|---|
NTILE(4) |
which of 4 equal-ish buckets the row falls in | 1 (with DESC order) |
4 |
PERCENT_RANK() |
(rank − 1) ÷ (rows − 1), from 0 to 1 | 1 | 0 |
CUME_DIST() |
share of rows at or below this one, above 0 up to 1 | 1 | 1 ÷ 19 |
The test case
2025 batters who faced at least 80 balls: their runs, balls, strike rate (one decimal), which quarter of the strike-rate table they're in (1 = fastest), and their PERCENT_RANK and CUME_DIST (two decimals, higher = faster).
Expected result
| PLAYER_NAME | RUNS | BALLS | SR | QUARTILE | PR | CD |
|---|---|---|---|---|---|---|
| Tyler Vance | 123 | 82 | 150 | 1 | 1 | 1 |
| Karthik Raman | 366 | 245 | 149.4 | 1 | 0.94 | 0.95 |
| Oliver Hartley | 234 | 157 | 149 | 1 | 0.89 | 0.89 |
| Harry Whitcombe | 191 | 131 | 145.8 | 1 | 0.83 | 0.84 |
| Ryan Govender | 215 | 148 | 145.3 | 1 | 0.78 | 0.79 |
| Siddharth Iyer | 273 | 190 | 143.7 | 2 | 0.72 | 0.74 |
| Hamza Siddiqui | 257 | 181 | 142 | 2 | 0.67 | 0.68 |
| Finn McKenzie | 120 | 87 | 137.9 | 2 | 0.61 | 0.63 |
| Sipho Ndlovu | 272 | 202 | 134.7 | 2 | 0.56 | 0.58 |
| Rory Paterson | 116 | 87 | 133.3 | 2 | 0.5 | 0.53 |
| Asif Mirza | 104 | 81 | 128.4 | 3 | 0.44 | 0.47 |
| Faisal Qureshi | 123 | 96 | 128.1 | 3 | 0.39 | 0.42 |
| Pieter van Wyk | 172 | 138 | 124.6 | 3 | 0.33 | 0.37 |
| Arjun Mehta | 165 | 142 | 116.2 | 3 | 0.28 | 0.32 |
| Jaylen Brooks | 123 | 108 | 113.9 | 3 | 0.22 | 0.26 |
| Josh Kerrigan | 125 | 110 | 113.6 | 4 | 0.17 | 0.21 |
| Vignesh Babu | 145 | 130 | 111.5 | 4 | 0.11 | 0.16 |
| James Thornton | 89 | 80 | 111.3 | 4 | 0.06 | 0.11 |
| Tane Parata | 143 | 133 | 107.5 | 4 | 0 | 0.05 |
19 batters don't split evenly into 4: NTILE makes the first buckets one bigger, so it's 5, 5, 5
and 4. Tyler Vance at number 5 for Tampa tops the strike rates, just ahead of Karthik Raman: proof
that you don't need many balls to be a nuisance.
Way 1: the three functions
with faced as (
select batter_id, count(*) as balls, sum(runs_batter) as runs
from cr_balls_v
where season = 2025 and (extra_type is null or extra_type <> 'WD')
group by batter_id
having count(*) >= 80
)
select p.player_name, f.runs, f.balls,
round(100 * f.runs / f.balls, 1) as sr,
ntile(4) over (order by f.runs / f.balls desc, p.player_name) as quartile,
round(percent_rank() over (order by f.runs / f.balls), 2) as pr,
round(cume_dist() over (order by f.runs / f.balls), 2) as cd
from faced f
join cr_players p on p.player_id = f.batter_id
order by sr desc, p.player_name;
NTILE sorts fastest first (DESC) so bucket 1 is the fastest; the other two sort ascending so
that higher means faster. Each window can have its own order.
Way 2: the arithmetic behind them
with faced as (
select batter_id, count(*) as balls, sum(runs_batter) as runs
from cr_balls_v
where season = 2025 and (extra_type is null or extra_type <> 'WD')
group by batter_id
having count(*) >= 80
),
ranked as (
select p.player_name, f.runs, f.balls,
row_number() over (order by f.runs / f.balls desc, p.player_name) as rn,
rank() over (order by f.runs / f.balls) as rnk_up,
count(*) over (order by f.runs / f.balls) as at_or_below,
count(*) over () as n
from faced f
join cr_players p on p.player_id = f.batter_id
),
sized as (
select r.*, trunc(n / 4) as q, mod(n, 4) as extra from ranked r -- 19 rows: q = 4, extra = 3
)
select player_name, runs, balls,
round(100 * runs / balls, 1) as sr,
case when rn <= extra * (q + 1) then ceil(rn / (q + 1)) -- the first buckets get one more row
else extra + ceil((rn - extra * (q + 1)) / q)
end as quartile,
round((rnk_up - 1) / (n - 1), 2) as pr,
round(at_or_below / n, 2) as cd
from sized
order by sr desc, player_name;
What the functions mean, written out:
NTILE(4): with 19 rows, 4 buckets of 4 and 3 left over. The first 3 buckets each get one of the leftovers (5 rows), the last gets 4.PERCENT_RANK:(rank − 1) / (n − 1).CUME_DIST: rows at or below this one, divided byn.count(*) over (order by ...)is a running count with the defaultRANGEframe, so tied rows are counted together: exactly "at or below".
Worth understanding, not worth maintaining. Use Way 1.
19c and 26ai
The same on both.
Gotchas
NTILEsplits rows, not values. Two batters with the same strike rate can land in different quartiles. Add a tie-breaker so it's at least repeatable.PERCENT_RANKof the first row is 0, never "1st percentile".CUME_DISTnever reaches 0.- Small groups make coarse percentiles. With 19 batters, each step is about 5%.