Skip to main content
โšก Calmops

PostgreSQL: The Complete Guide to the World's Most Advanced Open Source Database

Introduction

PostgreSQL, often called “Postgres,” is the world’s most advanced open-source relational database็ฎก็†็ณป็ปŸ. With over 35 years of development, PostgreSQL has evolved from a university research project into a robust, feature-rich database that powers applications ranging from small startups to enterprise giants like Uber, Netflix, and Instagram.

In 2026, PostgreSQL continues to lead the database landscape with support for JSON, vectors, full-text search, geospatial data, and enterprise-grade features. This comprehensive guide covers everything you need to get started with PostgreSQL.


What is PostgreSQL?

PostgreSQL is an object-relational database management system (ORDBMS) that emphasizes extensibility and SQL compliance. Unlike simple key-value stores, PostgreSQL provides:

  • ACID Compliance: Atomicity, Consistency, Isolation, Durability
  • Rich Data Types: From basic integers to JSON, arrays, and custom types
  • Advanced Indexing: Multiple index types for different use cases
  • Extensibility: User-defined functions, types, and operators
  • Concurrency: Multi-Version Concurrency Control (MVCC)
  • Replication: Both physical and logical replication

PostgreSQL vs MySQL

Feature PostgreSQL MySQL
Data Types More (JSON, array, range, etc.) Fewer
Index Types B-tree, Hash, GiST, GIN, BRIN, etc. B-tree, Hash, Full-text
MVCC Full support Partial (InnoDB)
Subqueries Full support Limited
Stored Procedures Multiple languages MySQL only
Foreign Keys Full support Full support
JSON Native JSON + JSONB JSON support
GIS PostGIS (advanced) Basic

Installation

# Start PostgreSQL container
docker run --name postgres \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -e POSTGRES_DB=mydb \
  -v postgres_data:/var/lib/postgresql/data \
  -p 5432:5432 \
  -d postgres:17

# Connect to PostgreSQL
docker exec -it postgres psql -U postgres -d mydb

Linux Installation

# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib

# Start PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql

# Connect as postgres user
sudo -u postgres psql

macOS Installation

# Using Homebrew
brew install postgresql@17
brew services start postgresql@17

# Connect
psql -U $(whoami) -d postgres

Windows Installation

Download the installer from https://www.postgresql.org/download/windows/ or use Chocolatey:

choco install postgresql

Basic SQL Operations

Creating Databases and Tables

-- Create database
CREATE DATABASE myapp;

-- Connect to database
\c myapp

-- Create table
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT TRUE
);

-- Create orders table with foreign key
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CRUD Operations

-- INSERT: Create new records
INSERT INTO users (username, email, password_hash)
VALUES ('john_doe', '[email protected]', 'hashed_password');

-- Insert multiple rows
INSERT INTO users (username, email, password_hash)
VALUES 
    ('alice', '[email protected]', 'hash1'),
    ('bob', '[email protected]', 'hash2');

-- INSERT...RETURNING
INSERT INTO users (username, email, password_hash)
VALUES ('new_user', '[email protected]', 'hash')
RETURNING id, username;

-- SELECT: Read data
-- Get all users
SELECT * FROM users;

-- Get specific columns
SELECT username, email FROM users;

-- WHERE clause
SELECT * FROM users WHERE is_active = TRUE;

-- ORDER BY
SELECT * FROM users ORDER BY created_at DESC;

-- LIMIT and OFFSET
SELECT * FROM users LIMIT 10 OFFSET 20;

-- UPDATE: Update records
UPDATE users 
SET email = '[email protected]' 
WHERE username = 'john_doe';

-- Update multiple columns
UPDATE users 
SET email = '[email protected]', is_active = FALSE 
WHERE id = 1;

-- DELETE: Delete records
DELETE FROM users WHERE id = 1;

-- Delete with returning
DELETE FROM users WHERE is_active = FALSE RETURNING username, email;

Query Filtering and Operators

-- Comparison operators
SELECT * FROM users WHERE age >= 18;
SELECT * FROM products WHERE price < 100;

-- IN and NOT IN
SELECT * FROM users WHERE country IN ('USA', 'Canada', 'UK');
SELECT * FROM users WHERE country NOT IN ('China', 'Russia');

-- BETWEEN
SELECT * FROM orders WHERE total_amount BETWEEN 100 AND 500;

-- LIKE and ILIKE (case-insensitive)
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE username ILIKE '%john%';

-- IS NULL and IS NOT NULL
SELECT * FROM users WHERE phone IS NOT NULL;

-- AND, OR, NOT
SELECT * FROM users WHERE is_active = TRUE AND country = 'USA';
SELECT * FROM products WHERE price < 10 OR price > 100;
SELECT * FROM users WHERE NOT is_active;

Data Types

PostgreSQL offers the most comprehensive set of data types among open-source databases.

Numeric Types

-- Integer types
SMALLINT      -- -32,768 to 32,767
INTEGER       -- -2,147,483,648 to 2,147,483,647
BIGINT        -- -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

