TEXT vs VARCHAR(n) in PostgreSQL: the length limit myth
In PostgreSQL, TEXT and VARCHAR(n) are the same
type with different clothes. Same storage (varlena, TOAST when large), same
performance, same indexes. The (n) is not an optimization hint and
does not reserve space; it's a length check the server runs on write, nothing
more. If you came from Oracle or MySQL expecting VARCHAR(255) to be
leaner than TEXT, Postgres has no such distinction.
So the limit is useless?
No, it's a constraint, and constraints are about intent, not speed. The question for each column is: does a maximum length mean something in the domain?
uf CHAR(2),currency CHAR(3), a SKU with a fixed format: yes, the bound is part of the definition.email VARCHAR(320),name VARCHAR(120): a contract with the outside world. Downstream systems, UI layouts and partner APIs break on absurd values; the check stops a 2 MB "name" at the door.description,notes,payload: no natural bound exists.VARCHAR(255)here is a guess wearing a constraint's uniform, and the guess will be wrong.
What a wrong guess costs
The failure arrives as ERROR: value too long for type character
varying(255) in production, on data a user legitimately typed. The fix
is cheap in modern Postgres, but not free:
ALTER TABLE articles ALTER COLUMN summary TYPE VARCHAR(2000);
-- or drop the bound entirely
ALTER TABLE articles ALTER COLUMN summary TYPE TEXT;
Widening a varchar doesn't rewrite the table, but it does take an
ACCESS EXCLUSIVE lock for its (brief) duration, needs a migration,
a deploy and usually a matching change in the application layer
(@Column(length), DTO validation, form field). Multiply by every
column where 255 was a reflex.
A sane default policy
CREATE TABLE articles (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slug VARCHAR(80) UNIQUE NOT NULL, -- bounded: it's a URL segment
title VARCHAR(200) NOT NULL, -- bounded: UI and SEO contract
summary TEXT, -- unbounded prose
body TEXT NOT NULL -- unbounded prose
);
Bound what has a real limit, use TEXT for prose, and avoid the
third option people reach for: VARCHAR with no length. It's legal
and it behaves exactly like TEXT, but it reads like someone forgot
the number, and every ORM and codegen tool has to pick a default for it
(Schemint flags it and defaults the Entity to 255, which is exactly the kind of
silent guess you don't want).
If you want a bound on a TEXT-typed column without the
type-change ceremony later, a CHECK gives you the same protection and drops
without rewriting anything:
ALTER TABLE articles
ADD CONSTRAINT summary_len CHECK (length(summary) <= 2000);
How it maps to code
Either way you get String in Java and string in
TypeScript; the length never changes the type. What it changes is metadata:
Schemint carries VARCHAR(n) into @Column(length = n)
on the JPA entity and into z.string().max(n) in the Zod schema, so
the same bound is enforced at the API edge, in the model and in the database.
A TEXT column produces an unbounded String, which is
also correct: it tells the reader "prose lives here".
One MySQL habit worth dropping at the border: TINYTEXT,
MEDIUMTEXT and LONGTEXT don't exist in Postgres, and
CHAR(n) pads with spaces to exactly n, which then leaks into
comparisons and trailing-space bugs; unless a value is truly fixed-width, prefer
VARCHAR(n).