11.6. PostgreSQL Data Types
Table of Contents
Why Data Types Matter in PostgreSQL
In PostgreSQL, every column, parameter, and expression has a data type. Choosing the right type:
- Saves storage.
- Makes queries faster.
- Prevents invalid data from being stored.
- Simplifies your code and constraints.
In this chapter, you will see the most important PostgreSQL data types you will use in backend development, with many examples that match typical API and database use cases.
Rule: Always pick the most specific and correct data type for your data, not just text or varchar.
We will not cover JSON deeply here, because there is a separate chapter for JSON in PostgreSQL.
Text Types
Textual data types store strings like names, emails, descriptions, and logs.
Main string types
PostgreSQL provides several closely related text types:
| Type | Description | Length limit stored in type? |
|---|---|---|
text | Variable length, practically unlimited | No |
varchar(n) | Variable length, with a maximum length n | Yes |
char(n) | Fixed length, padded with spaces to length n | Yes |
All three store strings, but there are some behavior differences.
`text`
text is the most flexible general string type:
CREATE TABLE users (
id serial PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE
);
Use text for:
- Descriptions.
- Comments.
- Logs.
- Any string where you do not need an enforced length limit.
`varchar(n)`
varchar(n) enforces a maximum length at the database level:
CREATE TABLE products (
id serial PRIMARY KEY,
sku varchar(32) UNIQUE NOT NULL,
title varchar(255) NOT NULL,
short_code varchar(10)
);
If you insert a value longer than n characters, PostgreSQL will throw an error:
INSERT INTO products (sku, title)
VALUES ('ABC123', 'This title is way too long ...'); -- OK if <= 255 chars
Use varchar(n) when:
- There is a real, meaningful maximum length, for example:
- ISO country codes:
varchar(2). - Phone country code:
varchar(3). - Zip code:
varchar(10).
`char(n)`
char(n) is padded with spaces to the given length and is rarely needed in modern applications:
CREATE TABLE codes (
id serial PRIMARY KEY,
code char(3) NOT NULL
);
A value 'A' stored in char(3) becomes 'A ' internally.
Use char(n) only when you integrate with legacy systems that require fixed-width fields.
Practical guidance for web apps
- For most text columns, use
text. - Use
varchar(n)where business rules require a maximum length. - Avoid
char(n)unless strictly necessary.
Numeric Types
Numeric types store numbers. PostgreSQL distinguishes between:
- Integer types for whole numbers.
- Floating-point types for approximate real numbers.
- Exact numeric types, mainly
numeric, for precise values like money.
Integer types
| Type | Storage | Range (approximate) |
|---|---|---|
smallint | 2 bytes | β32,768 to 32,767 |
integer | 4 bytes | β2.1 billion to 2.1 billion |
bigint | 8 bytes | β9.2 quintillion to 9.2 quintillion |
Typical usage:
CREATE TABLE orders (
id bigserial PRIMARY KEY,
user_id integer NOT NULL,
item_count integer NOT NULL CHECK (item_count >= 0),
status text NOT NULL
);Use:
integerfor most IDs, counts, and small numeric values.bigintfor very large counts or IDs in high-traffic systems.smallintonly when you are sure the values will stay small, for example status codes.
Auto-incrementing integers
PostgreSQL has convenience pseudo types for auto incrementing integers:
| Pseudo type | Real type |
|---|---|
serial | integer + sequence |
bigserial | bigint + sequence |
Example:
CREATE TABLE users (
id serial PRIMARY KEY,
username text NOT NULL UNIQUE
);
serial creates an integer column and an associated sequence that auto increments.
Today, you will often use identity columns instead of serial:
CREATE TABLE users (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username text NOT NULL UNIQUE
);Identity columns are the SQL standard approach and are preferable in new designs.
Floating-point types
| Type | Description |
|---|---|
real | 4-byte floating point (float4) |
double precision | 8-byte floating point (float8) |
They can store very large or very small numbers, but with approximation:
CREATE TABLE measurements (
id serial PRIMARY KEY,
temperature double precision,
humidity real
);Floating-point numbers are not exact. That means:
SELECT 0.1 + 0.2;
-- may return 0.30000000000000004So you must not use them for money or other exact values.
Exact numeric type: `numeric` / `decimal`
numeric (alias decimal) stores numbers with exact precision and scale.
CREATE TABLE invoices (
id serial PRIMARY KEY,
total_amount numeric(10, 2) NOT NULL
);Here:
10is precision, total number of digits.2is scale, digits after the decimal point.
So numeric(10, 2) allows values from -99999999.99 to 99999999.99.
Rule: Use numeric(p, s) for monetary amounts and other values that must be exact. Do not use float for prices.
Example:
INSERT INTO invoices (total_amount) VALUES (19.99);
INSERT INTO invoices (total_amount) VALUES (1000000.01);Boolean Type
Boolean values represent truth values: true or false.
CREATE TABLE feature_flags (
id serial PRIMARY KEY,
name text NOT NULL,
is_enabled boolean NOT NULL DEFAULT false
);
Accepted input values for boolean include:
Value interpreted as true | Value interpreted as false |
|---|---|
true, 't', 'yes', 'y', 'on', 1 | false, 'f', 'no', 'n', 'off', 0 |
Examples:
INSERT INTO feature_flags (name, is_enabled)
VALUES ('new_checkout', true),
('beta_profile', 'off');Result:
new_checkoutβis_enabled = true.beta_profileβis_enabled = false.
Use boolean instead of magic values like 0/1 or 'Y'/'N' for clarity.
Date and Time Types
Time-related types are critical in backend systems, for orders, logs, sessions, and more.
Main types:
| Type | Stores |
|---|---|
date | Date only (year, month, day) |
time [without time zone] | Time of day only |
time with time zone | Time of day with time zone |
timestamp without time zone | Date and time, no time zone info |
timestamp with time zone | Date and time, stored in UTC internally |
interval | Duration, difference between times |
`date`
For values that have a date but no time or time zone:
CREATE TABLE users (
id serial PRIMARY KEY,
email text NOT NULL,
date_of_birth date
);Example inserts:
INSERT INTO users (email, date_of_birth)
VALUES ('a@example.com', '1990-05-10');`timestamp with time zone` (`timestamptz`)
This is the most important type for backend systems.
timestamp with time zone is often written as timestamptz:
CREATE TABLE sessions (
id uuid PRIMARY KEY,
user_id integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL
);Key points:
- PostgreSQL stores
timestamptzin UTC. - When you insert a timestamp with a time zone, it will convert it to UTC internally.
- When you retrieve it, PostgreSQL converts it to your session time zone.
Example:
SET TIMEZONE = 'Europe/Berlin';
INSERT INTO sessions (id, user_id, expires_at)
VALUES ('00000000-0000-0000-0000-000000000001', 1, '2026-12-31 23:59:59+09'); -- JST
SELECT id, created_at, expires_at FROM sessions;
PostgreSQL will convert '2026-12-31 23:59:59+09' to UTC internally and show the result in Europe/Berlin local time.
Rule: For application events and logs, always use timestamp with time zone (timestamptz) and store everything in UTC.
`timestamp without time zone`
timestamp (no time zone) stores plain date and time without any offset.
Use it only when the time zone is not meaningful, for example:
- A store opening time pattern, like "opens at 09:00".
You must not use it for events that happen in real time across different time zones.
`interval`
interval represents durations like "2 days" or "3 hours 15 minutes".
CREATE TABLE plans (
id serial PRIMARY KEY,
name text NOT NULL,
trial_period interval
);Insert:
INSERT INTO plans (name, trial_period)
VALUES ('pro', '14 days'),
('enterprise', '1 month');You can add an interval to a timestamp:
SELECT now() + interval '7 days';
You will often use interval in queries or default expressions rather than storing many intervals as columns.
UUID
UUID is a 128-bit globally unique identifier type, ideal for resource IDs in APIs.
Format example: 550e8400-e29b-41d4-a716-446655440000.
CREATE TABLE api_keys (
id uuid PRIMARY KEY,
user_id integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);Benefits in backend APIs:
- Harder to guess than sequential integers.
- Safer to expose in URLs.
- Good for distributed systems where IDs are generated in multiple places.
Generating UUIDs in PostgreSQL
You need the uuid-ossp extension.
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
email text NOT NULL UNIQUE
);
Now you can insert without specifying id:
INSERT INTO users (email) VALUES ('user@example.com');A random version 4 UUID will be generated automatically.
You can also generate UUIDs from your backend code and send them to PostgreSQL.
Enumerated Types (ENUM)
enum types allow you to define a column that can only have one value from a predefined list.
First create the enum type:
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');Then use it in a table:
CREATE TABLE orders (
id serial PRIMARY KEY,
user_id integer NOT NULL,
status order_status NOT NULL DEFAULT 'pending'
);Insert values:
INSERT INTO orders (user_id, status)
VALUES (1, 'paid'),
(2, 'pending');
If you try to insert 'unknown' you will get an error.
Benefits:
- Strong constraint on allowed values.
- Easy to query.
Drawbacks:
- Changing enum values requires
ALTER TYPE, not as flexible as plain text + check constraints. - In large systems with frequent changes of allowed values, enums can be inconvenient.
Alternative: text column with a CHECK constraint:
CREATE TABLE orders (
id serial PRIMARY KEY,
user_id integer NOT NULL,
status text NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
);Both approaches enforce valid values. Which you choose depends on how often you expect valid values to change.
Arrays
PostgreSQL supports array types for any base type, such as integer[], text[], uuid[].
CREATE TABLE posts (
id serial PRIMARY KEY,
title text NOT NULL,
tags text[] -- array of text
);Insert arrays:
INSERT INTO posts (title, tags)
VALUES ('Intro to PostgreSQL', ARRAY['database', 'postgres', 'sql']),
('Cooking Pasta', ARRAY['food', 'pasta']);Query elements:
-- Posts that have tag 'postgres'
SELECT * FROM posts
WHERE 'postgres' = ANY (tags);You can also use array literal syntax:
INSERT INTO posts (title, tags)
VALUES ('Travel Tips', '{travel,tips}');Use arrays carefully:
- Good when a field naturally has multiple values and they are not heavily queried separately.
- For complex relationships, prefer a separate table with a foreign key (normalized design).
Example of normalized alternative to tags text[]:
CREATE TABLE tags (
id serial PRIMARY KEY,
name text UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id integer NOT NULL REFERENCES posts(id),
tag_id integer NOT NULL REFERENCES tags(id),
PRIMARY KEY (post_id, tag_id)
);JSON and JSONB (Overview only)
PostgreSQL has powerful JSON support through json and jsonb types. There is a full chapter dedicated to JSON data, so here is just a quick overview.
| Type | Description |
|---|---|
json | Stores JSON text exactly as given |
jsonb | Stores JSON in binary format, efficient for queries |
Example:
CREATE TABLE events (
id serial PRIMARY KEY,
type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO events (type, payload)
VALUES (
'user_registered',
'{"user_id": 123, "email": "test@example.com"}'
);You can query inside JSONB with special operators, which you will learn in the JSON chapter.
For backend development, jsonb is usually the better choice.
Binary Data (BYTEA)
bytea stores binary data such as small files, hashes, or encrypted tokens.
CREATE TABLE files (
id serial PRIMARY KEY,
filename text NOT NULL,
content bytea NOT NULL
);
You usually read and write bytea from your application code using parameterized queries.
In many real applications:
- You do not store large files in the database.
- You store them in object storage (like S3) and keep only metadata and URLs in PostgreSQL.
But bytea is useful for:
- Password reset token hashes.
- API key hashes.
- Small configuration binaries or certificates.
Geometric and Network Types (Brief overview)
PostgreSQL also supports some specialized types that are very useful in specific applications.
Geometric types
For applications that work with geometric shapes:
pointlinelsegboxcirclepolygon
Example:
CREATE TABLE locations (
id serial PRIMARY KEY,
name text NOT NULL,
coord point NOT NULL
);This is relevant for GIS or mapping applications, though in many cases you will use PostGIS, an extension, instead.
Network types
For IP and network related data:
| Type | Description |
|---|---|
inet | IPv4 or IPv6 address |
cidr | Network specification |
macaddr | MAC address |
Example:
CREATE TABLE access_logs (
id bigserial PRIMARY KEY,
user_id integer,
ip_address inet NOT NULL,
accessed_at timestamptz NOT NULL DEFAULT now()
);This is useful for logging, rate limiting, security analysis, and similar tasks.
Choosing the Right Data Type
Here are some common backend fields and recommended data types:
| Concept | Recommended type | Example |
|---|---|---|
| User ID | integer or uuid | id uuid PRIMARY KEY DEFAULT uuid_generate_v4() |
text or varchar(320) | email text NOT NULL | |
| Password hash | text or bytea | password_hash text NOT NULL |
| Username | varchar(30) or text with constraint | username varchar(30) UNIQUE NOT NULL |
| Price/amount | numeric(10,2) | price numeric(10,2) NOT NULL |
| Boolean flag | boolean | is_active boolean NOT NULL DEFAULT true |
| Created / updated at | timestamptz | created_at timestamptz NOT NULL DEFAULT now() |
| Status field | enum or text + check | status order_status NOT NULL |
| Tags | text[] or normalized join table | tags text[] |
| JSON metadata | jsonb | metadata jsonb |
| IP address | inet | ip inet NOT NULL |
Guideline summary:
- Use
integer/bigintfor numeric IDs unless you need UUIDs. - Use
numeric(p, s)for all monetary values. - Use
timestamptzfor timestamps in web applications. - Use
textby default for strings,varchar(n)where a real length limit exists. - Use
booleanfor flags instead ofintegeror text. - Consider
uuidfor public facing resource identifiers.
Choosing good data types early helps keep your schema consistent, your data clean, and your queries efficient.
Views: 8
KAHIBARO