-- Floating point
REAL          -- Single precision
DOUBLE PRECISION -- Double precision
DECIMAL(p, s) -- Exact numeric (p=precision, s=scale)
NUMERIC(p, s) -- Same as DECIMAL

-- Examples
CREATE TABLE financials (
    amount DECIMAL(15, 2),  -- 15 digits, 2 after decimal
    rate NUMERIC(5, 4),      -- 0.0000 to 999.99
    quantity INTEGER
);

Character Types

-- Character types
CHAR(n)        -- Fixed length, padded
VARCHAR(n)     -- Variable length, max n
TEXT           -- Unlimited length

-- Examples
CREATE TABLE profiles (
    code CHAR(2),      -- Always 2 characters
    name VARCHAR(100), -- Up to 100 characters
    bio TEXT           -- Unlimited
);

Date/Time Types

-- Date and time types
DATE           -- Date only (2026-03-05)
TIME           -- Time only (14:30:00)
TIMESTAMP      -- Date and time (2026-03-05 14:30:00)
TIMESTAMPTZ    -- Timezone-aware timestamp
INTERVAL       -- Time interval

-- Examples
CREATE TABLE events (
    event_date DATE,
    start_time TIME,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    duration INTERVAL
);

-- Date/time functions
SELECT CURRENT_DATE;
SELECT CURRENT_TIMESTAMP;
SELECT NOW();
SELECT EXTRACT(YEAR FROM created_at) FROM orders;
SELECT DATE_TRUNC('month', created_at) FROM orders;

JSON Types

-- JSON and JSONB
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200),
    content JSON,
    metadata JSONB
);

-- Insert JSON
INSERT INTO documents (title, content, metadata)
VALUES (
    'Invoice',
    '{"items": [{"qty": 2, "price": 10}]}',
    '{"author": "John", "version": 1}'
);

-- Query JSON
SELECT content->'items' FROM documents;
SELECT content->>'items' FROM documents;  -- As text

-- JSONB operators
SELECT * FROM documents WHERE metadata @> '{"author": "John"}';
SELECT * FROM documents WHERE content ? 'items';

Array Types

-- Array types
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    tags TEXT[],
    prices DECIMAL(10, 2)[]
);

-- Insert arrays
INSERT INTO products (name, tags, prices)
VALUES (
    'Laptop',
    ARRAY['electronics', 'computer', 'sale'],
    ARRAY[999.99, 1299.99]
);

-- Query arrays
SELECT * FROM products WHERE 'electronics' = ANY(tags);
SELECT * FROM products WHERE tags[1] = 'electronics';
SELECT array_length(tags, 1) FROM products;

Range Types

-- Built-in range types
-- int4range, int8range, numrange, tsrange, tstzrange, daterange

CREATE TABLE reservations (
    id SERIAL PRIMARY KEY,
    room_id INTEGER,
    date_range DATERANGE,
    price DECIMAL(10, 2)
);

-- Range operators
SELECT * FROM reservations 
WHERE daterange '[2026-03-01, 2026-03-10)' 
    && daterange '[2026-03-05, 2026-03-15)';

Constraints

Constraints enforce data integrity at the database level.

Primary Key

-- Single column primary key
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

-- Composite primary key
CREATE TABLE order_items (
    order_id INTEGER,
    product_id INTEGER,
    quantity INTEGER,
    PRIMARY KEY (order_id, product_id)
);

Foreign Key

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total DECIMAL(10, 2)
);

-- With ON DELETE/UPDATE actions
CREATE TABLE order_items (
    order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
    product_id INTEGER REFERENCES products(id) ON DELETE RESTRICT,
    quantity INTEGER
);

Unique Constraint

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(100) UNIQUE,
    phone VARCHAR(20)
);

-- Table-level unique constraint
CREATE TABLE user_sessions (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    token VARCHAR(255),
    UNIQUE(user_id, token)
);

Check Constraint

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10, 2) CHECK (price > 0),
    quantity INTEGER CHECK (quantity >= 0),
    discount DECIMAL(5, 2) CHECK (discount >= 0 AND discount <= 100)
);

Not Null Constraint

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    age INTEGER CHECK (age >= 18)
);

Indexes

Indexes improve query performance but slow down writes.

B-Tree Index (Default)

-- Create B-tree index
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_products_price ON products(price DESC);

-- Unique index
CREATE UNIQUE INDEX idx_users_username ON users(username);

Partial Index

-- Index only active users
CREATE INDEX idx_active_users ON users(email) WHERE is_active = TRUE;

-- Index expensive orders
CREATE INDEX idx_large_orders ON orders(total_amount) WHERE total_amount > 1000;

Composite Index

-- Composite index (order matters)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- Covering index (include additional columns)
CREATE INDEX idx_orders_user_include ON orders(user_id) INCLUDE (total_amount, status);

Specialized Indexes

