The right column for an email address in MySQL is VARCHAR(255) with the utf8mb4 character set and a UNIQUE index. That single line covers the length, the encoding, and the uniqueness constraint that 99% of users tables need. Below is why 255 (and not 320 or TEXT), why utf8mb4 matters even for an address that looks like plain ASCII, how to make the uniqueness case-insensitive, and a worked schema you can paste in.
Short answer: email VARCHAR(255) NOT NULL with UNIQUE KEY uq_users_email (email), on a utf8mb4 column. Validate the format in your application, not the database. Reach for TEXT never, CHAR never, and VARCHAR(320) only if you have a specific reason to budget for the full RFC local-part-plus-domain maximum. This is the conventional choice every framework ships, and it is the right one.
Why VARCHAR(255)
An email address is a variable-length string, so VARCHAR is the obvious family. The length is the part people overthink.
RFC 5321 (the SMTP spec) caps the path that carries an address at 256 octets including the surrounding angle brackets, which works out to a 254-character maximum for the address itself (local@domain). The local part alone is limited to 64 characters and the domain to 255, but the combined, practically-deliverable address tops out at 254. So VARCHAR(254) is technically the tightest correct fit.
In practice almost everyone uses VARCHAR(255), and so do the frameworks (Laravel, Django, Rails all default a string column to 255). It covers the 254-character limit with a byte of margin, and 255 is a natural boundary in MySQL: it is the largest length a VARCHAR can store using a single-byte length prefix. At 256 and above the prefix becomes two bytes, so 255 is the sweet spot where you get the most room for the least per-row overhead. Whether you write 254 or 255 makes no real difference to storage (a VARCHAR only consumes the bytes the value actually uses plus the length prefix), so I default to 255 and move on. For the full sizing rules behind that prefix behavior, see MySQL data types and sizes.
Use utf8mb4, not plain ASCII
The instinct is to declare the column latin1 or ascii because "email addresses are ASCII." That used to be true and no longer is.
Internationalized email (EAI, defined in RFC 6531) allows Unicode in both the local part and the domain. Addresses like 用户@例子.广告 or δοκιμή@παράδειγμα.δοκιμή are real and deliverable on EAI-capable mail servers. If you pin the column to ascii, those addresses either fail to insert or get silently mangled. Declaring the column utf8mb4 costs you nothing for plain-ASCII addresses (those still store one byte per character) and keeps the door open for the Unicode ones.
utf8mb4 is also the default character set in MySQL 8.0, so on a fresh install you usually get it without asking. The thing to watch is the index budget. A utf8mb4 column reserves up to 4 bytes per character for index-length purposes, so a VARCHAR(255) UNIQUE index has a key up to 1020 bytes. That fits comfortably under InnoDB's 3072-byte index-key limit on ROW_FORMAT=DYNAMIC (the default in MySQL 8.0), so a single-column email index is never a problem. You only hit trouble on old setups with the legacy 767-byte limit (pre-DYNAMIC row format), where utf8mb4 caps a usable index at 191 characters and you would need a shorter column or a prefix index. If you are still on that footing, the fix is to move the table to utf8mb4 and DYNAMIC properly; I walk through that in the utf8 to utf8mb4 migration guide.
Everything here applies to MariaDB as well. The VARCHAR(255) column, the utf8mb4 charset, and the UNIQUE index behave identically; the only divergence is that MariaDB's default character set has historically lagged MySQL's, so check your server default rather than assuming utf8mb4.
Don't use TEXT (or CHAR)
Two column types tempt people and both are wrong for an email.
TEXT feels safe because it is "unlimited," but it is the wrong tool. A TEXT column can only take a DEFAULT written as an expression (since MySQL 8.0.13), not a plain literal default the way VARCHAR does, it can only be prefix-indexed (you cannot put a plain UNIQUE index on the whole thing, only on the first N characters, which defeats the uniqueness guarantee), and InnoDB stores long TEXT values off-page in overflow pages, adding a pointer hop to every read. An email address is short and bounded, so none of TEXT's tradeoffs buy you anything. VARCHAR(255) indexes the entire value, supports a real UNIQUE constraint, and stores inline.
CHAR is wrong in the opposite direction. CHAR(N) is fixed-length: a CHAR(255) column reserves space for 255 characters on every row regardless of the actual address length, so a 12-character address wastes the rest. Email lengths vary wildly, which is exactly the case VARCHAR exists for.
VARCHAR(255) vs VARCHAR(320) vs TEXT vs CHAR
| Type | Holds a full email | Index the whole value | Default value allowed | Per-row cost | Verdict |
|---|---|---|---|---|---|
VARCHAR(255) | Yes (covers RFC's 254) | Yes | Yes | length + 1-byte prefix | Recommended default |
VARCHAR(320) | Yes, with headroom | Yes | Yes | length + 2-byte prefix | Only if you want the 64+1+255 ceiling |
TEXT | Yes | No (prefix only) | Expression only | inline ≤ limit, else off-page | Avoid |
CHAR(255) | Yes | Yes | Yes | fixed 255 chars, every row | Avoid (wastes space) |
VARCHAR(320) shows up in guides that add the maximum local part (64), the @, and the maximum domain (255) to get 320. That ceiling is real on paper but not reachable as a single deliverable address (the 254-character path limit binds first), and crossing 255 pushes the column to a 2-byte length prefix. It is not wrong, just unnecessary; pick it only if you specifically want to store the theoretical maximum local-plus-domain without the SMTP path cap. For everyone else, 255 is the answer.
Uniqueness and case sensitivity
You almost always want at most one account per email, which means a UNIQUE index. The subtlety is case.
By the letter of the spec, the local part of an email (everything before the @) is case-sensitive, so Bob@example.com and bob@example.com are technically different mailboxes. The domain part is always case-insensitive. In the real world, essentially no mail provider treats the local part case-sensitively, and users absolutely expect Bob@ and bob@ to be the same login. So you want case-insensitive uniqueness, and there are two clean ways to get it.
Option A: a case-insensitive collation. If the column uses a _ci (case-insensitive) collation, the UNIQUE index treats Bob@example.com and bob@example.com as a collision automatically. The MySQL 8.0 default collation for utf8mb4 is utf8mb4_0900_ai_ci, which is both accent-insensitive and case-insensitive, so a plain UNIQUE KEY on a default-collation column already gives you case-insensitive uniqueness with no extra work. This is the simplest option and the one I reach for.
email VARCHAR(255) NOT NULL COLLATE utf8mb4_0900_ai_ci,
UNIQUE KEY uq_users_email (email)The one wrinkle: ai_ci is also accent-insensitive, so café@x.com and cafe@x.com would collide too. For email that is almost always fine (and arguably desirable), but if you need accents to matter, use utf8mb4_0900_as_ci (accent-sensitive, case-insensitive) instead.
Option B: a normalized generated column. Store the address as the user typed it, and add a generated column that lowercases it, with the UNIQUE index on the generated column. This keeps the original casing visible while enforcing uniqueness on the canonical form, and it works regardless of the base column's collation.
email VARCHAR(255) NOT NULL,
email_norm VARCHAR(255) AS (LOWER(email)) STORED,
UNIQUE KEY uq_users_email_norm (email_norm)Option B costs you a second indexed column but gives you explicit control over normalization (you can extend the expression later, e.g. trimming whitespace). Option A is less machinery and is what I use unless I have a reason to preserve the raw casing separately. Either way, the better practice is to normalize the address in your application before insert (lowercase it, trim it) so what lands in the row is already canonical, and let the index be the backstop.
Validation belongs in the app
Do not try to make MySQL validate email format. The full RFC 5322 grammar for a valid address is famously baroque (quoted strings, comments, domain literals), and no CHECK constraint or regex you write in SQL will get it right without also rejecting valid addresses. Validate in your application, where you have a real library and where you are going to send a confirmation email anyway, which is the only check that actually proves the address works.
MySQL does support CHECK constraints, and they have been enforced since MySQL 8.0.16 (before that they parsed but were silently ignored). If you want a cheap sanity backstop at the database layer, a minimal one is fine: assert the value contains an @ and a dot after it. Keep it light. It is a guard against obviously-garbage data, not a validator.
email VARCHAR(255) NOT NULL
CHECK (email LIKE '%_@_%._%')That pattern just insists there is something before the @, something between @ and the dot, and something after the dot. It will pass plenty of invalid addresses and that is the point: the database's job is storage and uniqueness, the application's job is validation, and the confirmation email is the real test.
A worked schema
A minimal users table: email is VARCHAR(255), NOT NULL, with a UNIQUE index, on a case-insensitive collation so the uniqueness is case-folded for free.
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- signup: app has already lowercased and trimmed the address
INSERT INTO users (email) VALUES ('a@example.com');
-- a second account on the same address, any casing, is rejected by the UNIQUE index
INSERT INTO users (email) VALUES ('A@Example.com'); -- ERROR 1062: Duplicate entry
-- login lookup hits the unique index directly
SELECT id FROM users WHERE email = ?;The lookup pattern is the important part. You fetch the row by email, and the UNIQUE index doubles as the index that makes that WHERE email = ? lookup a fast single-row seek. Because the collation is case-insensitive, WHERE email = 'A@EXAMPLE.COM' finds the row stored as a@example.com without any LOWER() wrapping that would defeat the index. Pass the parameter as the user typed it (after your app-side normalization) and the index does the rest. For the conversion and DDL idioms on one page, the MySQL cheat sheet covers VARCHAR, UNIQUE, and the charset clauses used here.
What to do next
- For exact byte sizes and length-prefix behavior of every MySQL column type, see MySQL data types and sizes.
- For moving an older
utf8table toutf8mb4so it can hold internationalized addresses and index them, see the utf8 to utf8mb4 migration guide. - For the sibling decision on the password column that sits next to
emailin the sameuserstable, see the MySQL password column type.
FAQ
See also
- Storing a phone number in MySQL, the other free-text contact field with the same "validate in the app, store the canonical form" decision.
- Picking the right MySQL password column type for the hash that sits next to
emailin the sameuserstable. - The full MySQL field types and sizes reference for the exact storage and length-prefix rules behind
VARCHAR(255). - The MySQL cheat sheet for the
CREATE TABLE,UNIQUE, and charset idioms used in the worked schema above. - Adding a column to an existing MySQL table when you need to bolt an
emailcolumn onto a table that is already live. - Running MySQL in Docker to spin up a throwaway 8.0 instance and test the schema and collation behavior.
Sources
Authoritative references this article was fact-checked against.
- RFC 5321: Simple Mail Transfer Protocol (Section 4.5.3.1, size limits)rfc-editor.org
- RFC 6531: SMTP Extension for Internationalized Email (EAI)rfc-editor.org
- MySQL 8.0 Reference Manual: The CHAR and VARCHAR Typesdev.mysql.com
- MySQL 8.0 Reference Manual: Configuring Application Character Set (utf8mb4 default)dev.mysql.com
- MySQL 8.0 Reference Manual: CHECK Constraints (enforced since 8.0.16)dev.mysql.com





