PostgreSQL Data Types
PostgreSQL Data Types
Choosing the correct data type in PostgreSQL is not just about storage — it’s about performance, maintainability, and scalability. Yet, many developers fall into long-standing myths or carry over habits from other databases like MySQL or Oracle.
This article will guide you through:
- Common myths vs facts about PostgreSQL data types
- How type choice impacts performance and storage
- A full list of PostgreSQL native and custom data types
- Detailed explanation of
enum,domain,composite, andrangetypes

Myths and Facts: Don’t Fall for These Traps
Myth 1: varchar(n) uses less space than text
Fact: Both text and varchar(n) are stored the same internally. The only difference is the length constraint. No performance or storage gain exists in modern PostgreSQL versions (>= 9.6).
Myth 2: char(n) is better for fixed strings
Fact: char(n) pads strings with spaces, resulting in wasted storage and slower comparisons. Only use it if required by external systems or strict formatting.
Myth 3: Always use numeric for numbers
Fact: numeric(p,s) is flexible but CPU-intensive. It’s ideal for precise financial calculations. Otherwise, prefer integer, bigint, or real for performance.
Myth 4: json and jsonb are the same
Fact: jsonb is binary, indexable, and faster to query. json is plain text. Always prefer jsonb unless you explicitly need formatting.
Myth 5: Use smallint for booleans
Fact: boolean uses 1 byte. smallint uses 2 bytes. Don’t sacrifice semantics and performance for imagined flexibility.
Myth 6: Always define a maximum length like varchar(255)
Fact: This is a legacy from MySQL. PostgreSQL can handle text efficiently. Adding unnecessary limits only increases maintenance complexity.
Performance Impact of Type Selection
Choosing the wrong data type leads to:
- Wasted disk space
- Bloated indexes
- Slower queries
- Higher memory and CPU usage
| Field | Type | Storage Efficiency |
| ---------- | -------------- | -------------------------------- |
| username | text | Efficient |
| username | varchar(200) | Same as text |
| username | char(200) | 200 characters always reserved |
Avoid char(n) unless truly necessary — it reserves space even if data is shorter.
PostgreSQL Native Data Types
| Type | Description | Size |
| ------------------ | ----------------------------------- | -------- |
| smallint | 2-byte integer (±32k) | 2 bytes |
| integer / int | 4-byte standard integer | 4 bytes |
| bigint | 8-byte large integer | 8 bytes |
| decimal(p,s) | Arbitrary precision decimal | variable |
| numeric(p,s) | Same as decimal (ideal for finance) | variable |
| real | Single-precision float | 4 bytes |
| double precision | Double-precision float | 8 bytes |
| serial | Auto-increment integer | 4 bytes |
| bigserial | Auto-increment bigint | 8 bytes |
Date & Time Types
| Type | Description |
| ------------- | ----------------------------------- |
| timestamp | Date and time (no timezone) |
| timestamptz | Timestamp with time zone |
| date | Only date |
| time | Only time |
| interval | Time span (e.g., '1 hour 10 min') |
Character Types
| Type | Description |
| ------------ | --------------------------------|
| text | Unlimited-length string |
| varchar(n) | Variable-length with limit |
| char(n) | Fixed-length string, space-padded |
| name | System object names |
Logical Type
| Type | Description |
| --------- | -------------------------- |
| boolean | true, false, or null |
Binary, Bit & Byte Types
| Type | Description |
| ---------------- | ----------------------------- |
| bytea | Binary data (e.g. file blobs) |
| bit(n) | Bit string of fixed length |
| bit varying(n) | Bit string of variable length |
JSON & Arrays
| Type | Description |
| ------- | ------------------------------- |
| json | Raw JSON string |
| jsonb | Binary JSON (faster, indexable) |
| array | Arrays of any PostgreSQL type |
Special Types
| Type | Description |
| --------- | -------------------------- |
| boolean | true, false, or null |
Geographic Types (PostGIS)
| Type | Description |
| --------- | -------------------------- |
| boolean | true, false, or null |
(Requires PostGIS extension)
PostgreSQL Custom Data Types
PostgreSQL supports powerful custom types for domain-specific modeling:
enum – Enumerated Types
- Store a fixed set of values like
'active','inactive' - Saves space and improves semantic integrity
- Example:
CREATE TYPE status AS ENUM ('active', 'inactive', 'pending');
Use in tables:
status status NOT NULL
domain – Constrained Types
- Define reusable types with built-in constraints
- Cleaner and DRY schema design
Example:
CREATE DOMAIN email AS TEXT
CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
Now use email just like any data type.
composite – Row Types
- Define complex records as a single type
- Useful in functions and for logical modeling
Example:
CREATE TYPE full_name AS (
first_name text,
last_name text
);
range – Range Types
- Represent a range of values (e.g.,
daterange,int4range) - Great for versioning, timelines, period logic
Example:
SELECT '[2023-01-01,2023-12-31]'::daterange;
PostgreSQL gives you incredible flexibility with data types. But with great power comes great responsibility. Here’s what you should keep in mind:
Prefertextover arbitraryvarchar(n)
Useboolean,enum, andjsonbwisely
Avoidchar(n)andnumericunless justified
Explore custom types (domain,range,composite) for better design
Know that type choice directly impacts performance and storage
← PostgreSQL Blog