PostgreSQL Introduction - Relational Database Basics

beginner | 65 min read | 2024.12.19

What You’ll Learn in This Tutorial

✓ PostgreSQL setup
✓ Basic SQL statements (SELECT, INSERT, UPDATE, DELETE)
✓ Table creation and relations
✓ Indexes and performance
✓ Transactions
✓ Practical query patterns

Prerequisites

  • Basic command line operations
  • Basic programming knowledge

Step 1: PostgreSQL Setup

Installation

# macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16

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

# Windows
# Download installer from PostgreSQL official site

Connection Check

# Connect to PostgreSQL
psql -U postgres

# Check version
SELECT version();

# Exit
\q

Creating a Database

-- Create database
CREATE DATABASE tutorial_db;

-- List databases
\l

-- Connect to database
\c tutorial_db

-- Check current database
SELECT current_database();

Step 2: Table Creation and Basic Operations

Creating Tables

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

-- Products table
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
    stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
    category VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

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

-- Order items table
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id INTEGER NOT NULL REFERENCES products(id),
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(10, 2) NOT NULL
);

Checking Tables

-- List tables
\dt

-- Check table structure
\d users
\d+ products

-- Detailed information
SELECT
    column_name,
    data_type,
    character_maximum_length,
    is_nullable,
    column_default
FROM information_schema.columns
WHERE table_name = 'users';

Step 3: Inserting Data (INSERT)

Basic INSERT

-- Insert single row
INSERT INTO users (username, email, password_hash, full_name)
VALUES ('john', 'john@example.com', 'hash123', 'John Doe');

-- Insert multiple rows
INSERT INTO users (username, email, password_hash, full_name)
VALUES
    ('jane', 'jane@example.com', 'hash456', 'Jane Smith'),
    ('bob', 'bob@example.com', 'hash789', 'Bob Wilson'),
    ('alice', 'alice@example.com', 'hash012', 'Alice Brown');

-- Insert product data
INSERT INTO products (name, description, price, stock, category)
VALUES
    ('Laptop', 'High-performance laptop', 980, 50, 'electronics'),
    ('Mouse', 'Wireless mouse', 35, 200, 'electronics'),
    ('Keyboard', 'Mechanical keyboard', 120, 80, 'electronics'),
    ('T-shirt', '100% cotton', 25, 150, 'clothing'),
    ('Jeans', 'Slim fit', 80, 60, 'clothing');

RETURNING Clause

-- Get inserted data
INSERT INTO users (username, email, password_hash)
VALUES ('newuser', 'newuser@example.com', 'newhash')
RETURNING id, username, created_at;

Step 4: Retrieving Data (SELECT)

Basic SELECT

-- Get all columns
SELECT * FROM users;

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

-- Conditional retrieval
SELECT * FROM users WHERE is_active = true;

-- Multiple conditions
SELECT * FROM products
WHERE category = 'electronics' AND price < 100;

-- OR condition
SELECT * FROM products
WHERE category = 'electronics' OR category = 'clothing';

-- IN clause
SELECT * FROM products
WHERE category IN ('electronics', 'clothing');

-- LIKE (partial match)
SELECT * FROM users WHERE email LIKE '%@example.com';

-- NULL check
SELECT * FROM users WHERE full_name IS NOT NULL;

Sorting and Limits

-- Sort (ascending)
SELECT * FROM products ORDER BY price ASC;

-- Sort (descending)
SELECT * FROM products ORDER BY price DESC;

-- Sort by multiple columns
SELECT * FROM products ORDER BY category ASC, price DESC;

-- Limit rows
SELECT * FROM products LIMIT 10;

-- With offset (pagination)
SELECT * FROM products
ORDER BY id
LIMIT 10 OFFSET 20;  -- 10 rows starting from row 21

Aggregate Functions

-- Count
SELECT COUNT(*) FROM users;

-- Sum
SELECT SUM(price * stock) as total_value FROM products;

-- Average
SELECT AVG(price) as average_price FROM products;

-- Max/Min
SELECT MAX(price), MIN(price) FROM products;

-- Group by
SELECT category, COUNT(*) as count, AVG(price) as avg_price
FROM products
GROUP BY category;

-- HAVING (filter groups)
SELECT category, COUNT(*) as count
FROM products
GROUP BY category
HAVING COUNT(*) >= 2;

