This episode covers PostgreSQL's main data types: numeric, character, date and time, boolean, and UUID. It also covers constraint management such as NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY with referential actions, and CHECK to maintain data integrity.

Welcome to episode 3 of the Learn SQL with PostgreSQL series! In episode 2, we learned how to design tables with DDL. Now we'll dive deeper into the raw materials of every table: data types and constraints. Understanding both determines how safe, fast, and correct your database will be later on.
In this episode, we'll go through PostgreSQL's main data types one by one — numeric, character, date and time, boolean, and UUID — then learn how to manage constraints correctly.
PostgreSQL provides a fairly complete family of numeric types, and each type has a different use case. Choosing the right type is an important design decision.
| Type | Value range | Use case |
|---|---|---|
SMALLINT | -32,768 to 32,767 | Storing small numbers: quantity |
INTEGER / INT | -2.1 billion to 2.1 billion | Default for ids and counters |
BIGINT | Range of ±9.2 trillion | Data with massive volumes |
NUMERIC(p,s) | Exact precision, configurable | Money, precision calculations |
REAL | 4-byte floating point | Measurements where precision isn't critical |
SELECT 0.1::REAL + 0.2::REAL AS floating_error;
SELECT 0.1::NUMERIC(10,2) + 0.2::NUMERIC(10,2) AS exact_sum;Warning
The golden rule: never store money with REAL or DOUBLE PRECISION. Floating point rounding errors can cause your financial reports to be off by a few cents, which then becomes a huge difference when multiplied by millions of transactions. For money, always use NUMERIC or BIGINT in cents.
For auto-increment columns, the older generation uses SERIAL; modern SQL standards recommend GENERATED ALWAYS AS IDENTITY, which is more explicit and can't be overridden.
CREATE TABLE old_style (
id SERIAL PRIMARY KEY
);
CREATE TABLE modern_style (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);With GENERATED ALWAYS, PostgreSQL rejects manual id values — the database manages the values itself, avoiding duplicate-id bugs.
PostgreSQL provides three types for text: VARCHAR(n) (text with a length limit), CHAR(n) (fixed-length text, short values are padded with spaces), and TEXT (text without a length limit). One important fact: there is no performance penalty between VARCHAR and TEXT — the length limit in VARCHAR(n) is purely validation, not a storage optimization.
CREATE TABLE articles (
title VARCHAR(200) NOT NULL,
slug TEXT NOT NULL,
country CHAR(2) NOT NULL
);Tip
In PostgreSQL, use TEXT as the default for text columns, and only use VARCHAR(n) when you actually want to enforce a length limit as a business rule. Meanwhile CHAR(n) is rarely needed — it makes the most sense for fixed-size codes like ISO country codes.
TIMESTAMP: date and time WITHOUT timezone information. A value like 2026-08-03 10:00:00 is interpreted "as-is".TIMESTAMPTZ: date and time WITH timezone awareness. The value is stored in UTC internally, and rendered according to the session timezone.SELECT now() AS waktu_sekarang;
SELECT now() AT TIME ZONE 'Asia/Jakarta' AS waktu_jakarta;
SHOW timezone;SELECT now() + INTERVAL '7 days' AS seminggu_lagi;
SELECT age('2026-08-03', '2020-01-01') AS umur_akun;CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);UUID is a 128-bit identity that is practically impossible to collide across systems.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL,
token TEXT NOT NULL
);The gen_random_uuid() function generates random UUID v4 values; in PostgreSQL 13+ it's built in without an extension.
Constraints are the database's "police" that ensure incoming data always meets the rules — the last line of defense that works even if the application forgets to validate.
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
phone TEXT,
referral_code TEXT UNIQUE
);NOT NULL: the column is required; phone above can be empty because it has no constraint.UNIQUE: no duplicate values allowed; one email for only one customer.PRIMARY KEY: a combination of NOT NULL + UNIQUE + row identity.| Action | Behavior |
|---|---|
ON DELETE CASCADE | Delete child rows along with the parent |
ON DELETE RESTRICT | Reject deletion if child rows still exist |
ON DELETE SET NULL | Set the child FK column to NULL |
ON UPDATE CASCADE | Propagate parent PK changes to child FKs |
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
total NUMERIC(12,2) NOT NULL CHECK (total >= 0)
);
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_name TEXT NOT NULL
);CHECK is a free-form custom value validation — the last line of defense that works even if the application forgets to validate.
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
price NUMERIC(12,2) NOT NULL CHECK (price > 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0)
);Key takeaways:
NUMERIC for money, not REAL — floating point can ruin precision.GENERATED ALWAYS AS IDENTITY over SERIAL for modern auto-increment.TIMESTAMPTZ to store points in time, unless there's a special reason.CHECK, FK with referential actions, and UNIQUE are your best friends.In episode 4, we'll start filling the database with data: Data Manipulation Language (DML) and Basic Querying — from INSERT to insert data, UPDATE to modify it, DELETE to remove it, up to PostgreSQL's signature RETURNING feature which is very useful in the real world. Get the tables you created ready, because the next episode is full of hands-on practice!