-- GIN index for JSON/arrays
CREATE INDEX idx_documents_content ON documents USING GIN(content);

-- GiST index for full-text search
CREATE INDEX idx_articles_content ON articles USING GIST(to_tsvector('english', content));

-- BRIN index for sequential data
CREATE INDEX idx_logs_created ON logs USING BRIN(created_at);

-- Hash index
CREATE INDEX idx_users_email_hash ON users USING HASH(email);

Joins

-- INNER JOIN
SELECT o.id, u.username, o.total_amount
FROM orders o
INNER JOIN users u ON o.user_id = u.id;

-- LEFT JOIN
SELECT u.username, o.id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- RIGHT JOIN
SELECT u.username, o.id
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- FULL OUTER JOIN
SELECT u.username, o.id
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;

-- Multiple joins
SELECT o.id, u.username, p.name, oi.quantity
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;

Aggregations and Grouping

-- COUNT, SUM, AVG, MIN, MAX
SELECT COUNT(*) FROM users;
SELECT SUM(total_amount) FROM orders;
SELECT AVG(price) FROM products;
SELECT MIN(created_at) FROM orders;
SELECT MAX(price) FROM products;

-- GROUP BY
SELECT user_id, COUNT(*) as order_count, SUM(total_amount) as total_spent
FROM orders
GROUP BY user_id;

-- HAVING (filter groups)
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

-- Multiple aggregations
SELECT 
    status,
    COUNT(*) as count,
    SUM(total_amount) as total,
    AVG(total_amount) as average,
    MIN(total_amount) as min_amount,
    MAX(total_amount) as max_amount
FROM orders
GROUP BY status;

Window Functions

-- ROW_NUMBER
SELECT 
    username,
    ROW_NUMBER() OVER (ORDER BY created_at) as row_num
FROM users;

-- RANK and DENSE_RANK
SELECT 
    product_name,
    sales,
    RANK() OVER (ORDER BY sales DESC) as rank,
    DENSE_RANK() OVER (ORDER BY sales DESC) as dense_rank
FROM products;

-- Running total
SELECT 
    created_at,
    amount,
    SUM(amount) OVER (ORDER BY created_at) as running_total
FROM transactions;

-- Partition by
SELECT 
    department,
    employee_name,
    salary,
    AVG(salary) OVER (PARTITION BY department) as dept_avg
FROM employees;

Subqueries and Common Table Expressions (CTEs)

-- Subquery in WHERE
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- Subquery in FROM (derived table)
SELECT dept_avg.*
FROM (SELECT department, AVG(salary) as avg_salary FROM employees GROUP BY department) dept_avg
WHERE dept_avg.avg_salary > 50000;

-- EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

-- CTE (Common Table Expression)
WITH active_orders AS (
    SELECT * FROM orders WHERE status = 'completed'
),
high_value_orders AS (
    SELECT * FROM active_orders WHERE total_amount > 1000
)
SELECT u.username, hvo.*
FROM high_value_orders hvo
JOIN users u ON hvo.user_id = u.id;

-- Recursive CTE
WITH RECURSIVE org_chart AS (
    -- Base case
    SELECT id, name, manager_id, 1 as level
    FROM employees WHERE manager_id IS NULL
    
    UNION ALL
    
    -- Recursive case
    SELECT e.id, e.name, e.manager_id, oc.level + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart;

Transactions

-- Basic transaction
BEGIN;
INSERT INTO accounts (id, balance) VALUES (1, 1000);
INSERT INTO accounts (id, balance) VALUES (2, 500);
COMMIT;

-- Rollback
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- If error occurs:
ROLLBACK;

-- Savepoints
BEGIN;
INSERT INTO orders (total_amount) VALUES (100);
SAVEPOINT order_1;
INSERT INTO order_items (order_id, product_id) VALUES (1, 1);
ROLLBACK TO SAVEPOINT order_1;
COMMIT;

Isolation Levels

-- Set isolation level
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Or at start
BEGIN ISOLATION LEVEL REPEATABLE READ;

Views

-- Simple view
CREATE VIEW active_users AS
SELECT id, username, email FROM users WHERE is_active = TRUE;

-- Updatable view
CREATE VIEW order_summary AS
SELECT 
    o.id,
    u.username,
    o.total_amount,
    o.status,
    o.created_at
FROM orders o
JOIN users u ON o.user_id = u.id;

-- Materialized view (stores data physically)
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT 
    DATE_TRUNC('month', created_at) as month,
    COUNT(*) as order_count,
    SUM(total_amount) as total_sales
FROM orders
GROUP BY DATE_TRUNC('month', created_at);

-- Refresh materialized view
REFRESH MATERIALIZED VIEW monthly_sales;

Conclusion

PostgreSQL provides a powerful foundation for building any data-driven application. Its rich feature set, combined with excellent performance and reliability, makes it an ideal choice for modern development.

In the next article, we’ll explore PostgreSQL operations: backup strategies, replication, high availability, and monitoring.

Comments