TechEarl

How to Store an Email Address in MySQL: Column Type, Length, and Uniqueness

The right column for an email in MySQL is VARCHAR(255), utf8mb4, with a UNIQUE index. Why 255, why not TEXT, how to enforce case-insensitive uniqueness, and a worked users schema.

Ishan Karunaratne⏱️ 13 min readUpdated
Share thisCopied
How to store an email address in MySQL: VARCHAR(255) versus VARCHAR(320), TEXT, and CHAR, the utf8mb4 charset, a UNIQUE index for case-insensitive uniqueness, and a worked users-table schema.

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

TypeHolds a full emailIndex the whole valueDefault value allowedPer-row costVerdict
VARCHAR(255)Yes (covers RFC's 254)YesYeslength + 1-byte prefixRecommended default
VARCHAR(320)Yes, with headroomYesYeslength + 2-byte prefixOnly if you want the 64+1+255 ceiling
TEXTYesNo (prefix only)Expression onlyinline ≤ limit, else off-pageAvoid
CHAR(255)YesYesYesfixed 255 chars, every rowAvoid (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.

sql
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.

sql
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.

sql
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.

sql
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

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsMySQLEmailVARCHARUnique IndexSchema Designutf8mb4Character Set

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