SynfraCore
Synfracore
Start Learning
Navigation

Academies

Platform

RoadmapsLabsCertificationsInterviewPYQsAI AssistantCareer
Start Learning Free🗺️ Learning Roadmaps

PostgreSQLFundamentals

Core concepts and commands — hands-on from the start

✍️
Written by senior engineers. Reviewed for technical accuracy.· Updated 2025 · SynfraCore PostgreSQL Team
Expert Content

PostgreSQL — Fundamentals

Connect and Basic Operations

bash
# Connect
psql -h localhost -U postgres -d mydb
psql "postgresql://user:pass@localhost:5432/mydb"

# Inside psql
\l          -- List databases
\c mydb     -- Connect to database
\dt         -- List tables
\d users    -- Describe table
\timing     -- Show query execution time
\x          -- Toggle expanded display
\e          -- Open editor for query
\q          -- Quit

Data Types

sql
-- Common types
INTEGER, BIGINT, SMALLINT          -- Whole numbers
NUMERIC(10,2), DECIMAL             -- Exact decimal (money)
REAL, DOUBLE PRECISION             -- Floating point
TEXT, VARCHAR(n), CHAR(n)          -- Strings
BOOLEAN                            -- true/false
DATE, TIME, TIMESTAMP, TIMESTAMPTZ -- Date/time
UUID                               -- Universally unique identifier
JSONB                              -- Binary JSON (indexed, fast)
ARRAY                              -- e.g., TEXT[], INTEGER[]
SERIAL, BIGSERIAL                  -- Auto-increment (legacy)

-- Modern: use IDENTITY instead of SERIAL
CREATE TABLE users (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email       TEXT NOT NULL UNIQUE,
    name        TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    metadata    JSONB DEFAULT '{}',
    tags        TEXT[] DEFAULT '{}'
);

Essential SQL Patterns

sql
-- Upsert (INSERT or UPDATE)
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice')
ON CONFLICT (email)
DO UPDATE SET
    name = EXCLUDED.name,
    updated_at = NOW();

-- Returning inserted values
INSERT INTO orders (user_id, total)
VALUES (1, 99.99)
RETURNING id, created_at;

-- Window functions (don't need GROUP BY)
SELECT
    user_id,
    order_date,
    total,
    SUM(total) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total,
    RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS rank_by_amount,
    LAG(total) OVER (PARTITION BY user_id ORDER BY order_date) AS prev_order_total
FROM orders;

-- CTE (Common Table Expression)
WITH monthly_revenue AS (
    SELECT
        DATE_TRUNC('month', created_at) AS month,
        SUM(total) AS revenue
    FROM orders
    WHERE created_at > NOW() - INTERVAL '12 months'
    GROUP BY 1
),
growth AS (
    SELECT
        month,
        revenue,
        LAG(revenue) OVER (ORDER BY month) AS prev_revenue
    FROM monthly_revenue
)
SELECT
    month,
    revenue,
    ROUND((revenue - prev_revenue) / prev_revenue * 100, 2) AS growth_pct
FROM growth;

-- JSONB queries
SELECT * FROM users
WHERE metadata @> '{"plan": "enterprise"}';  -- contains

SELECT metadata->'address'->>'city' AS city
FROM users
WHERE metadata ? 'address';  -- has key

-- Array operations
SELECT * FROM posts WHERE 'python' = ANY(tags);
SELECT * FROM posts WHERE tags @> ARRAY['python', 'devops'];

Indexes

sql
-- B-tree (default) — for equality and range queries
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_created ON orders(created_at DESC);

-- Partial index — only index rows matching condition
CREATE INDEX idx_orders_pending ON orders(user_id)
WHERE status = 'pending';  -- Only indexes pending orders — much smaller

-- Composite index — order matters (leftmost prefix rule)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Supports: WHERE user_id = 1
-- Supports: WHERE user_id = 1 AND status = 'pending'
-- Does NOT support: WHERE status = 'pending' alone

-- JSONB GIN index — for @>, ?, @? operators
CREATE INDEX idx_users_metadata ON users USING GIN (metadata);

-- Full-text search
CREATE INDEX idx_posts_search ON posts
USING GIN (to_tsvector('english', title || ' ' || body));

SELECT * FROM posts
WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('english', 'devops & kubernetes');

-- Check index usage
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1;
-- Look for "Index Scan" (good) vs "Seq Scan" (full table scan)

Performance

PostgreSQL uses MVCC (Multi-Version Concurrency Control): instead of locking rows for reads, every transaction sees its own consistent snapshot of the data, so readers never block writers and writers never block readers. The tradeoff is that UPDATE/DELETE don't remove old row versions immediately — they mark them dead, and VACUUM is what actually reclaims that space. This is why VACUUM/ANALYZE below aren't optional housekeeping — skip them and dead rows accumulate (table bloat), slowing queries down over time.

sql
-- Slow query log: find queries over 1 second
ALTER SYSTEM SET log_min_duration_statement = 1000;
SELECT pg_reload_conf();

-- See running queries
SELECT pid, now() - pg_stat_activity.query_start AS duration,
       query, state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes';

-- Kill long-running query
SELECT pg_terminate_backend(pid);

-- Index usage stats
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

-- Unused indexes (candidates for removal)
SELECT indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexname NOT LIKE '%pkey%';

-- Table sizes
SELECT tablename,
       pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC;

-- VACUUM and ANALYZE (maintain performance)
VACUUM ANALYZE users;   -- Manual run
-- Or configure autovacuum (usually leave at defaults)

Connection Pooling with PgBouncer

ini
# pgbouncer.ini
[databases]
myapp = host=postgres port=5432 dbname=myapp

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 5432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction   # transaction pooling (most efficient)
max_client_conn = 1000
default_pool_size = 20    # Connections to PostgreSQL
Share:
Join our Community
Daily tips, job alerts, interview help — join engineers learning together
Up Next
PostgreSQLIntermediate
Real-world patterns, best practices, and deeper topics
Also Worth Exploring
← Back to all PostgreSQL modules
InstallationIntermediate