
PostgreSQL treats VARCHAR and TEXT identically at the storage layer, so there is no meaningful speed or space difference between them. VARCHAR(n) adds one thing: a length check on write. Pick TEXT by default, and enforce limits explicitly where the business rule demands it.
The official documentation says the same: there is no performance difference among the character types, apart from the extra storage of blank-padded CHAR(n) and a few CPU cycles for the length check. The expensive mistakes here are not type choice. They are table-rewriting migrations, unbounded input, oversized indexes, and ORMs that silently pick a type for you. This article covers the internals, gives you a benchmark harness you can run on your own hardware, and shows where the real costs hide.
Table of Contents
- ▹How PostgreSQL Stores VARCHAR and TEXT
- ▹The Benchmark Harness
- ▹Reading the Results
- ▹Where the Real Costs Hide
- ▹Constraints: Enforcing Length the Right Way
- ▹Lock-Safe Migrations
- ▹Application-Layer Validation
- ▹Decision Framework
- ▹Production Implementation with ByteForth
- ▹Frequently Asked Questions
How PostgreSQL Stores VARCHAR and TEXT
Both types are variable-length ("varlena") types. They share the same on-disk representation, the same comparison functions, the same collation behavior, and the same TOAST handling. VARCHAR(n) is TEXT plus a type modifier that triggers a length check during input.
The mechanics that matter:
- ▹Header overhead: short values carry a 1-byte header, longer values a 4-byte header. This is identical for both types.
- ▹TOAST: when a row exceeds roughly 2 kB, PostgreSQL compresses and/or moves large values out of line into a TOAST table. Again, identical for both types. Read the TOAST docs once; it explains most "why is my table bloated" tickets.
- ▹Length semantics:
ncounts characters, not bytes. Multi-byte UTF-8 is handled correctly. - ▹Maximum size: a single value tops out around 1 GB regardless of type.
- ▹
CHAR(n): the outlier. It blank-pads ton, wastes space, and has surprising comparison semantics. Avoid it.
You can verify the storage claim in thirty seconds:
SELECT
pg_column_size('hello world'::varchar) AS varchar_bytes,
pg_column_size('hello world'::text) AS text_bytes,
pg_column_size('hello world'::char(20)) AS char_bytes;
The first two match. The CHAR(20) result is larger because of padding.
The Benchmark Harness
Most online "benchmarks