The Coastal Cricket League (CCL) is made up. Six teams, two seasons (2025 and 2026), 32 matches, and every one of its 7,551 deliveries. The teams, players and grounds are invented; any resemblance to a real player is a coincidence (and no agent will ring about image rights). What is real is the shape of the data: it behaves like real T20 cricket, so the questions you ask of it are the ones analysts ask of real matches.
The league
| TEAM_CODE | TEAM_NAME | VENUE_NAME |
|---|---|---|
| TPA | Tampa Tides | Bayshore Oval |
| CHN | Chennai Comets | Marina Ground |
| LDS | Leeds Larks | Kirkstall Park |
| DBN | Durban Dhows | Umgeni Oval |
| AKL | Auckland Albatross | Harbourside Park |
| KHI | Karachi Kites | Clifton Ground |
select t.team_code, t.team_name, v.venue_name
from cr_teams t
join cr_venues v on v.venue_id = t.home_venue_id
order by t.team_id;
Each season, every team plays every other team once (15 league matches, at the home team's ground), then the top two meet in a final. Winning is worth 2 points, a tie 1.
| SEASON | CHAMPION | RUNNER_UP | WIN_TYPE | WIN_MARGIN |
|---|---|---|---|---|
| 2025 | Chennai Comets | Durban Dhows | WICKETS | 6 |
| 2026 | Leeds Larks | Chennai Comets | WICKETS | 6 |
select m.season,
w.team_name as champion,
l.team_name as runner_up,
m.win_type,
m.win_margin
from cr_matches m
join cr_teams w on w.team_id = m.winner_id
join cr_teams l on l.team_id = case when m.winner_id = m.home_team_id
then m.away_team_id
else m.home_team_id end
where m.stage = 'FINAL'
order by m.season;
The tables
CR_VENUES 1──< CR_TEAMS 1──< CR_PLAYERS
│ │ │
└────< CR_MATCHES >───────────┘ (player of the match)
│
1
│
^
CR_DELIVERIES (batter, non-striker, bowler, player out, fielder → CR_PLAYERS)
CR_MATCHES, one row per match:
| Column | Meaning |
|---|---|
MATCH_ID |
1 to 32, in date order |
SEASON, MATCH_NO |
2025 or 2026; 1 to 16 within the season (16 is the final) |
STAGE |
LEAGUE or FINAL |
MATCH_DATE, VENUE_ID |
When and where |
HOME_TEAM_ID, AWAY_TEAM_ID |
The two teams. "Home" just means it was their ground |
TOSS_WINNER_ID, TOSS_DECISION |
Who won the toss, and whether they chose to BAT or FIELD first |
WINNER_ID |
The winner; null for a tie |
WIN_TYPE, WIN_MARGIN |
RUNS (batting first won), WICKETS (the chase won) or TIE; and by how many |
PLAYER_OF_MATCH_ID |
The award |
CR_DELIVERIES, one row per ball. The key is (MATCH_ID, INNINGS_NO, SEQ_NO):
| Column | Meaning |
|---|---|
INNINGS_NO |
1 (batting first) or 2 (the chase) |
SEQ_NO |
1, 2, 3... every delivery of the innings in order, wides and no-balls included. This is the column to order by |
OVER_NO |
1 to 20 |
BALL_NO |
Delivery number within the over, extras included (so it can reach 7 or 8) |
BATTING_TEAM_ID, BOWLING_TEAM_ID |
Who's batting and who's bowling |
BATTER_ID, NON_STRIKER_ID, BOWLER_ID |
Who faced it, who was at the other end, who bowled it |
RUNS_BATTER |
Runs off the bat: 0, 1, 2, 3, 4 or 6 |
RUNS_EXTRAS, EXTRA_TYPE |
Extras and their kind: WD, NB, LB, B (null if none) |
WICKET_KIND |
bowled, caught, lbw, stumped, run out; null if nobody was out |
PLAYER_OUT_ID, FIELDER_ID |
Who was out (on a run out it can be the non-striker), and who caught or ran them out |
CR_BALLS_V is CR_DELIVERIES joined to CR_MATCHES (for SEASON, MATCH_NO,
MATCH_DATE), plus columns most questions need:
| Column | How it's worked out |
|---|---|
OVER_BALL |
Scorer's notation like 15.2, counting legal balls only |
TOTAL_RUNS |
RUNS_BATTER + RUNS_EXTRAS |
IS_LEGAL |
1 unless it was a wide or a no-ball |
IS_FOUR, IS_SIX |
1 for a four or a six off the bat |
IS_DOT |
1 when nothing at all was scored and it was a legal ball (a wicket ball can be a dot) |
IS_WICKET |
1 for any dismissal |
IS_BOWLER_WICKET |
1 for a dismissal credited to the bowler (not run outs) |
The 1/0 columns are numbers on purpose, so you can add them up: sum(is_six) is the number
of sixes. (26ai has a real BOOLEAN type; Part 10 shows it. The dataset sticks to what runs on 19c.)
A quick feel for it, the top run-scorers of 2025:
| PLAYER_NAME | TEAM_CODE | RUNS | BALLS | STRIKE_RATE |
|---|---|---|---|---|
| Karthik Raman | CHN | 366 | 245 | 149.4 |
| Siddharth Iyer | CHN | 273 | 190 | 143.7 |
| Sipho Ndlovu | DBN | 272 | 202 | 134.7 |
| Hamza Siddiqui | KHI | 257 | 181 | 142 |
| Oliver Hartley | LDS | 234 | 157 | 149 |
select p.player_name,
t.team_code,
sum(b.runs_batter) as runs,
sum(case when b.extra_type = 'WD' then 0 else 1 end) as balls,
round(100 * sum(b.runs_batter)
/ sum(case when b.extra_type = 'WD' then 0 else 1 end), 1) as strike_rate
from cr_balls_v b
join cr_players p on p.player_id = b.batter_id
join cr_teams t on t.team_id = p.team_id
where b.season = 2025
group by p.player_name, t.team_code
order by runs desc
fetch first 5 rows only;
Balls faced leave out wides, because a wide isn't counted as a ball the batter faced. Notice
case when b.extra_type = 'WD' then 0 else 1 end: when EXTRA_TYPE is null, null = 'WD' is
not true, so the ELSE branch counts the ball. That's NULL logic working for us, and Part 1
covers it properly.
How we made the data
The data comes from a small cricket simulator (tools/make_cricket.py in the course repo),
run once with a fixed random seed. The same seed always gives the same matches, ball for ball,
so every expected result in this guide stays true for everyone.
What the simulator does, ball by ball:
- Picks the bowler for each over from the fielding side's five best bowlers: at most 4 overs each, never two overs in a row, fast bowlers favored in the powerplay and at the death, spinners in the middle overs.
- Decides if it's a wide or a no-ball (about 3% and 0.7% of deliveries). Those are bowled again, so the over gets an extra delivery.
- Decides the outcome of a legal ball: a dot, 1, 2, 3, 4, 6 or a wicket. The chances depend on the phase (more fours in the powerplay, more sixes and more wickets at the death) and on skill ratings: an opener scores more and gets out less than a tail-ender, and a strike bowler concedes less than a part-timer.
- Picks how a wicket happened: mostly caught, then bowled, lbw, run out, and stumped (only off spinners). A catch goes to a fielder, sometimes the keeper, sometimes the bowler ("caught and bowled"). A run out can dismiss the non-striker.
- Rotates the strike on odd runs and at the end of each over, and sends in the next batter after a wicket.
- Ends the innings after 20 overs or 10 wickets, and ends the chase the moment the target is reached.
Then it works out the result, the points table, which two teams reach the final, and the player of the match (the best all-round contribution on the winning side).
The numbers come out like real T20: first innings average about 162, the highest score is 207, there are two centuries, and teams are bowled out now and then.
Planted on purpose
A few patterns are rare in real cricket but too good to leave out of a SQL guide, so the simulator forces them at fixed places. Everything else is left to chance.
| Match | What we planted | Used in |
|---|---|---|
| 1 (2025), 1st innings | A hat-trick in one over: over 18, balls 2, 3, 4 | Part 6 |
| 20 (2026), 1st innings | A hat-trick spread across two overs: last ball of over 17, first two of over 19, same bowler | Part 6 |
| 3 (2025), 1st innings | Four sixes in a row by one batter: over 16 | Part 6 |
| 25 (2026), 2nd innings | A collapse: wickets tumbling in overs 9 to 11 | Part 6 |
| 11 (2025), 1st innings | A maiden over in the powerplay: over 3 | Part 5 |
What the simulator leaves out
Real cricket has more than our data does: rain-shortened matches, super overs, free hits after no-balls, rarer dismissals (hit wicket, obstructing the field), retired batters and substitutes. None of that changes how the SQL works, and leaving it out keeps the data easy to reason about.
Try it yourself
How many matches did each team win in 2025? (Hint: one team didn't win any. Make sure it still shows up with 0. And the wins only add up to 15 of the 16 matches: one was a tie.)
Try itShow the answer
| TEAM_NAME | WINS |
|---|---|
| Durban Dhows | 5 |
| Chennai Comets | 4 |
| Karachi Kites | 3 |
| Tampa Tides | 2 |
| Leeds Larks | 1 |
| Auckland Albatross | 0 |
select t.team_name, count(m.match_id) as wins
from cr_teams t
left join cr_matches m on m.winner_id = t.team_id and m.season = 2025
group by t.team_name
order by wins desc, t.team_name;
The season filter sits in the ON clause, not in WHERE. In WHERE, m.season = 2025 would
throw away Auckland's row (its M.SEASON is null) and the 0 would disappear. And count(m.match_id),
not count(*): count(*) would count Auckland's empty row as 1.
Downloads
Plain SQL text: open it in any editor before you run it.
- Cricket datacricket_install.sqlCreates the CR_ tables and loads the league (19c and 26ai)
- As CSV filescricket-csv.zipThe same five tables as CSV, for other tools
Part 0 quiz
5 questions on all of Part 0. Get 4 right to pass. Retake it as often as you like.