Loading learning content...
Real-world database hierarchies rarely fit perfectly into either Single Table Inheritance (STI) or Table-Per-Type (TPT). Complex domains present mixed characteristics— some subtypes are nearly identical while others are vastly different; some parts of the hierarchy are queried together while others are always accessed separately.
The Hybrid Approach recognizes that STI and TPT aren't mutually exclusive. Skilled database architects strategically combine both patterns within the same hierarchy, applying each where it excels:
This page teaches you to analyze hierarchies systematically and design hybrid schemas that capture the best of both worlds.
By the end of this page, you will master hybrid pattern identification, learn to decompose complex hierarchies into optimal sub-patterns, implement production-ready hybrid schemas, and apply these techniques to real-world case studies from e-commerce, healthcare, and media systems.
Before exploring hybrid solutions, let's understand why pure approaches sometimes create unacceptable trade-offs:
Scenario 1: Mixed Subtype Similarity
Consider an e-commerce Product hierarchy:
Product (supertype)├── PhysicalProduct│ ├── Electronics (many unique attrs: voltage, warranty, specs)│ ├── Clothing (many unique attrs: size, color, material, care)│ └── Furniture (many unique attrs: dimensions, weight, assembly)├── DigitalProduct │ ├── Software (few unique attrs: license_type, platform)│ ├── Ebook (few unique attrs: format, page_count)│ └── MusicTrack (few unique attrs: duration, bitrate)└── ServiceProduct ├── Subscription (unique attrs: billing_cycle, renewal_date) └── Consultation (unique attrs: hours, consultant_id) PROBLEM ANALYSIS:- Pure STI: Would need 30+ columns; Electronics/Clothing/Furniture are too different for one table- Pure TPT: 9 subtype tables; overkill for similar DigitalProducts- Neither pure approach is optimalScenario 2: Mixed Query Patterns
Different parts of the hierarchy have different access patterns:
| Entity Group | Primary Query Pattern | Query Frequency | Optimal Pattern |
|---|---|---|---|
| PhysicalProducts together | Inventory, shipping queries span all physical types | Very High | Benefits from STI (no joins) |
| Electronics specifically | Tech specs queries, warranty lookups | High | Benefits from TPT (dedicated table) |
| DigitalProducts together | Download links, license checks | Medium | Benefits from STI (simple) |
| All Products together | Catalog search, pricing updates | Medium | Benefits from supertype table |
Scenario 3: Different Relationship Requirements
Some subtypes need FK relationships; others don't:
When analyzing a hierarchy reveals mixed similarity levels, mixed query patterns, or mixed relationship needs, a hybrid approach is likely optimal. The goal is to partition the hierarchy into groups that internally benefit from the same pattern.
There are several established hybrid patterns. Understanding each helps you select and combine them appropriately.
Pattern 1: TPT at Top, STI for Leaf Clusters
Use TPT to separate major categories, then STI within similar subcategories:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
-- PATTERN 1: TPT at top level, STI for similar leaves-- E-commerce Product Hierarchy -- Level 0: Root supertype (all products)CREATE TABLE product ( product_id SERIAL PRIMARY KEY, sku VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, description TEXT, base_price DECIMAL(12,2) NOT NULL, currency CHAR(3) DEFAULT 'USD', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- Level 1: Major categories via TPT-- Physical products with inventory/shipping concernsCREATE TABLE physical_product ( product_id INTEGER PRIMARY KEY REFERENCES product(product_id), weight_kg DECIMAL(8,3) NOT NULL, length_cm DECIMAL(8,2), width_cm DECIMAL(8,2), height_cm DECIMAL(8,2), warehouse_id INTEGER REFERENCES warehouse(warehouse_id), inventory_count INTEGER DEFAULT 0, reorder_level INTEGER DEFAULT 10); -- Digital products with download concernsCREATE TABLE digital_product ( product_id INTEGER PRIMARY KEY REFERENCES product(product_id), -- STI for digital subtypes (they're similar) digital_type VARCHAR(20) NOT NULL CHECK (digital_type IN ('SOFTWARE', 'EBOOK', 'MUSIC', 'VIDEO')), file_url TEXT NOT NULL, file_size_mb DECIMAL(10,2), download_limit INTEGER, -- Type-specific but same-table (STI within TPT) license_type VARCHAR(30), -- SOFTWARE page_count INTEGER, -- EBOOK duration_sec INTEGER, -- MUSIC, VIDEO format VARCHAR(20) -- All); -- Service products with scheduling concernsCREATE TABLE service_product ( product_id INTEGER PRIMARY KEY REFERENCES product(product_id), service_type VARCHAR(20) NOT NULL CHECK (service_type IN ('SUBSCRIPTION', 'CONSULTATION', 'INSTALLATION')), -- Service-specific attributes billing_cycle VARCHAR(20), -- SUBSCRIPTION consultant_id INTEGER, -- CONSULTATION estimated_hours DECIMAL(5,2) -- CONSULTATION, INSTALLATION); -- Level 2: Further TPT only where needed (complex physical products)CREATE TABLE electronics ( product_id INTEGER PRIMARY KEY REFERENCES physical_product(product_id), voltage VARCHAR(20), wattage DECIMAL(8,2), warranty_months INTEGER DEFAULT 12, energy_rating VARCHAR(10), tech_specs JSONB); CREATE TABLE clothing ( product_id INTEGER PRIMARY KEY REFERENCES physical_product(product_id), sizes_available VARCHAR(100), -- e.g., "S,M,L,XL" colors_available VARCHAR(200), material VARCHAR(100), care_instructions TEXT, gender VARCHAR(20));Pattern 2: Shared Core with Separate Extensions
Keep shared attributes in supertype, put subtype-specific in extension tables:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
-- PATTERN 2: Core table + optional extension tables-- User system with varying detail levels -- Core user table (STI-like: contains most data)CREATE TABLE user_account ( user_id SERIAL PRIMARY KEY, user_type VARCHAR(20) NOT NULL CHECK (user_type IN ('BASIC', 'PREMIUM', 'ENTERPRISE', 'ADMIN')), email VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, display_name VARCHAR(100), avatar_url TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, -- Common preferences (STI-style, some may be null) timezone VARCHAR(50) DEFAULT 'UTC', locale VARCHAR(10) DEFAULT 'en_US', email_notifications BOOLEAN DEFAULT TRUE); -- OPTIONAL extension for Premium users (extra details)CREATE TABLE premium_user_profile ( user_id INTEGER PRIMARY KEY REFERENCES user_account(user_id), subscription_id VARCHAR(100), billing_email VARCHAR(255), plan_tier VARCHAR(20) NOT NULL, renewal_date DATE, features_json JSONB, storage_quota_gb INTEGER DEFAULT 100, api_calls_limit INTEGER DEFAULT 10000); -- OPTIONAL extension for Enterprise users (complex requirements)CREATE TABLE enterprise_user_profile ( user_id INTEGER PRIMARY KEY REFERENCES user_account(user_id), organization_id INTEGER NOT NULL REFERENCES organization(org_id), employee_id VARCHAR(50), department VARCHAR(100), sso_provider VARCHAR(50), sso_id VARCHAR(255), access_level VARCHAR(20), data_region VARCHAR(20), compliance_flags JSONB); -- OPTIONAL extension for Admin users (sensitive attributes)CREATE TABLE admin_user_profile ( user_id INTEGER PRIMARY KEY REFERENCES user_account(user_id), admin_level INTEGER NOT NULL CHECK (admin_level BETWEEN 1 AND 5), permissions JSONB NOT NULL, requires_2fa BOOLEAN DEFAULT TRUE, audit_all_actions BOOLEAN DEFAULT TRUE, last_security_review DATE); -- Query patterns:-- Basic users: just query user_account-- Premium users: LEFT JOIN premium_user_profile (optional)-- Enterprise: INNER JOIN enterprise_user_profile (required) SELECT u.*, p.plan_tier, p.renewal_dateFROM user_account uLEFT JOIN premium_user_profile p ON u.user_id = p.user_idWHERE u.user_type = 'PREMIUM';Pattern 3: Concrete Tables with Shared Lookup
Table-Per-Concrete-Class (TPC) with shared reference data:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
-- PATTERN 3: TPC + shared reference tables-- Payment methods with type-specific tables -- Shared identity tracking (not full supertype)CREATE TABLE payment_method_registry ( payment_method_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), method_type VARCHAR(20) NOT NULL, user_id INTEGER NOT NULL REFERENCES user_account(user_id), is_default BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Pointer to type-specific table -- NOT a FK (polymorphic reference) UNIQUE (user_id, is_default) -- Partial unique: only one default per user -- Actually need WHERE is_default = true); -- Concrete table: Credit cardsCREATE TABLE credit_card ( payment_method_id UUID PRIMARY KEY REFERENCES payment_method_registry(payment_method_id), card_last_four CHAR(4) NOT NULL, card_brand VARCHAR(20) NOT NULL, expiry_month INTEGER NOT NULL CHECK (expiry_month BETWEEN 1 AND 12), expiry_year INTEGER NOT NULL, cardholder_name VARCHAR(100) NOT NULL, billing_address_id INTEGER REFERENCES address(address_id), stripe_token VARCHAR(255) -- tokenized storage); -- Concrete table: Bank accountsCREATE TABLE bank_account ( payment_method_id UUID PRIMARY KEY REFERENCES payment_method_registry(payment_method_id), account_last_four CHAR(4) NOT NULL, routing_number VARCHAR(20) NOT NULL, bank_name VARCHAR(100) NOT NULL, account_type VARCHAR(20) NOT NULL CHECK (account_type IN ('CHECKING', 'SAVINGS')), plaid_access_token VARCHAR(255)); -- Concrete table: Digital wallets CREATE TABLE digital_wallet ( payment_method_id UUID PRIMARY KEY REFERENCES payment_method_registry(payment_method_id), wallet_provider VARCHAR(20) NOT NULL CHECK (wallet_provider IN ('PAYPAL', 'APPLE_PAY', 'GOOGLE_PAY')), wallet_email VARCHAR(255), wallet_account_id VARCHAR(255)); -- Query a user's payment methods:SELECT r.payment_method_id, r.method_type, r.is_default, COALESCE( 'Card ending ' || cc.card_last_four, 'Bank account ending ' || ba.account_last_four, dw.wallet_provider || ' wallet' ) AS display_nameFROM payment_method_registry rLEFT JOIN credit_card cc ON r.payment_method_id = cc.payment_method_idLEFT JOIN bank_account ba ON r.payment_method_id = ba.payment_method_idLEFT JOIN digital_wallet dw ON r.payment_method_id = dw.payment_method_idWHERE r.user_id = 123;Let's apply hybrid design to a complex real-world scenario: a healthcare records system with multiple participant types and document classifications.
Requirements:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
-- HEALTHCARE RECORDS: Hybrid Schema Design -- ═══════════════════════════════════════════════════════════════-- PARTICIPANT HIERARCHY (TPT for major types, STI for provider subtypes)-- ═══════════════════════════════════════════════════════════════ -- Base participant (supertype)CREATE TABLE participant ( participant_id SERIAL PRIMARY KEY, participant_type VARCHAR(20) NOT NULL CHECK (participant_type IN ('PATIENT', 'PROVIDER', 'ADMIN')), -- Universal attributes first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, date_of_birth DATE NOT NULL, ssn_encrypted BYTEA, -- Encrypted SSN email VARCHAR(255) UNIQUE, phone VARCHAR(20), address_id INTEGER REFERENCES address(address_id), emergency_contact JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT TRUE); -- Patient-specific table (TPT - many unique attributes)CREATE TABLE patient ( participant_id INTEGER PRIMARY KEY REFERENCES participant(participant_id), mrn VARCHAR(20) NOT NULL UNIQUE, -- Medical Record Number insurance_id INTEGER REFERENCES insurance_policy(policy_id), blood_type VARCHAR(3), allergies JSONB, primary_physician_id INTEGER, -- Will reference provider preferred_pharmacy_id INTEGER REFERENCES pharmacy(pharmacy_id), advance_directive BOOLEAN DEFAULT FALSE, organ_donor BOOLEAN DEFAULT FALSE); -- Provider table (TPT from participant, STI within for subtypes)CREATE TABLE provider ( participant_id INTEGER PRIMARY KEY REFERENCES participant(participant_id), -- STI discriminator for provider subtypes provider_type VARCHAR(20) NOT NULL CHECK (provider_type IN ('PHYSICIAN', 'NURSE', 'TECHNICIAN', 'THERAPIST')), npi_number VARCHAR(10) NOT NULL UNIQUE, -- National Provider ID license_number VARCHAR(50) NOT NULL, license_state CHAR(2) NOT NULL, license_expiry DATE NOT NULL, department_id INTEGER REFERENCES department(dept_id), hire_date DATE NOT NULL, -- STI COLUMNS: Provider-type-specific attributes -- PHYSICIAN specific specialty VARCHAR(100), -- NULL for non-physicians board_certified BOOLEAN, -- NULL for non-physicians accepting_patients BOOLEAN, -- NULL for non-physicians -- NURSE specific nurse_type VARCHAR(20), -- NULL for non-nurses (RN, LPN, NP) certifications JSONB, -- NULL for non-nurses -- TECHNICIAN specific tech_specialty VARCHAR(50), -- NULL for non-techs (Radiology, Lab, etc.) equipment_certified JSONB -- NULL for non-techs); -- FK from patient to provider (now valid)ALTER TABLE patient ADD CONSTRAINT fk_primary_physicianFOREIGN KEY (primary_physician_id) REFERENCES provider(participant_id); -- Admin staff (TPT - separate concerns)CREATE TABLE admin_staff ( participant_id INTEGER PRIMARY KEY REFERENCES participant(participant_id), employee_id VARCHAR(20) NOT NULL UNIQUE, department_id INTEGER REFERENCES department(dept_id), role VARCHAR(50) NOT NULL, access_level INTEGER NOT NULL CHECK (access_level BETWEEN 1 AND 5), supervisor_id INTEGER REFERENCES admin_staff(participant_id)); -- ═══════════════════════════════════════════════════════════════-- MEDICAL RECORD HIERARCHY (TPT for categories, STI for similar types)-- ═══════════════════════════════════════════════════════════════ -- Base medical recordCREATE TABLE medical_record ( record_id SERIAL PRIMARY KEY, record_category VARCHAR(20) NOT NULL CHECK (record_category IN ('CLINICAL', 'DIAGNOSTIC', 'ADMINISTRATIVE')), patient_id INTEGER NOT NULL REFERENCES patient(participant_id), provider_id INTEGER NOT NULL REFERENCES provider(participant_id), encounter_date TIMESTAMP NOT NULL, facility_id INTEGER REFERENCES facility(facility_id), status VARCHAR(20) DEFAULT 'DRAFT', signed_at TIMESTAMP, signed_by INTEGER REFERENCES provider(participant_id)); -- Clinical records (complex, TPT extends further)CREATE TABLE clinical_record ( record_id INTEGER PRIMARY KEY REFERENCES medical_record(record_id), clinical_type VARCHAR(30) NOT NULL CHECK (clinical_type IN ('PROGRESS_NOTE', 'PROCEDURE', 'CONSULTATION', 'DISCHARGE_SUMMARY')), chief_complaint TEXT, assessment TEXT, plan TEXT, icd_codes VARCHAR(10)[], cpt_codes VARCHAR(10)[]); -- Diagnostic records (STI within - all diagnostics are similar)CREATE TABLE diagnostic_record ( record_id INTEGER PRIMARY KEY REFERENCES medical_record(record_id), -- STI for diagnostic subtypes diagnostic_type VARCHAR(20) NOT NULL CHECK (diagnostic_type IN ('LAB', 'IMAGING', 'PATHOLOGY', 'GENETIC')), order_id INTEGER REFERENCES diagnostic_order(order_id), results_json JSONB NOT NULL, reference_ranges JSONB, interpretation TEXT, is_abnormal BOOLEAN, -- Type-specific (STI columns) -- LAB specific specimen_type VARCHAR(50), -- Blood, Urine, etc. collection_time TIMESTAMP, -- IMAGING specific modality VARCHAR(20), -- CT, MRI, X-Ray, etc. body_part VARCHAR(50), contrast_used BOOLEAN, dicom_study_uid VARCHAR(100), -- Link to imaging system -- PATHOLOGY specific gross_description TEXT, microscopic_description TEXT, diagnosis_text TEXT);| Component | Pattern Used | Rationale |
|---|---|---|
| Participant hierarchy | TPT (major types) | Patient, Provider, Admin have very different attributes and relationships |
| Provider subtypes | STI within TPT | Physician, Nurse, Technician share many attributes (license, department); few unique attrs |
| Clinical records | TPT extension | Progress notes, procedures need specialized structure; often queried by type |
| Diagnostic records | STI within TPT | Labs, imaging similar structure; often queried together for patient overview |
This hybrid design enables FK from Patient to Provider (impossible in pure STI), supports type-specific queries efficiently, and avoids excessive tables for similar diagnostic types. The schema adapts to the natural structure of healthcare data.
Use this systematic process to design hybrid schemas for any complex hierarchy:
Step 1: Map the Complete Hierarchy
Start by documenting the full hierarchy with ALL attributes for each entity type. Don't simplify prematurely.
Step 2: Analyze Attribute Distribution
For each subtype, classify attributes: ┌─────────────────────────────────────────────────────────────┐│ ATTRIBUTE DISTRIBUTION MATRIX ││─────────────────────────────────────────────────────────────││ Entity │Shared Attrs│Unique Attrs│Similarity Score│ ││─────────────────────────────────────────────────────────────││ Supertype │ 10 │ 0 │ N/A │ ││ Subtype A │ 10 │ 3 │ HIGH (77%) │ ││ Subtype B │ 10 │ 4 │ HIGH (71%) │ ││ Subtype C │ 10 │ 15 │ LOW (40%) │ ││ Subtype D │ 10 │ 20 │ LOW (33%) │ │└─────────────────────────────────────────────────────────────┘ Similarity Score = Shared / (Shared + Unique) Guidelines: HIGH (>60%): Good candidate for STI grouping MEDIUM (40-60%): Case-by-case evaluation LOW (<40%): Strong candidate for TPT separationStep 3: Analyze Query Patterns
For each major query pattern, identify scope: ┌──────────────────────────────────────────────────────────────┐│ QUERY PATTERN ANALYSIS ││──────────────────────────────────────────────────────────────││ Query Description │ Scope │ Frequency ││──────────────────────────────────────────────────────────────││ "Search all products" │ Full hierarchy │ Very High ││ "List physical inventory" │ Physical subtree │ High ││ "Get electronics specs" │ Single subtype │ High ││ "Process digital download" │ Digital subtree │ Medium ││ "Admin: all items for tax" │ Full hierarchy │ Low │└──────────────────────────────────────────────────────────────┘ STI favored for: Queries spanning multiple types togetherTPT favored for: Queries targeting single types with type-specific dataStep 4: Identify Relationship Requirements
Step 5: Group and Partition
Based on analysis, partition the hierarchy into groups:
Decision Tree for Each Subtype: ┌───────────────────────┐ │ Does subtype have FK │ │ relationships TO it? │ └───────────┬───────────┘ │ ┌───────YES─┴─NO────────┐ ▼ ▼ ┌──────────┐ ┌─────────────────┐ │ Must use │ │Similar to other │ │ TPT │ │ subtypes (>60%)?│ └──────────┘ └────────┬────────┘ │ ┌───────YES─┴─NO─────────┐ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ Group together │ │Many unique │ │ for STI │ │attributes? │ └────────────────┘ └────────┬───────┘ │ ┌──────YES─┴─NO─────┐ ▼ ▼ ┌──────────┐ ┌──────────┐ │ Use TPT │ │ Use STI │ └──────────┘ └──────────┘Step 6: Design Schema and Validate
Create the schema, then validate against original requirements:
Hybrid schemas introduce unique challenges. Let's address the common ones:
Challenge 1: Querying Across Pattern Boundaries
When STI and TPT sections need to be joined:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
-- Scenario: Query physicalProduct (TPT) and its electronics details (also TPT)-- where electronics is a further specialization -- Straightforward: TPT-to-TPTSELECT p.name, pp.weight_kg, e.voltage, e.warranty_monthsFROM product pJOIN physical_product pp ON p.product_id = pp.product_idJOIN electronics e ON pp.product_id = e.product_id; -- Complex: Mix of TPT and STI in same query-- Query all digital products (STI) with their download statsSELECT p.name, dp.digital_type, dp.file_size_mb, CASE dp.digital_type WHEN 'SOFTWARE' THEN dp.license_type WHEN 'EBOOK' THEN dp.page_count::TEXT WHEN 'MUSIC' THEN (dp.duration_sec / 60)::TEXT || ' minutes' END AS type_specific_info, ds.download_countFROM product pJOIN digital_product dp ON p.product_id = dp.product_idLEFT JOIN download_stats ds ON p.product_id = ds.product_idORDER BY ds.download_count DESC; -- Create views to abstract complexityCREATE VIEW v_electronics ASSELECT p.*, pp.weight_kg, pp.inventory_count, e.voltage, e.warranty_months, e.tech_specsFROM product pJOIN physical_product pp ON p.product_id = pp.product_idJOIN electronics e ON pp.product_id = e.product_id; CREATE VIEW v_digital_products ASSELECT p.*, dp.digital_type, dp.file_url, dp.file_size_mb, dp.license_type, dp.page_count, dp.duration_secFROM product pJOIN digital_product dp ON p.product_id = dp.product_id;Challenge 2: ORM Mapping for Hybrid Schemas
Most ORMs prefer either pure STI or pure TPT. Hybrid requires careful configuration:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
# SQLAlchemy: Hybrid inheritance mappingfrom sqlalchemy import Column, Integer, String, ForeignKeyfrom sqlalchemy.orm import relationshipfrom sqlalchemy.ext.declarative import declared_attr # Base Product (top level)class Product(Base): __tablename__ = 'product' product_id = Column(Integer, primary_key=True) sku = Column(String(50), nullable=False, unique=True) name = Column(String(200), nullable=False) base_price = Column(Numeric(12, 2), nullable=False) # Discriminator for top-level TPT product_category = Column(String(20)) __mapper_args__ = { 'polymorphic_on': product_category, 'polymorphic_identity': 'product' } # Physical Product (TPT from Product)class PhysicalProduct(Product): __tablename__ = 'physical_product' product_id = Column(Integer, ForeignKey('product.product_id'), primary_key=True) weight_kg = Column(Numeric(8, 3)) inventory_count = Column(Integer, default=0) __mapper_args__ = { 'polymorphic_identity': 'physical' } # Electronics (TPT from PhysicalProduct)class Electronics(PhysicalProduct): __tablename__ = 'electronics' product_id = Column(Integer, ForeignKey('physical_product.product_id'), primary_key=True) voltage = Column(String(20)) warranty_months = Column(Integer) __mapper_args__ = { 'polymorphic_identity': 'electronics' } # Digital Product (TPT from Product, STI within)class DigitalProduct(Product): __tablename__ = 'digital_product' product_id = Column(Integer, ForeignKey('product.product_id'), primary_key=True) digital_type = Column(String(20), nullable=False) # STI discriminator file_url = Column(String) file_size_mb = Column(Numeric(10, 2)) # STI attributes (shared table) license_type = Column(String(30)) # SOFTWARE page_count = Column(Integer) # EBOOK duration_sec = Column(Integer) # MUSIC __mapper_args__ = { 'polymorphic_identity': 'digital', 'polymorphic_on': digital_type # Second-level discriminator } # Software (STI child of DigitalProduct - no new table!)class Software(DigitalProduct): # No __tablename__ - uses parent table (STI) __mapper_args__ = { 'polymorphic_identity': 'SOFTWARE' } class Ebook(DigitalProduct): __mapper_args__ = { 'polymorphic_identity': 'EBOOK' }Challenge 3: Schema Evolution
Adding new subtypes in hybrid schemas requires thinking about where they fit:
We've explored how to design and implement hybrid specialization mappings that combine the best aspects of STI and TPT.
What's Next:
The final page provides a comprehensive Trade-offs Analysis—a quantitative comparison of all approaches across multiple dimensions with decision frameworks for real-world selection.
You now understand how to design hybrid specialization mappings that optimize for real-world requirements. You can analyze hierarchies systematically, identify optimal pattern combinations, and implement production-ready hybrid schemas.