Loading learning content...
We've now thoroughly explored categories (union types) at the conceptual level—what they are, how selective inheritance works, and when to use partial versus total participation. But conceptual models don't run on servers. The final, critical step is mapping these EER constructs to actual relational database schemas.
Mapping categories presents unique challenges that don't arise with regular specialization/generalization:
This page provides a complete guide to category mapping strategies, their trade-offs, and implementation patterns used in production systems.
By the end of this page, you will understand the primary category mapping strategies, how to implement polymorphic references, trade-offs between approaches, and best practices for real-world category implementations.
Mapping categories to relational schemas follows a different process than mapping regular specialization. The key challenges are:
1. Heterogeneous Primary Keys Superclass tables often have different primary key structures:
2. Polymorphic References The category table must reference ANY of its superclass tables—but standard foreign keys can only reference ONE table.
3. Uniform Access Requirement Relationships TO the category (e.g., VEHICLE.owner_id → OWNER) need a single, uniform key to reference.
The Fundamental Mapping Approach:
Unlike specialization (which can use shared primary keys), categories typically require:
Category Table Structure:
┌────────────────────────────────────────────────────────┐
│ OWNER │
├────────────────────────────────────────────────────────┤
│ owner_id (PK) - Surrogate key │
│ owner_type - Discriminator ('PERSON', 'COMPANY'...) │
│ ref_id - FK to superclass (type-dependent) │
│ [category-own attributes...] │
└────────────────────────────────────────────────────────┘
Surrogate keys solve the heterogeneous PK problem. Whether the underlying superclass uses SSN, INT, or UUID, the category's surrogate key provides a uniform reference point. Relationships like VEHICLE.owner_id always reference the category's owner_id, never directly to superclass PKs.
The standard category mapping strategy creates a dedicated table for the category with discriminator and polymorphic reference columns. This is the most widely used approach.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
-- ============================================-- SUPERCLASS TABLES (Independent existence)-- ============================================ CREATE TABLE person ( ssn CHAR(11) PRIMARY KEY, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, date_of_birth DATE NOT NULL, email VARCHAR(100), phone VARCHAR(20)); CREATE TABLE company ( company_id INT PRIMARY KEY AUTO_INCREMENT, company_name VARCHAR(100) NOT NULL, tax_id CHAR(10) UNIQUE NOT NULL, founded_date DATE, industry VARCHAR(50), employee_count INT); CREATE TABLE government_agency ( agency_id INT PRIMARY KEY AUTO_INCREMENT, agency_name VARCHAR(100) NOT NULL, agency_code VARCHAR(10) UNIQUE NOT NULL, jurisdiction ENUM('FEDERAL', 'STATE', 'LOCAL') NOT NULL, parent_agency_id INT, FOREIGN KEY (parent_agency_id) REFERENCES government_agency(agency_id)); -- ============================================-- CATEGORY TABLE-- ============================================ CREATE TABLE owner ( -- Surrogate Primary Key owner_id INT PRIMARY KEY AUTO_INCREMENT, -- Type Discriminator owner_type ENUM('PERSON', 'COMPANY', 'GOVERNMENT_AGENCY') NOT NULL, -- Polymorphic Reference (stored as string for flexibility) ref_key VARCHAR(50) NOT NULL, -- Category-own attributes registration_date DATE NOT NULL DEFAULT (CURRENT_DATE), credit_score INT CHECK (credit_score BETWEEN 300 AND 850), is_active BOOLEAN NOT NULL DEFAULT TRUE, -- Composite unique constraint: no duplicate references per type UNIQUE (owner_type, ref_key), -- Index for efficient lookup by type INDEX idx_owner_type (owner_type), INDEX idx_owner_ref (owner_type, ref_key)); -- ============================================-- RELATIONSHIP USING CATEGORY-- ============================================ CREATE TABLE vehicle ( vin CHAR(17) PRIMARY KEY, make VARCHAR(50) NOT NULL, model VARCHAR(50) NOT NULL, year YEAR NOT NULL, color VARCHAR(30), -- Reference to CATEGORY (not directly to superclasses) owner_id INT NOT NULL, purchase_date DATE, FOREIGN KEY (owner_id) REFERENCES owner(owner_id) ON DELETE RESTRICT);Key Elements of This Mapping:
| Element | Purpose |
|---|---|
owner_id (surrogate PK) | Provides uniform reference for all category instances |
owner_type (discriminator) | Identifies which superclass table to join |
ref_key (polymorphic FK) | Stores the actual superclass primary key value |
UNIQUE (owner_type, ref_key) | Prevents duplicate registrations of same superclass instance |
| Category-own attributes | Attributes that apply to all owners regardless of type |
The polymorphic reference (how the category points to its superclass) can be implemented in several ways, each with trade-offs.
Strategy 1: Single Polymorphic Column
Store the superclass key in a single, type-flexible column:
1234567891011121314151617181920
CREATE TABLE owner ( owner_id INT PRIMARY KEY AUTO_INCREMENT, owner_type ENUM('PERSON', 'COMPANY', 'GOVERNMENT_AGENCY') NOT NULL, -- Single column stores any PK type (as string) ref_key VARCHAR(50) NOT NULL, -- ... other columns UNIQUE (owner_type, ref_key)); -- Query joining to PERSON superclassSELECT o.owner_id, p.first_name, p.last_nameFROM owner oJOIN person p ON o.owner_type = 'PERSON' AND o.ref_key = p.ssn; -- Query joining to COMPANY superclass SELECT o.owner_id, c.company_nameFROM owner oJOIN company c ON o.owner_type = 'COMPANY' AND o.ref_key = CAST(c.company_id AS CHAR);Pros:
Cons:
For most scenarios, the Multiple Nullable Columns strategy provides the best balance of referential integrity and query simplicity. Use Single Column when you have many superclass types or frequently add new ones. Use Junction Tables when you need junction-specific attributes or maximum normalization.
The single-column polymorphic reference strategy cannot use standard FK constraints. Referential integrity must be enforced through triggers or application logic.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
-- ============================================-- Trigger to enforce polymorphic referential integrity-- ============================================ DELIMITER // CREATE TRIGGER owner_ref_integrity_insertBEFORE INSERT ON ownerFOR EACH ROWBEGIN DECLARE ref_exists INT DEFAULT 0; DECLARE error_msg VARCHAR(200); -- Check reference based on type CASE NEW.owner_type WHEN 'PERSON' THEN SELECT COUNT(*) INTO ref_exists FROM person WHERE ssn = NEW.ref_key; SET error_msg = CONCAT('Person SSN ', NEW.ref_key, ' does not exist'); WHEN 'COMPANY' THEN SELECT COUNT(*) INTO ref_exists FROM company WHERE company_id = CAST(NEW.ref_key AS UNSIGNED); SET error_msg = CONCAT('Company ID ', NEW.ref_key, ' does not exist'); WHEN 'GOVERNMENT_AGENCY' THEN SELECT COUNT(*) INTO ref_exists FROM government_agency WHERE agency_id = CAST(NEW.ref_key AS UNSIGNED); SET error_msg = CONCAT('Government Agency ID ', NEW.ref_key, ' does not exist'); END CASE; IF ref_exists = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = error_msg; END IF;END // -- Also trigger on UPDATECREATE TRIGGER owner_ref_integrity_updateBEFORE UPDATE ON ownerFOR EACH ROWBEGIN DECLARE ref_exists INT DEFAULT 0; -- Only check if ref_key or owner_type changed IF NEW.ref_key != OLD.ref_key OR NEW.owner_type != OLD.owner_type THEN CASE NEW.owner_type WHEN 'PERSON' THEN SELECT COUNT(*) INTO ref_exists FROM person WHERE ssn = NEW.ref_key; WHEN 'COMPANY' THEN SELECT COUNT(*) INTO ref_exists FROM company WHERE company_id = CAST(NEW.ref_key AS UNSIGNED); WHEN 'GOVERNMENT_AGENCY' THEN SELECT COUNT(*) INTO ref_exists FROM government_agency WHERE agency_id = CAST(NEW.ref_key AS UNSIGNED); END CASE; IF ref_exists = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Referenced entity does not exist'; END IF; END IF;END // -- Prevent superclass deletion if referenced by ownerCREATE TRIGGER person_delete_checkBEFORE DELETE ON personFOR EACH ROWBEGIN IF EXISTS (SELECT 1 FROM owner WHERE owner_type = 'PERSON' AND ref_key = OLD.ssn) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete person: referenced as owner'; END IF;END // CREATE TRIGGER company_delete_checkBEFORE DELETE ON companyFOR EACH ROWBEGIN IF EXISTS (SELECT 1 FROM owner WHERE owner_type = 'COMPANY' AND ref_key = CAST(OLD.company_id AS CHAR)) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete company: referenced as owner'; END IF;END // DELIMITER ;Each new superclass requires new trigger logic or trigger updates. When using triggers for RI, document them thoroughly and include them in code reviews. Forgotten trigger updates when adding superclasses cause subtle data integrity issues.
Working with mapped categories requires understanding common query patterns for accessing data across the heterogeneous structure.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
-- ============================================-- PATTERN 1: Get all category instances with resolved names-- ============================================SELECT o.owner_id, o.owner_type, o.registration_date, CASE o.owner_type WHEN 'PERSON' THEN ( SELECT CONCAT(first_name, ' ', last_name) FROM person WHERE ssn = o.ref_key ) WHEN 'COMPANY' THEN ( SELECT company_name FROM company WHERE company_id = CAST(o.ref_key AS UNSIGNED) ) WHEN 'GOVERNMENT_AGENCY' THEN ( SELECT agency_name FROM government_agency WHERE agency_id = CAST(o.ref_key AS UNSIGNED) ) END AS owner_nameFROM owner oWHERE o.is_active = TRUE; -- ============================================-- PATTERN 2: Unified view for common access-- ============================================CREATE VIEW v_owner_unified ASSELECT o.owner_id, o.owner_type, o.ref_key, o.registration_date, o.credit_score, o.is_active, -- Unified name column COALESCE( CONCAT(p.first_name, ' ', p.last_name), c.company_name, g.agency_name ) AS display_name, -- Type-specific columns (null for non-matching types) p.email AS person_email, p.date_of_birth AS person_dob, c.tax_id AS company_tax_id, c.employee_count AS company_employees, g.jurisdiction AS agency_jurisdictionFROM owner oLEFT JOIN person p ON o.owner_type = 'PERSON' AND o.ref_key = p.ssnLEFT JOIN company c ON o.owner_type = 'COMPANY' AND o.ref_key = CAST(c.company_id AS CHAR)LEFT JOIN government_agency g ON o.owner_type = 'GOVERNMENT_AGENCY' AND o.ref_key = CAST(g.agency_id AS CHAR); -- ============================================-- PATTERN 3: Type-filtered queries-- ============================================ -- All corporate owners with their detailsSELECT o.owner_id, c.company_name, c.tax_id, c.employee_count, COUNT(v.vin) AS vehicles_ownedFROM owner oJOIN company c ON o.ref_key = CAST(c.company_id AS CHAR)LEFT JOIN vehicle v ON v.owner_id = o.owner_idWHERE o.owner_type = 'COMPANY' AND o.is_active = TRUEGROUP BY o.owner_id, c.company_name, c.tax_id, c.employee_count; -- ============================================-- PATTERN 4: Navigation through category in relationships-- ============================================ -- All vehicles with owner information (any type)SELECT v.vin, v.make, v.model, v.year, vu.display_name AS owner_name, vu.owner_typeFROM vehicle vJOIN v_owner_unified vu ON v.owner_id = vu.owner_id; -- ============================================-- PATTERN 5: Existence checks across types-- ============================================ -- Find persons who are NOT vehicle ownersSELECT p.ssn, p.first_name, p.last_nameFROM person pWHERE NOT EXISTS ( SELECT 1 FROM owner o WHERE o.owner_type = 'PERSON' AND o.ref_key = p.ssn); -- Find any superclass instance that IS an ownerSELECT 'PERSON' AS type, p.ssn AS id, CONCAT(p.first_name, ' ', p.last_name) AS nameFROM person pWHERE EXISTS (SELECT 1 FROM owner o WHERE o.owner_type = 'PERSON' AND o.ref_key = p.ssn)UNION ALLSELECT 'COMPANY', CAST(c.company_id AS CHAR), c.company_nameFROM company cWHERE EXISTS (SELECT 1 FROM owner o WHERE o.owner_type = 'COMPANY' AND o.ref_key = CAST(c.company_id AS CHAR));Create a unified VIEW (like v_owner_unified) for your category. Application code references the view for common operations, hiding the join complexity. Type-specific queries can still access underlying tables directly when needed.
Choosing the right mapping strategy depends on your specific requirements. Here's a comprehensive comparison to guide your decision.
| Criteria | Single Column | Multiple Columns | Junction Tables |
|---|---|---|---|
| Schema Simplicity | ✓✓ Simple | ✓ Moderate | ✗ Complex |
| Referential Integrity | ✗ Triggers only | ✓✓ Native FKs | ✓✓ Native FKs |
| Query Complexity | ✓ Moderate | ✓✓ Simple | ✗ Complex |
| Adding Superclasses | ✓✓ No schema change | ✗ Add column + FK | ✗ Add table |
| Storage Efficiency | ✓✓ Minimal | ✓ Some NULLs | ✓ Normalized |
| Type-Safe Operations | ✗ String casting | ✓✓ Native types | ✓✓ Native types |
| ORM Support | ✓ Moderate | ✓✓ Good | ✓ Moderate |
Real-world category implementations often combine strategies and include additional production concerns. Here are patterns used in enterprise systems.
Pattern: Externalized Type Definition
Instead of ENUM, use a lookup table for owner types. This enables metadata and extensibility:
12345678910111213141516171819202122232425262728293031323334353637383940
-- Type lookup table for extensibilityCREATE TABLE owner_type ( type_code VARCHAR(20) PRIMARY KEY, display_name VARCHAR(50) NOT NULL, table_name VARCHAR(50) NOT NULL, -- Superclass table pk_column VARCHAR(50) NOT NULL, -- PK column in superclass pk_type VARCHAR(20) NOT NULL, -- 'INT', 'CHAR', 'UUID' is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); INSERT INTO owner_type VALUES('PERSON', 'Individual Person', 'person', 'ssn', 'CHAR', TRUE, NOW()),('COMPANY', 'Business Entity', 'company', 'company_id', 'INT', TRUE, NOW()),('GOVT_AGENCY', 'Government Agency', 'government_agency', 'agency_id', 'INT', TRUE, NOW()); CREATE TABLE owner ( owner_id INT PRIMARY KEY AUTO_INCREMENT, owner_type_code VARCHAR(20) NOT NULL, ref_key VARCHAR(50) NOT NULL, -- ... other columns FOREIGN KEY (owner_type_code) REFERENCES owner_type(type_code), UNIQUE (owner_type_code, ref_key)); -- Dynamic query generation using type metadataSELECT CONCAT( 'SELECT o.owner_id, ', 't.display_name, ', 's.', t.pk_column, ' AS source_key ', 'FROM owner o ', 'JOIN owner_type t ON o.owner_type_code = t.type_code ', 'JOIN ', t.table_name, ' s ON o.ref_key = ', CASE t.pk_type WHEN 'INT' THEN CONCAT('CAST(s.', t.pk_column, ' AS CHAR)') ELSE CONCAT('s.', t.pk_column) END) AS generated_queryFROM owner_type tWHERE t.type_code = 'PERSON';We've completed our exploration of category mapping—the final step in translating EER categories to working database implementations. Let's consolidate the key insights:
Congratulations! You've completed the comprehensive study of categories in EER modeling. You now understand: what categories are and when to use them, how selective inheritance works, the difference between partial and total participation, and how to map categories to relational schemas. This knowledge enables you to model complex real-world scenarios where entities from different types need to play unified roles.
Categories in the Broader EER Context:
Categories complete our coverage of EER's advanced modeling constructs:
| Construct | Purpose | Key Insight |
|---|---|---|
| Specialization | Refine superclass into subtypes | Top-down, attribute refinement |
| Generalization | Abstract subtypes into superclass | Bottom-up, common abstraction |
| Inheritance | Share attributes/relationships | Single path (specialization) or selective (category) |
| Categories | Unify unrelated types into role | Multiple superclasses, mutual exclusivity |
Together, these constructs enable sophisticated semantic modeling that captures real-world complexity far beyond basic ER diagrams.
Next Steps:
With EER modeling mastered, you're prepared to:
The conceptual modeling skills from this chapter serve as the foundation for all subsequent database design work.