Before engines gained built-in partitioning syntax, teams simulated it with parent/child table inheritance plus routing triggers and CHECK constraints. Compare that approach with modern declarative partitioning, and say when you would still see the old style.
answer
- Inheritance + CHECK + routing trigger
- Trigger cost per row, edit it monthly
- Overlaps and gaps unenforced; rows stuck in the parent
- Constraint exclusion = plan time only
- Declarative: bounds as metadata, runtime pruning
basics
~20 sThe old style built partitions manually: child tables with CHECK constraints, a trigger or rule to route inserts, and the planner excluding children by constraint. Declarative partitioning makes the engine own routing, bounds and pruning — faster, correct by construction, and far less code to maintain.
solid answer
~60 s**Inheritance-based partitioning** was assembled by hand: a parent table, child tables each carrying a `CHECK` constraint describing its slice, and a `BEFORE INSERT` trigger (or rules) that redirected each row to the right child. Partition elimination came from *constraint exclusion*, where the planner compared query predicates to the CHECK constraints. Its problems were structural: the trigger ran per row and cost real throughput; nothing enforced that the children's constraints were disjoint or complete, so a bug silently duplicated or dropped rows; `COPY` and bulk loads needed care; the routing logic had to be edited every time a partition was added; and constraint exclusion happened at plan time only, so parameterised queries often could not prune. **Declarative partitioning** (PostgreSQL 10+; MySQL has had its own `PARTITION BY` for far longer) moves all of this into the engine: bounds are metadata, routing is built into the insert path with no trigger, bounds cannot overlap, and pruning improved to work at execution time for parameterised plans. You still meet inheritance in legacy schemas, and occasionally where partitions must have differing column sets or ad-hoc membership rules that declarative bounds cannot express.
code
sql · 7 linesCREATE TABLE events (id bigint, occurred_at timestamptz, payload jsonb);
CREATE TABLE events_2026_01 (
CHECK (occurred_at >= '2026-01-01' AND occurred_at < '2026-02-01')
) INHERITS (events);
CREATE INDEX ON events_2026_01 (occurred_at);go deeper
Know that partitioning used to be hand-built from child tables, CHECK constraints and an insert trigger, and that engines now do it natively.
Contrast the mechanisms and name concrete failures of the old approach — trigger cost, unenforced bounds, trigger edits per partition.
Add the pruning story (plan-time constraint exclusion versus execution-time pruning) and the practical implications for prepared statements and bulk loads.
Judge migration: what a legacy inheritance layout costs annually in incidents and toil versus the rewrite required to move to declarative, and which exotic requirements would justify keeping it.
## The old mechanism Before engines offered a first-class partitioning declaration, the standard PostgreSQL recipe used table inheritance: ```sql CREATE TABLE events (id bigint, occurred_at timestamptz, payload jsonb); CREATE TABLE events_2026_01 ( CHECK (occurred_at >= '2026-01-01' AND occurred_at < '2026-02-01') ) INHERITS (events); CREATE FUNCTION events_insert() RETURNS trigger AS $$ BEGIN IF NEW.occurred_at >= '2026-01-01' AND NEW.occurred_at < '2026-02-01' THEN INSERT INTO events_2026_01 VALUES (NEW.*); ELSIF ... THEN ... ELSE RAISE EXCEPTION 'no partition for %', NEW.occurred_at; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; ``` Three separate mechanisms had to agree: inheritance made a query on the parent read the children; the `CHECK` constraints told the planner what each child could contain; the trigger made inserts land in the right place. Nothing tied them together. ## Everything that went wrong with it **Per-row trigger cost.** Every insert executed a procedural function, often with a chain of comparisons. On bulk loads this was a measurable, sometimes dominant, share of the time. Some teams pushed routing into the application to avoid it, which then leaked partition layout into application code. **No integrity of the partition set.** The engine never checked that children's constraints were mutually exclusive or that they covered the key space. Two overlapping CHECKs meant a row could legitimately live in either child and queries could double-count if routing was inconsistent; a gap meant inserts raised an exception, or worse, landed in the parent table itself, where they were easy to lose track of. Rows sitting in the parent were a classic production surprise. **Fragile maintenance.** Adding next month's partition meant creating the table, writing its CHECK, *and editing the routing function*. Forgetting the third step broke inserts at midnight on the first of the month. Teams wrote cron jobs to generate all three, and those jobs became load-bearing infrastructure. **Weak pruning.** Constraint exclusion ran at planning time by proving that a child's CHECK contradicted the query predicate. That worked for literal predicates but not for parameters bound after planning, and it had to be enabled by a setting. It also scaled poorly, since the planner evaluated every child's constraints. **Awkward semantics.** Inheritance is a general feature, not a partitioning feature: children could have extra columns, `ALTER TABLE` on the parent did not always propagate as expected, and a query could deliberately address only the parent with `ONLY`. ## What declarative partitioning changed Declaring `PARTITION BY RANGE/LIST/HASH` and attaching partitions with explicit `FOR VALUES` bounds makes the partition set part of the table's definition: - **Routing is in the engine.** Tuple routing happens inside the insert path in C, with no trigger and no procedural code, and it works for `COPY` and multi-row inserts. - **Bounds cannot overlap.** The system rejects an attach whose bounds intersect an existing partition, so the set is disjoint by construction. A `DEFAULT` partition (where supported) catches unmatched values rather than letting them vanish. - **No rows in the parent.** The parent is purely a router; there is no storage to accidentally accumulate rows. - **Better pruning.** Partition pruning uses the declared bounds directly rather than proving constraint contradictions, and later versions extended it to run at execution time, so prepared statements and parameterised queries prune too. - **Coordinated DDL.** Indexes created on the parent are created on each partition and on future ones; attaching and detaching partitions is a supported operation with defined locking. The cost is expressiveness: bounds must be simple interval/enumeration/hash rules on the declared key, partitions must share the parent's column set, and every unique constraint must include the partition key. ## Where the old style still appears - **Legacy schemas.** Plenty of long-lived systems still run inheritance-based partitioning because migrating means rewriting the table. Recognising the pattern — child tables with CHECK constraints and a routing trigger — is a practical skill during code archaeology. - **Rules declarative bounds cannot express.** Membership determined by something other than a simple function of the key, partitions with divergent columns, or hand-rolled placement across tablespaces with unusual criteria. - **Mixed hierarchies.** Occasionally inheritance is used for genuine sub-typing rather than partitioning, which is a different feature use entirely and not something to migrate away. ## MySQL's position MySQL has had native `PARTITION BY RANGE/LIST/HASH/KEY` for a long time, so the trigger-based workaround is largely a PostgreSQL-era story there. MySQL's own constraints differ — notably that every unique key must contain all columns of the partitioning expression, the same rule for the same reason, and that some storage engines and features restrict partitioning. ## How to answer Describe the three moving parts of the old approach (inheritance, CHECK constraints, routing trigger), name its concrete failures (per-row trigger cost, overlapping or missing bounds, editing the trigger for each new partition, plan-time-only pruning), state that declarative partitioning folds all of it into engine metadata with runtime pruning, and note that legacy systems and exotic membership rules are where you still see the old form.
- With inheritance-based partitioning, how could rows end up in the parent table itself, and why is that dangerous?The parent is a real, storable table under inheritance, so any insert that bypasses the routing trigger — a direct insert while the trigger is disabled, a code path targeting the parent with a mechanism the trigger does not cover, or a value no branch matches with a fallback that inserts locally — lands its row in the parent's own storage. Those rows are still returned by queries on the parent, so they are easy to miss, but they are excluded from per-partition maintenance and are lost if someone assumes the parent is empty and truncates it.
- Why is execution-time partition pruning more valuable than plan-time constraint exclusion for application traffic?Most application queries are prepared statements with bound parameters, so at plan time the engine does not know which time window or which key value will be supplied and cannot exclude partitions. Execution-time pruning defers the decision until the parameter values are known, so a prepared statement can still touch a single partition. Plan-time-only exclusion meant teams had to inline literals to get pruning, defeating statement reuse.
saying these in an interview costs you the question
- Believing constraint exclusion works as well as declarative pruning for parameterised queries
- Thinking overlapping CHECK constraints across child tables are detected by the engine
- Forgetting that the routing trigger must be updated whenever a partition is added
- Claiming declarative partitioning is only syntactic sugar over inheritance
- Assuming the declarative parent table can store rows of its own