Loading learning content...
Throughout this module, we've explored the theoretical foundations of ternary relationships: what they are, how to specify cardinality, when to use them, and whether to decompose them. Now it's time to consolidate this knowledge through detailed, realistic examples.
Each example in this page follows a complete workflow:
These examples span healthcare, education, supply chain, and entertainment domains—giving you exposure to ternary relationships across diverse contexts.
By the end of this page, you will be able to recognize, analyze, model, and implement ternary relationships confidently in real-world database design projects. These worked examples serve as templates for your own designs.
Business Scenario: A hospital needs to track which doctors prescribe which medications to which patients. The system must record prescription details and enforce that only authorized doctors can prescribe certain controlled substances.
Requirements Gathered:
Atomic Fact Test: 'Dr. Smith prescribed Lisinopril to Patient Jones on 2024-01-15' — All three entities are essential. ✓
Independent Existence Test: Doctor-Patient (treatment relationship) exists independently? Possibly. Doctor-Medication (authorization) exists independently? Yes. Patient-Medication (receiving) only makes sense with a doctor. The prescription event is the core fact. ✓
Relationship Attributes: Dosage, date, refills depend on all three. ✓
Conclusion: True ternary relationship required.
Cardinality Analysis:
Apply the hold-two-constant test:
Result: M:M:M cardinality — The most flexible pattern.
123456789101112131415161718192021222324252627282930313233343536373839404142
-- Relational Schema for Medical PrescriptionsCREATE TABLE Doctor ( doctor_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, specialty VARCHAR(50), license_number VARCHAR(20) UNIQUE NOT NULL); CREATE TABLE Medication ( medication_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, generic_name VARCHAR(100), controlled_class CHAR(2) -- NULL, C2, C3, C4, C5); CREATE TABLE Patient ( patient_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, birth_date DATE, insurance_id VARCHAR(20)); -- Ternary relationship as associative entityCREATE TABLE Prescription ( prescription_id INT PRIMARY KEY, doctor_id INT NOT NULL REFERENCES Doctor(doctor_id), medication_id INT NOT NULL REFERENCES Medication(medication_id), patient_id INT NOT NULL REFERENCES Patient(patient_id), dosage VARCHAR(50) NOT NULL, prescription_date DATE NOT NULL, refills INT DEFAULT 0, instructions TEXT, -- M:M:M allows multiple prescriptions for same triple (over time) -- If we wanted at most one active prescription per triple: -- UNIQUE (doctor_id, medication_id, patient_id) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Create indexes for common query patternsCREATE INDEX idx_prescription_doctor ON Prescription(doctor_id);CREATE INDEX idx_prescription_patient ON Prescription(patient_id);CREATE INDEX idx_prescription_medication ON Prescription(medication_id);Business Scenario: A manufacturing company sources parts from multiple suppliers for various construction projects. Each project has specific requirements, and suppliers may offer the same part at different prices depending on the project and negotiated contracts.
Requirements Gathered:
Cardinality for SUPPLY relationship:
Result: M:M:M with a business rule (one primary per Part-Project) enforced via constraint.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
-- Relational Schema for Supply ChainCREATE TABLE Supplier ( supplier_id INT PRIMARY KEY, company_name VARCHAR(100) NOT NULL, contact_name VARCHAR(100), email VARCHAR(100), phone VARCHAR(20), rating DECIMAL(2,1) CHECK (rating BETWEEN 1.0 AND 5.0)); CREATE TABLE Part ( part_id INT PRIMARY KEY, part_number VARCHAR(50) UNIQUE NOT NULL, description VARCHAR(200), unit_of_measure VARCHAR(20), standard_cost DECIMAL(12,2)); CREATE TABLE Project ( project_id INT PRIMARY KEY, project_name VARCHAR(100) NOT NULL, start_date DATE, end_date DATE, budget DECIMAL(15,2), status VARCHAR(20) DEFAULT 'Planning'); -- Ternary relationship: SUPPLYCREATE TABLE Supply ( supply_id INT PRIMARY KEY, supplier_id INT NOT NULL REFERENCES Supplier(supplier_id), part_id INT NOT NULL REFERENCES Part(part_id), project_id INT NOT NULL REFERENCES Project(project_id), quantity INT NOT NULL CHECK (quantity > 0), unit_price DECIMAL(12,2) NOT NULL, is_primary BOOLEAN DEFAULT FALSE, contract_date DATE, delivery_schedule VARCHAR(100), lead_time_days INT, -- Allow multiple supply records for same triple (different contracts/times) UNIQUE (supplier_id, part_id, project_id, contract_date)); -- Enforce: at most one primary supplier per (Part, Project)-- Using partial unique index (PostgreSQL syntax)CREATE UNIQUE INDEX idx_supply_primary ON Supply(part_id, project_id) WHERE is_primary = TRUE; -- Sample queries demonstrating ternary relationship usage -- Q1: Who supplies Widget-A to Project Alpha?-- SELECT s.company_name, sp.unit_price-- FROM Supply sp-- JOIN Supplier s ON sp.supplier_id = s.supplier_id-- WHERE sp.part_id = (SELECT part_id FROM Part WHERE part_number = 'Widget-A')-- AND sp.project_id = (SELECT project_id FROM Project WHERE project_name = 'Alpha'); -- Q2: What's the total cost for Project Alpha from each supplier?-- SELECT s.company_name, SUM(sp.quantity * sp.unit_price) as total_cost-- FROM Supply sp-- JOIN Supplier s ON sp.supplier_id = s.supplier_id-- WHERE sp.project_id = (SELECT project_id FROM Project WHERE project_name = 'Alpha')-- GROUP BY s.company_name;Business Scenario: A university research department tracks which professors advise which students on which research projects. Each advising relationship has a role (primary advisor, co-advisor, committee member) and spans a specific time period.
Requirements Gathered:
Key Question: Is 'Professor advises Student on Project' an atomic fact?
Yes! Professor Adams advising Student Baker is only meaningful in the context of a specific project. The same professor-student pair might have a different (or no) relationship on a different project.
Relationship Attributes: Role, start_date, end_date, meeting_frequency all depend on the specific (Professor, Student, Project) combination.
Conclusion: True ternary relationship required.
Cardinality Analysis:
Apply the hold-two-constant test:
Result: M:M:M cardinality with business rules (one primary per student-project) enforced via constraints.
Constraint Note: The rule 'one primary advisor per student per project' means for each (Student, Project) with role='Primary', there's at most one Professor. This is a constrained subset within the M:M:M relationship.
1234567891011121314151617181920212223242526272829303132333435363738394041
-- Key queries for the advising system -- Q1: Find all advisors for a specific student on a specific projectSELECT p.name as professor, a.role, a.start_dateFROM Advises aJOIN Professor p ON a.professor_id = p.professor_idWHERE a.student_id = 123 AND a.project_id = 456ORDER BY CASE a.role WHEN 'Primary' THEN 1 WHEN 'Co-Advisor' THEN 2 ELSE 3 END; -- Q2: Find all students advised by Prof. Adams across all projectsSELECT s.name as student, pr.title as project, a.roleFROM Advises aJOIN Student s ON a.student_id = s.student_idJOIN Project pr ON a.project_id = pr.project_idWHERE a.professor_id = (SELECT professor_id FROM Professor WHERE name = 'Adams') AND a.end_date IS NULL -- Currently activeORDER BY pr.title, a.role; -- Q3: Projects where the same professor-student pair appearsSELECT p.name as professor, s.name as student, STRING_AGG(pr.title, ', ') as projects, COUNT(*) as project_countFROM Advises aJOIN Professor p ON a.professor_id = p.professor_idJOIN Student s ON a.student_id = s.student_idJOIN Project pr ON a.project_id = pr.project_idGROUP BY p.professor_id, p.name, s.student_id, s.nameHAVING COUNT(*) > 1; -- Q4: Verify constraint - at most one primary per (student, project)SELECT student_id, project_id, COUNT(*) as primary_countFROM AdvisesWHERE role = 'Primary'GROUP BY student_id, project_idHAVING COUNT(*) > 1; -- Should return empty if constraint holdsBusiness Scenario: A sports analytics company tracks player performance across teams and seasons. Players can transfer between teams, and historical statistics must be preserved. Performance metrics are recorded for each player-team-season combination.
Requirements Gathered:
Important Finding: 'A player can be on at most one team per season.'
This means: For each (Player, Season) pair, there exists at most one Team.
In ternary cardinality notation:
This is NOT M:M:M! The cardinality is M:1:M.
Detailed Cardinality Analysis:
Result: M:1:M cardinality
Functional Dependency Identified:
Player_ID, Season_ID → Team_ID
This FD means the candidate key for the relationship table is (Player_ID, Season_ID), and Team_ID is a dependent attribute!
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
-- Relational Schema for Sports Statistics-- Note: This exploits the M:1:M cardinality for optimal design CREATE TABLE Player ( player_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, birth_date DATE, nationality VARCHAR(50), position VARCHAR(30)); CREATE TABLE Team ( team_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, city VARCHAR(50), league VARCHAR(50), founded_year INT); CREATE TABLE Season ( season_id INT PRIMARY KEY, year_start INT NOT NULL, year_end INT NOT NULL, description VARCHAR(50), -- e.g., "2023-2024 Regular Season" UNIQUE(year_start, year_end)); -- Ternary relationship with M:1:M cardinality-- Key insight: PRIMARY KEY is (player_id, season_id) due to FDCREATE TABLE PlayerSeasonStats ( player_id INT NOT NULL REFERENCES Player(player_id), season_id INT NOT NULL REFERENCES Season(season_id), team_id INT NOT NULL REFERENCES Team(team_id), -- Determined by player+season -- Statistics (relationship attributes) games_played INT DEFAULT 0, goals INT DEFAULT 0, assists INT DEFAULT 0, minutes_played INT DEFAULT 0, yellow_cards INT DEFAULT 0, red_cards INT DEFAULT 0, rating DECIMAL(3,2), -- Primary key exploits the functional dependency PRIMARY KEY (player_id, season_id) -- Note: team_id is NOT part of the key -- because Player + Season → Team); -- The FD means we can query efficiently: -- Q1: What team was Player X on in 2023 season?-- Single lookup by primary key!SELECT t.name as teamFROM PlayerSeasonStats pssJOIN Team t ON pss.team_id = t.team_idWHERE pss.player_id = 123 AND pss.season_id = (SELECT season_id FROM Season WHERE year_start = 2023); -- Q2: All players on Team Y in Season ZSELECT p.name, pss.goals, pss.assistsFROM PlayerSeasonStats pssJOIN Player p ON pss.player_id = p.player_idWHERE pss.team_id = 456 AND pss.season_id = 789ORDER BY pss.goals DESC; -- Q3: Player's career across all teamsSELECT s.description as season, t.name as team, pss.games_played, pss.goals, pss.assistsFROM PlayerSeasonStats pssJOIN Season s ON pss.season_id = s.season_idJOIN Team t ON pss.team_id = t.team_idWHERE pss.player_id = 123ORDER BY s.year_start;Let's synthesize the patterns observed across our four examples:
| Example | Cardinality | Key of Relationship Table | Key Design Insight |
|---|---|---|---|
| Prescription | M:M:M | prescription_id (surrogate) | Full flexibility; surrogate key for repeated triples over time |
| Supply Chain | M:M:M | supply_id (surrogate) | Business rule (one primary) enforced via partial unique index |
| Academic Advising | M:M:M | advising_id (surrogate) | Role attribute distinguishes primary from other advisors |
| Sports Statistics | M:1:M | (player_id, season_id) | FD allows reduced key; team_id is dependent attribute |
Notice how the Sports Statistics example's M:1:M cardinality led to a fundamentally different (and more efficient) table design than the M:M:M examples. Correct cardinality analysis directly impacts physical schema design and query performance.
Key Patterns Observed:
M:M:M requires surrogate keys: When all cardinalities are M, the natural key would be the full triple. Using a surrogate key (prescription_id, supply_id, advising_id) simplifies foreign key references and allows multiple records for the same triple over time.
Constraints beyond cardinality: The 'one primary' rules in Supply and Advising are additional business rules that cardinality doesn't capture. These require triggers, partial indexes, or application logic.
FDs reduce key size: When cardinality includes 1s, functional dependencies reduce the candidate key, simplifying indexing and referential integrity.
Relationship attributes are always present: Every example has meaningful attributes on the ternary relationship—confirming that ternary was the right choice.
Learning from mistakes is as valuable as learning from successes. Here are common anti-patterns where ternary relationships were used incorrectly, with corrections:
A common anti-pattern is modeling transitive relationships as ternary. If A→B and B→C naturally, don't model A-B-C as ternary. Use the two binary relationships and derive the transitive connection via joins.
Use this comprehensive checklist when designing ternary relationships:
This module has provided a comprehensive treatment of ternary relationships—from theoretical foundations through practical implementation. The examples in this final page demonstrate how the concepts apply across diverse domains.
Congratulations! You have completed the comprehensive module on Ternary Relationships. You now possess the theoretical understanding and practical skills to recognize, analyze, design, and implement ternary relationships in real-world database projects. This capability is essential for modeling complex business scenarios that go beyond simple binary associations.