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)
| Type | Storage | Client ergonomics | ORM support | Verdict |
|---|---|---|---|---|
TINYINT(1) | 1 byte | Returns plain 0/1 integers | Universal (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 casting | Spotty, many tools mishandle it | Only for packed flags |
ENUM('Y','N') | 1 byte (plus a string lookup) | Returns 'Y'/'N' strings | Treated as enum, not boolean | Avoid for a boolean |
CHAR(1) | 1 byte (more in multibyte charsets) | Returns 'Y'/'1' strings, stringly-typed | Treated as a string | Avoid 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:
-- 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:
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 literalYou 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:
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
TINYINTDDL, theBOOLalias, 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
- Choosing the right MySQL column type lays out the byte cost and range of
TINYINT,BIT, and every other type on one page. - The MySQL cheat sheet keeps the create-table,
BOOLalias, and query syntax in quick-reference form. - When a flag grows into a status with more than two values, weigh ENUM against a lookup table.
- PostgreSQL has a real
booleantype, but its array storage is the closest sibling guide if you are deciding between Postgres and MySQL. - Spin up a throwaway instance to test schemas with MySQL in Docker.
- Need to flip an existing flag column? See changing a MySQL column type.
Sources
Authoritative references this article was fact-checked against.
- MySQL 8.0 Reference Manual: Numeric Type Syntax (BOOL, BOOLEAN, TRUE/FALSE, display-width deprecation)dev.mysql.com
- MySQL 8.0 Reference Manual: Integer Types (TINYINT storage and range)dev.mysql.com
- MySQL 8.0 Reference Manual: The BIT Typedev.mysql.com
- MySQL 8.0 Reference Manual: Data Type Storage Requirementsdev.mysql.com





