{"post":{"slug":"exclusion-constraints-explained-2026","title":"Exclusion Constraints: Preventing Overlaps in the Database","excerpt":"Two bookings for the same room at overlapping times is a rule no unique constraint can express. An exclusion constraint enforces it, atomically, without application-level locking.","category":"code","date":"January 12, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span> booking system must prevent two reservations for the same resource at overlapping times. The obvious implementation queries for conflicts, finds none, and inserts.\n\nTwo requests arriving simultaneously both find no conflict and both insert. The double booking exists, and no amount of care in the application prevents it — the gap between checking and inserting is where it happens, and that gap cannot be closed in application code alone.\n\n## What changed in 2026\n\n- **Range types became the standard model.** Representing an interval as a single range column, rather than two timestamps, became normal practice where supported.\n- **Scheduling systems adopted them.** Booking, resource allocation, and calendaring workloads moved to database-enforced overlap prevention.\n- **Multi-tenant patterns spread.** Combining equality on a tenant column with overlap on a range became a common shape.\n- **Engine support stayed uneven.** Availability remained concentrated in engines with rich type systems, so portability requires checking.\n\n## Beyond equality\n\nA unique constraint asks whether two rows have *equal* values for some columns, and rejects the second if so.\n\nAn exclusion constraint generalises that: it asks whether two rows relate by *any* operator you specify, and rejects the second if they do. Equality is one option; range overlap is the useful one.\n\nSo the rule \"no two rows may have the same room and overlapping time ranges\" becomes a constraint combining an equality test on the room and an overlap test on the range. The database enforces it across concurrent transactions the same way it enforces uniqueness.\n\n| Rule | Mechanism |\n| --- | --- |\n| No duplicate email | Unique constraint |\n| No duplicate active email | Partial unique index |\n| No overlapping bookings per room | Exclusion constraint |\n| No overlapping validity periods per record | Exclusion constraint |\n| No two rows within a distance of each other | Exclusion constraint, spatial operator |\n\n## Model the interval as a range\n\nThe practical enabler is representing the interval as a single range value rather than separate start and end columns.\n\nTwo separate timestamp columns make overlap a condition you write out, and there is no operator the constraint can use directly. A range type gives you an overlap operator the database understands and can index.\n\nRanges also settle the inclusive-exclusive question explicitly. A booking ending at 10:00 and another starting at 10:00 do not overlap if the ranges are half-open, which is almost always the intended behaviour and a recurring source of off-by-one bugs when modelled as separate columns.\n\nWhere ranges are unavailable, a generated column combining two timestamps into a range is a workable bridge — see [generated columns](/blog/generated-columns-explained-2026).\n\n## What it needs and costs\n\nExclusion constraints are backed by an index supporting the operators involved — typically a generalised search tree index rather than a B-tree, since B-trees only understand ordering.\n\nThat has consequences. The index is larger and slower to write than an equivalent B-tree, and the constraint check on insert is more expensive than a uniqueness check. On a high-write table with many overlapping candidates, that cost is real.\n\nIt is still dramatically cheaper than the alternatives: locking the whole table around every insert, or serialising bookings through application-level locks, both of which sacrifice concurrency to achieve what the constraint does natively.\n\nA practical note on error handling: a violation raises an error, so the application must catch it and present something sensible. A user attempting to book a taken slot should see a clear message, not a stack trace — which means the constraint name matters, per [check constraints](/blog/check-constraints-explained-2026).\n\n## Common mistakes\n\n- **Check-then-insert in application code.** The race the constraint exists to close.\n- **Separate start and end columns.** No operator to constrain on.\n- **Inclusive ranges where half-open was meant.** Adjacent intervals falsely conflict.\n- **Forgetting the tenant or resource column.** Prevents all overlaps globally rather than per resource.\n- **Not handling the violation error.** Users see a raw database error.\n- **Using one where a unique constraint suffices.** Heavier for no benefit.\n- **Assuming portability.** Support varies considerably by engine.\n\n## FAQ\n\n**Does this work across transactions?**\nYes — that is the point. The database enforces it against concurrent transactions the same way it enforces uniqueness, closing the race that application checks cannot.\n\n**Can I allow overlaps in some cases?**\nCombine it with a partial index condition so the constraint applies only to rows meeting a predicate — cancelled bookings excluded, for instance. Same reasoning as [partial indexes](/blog/partial-indexes-explained-2026).\n\n**What if my database does not support them?**\nThe fallbacks are serialisable isolation with retry, or explicit locking on a parent row. Both work and both cost concurrency — see [database isolation levels](/blog/database-isolation-levels-2026).\n\n**Are they slow?**\nMore expensive than a unique constraint, and far cheaper than the locking strategies they replace. Measure on your write volume.\n\n## Where to go next\n\nFor simpler constraint types, read [check constraints](/blog/check-constraints-explained-2026) and [partial indexes](/blog/partial-indexes-explained-2026). For the concurrency problem they solve, [database isolation levels](/blog/database-isolation-levels-2026).","hue":302,"metaTitle":"Exclusion Constraints: Preventing Overlaps in the DB","metaDescription":"How exclusion constraints prevent overlapping bookings and ranges atomically, why they beat check-then-insert, and what index support they need.","keywords":["exclusion constraint","overlapping ranges sql","booking conflict prevention","range types postgres","gist index","concurrency safe constraint","scheduling database","database design 2026"],"tldr":["**Unique constraints test equality; exclusion constraints test any operator** — including range overlap.","**It closes the check-then-insert race properly** — no application lock required.","**Range types are what make it practical** — model the interval as one column, not two.","**It needs a suitable index type** — typically a generalised search tree index.","**Skip:** exclusion constraints where a unique constraint suffices. They are heavier and unnecessary for equality."],"readTime":"4 min read","wordCount":830},"related":[{"slug":"generated-columns-explained-2026","title":"Generated Columns: Let the Database Compute It","excerpt":"A value derived from other columns can be maintained by the database itself, indexed like any other column, and never fall out of sync with its inputs.","category":"code","date":"August 15, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":304},{"slug":"transactions-explained-2026","title":"Database transactions explained in 2026: ACID, isolation, and beyond","excerpt":"Transactions are the safety net of every database write. This 2026 guide covers ACID, isolation levels, deadlocks, and the patterns that prevent data corruption.","category":"code","date":"April 21, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":310},{"slug":"database-transactions-explained-2026","title":"Database transactions explained in 2026: ACID, isolation, and real examples","excerpt":"Transactions are the mechanism that keeps your database consistent when multiple operations run concurrently. This is the 2026 guide to ACID properties, isolation levels, and the patterns that prevent data corruption.","category":"code","date":"April 13, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":318}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"document-vault-guide-2026","title":"The Document Vault: Where the Important Papers Live","excerpt":"Somebody will eventually need your insurance policy, your will, or your passport number at short notice, possibly not you. One organised place makes that possible.","category":"guides","date":"January 12, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":143},"older":{"slug":"failover-testing-2026","title":"Failover Testing: Finding Out Before Production Does","excerpt":"Automatic failover is configured and has never fired. Whether it works, how long it takes, and whether your application reconnects are all unknowns until you trigger it deliberately.","category":"code","date":"January 12, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":308}}