TechEarl

How to Store a Boolean in MySQL: TINYINT(1) vs BIT(1)

MySQL has no real boolean type. BOOL and BOOLEAN are just aliases for TINYINT(1). Here is why TINYINT(1) is the idiomatic choice, how BIT(1) trips up ORMs, and a worked is_active schema.

Ishan Karunaratne⏱️ 11 min readUpdated
Share thisCopied
How to store a boolean in MySQL: BOOL and BOOLEAN are aliases for TINYINT(1), why TINYINT(1) beats BIT(1), ENUM('Y','N'), and CHAR(1), plus a worked is_active schema with NOT NULL DEFAULT.

MySQL has no real boolean type. BOOL and BOOLEAN are nothing more than aliases for TINYINT(1), and the keywords TRUE and FALSE are aliases for 1 and 0. So the question "what column type stores a boolean in MySQL" really asks "how do I store a 0/1 flag," and the answer almost every time is TINYINT(1) NOT NULL DEFAULT 0. The contenders are BIT(1) (same one-byte footprint but awkward to query and poorly handled by ORMs), ENUM('Y','N'), and CHAR(1) (both bigger and stringly-typed). Below is the comparison, the gotchas, and a worked is_active schema.

Short answer: use TINYINT(1) (or TINYINT UNSIGNED) and declare it NOT NULL DEFAULT 0. It is what every ORM expects, it is one byte, and BOOL/BOOLEAN/TRUE/FALSE all compile straight down to it so you can write whichever reads best. Reach for BIT(1) only when you are packing many flags and have measured a reason to.

Does MySQL have a boolean type?

No. When you write BOOL or BOOLEAN, MySQL silently rewrites the column as TINYINT(1). Run CREATE TABLE t (flag BOOLEAN) and then SHOW CREATE TABLE t, and you get flag tinyint(1) back. The MySQL manual is explicit: BOOL and BOOLEAN "are synonyms for TINYINT(1). A value of zero is considered false. Nonzero values are considered true."

That last sentence is the one to internalize. A boolean in MySQL is just an integer that the language treats as truthy or falsy in boolean contexts. The literals TRUE and FALSE are, again per the manual, "merely aliases for 1 and 0." So WHERE flag = TRUE is identical to WHERE flag = 1, and WHERE flag (with no comparison) is true for any nonzero value.

This has a sharp edge. TINYINT(1) does not constrain the column to 0 and 1. The (1) is only a display-width hint (and a deprecated one: MySQL deprecated integer display width in 8.0.17). The underlying TINYINT still stores any value from -128 to 127 in its single byte. Nothing stops UPDATE t SET flag = 42, and after that WHERE flag = TRUE is false (because 42 != 1) while WHERE flag is true (because 42 is nonzero). Keep your writes to 0 and 1 and this never bites you, but it is why a boolean column should carry a NOT NULL DEFAULT 0 and why some people add a CHECK (flag IN (0,1)).

All of this is identical on MariaDB: BOOL, BOOLEAN, TRUE, and FALSE are the same aliases over TINYINT(1), and a flag column behaves exactly the same way.

Jump to:

Which type? TINYINT(1) vs BIT(1) vs ENUM vs CHAR(1)

TypeStorageClient ergonomicsORM supportVerdict
TINYINT(1)1 byteReturns plain 0/1 integersUniversal (Rails, Django, Laravel map booleans here)Recommended default
BIT(1)1 byte ((M+7)/8)Returns a binary string b'\0'/b'\1'; needs b'1' or careful castingSpotty, many tools mishandle itOnly for packed flags
ENUM('Y','N')1 byte (plus a string lookup)Returns 'Y'/'N' stringsTreated as enum, not booleanAvoid for a boolean
CHAR(1)1 byte (more in multibyte charsets)Returns 'Y'/'1' strings, stringly-typedTreated as a stringAvoid for a boolean

The headline surprise is that storage is not the deciding factor. BIT(1), TINYINT(1), ENUM('Y','N'), and CHAR(1) in a single-byte charset all cost roughly one byte per row. BIT(M) packs M bits into (M+7)/8 bytes, so BIT(1) rounds up to one byte just like the rest. The decision is about ergonomics and tooling, and on those axes TINYINT(1) wins comfortably.

Why TINYINT(1) is the idiomatic choice

TINYINT(1) is the boolean every framework already assumes. Rails migrations with t.boolean, Django's BooleanField, and Laravel's $table->boolean() all generate a TINYINT(1) column. Their drivers read the column back as a plain integer and cast 0/1 to the host language's false/true without any special handling. If you ever drop down to raw SQL, the values are still just 0 and 1, which is what every client library, BI tool, and mysql prompt expects to see.

Because BOOL, BOOLEAN, TRUE, and FALSE all resolve to the same TINYINT(1) plumbing, you can write the version that reads clearest and it compiles to the same thing:

sql
-- all four of these create the same column
flag TINYINT(1) NOT NULL DEFAULT 0
flag BOOL       NOT NULL DEFAULT 0
flag BOOLEAN    NOT NULL DEFAULT FALSE

-- and these queries are identical
SELECT * FROM t WHERE flag = 1;
SELECT * FROM t WHERE flag = TRUE;
SELECT * FROM t WHERE flag;

If you have no use for negative values (a boolean never does), TINYINT UNSIGNED is a fine variant: still one byte, range 0 to 255, and it makes "no negatives here" explicit. But plain TINYINT(1) is the convention, and matching the convention is worth more than the micro-signal, because the next person and the next ORM both expect TINYINT(1).