Step 5: Updating and Deleting Data

UPDATE

-- Update single row
UPDATE users
SET full_name = 'John Doe (Updated)', updated_at = CURRENT_TIMESTAMP
WHERE id = 1;

-- Update multiple rows
UPDATE products
SET price = price * 0.9  -- 10% off
WHERE category = 'electronics';

-- RETURNING clause
UPDATE products
SET stock = stock - 1
WHERE id = 1
RETURNING id, name, stock;

DELETE

-- Conditional delete
DELETE FROM users WHERE id = 5;

-- Delete multiple rows
DELETE FROM products WHERE stock = 0;

-- Delete all (caution)
-- DELETE FROM products;

-- TRUNCATE for fast deletion
-- TRUNCATE TABLE products;

Step 6: JOIN for Table Combination

Test Data Preparation

-- Order data
INSERT INTO orders (user_id, total_amount, status)
VALUES
    (1, 1015, 'completed'),
    (2, 155, 'pending'),
    (1, 80, 'completed');

-- Order item data
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
    (1, 1, 1, 980),
    (1, 2, 1, 35),
    (2, 2, 2, 35),
    (2, 3, 1, 120),
    (3, 5, 1, 80);

INNER JOIN

-- Join users and orders
SELECT
    u.username,
    o.id as order_id,
    o.total_amount,
    o.status,
    o.ordered_at
FROM orders o
INNER JOIN users u ON o.user_id = u.id;

LEFT JOIN

-- All users with orders (including users without orders)
SELECT
    u.username,
    COUNT(o.id) as order_count,
    COALESCE(SUM(o.total_amount), 0) as total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;

Multiple Table JOIN

-- Order details (join users, orders, products)
SELECT
    u.username,
    o.id as order_id,
    p.name as product_name,
    oi.quantity,
    oi.unit_price,
    (oi.quantity * oi.unit_price) as subtotal
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON oi.product_id = p.id
ORDER BY o.id, oi.id;

Step 7: Subqueries and CTE

Subqueries

-- Subquery in WHERE
SELECT * FROM users
WHERE id IN (
    SELECT DISTINCT user_id FROM orders
);

-- Products above average price
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- Subquery in FROM
SELECT category, avg_price
FROM (
    SELECT category, AVG(price) as avg_price
    FROM products
    GROUP BY category
) as category_stats
WHERE avg_price > 50;

CTE (Common Table Expression)

-- Readable query with WITH clause
WITH user_orders AS (
    SELECT
        user_id,
        COUNT(*) as order_count,
        SUM(total_amount) as total_amount
    FROM orders
    GROUP BY user_id
)
SELECT
    u.username,
    u.email,
    uo.order_count,
    uo.total_amount
FROM users u
INNER JOIN user_orders uo ON u.id = uo.user_id
ORDER BY uo.total_amount DESC;

Step 8: Indexes

Creating Indexes

-- Single column index
CREATE INDEX idx_products_category ON products(category);

-- Compound index
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- Unique index
CREATE UNIQUE INDEX idx_users_email ON users(email);

-- Partial index
CREATE INDEX idx_active_users ON users(username) WHERE is_active = true;

Checking Indexes

-- List indexes for table
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'products';

-- Check query execution plan
EXPLAIN SELECT * FROM products WHERE category = 'electronics';

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1;

Step 9: Transactions

Basic Transaction

-- Start transaction
BEGIN;

-- Decrease stock and create order
UPDATE products SET stock = stock - 1 WHERE id = 1;
INSERT INTO orders (user_id, total_amount) VALUES (1, 980);

-- Commit if no problems
COMMIT;

-- Rollback if problems
-- ROLLBACK;

Best Practices

1. Index Design
   - Index columns used in WHERE and JOIN
   - Be conservative with frequently updated tables
   - Verify effectiveness with EXPLAIN

2. Query Optimization
   - Avoid SELECT *
   - Only retrieve needed columns
   - Use JOIN instead of N+1 queries

3. Transactions
   - Keep them short
   - Choose appropriate isolation level
   - Watch out for deadlocks

4. Security
   - Use parameterized queries
   - Principle of least privilege
   - Encrypt sensitive data

Summary

PostgreSQL is a powerful relational database. Mastering basic CRUD operations, JOINs, and transactions will enable you to handle data management for many applications. Index design for performance is also important.

← Back to list