All rules Rule BP002 · IX
PostgreSQL
note
IX · Index hygiene
hosted, paid, --remote

Is an index on a boolean column useful?

Index led by a boolean or near-constant column

Note: this works and blocks nothing, but a performance regression is likely.

What happens

A B-tree whose leading column has two values splits the table in half at best. The planner will usually ignore it in favor of a sequential scan, and when it does use it the index returns half the table. Either way every write pays to maintain it. The useful form is a partial index, WHERE is_active, that only contains the rows you actually look up.

Why it is dangerous on a populated table

Nothing blocks, but the cost scales with the table and shows up as a slower query plan or a longer maintenance window rather than an outage.

Fires on

CREATE INDEX idx_users_active ON users (is_active);

The safe pattern

Index the rows you query for, not the flag: a partial index (WHERE flag = true) is small and selective, or put the flag last in a composite index led by the column that narrows the search. Both spellings keep this rule silent.

-- Only the active rows live in the index:
CREATE INDEX CONCURRENTLY idx_users_active ON users (id) WHERE is_active;

Fixtures

The rule ships with these files and the test suite runs them on every change: the first set must fire, the second must stay silent.

Fires (3)

boolean leading composite
SET lock_timeout = '5s';
ALTER TABLE users ADD COLUMN is_active boolean NOT NULL DEFAULT true;
CREATE INDEX CONCURRENTLY idx_users_active_email ON users (is_active, email);
boolean leading
SET lock_timeout = '5s';
CREATE TABLE users (id bigint PRIMARY KEY, is_active boolean NOT NULL DEFAULT true, email text);
CREATE INDEX idx_users_active ON users (is_active);
live boolean column
SET lock_timeout = '5s';
CREATE INDEX CONCURRENTLY idx_users_deleted ON users (deleted);

Stays silent (3)

flag last
SET lock_timeout = '5s';
CREATE TABLE users (id bigint PRIMARY KEY, is_active boolean NOT NULL DEFAULT true, email text);
CREATE INDEX idx_users_email_active ON users (email, is_active);
partial on flag
SET lock_timeout = '5s';
CREATE TABLE users (id bigint PRIMARY KEY, is_active boolean NOT NULL DEFAULT true, email text);
CREATE INDEX idx_users_active ON users (id) WHERE is_active;
unknown type
SET lock_timeout = '5s';
CREATE INDEX CONCURRENTLY idx_users_flagged ON users (flagged);

How to check locally

Catch this before it ships

This rule runs in the hosted service on Startup and above: add --remote with a team token, or use the GitHub Action. No install, nothing leaves your machine:

npx bolvrk check migration.sql --remote