When BIT(1) is awkward

BIT(1) looks tempting: a type literally named for storing bits, and it costs the same one byte as TINYINT(1). The trouble is everything around it.

A BIT column comes back to most clients as a binary string, not a number. Querying it at the mysql prompt shows b'\0' or b'\1' rather than 0 or 1, and inserting needs the bit-literal syntax:

sql
CREATE TABLE bit_demo (flag BIT(1) NOT NULL DEFAULT b'0') ENGINE=InnoDB;

INSERT INTO bit_demo (flag) VALUES (b'1');   -- bit literal
INSERT INTO bit_demo (flag) VALUES (1);      -- also works, but easy to forget
SELECT flag + 0 FROM bit_demo;               -- cast to int to see 0/1
SELECT * FROM bit_demo WHERE flag = b'1';    -- compare against a bit literal

You end up sprinkling + 0 casts and b'...' literals through queries that would be trivially = 1 against a TINYINT. Worse, a lot of tooling handles BIT badly: ORMs and database GUIs that map cleanly to TINYINT(1) will surface a BIT(1) as a raw byte buffer, a byte[], or a string, and you wind up writing custom type-coercion just to get a boolean back. Some older PHP and Node MySQL drivers return it as "" and leave you to interpret it.

BIT(1) for a single flag is a net loss. Where BIT(M) genuinely earns its keep is packing many booleans into one column: a BIT(8) (one byte) or BIT(64) can hold 8 or 64 independent flags addressed with bitwise operators, which can matter when you have dozens of rarely-queried boolean attributes and want to avoid dozens of columns. That is a deliberate, measured optimization. For one boolean, use TINYINT(1).

ENUM('Y','N') and CHAR(1) deserve a quick mention because people do reach for them. Both are stringly-typed: you compare against 'Y'/'N' instead of 1/0, no ORM treats them as booleans, and CHAR(1) in a utf8mb4 table reserves up to four bytes per row for a single character. ENUM at least constrains the values, but a two-value enum is just a boolean wearing a costume, and you lose the integer semantics (SUM(flag) to count true rows, arithmetic in expressions) that make TINYINT pleasant. Skip both for booleans.

NULL and the tri-state

A real boolean has two states. If you only ever need true and false, declare the column NOT NULL DEFAULT 0 (or DEFAULT 1 if the natural state is "on"). That removes the "unset" ambiguity, keeps every row at a defined value, and means WHERE flag = 0 reliably finds the false rows without a surprise NULL slipping through.

Allowing NULL turns the column into a tri-state: true, false, and unknown. That is occasionally what you want (a survey answer that can be yes, no, or not-yet-answered), but then NULL is a real third value with real consequences. NULL = 1 is not true, it is NULL; WHERE flag = 0 will not return the NULL rows; you need IS NULL / IS NOT NULL to handle them. If you find yourself reaching for nullable booleans to mean "unknown," that is fine, just do it on purpose and remember the three-valued logic. For a plain on/off flag, NOT NULL DEFAULT 0 is the right call.

A worked schema

Here is the idiomatic shape. A users table with an is_active flag, defaulting to active, never null:

sql
CREATE TABLE users (
  id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  email      VARCHAR(255) NOT NULL,
  is_active  TINYINT(1) NOT NULL DEFAULT 1,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB;

-- insert: let the default ride, or set it explicitly with 1/0 or TRUE/FALSE
INSERT INTO users (email)             VALUES ('a@example.com');           -- is_active = 1
INSERT INTO users (email, is_active)  VALUES ('b@example.com', 0);        -- explicitly inactive
INSERT INTO users (email, is_active)  VALUES ('c@example.com', FALSE);    -- same as 0

-- query the active rows
SELECT id, email FROM users WHERE is_active = 1;
SELECT id, email FROM users WHERE is_active;        -- identical, reads cleaner

-- deactivate
UPDATE users SET is_active = 0 WHERE email = 'a@example.com';

-- count how many are active (TINYINT lets you just sum it)
SELECT SUM(is_active) AS active_count FROM users;

The SUM(is_active) at the end is a small reason TINYINT is nicer than BIT or a string type: because the column is an integer, counting the true rows is a plain SUM, no casting. Whether to index is_active is a separate question. A boolean is low-cardinality (two values), so a standalone index on it rarely helps a query that returns a large fraction of the table; it earns its keep as the leading or trailing column of a composite index ((is_active, created_at)) or as a partial-ish filter alongside a selective column. For exact byte costs of TINYINT and every other column type, see MySQL data types and sizes.

What to do next

  • For the byte size of TINYINT, BIT, and every other MySQL type on one page, see MySQL data types and sizes.
  • For the TINYINT DDL, the BOOL alias, and the rest of the syntax in quick-reference form, the MySQL cheat sheet has the create-table and query patterns.
  • When the value has more than two states (a status, a role, a type), the question becomes ENUM vs a lookup table.

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsMySQLBooleanTINYINTBITData TypesSchema Design

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Store a UUID in MySQL: BINARY(16) vs CHAR(36)

How to store a UUID in MySQL or MariaDB: the readable CHAR(36) string or the BINARY(16) you should usually reach for. Storage cost, index size, UUID_TO_BIN/BIN_TO_UUID, the swap_flag trick, and why random UUID primary keys wreck InnoDB inserts.

How to Store JSON in MySQL: The JSON Type vs TEXT

MySQL has a native JSON column type that validates on insert, stores a parsed binary format, and gives you path operators like ->>. When to reach for it over TEXT, how to index it with a generated column, and the MariaDB difference.