What You'll Build
In this exercise you design and implement the complete CricketVerse PostgreSQL schema: a normalised 3NF logical design covering all nine entities and their relationships, followed by the physical design (indexes and partitioning) that meets the non-functional requirements. This is the foundation on which Exercises 2 and 3 build. Attempt the design yourself before reading the reference solution — sketch the ER model, decide embedding versus referencing, identify the partition key for the deliveries fact table, and plan the index strategy from the functional requirements.
The exercise has three parts. Part A is the logical schema: CREATE TABLE statements for all nine entities with primary keys, foreign keys, NOT NULL and CHECK constraints, and UNIQUE constraints on natural identifiers. Part B is the physical schema: indexes on all foreign keys and analytical filter columns, plus range partitioning on the deliveries table by match date. Part C is the verification: an audit confirming 3NF compliance, foreign key index coverage, and correct partition configuration.
Requirements and Acceptance Criteria
Part A — Reference Solution: Logical Schema
The reference schema demonstrates several deliberate design decisions. The deliveries table records batter_id, bowler_id, non_striker_id, and fielder_id as four separate foreign keys to the same players table — a single player entity playing multiple roles per delivery. The fielder_id is nullable because not every delivery involves a fielder (a dot ball or a boundary has no fielder). The match_date column is denormalised into the innings and deliveries tables specifically to support the composite foreign key to the partitioned matches table — required because PostgreSQL mandates that foreign keys to partitioned tables include the partition key.
The DO block at the end programmatically creates yearly partitions for all three fact tables (matches, innings, deliveries) using a nested loop — generating CREATE TABLE PARTITION OF statements for years 2020 through 2025 plus a DEFAULT partition for each. This automation pattern is cleaner than writing 21 individual CREATE TABLE statements and demonstrates how partition management can be scripted. In production, the pg_partman extension would automate ongoing partition creation as new years arrive.
Part B — Reference Solution: Physical Design
The index strategy in Part B is driven directly by the functional requirements. The composite index idx_deliveries_h2h on (batter_id, bowler_id) with INCLUDE (runs_batter, is_wicket) directly supports FR2 (head-to-head analysis) — it allows an index-only scan to compute how a batter performs against a bowler without reading the heap. The partial index idx_deliveries_wickets on wicket deliveries only supports bowling analysis efficiently by indexing the small subset of deliveries that resulted in wickets. The three separate role indexes (batter, bowler, fielder) support the different player-centric analytical queries independently.
The fielder_id index uses a partial WHERE fielder_id IS NOT NULL clause because the fielder_id column is nullable — many deliveries (dot balls, boundaries) have no fielder. A partial index that excludes NULL values is smaller and faster than a full index, and fielding analysis queries always filter for non-null fielder_id anyway. This is the partial index pattern from Module 3 applied to a real requirement: index only the rows that queries actually need.