When designing relational database schemas, developers often obsess over storage efficiency. A classic dilemma arises when a table requires a column where 99% of the rows hold the exact same default value (such as a boolean flag like tagRemoved = false). Splitting this column into a separate 1-to-1 extension table to avoid storing "redundant" defaults might feel like good normalization, but it often leads to premature optimization and unnecessary query complexity.

The Short Answer: Keep It in the Main Table

In modern relational database management systems (RDBMS) like PostgreSQL, MySQL, SQL Server, and Oracle, the standard practice is simply to define the column directly on the main table with a DEFAULT constraint.

Splitting attributes into secondary metadata tables just to save a boolean or small integer usually introduces more operational overhead (joins, foreign key management, query latency) than the negligible disk space it saves.

ALTER TABLE clothes 
ADD COLUMN is_tag_removed BOOLEAN NOT NULL DEFAULT FALSE;

Why Storing Defaults in the Main Table is Usually Better

  • Minimal Storage Overhead: A boolean typically takes up a single byte or is even packed into a bitmask or row header null bitmap depending on the engine. For millions of rows, this amounts to a few megabytes—a trivial amount given current storage costs.
  • Eliminates Unnecessary Joins: Every time you need this attribute, you avoid executing an expensive LEFT JOIN against a secondary table.
  • Simpler Application Code: Your Object-Relational Mapper (ORM) and raw SQL queries stay straightforward without requiring fallback logic (e.g., COALESCE(m.tagRemoved, FALSE)).
  • Engine-Level Optimizations: In modern versions of PostgreSQL (v11+) and MySQL (8.0+), adding a column with a default value does not rewrite the table. It updates the catalog metadata instantly without consuming physical disk space for existing rows until they are rewritten.

When Should You Optimize? Better Alternatives

If you are working with billions of rows or dozens of sparsely populated attributes, here are standard, performant patterns to handle rare values without cluttering your data model with dozens of 1-to-1 tables.

1. Use Partial (Filtered) Indexes

If the main concern is query speed rather than disk footprint, a partial index is the ultimate solution. A partial index only indexes rows where the value deviates from the default, keeping the index microscopic and blindingly fast.

-- PostgreSQL / SQLite / SQL Server
CREATE INDEX idx_clothes_tag_removed 
ON clothes (id) 
WHERE is_tag_removed = TRUE;

This allows lookups like SELECT * FROM clothes WHERE is_tag_removed = TRUE to run in sub-millisecond time while storing practically nothing in the index for the FALSE rows.

2. Use Native Sparse Columns (SQL Server)

If you are using Microsoft SQL Server, you can take advantage of built-in Sparse Columns. Sparse columns optimize storage for NULL values by allocating zero bytes when the column is null, at the cost of a slight overhead when the value is populated.

CREATE TABLE clothes (
    id INT IDENTITY PRIMARY KEY,
    tagRemoved BIT SPARSE NULL
);

3. The Sparse Attribute / Semi-Structured Pattern (JSONB)

If you have dozens of flags or metadata fields that are rarely present, instead of creating dozens of extension tables, store rare optional attributes in a semi-structured JSONB (Postgres) or JSON column:

ALTER TABLE clothes ADD COLUMN attributes JSONB DEFAULT '{}'::jsonb;

-- Querying whether tagRemoved is set
SELECT * FROM clothes WHERE (attributes->>'tagRemoved')::boolean IS TRUE;

This prevents schema bloat, scales to hundreds of optional properties, and only consumes space when a property exists.

Conclusion

Creating a 1:1 table to store occasional boolean values is an anti-pattern known as over-normalization. For 99% of use cases, simply defining the column on the primary table with a default value is the cleanest, most maintainable, and most performant approach. If search performance on the rare values becomes a bottleneck, complement it with a partial index.