{"post":{"slug":"enum-vs-lookup-table-2026","title":"Enum vs Lookup Table: Where Should Your Fixed Values Live?","excerpt":"A status column with five possible values can be a database enum, a lookup table, or a check constraint. The right answer depends almost entirely on who needs to change the list.","category":"code","date":"January 9, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span>n order has a status, and it is one of a handful of values. There are three reasonable ways to model that, and teams argue about them on grounds that mostly do not matter while ignoring the one that does.\n\n## What changed in 2026\n\n- **Enum limitations stayed the deciding factor.** The awkwardness of removing or reordering enum values continued to push teams toward alternatives.\n- **Check constraints gained favour.** For fixed sets with no metadata, the lightest option became a more common choice.\n- **Lookup tables stayed standard for user-manageable lists.** Anything an administrator edits kept its own table.\n- **Type safety moved to the application.** Generated types from schema definitions reduced the argument for database-level enums.\n\n## The three options\n\n| | Enum type | Lookup table | Check constraint |\n| --- | --- | --- | --- |\n| Storage | Compact | Foreign key | The value itself |\n| Add a value | Usually easy | Insert a row | Migration |\n| Remove a value | Difficult | Delete or deactivate | Migration |\n| Reorder | Difficult | Trivial — it is data |\n| Attach metadata | No | Yes | No |\n| Requires a join | No | Yes | No |\n| Changeable at runtime | No | Yes | No |\n| Visible in the schema | Yes | Partly | Yes |\n\n## The question that decides it\n\n**Who changes this list, and how often?**\n\n**Nobody, essentially ever.** Order status in a system where the workflow is fixed. A check constraint is the lightest thing that works — no extra table, no join, the valid values documented in the schema. Adding a value is a migration, which is appropriate because adding a status is a code change anyway.\n\n**Developers, occasionally.** An enum or a check constraint both work. The enum is more explicit as a type and more awkward to modify; the check constraint is easier to change and slightly less discoverable. Either is defensible.\n\n**Administrators or users, through the product.** A lookup table, without question. Anything editable at runtime must be data, and the other options require a deployment to change.\n\n**The list carries metadata.** A lookup table. If each value needs a display label, a sort order, a colour, an active flag, or a description, that is a row with columns — the other options have nowhere to put it.\n\nThat framing resolves most disagreements. The performance argument — a join versus no join — is real and almost always negligible against a small lookup table that sits entirely in cache.\n\n## Enum-specific pain\n\nWorth stating because it is where the surprises are.\n\n**Adding** a value is generally straightforward, and in some engines it cannot be done inside a transaction, which complicates migration tooling.\n\n**Removing** a value is not directly supported in several engines. The path is creating a new type, migrating every column using the old one, and dropping the old type — a real migration for what looks like a small change.\n\n**Reordering** is similarly awkward, and it matters because enum ordering affects sorting and comparison.\n\n**Cross-database portability** is poor; enum syntax and behaviour vary considerably.\n\nNone of that is disqualifying for a genuinely stable list. All of it is a reason not to reach for an enum by default.\n\n## A note on the lookup table\n\nIf you use one, two details matter.\n\n**Use a meaningful key**, not an auto-increment integer. A status column containing `shipped` is readable in queries and logs; one containing `3` requires a join to understand, including during an incident when you are reading raw rows.\n\n**Deactivate rather than delete.** Removing a lookup row breaks historical references. An active flag lets you retire a value for new records while preserving old ones — the same reasoning as any reference data.\n\n## Common mistakes\n\n- **A lookup table for a fixed three-value list.** Join and complexity for unused flexibility.\n- **An enum for a list users edit.** Requires a deploy to change.\n- **Integer keys in a lookup table.** Unreadable data.\n- **Deleting lookup rows.** Breaks historical references.\n- **Assuming enum ordering is arbitrary.** It affects sorting.\n- **No constraint at all.** A free-text status column accumulates typos and variants.\n\n## FAQ\n\n**Is the join really negligible?**\nFor a small lookup table, effectively yes — it will be cached and the join is trivial. The cost only becomes interesting at very high query volume, where denormalising the label is an option.\n\n**What about application-level enums?**\nUseful for type safety in code and no substitute for a database constraint, since other writers bypass the application entirely — see [check constraints](/blog/check-constraints-explained-2026).\n\n**Can I migrate between approaches?**\nEnum to check constraint or lookup table is straightforward. Going the other direction is also possible. Neither is free on a large table.\n\n**What if the list is long?**\nA lookup table, generally — long lists tend to be data rather than schema, and are more likely to change.\n\n## Where to go next\n\nFor the lightest enforcement option, read [check constraints](/blog/check-constraints-explained-2026). For the deletion behaviour lookup tables need, [cascade deletes](/blog/cascade-deletes-explained-2026), and for changing the constraint later, [expand and contract migrations](/blog/expand-contract-migrations-2026).","hue":315,"metaTitle":"Enum vs Lookup Table: Where Fixed Values Belong","metaDescription":"Comparing database enums, lookup tables, and check constraints for fixed value sets, with the change-frequency question that decides between them.","keywords":["enum vs lookup table","database enum type","reference data modeling","check constraint status","fixed value sets","schema flexibility","status column design","database design 2026"],"tldr":["**Enums are compact and awkward to change** — adding is usually easy, removing and reordering is not.","**Lookup tables are flexible and let you attach metadata** — labels, sort order, active flags.","**Check constraints are the lightest option** — a schema migration to change, and no extra table.","**Ask who changes the list and how often** — that answers it faster than any performance argument.","**Skip:** a lookup table for a genuinely fixed three-value list. The join is not free and the flexibility is unused."],"readTime":"4 min read","wordCount":869},"related":[{"slug":"soft-delete-patterns-2026","title":"Soft Delete: The Flag That Infects Every Query","excerpt":"Adding a deleted_at column looks like the safe choice. Then every query needs a filter, every unique constraint breaks, and one forgotten WHERE clause leaks deleted data.","category":"code","date":"August 13, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":302},{"slug":"check-constraints-explained-2026","title":"Check Constraints: Rules the Database Enforces for You","excerpt":"Validation in application code holds until something else writes to the table. A check constraint holds for every writer, including the console session during an incident.","category":"code","date":"January 9, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":307},{"slug":"varchar-vs-text-2026","title":"Varchar vs Text: A Length Limit Is a Constraint, Not a Speedup","excerpt":"Declaring a column as varchar(255) does not make it smaller or faster in most databases. It adds a constraint, and constraints should be chosen deliberately.","category":"code","date":"January 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":301}],"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":"encrypted-messaging-explained-2026","title":"Encrypted Messaging: What End-to-End Actually Protects","excerpt":"End-to-end encryption means the provider cannot read your messages. It says nothing about who else can see that you sent them, or what happens on the device at either end.","category":"guides","date":"January 9, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":164},"older":{"slug":"gift-splitting-explained-2026","title":"Gift Splitting: Doubling the Annual Exclusion as a Couple","excerpt":"A married couple can treat a gift from one spouse as coming half from each, doubling the annual exclusion. It requires a gift tax return, and consenting once applies to all gifts that year.","category":"finance","date":"January 9, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":285}}