From harness-claude
Models inheritance hierarchies and polymorphic relationships in relational databases using single-table inheritance (STI), class-table inheritance (CTI), concrete-table inheritance, and shared foreign keys. For type-varying entities like notifications or payments.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Modeling inheritance hierarchies and type-varying relationships in relational databases using single-table inheritance (STI), class-table inheritance (CTI), or shared foreign key patterns.
Explains EAV schema pattern for dynamic user-defined attributes stored as rows, with SQL examples, tradeoffs vs JSONB, and use cases like product catalogs or medical records.
Designs relational and NoSQL database schemas with patterns for normalization/denormalization, entity relationships, indexing, migrations, audit trails, partitioning, and access pattern optimization.
Designs PostgreSQL/MySQL schemas with normalization (1NF-3NF), relationships, constraints, audit columns. For new databases, reviews, migrations, or fixing missing PKs/FKs, wrong types, denormalization, EAV.
Share bugs, ideas, or general feedback.
Modeling inheritance hierarchies and type-varying relationships in relational databases using single-table inheritance (STI), class-table inheritance (CTI), or shared foreign key patterns.
Three strategies exist for modeling "a row can reference one of several entity types":
1. Single-Table Inheritance (STI)
One table, a discriminator column (type), and nullable type-specific columns:
CREATE TABLE vehicles (
id serial PRIMARY KEY,
type varchar NOT NULL CHECK (type IN ('truck', 'car', 'motorcycle')),
make varchar NOT NULL,
model varchar NOT NULL,
-- truck-specific
payload_capacity_kg int,
-- car-specific
passenger_count int,
-- motorcycle-specific
engine_cc int
);
Trucks use payload_capacity_kg, cars use passenger_count, motorcycles use engine_cc. Unused columns are NULL.
Tradeoffs: fast queries (no JOINs), simple schema. But NULLs proliferate, per-type CHECK constraints become complex, and the table widens with every new type.
2. Class-Table Inheritance (CTI)
A shared base table plus per-type tables joined by foreign key:
CREATE TABLE vehicles (
id serial PRIMARY KEY,
type varchar NOT NULL,
make varchar NOT NULL,
model varchar NOT NULL
);
CREATE TABLE trucks (
vehicle_id int PRIMARY KEY REFERENCES vehicles(id),
payload_capacity_kg int NOT NULL
);
CREATE TABLE cars (
vehicle_id int PRIMARY KEY REFERENCES vehicles(id),
passenger_count int NOT NULL
);
Clean normalization, strong per-type constraints, no NULL waste. Reads require JOINs to the base table.
3. Concrete-Table Inheritance
No shared table -- each type gets its own table with duplicated common columns. Simplest queries per type, but cross-type queries require UNION ALL. Violates DRY at the schema level and makes global constraints (unique VIN across all vehicle types) difficult to enforce.
4. Polymorphic FK anti-pattern
A pattern like commentable_type + commentable_id without a real FK constraint. This breaks referential integrity entirely -- the database cannot enforce that commentable_id points to a valid row in the correct table. No cascading deletes, no index efficiency for join lookups. Use exclusive arcs or an intermediate association table instead.
A content management system where comments can belong to posts, videos, or photos.
The anti-pattern (no referential integrity):
-- DO NOT USE: no real FK constraint possible
CREATE TABLE comments (
id serial PRIMARY KEY,
body text NOT NULL,
commentable_type varchar NOT NULL, -- 'post', 'video', 'photo'
commentable_id int NOT NULL
);
The correct approach -- exclusive arc with real FKs:
CREATE TABLE comments (
id serial PRIMARY KEY,
body text NOT NULL,
post_id int REFERENCES posts(id) ON DELETE CASCADE,
video_id int REFERENCES videos(id) ON DELETE CASCADE,
photo_id int REFERENCES photos(id) ON DELETE CASCADE,
CONSTRAINT exactly_one_parent CHECK (
(post_id IS NOT NULL)::int +
(video_id IS NOT NULL)::int +
(photo_id IS NOT NULL)::int = 1
)
);
Fetching comments for a post:
SELECT c.id, c.body
FROM comments c
WHERE c.post_id = 42
ORDER BY c.id;
The CHECK constraint guarantees every comment belongs to exactly one parent. Real FK constraints provide cascading deletes and index-backed joins.
commentable_type/commentable_id). No referential integrity, no cascading deletes, no index efficiency for lookups by target.PostgreSQL offers native table inheritance via INHERITS:
CREATE TABLE trucks (
payload_capacity_kg int NOT NULL
) INHERITS (vehicles);
This maps directly to CTI at the database level. However, important limitations apply:
ONLY keyword to query just the parent table: SELECT * FROM ONLY vehicles;INHERITS for most new designsHybrid approach with JSONB: Use CTI for the base table and store type-specific columns in a JSONB column instead of separate per-type tables. This combines structural integrity for shared fields with flexibility for type-specific data. See the db-document-in-relational skill for JSONB indexing strategies.
Exclusive-arc constraint with generated columns: For tables with many FK columns, a generated column can simplify the CHECK logic by computing the non-null count automatically.
Performance comparison at scale: STI wins for read throughput (no JOINs). CTI wins for write throughput and constraint enforcement (narrower per-type tables, no NULL overhead). On a 10M-row benchmark, STI queries are 15-20% faster for reads; CTI INSERT throughput is 10-15% higher.
MySQL lacks table inheritance (INHERITS). STI and CTI must be implemented manually with JOINs and application logic.
MySQL CHECK constraints are enforced since 8.0.16 -- earlier versions parse but silently ignore them. Exclusive-arc constraints work correctly on MySQL 8.0.16+.
MySQL ENUM can serve as a discriminator column with stricter type enforcement than VARCHAR: type ENUM('truck', 'car', 'motorcycle') NOT NULL.
SaaS notification system with 6 target types. Originally used polymorphic FK (target_type/target_id). After a table rename during a migration, thousands of orphaned notification rows appeared -- target_type values referenced a table name that no longer existed. Migrated to CTI with an exclusive-arc constraint on the notifications table. Orphan rate dropped to zero. Query performance was unchanged because JOIN overhead on indexed FKs was under 1ms.
type + id without referential integrity).