Programming Languages
Learn PostgreSQL
A comprehensive PostgreSQL course.
Welcome to the comprehensive PostgreSQL course. PostgreSQL is the world's most advanced open-source relational database.
Table of contents
-
Getting Started
-
Chapter I
-
Chapter II
-
Chapter III
-
Chapter IV
-
Chapter V
-
Appendix
What is PostgreSQL?
PostgreSQL is a powerful, open-source object-relational database system that uses and extends the SQL language.
Key Characteristics
- ACID Compliant: Full support for transactions
- Extensible: User-defined types, functions, operators
- Standards Compliant: Full SQL standard compliance
- MVCC: Multi-version concurrency control
- JSON Support: Native JSON/JSONB support
- Full-Text Search: Built-in search capabilities
- NoSQL Features: Key-value, document storage
Why learn PostgreSQL?
1. Most Advanced Open Source DB
Handles complex queries and large datasets efficiently.
2. Strong Standards Compliance
Works with any SQL standard application.
3. Rich Feature Set
Advanced features not found in other databases.
4. Great Performance
Excellent for high-volume applications.
5. Growing Popularity
Used by Apple, Instagram, Spotify, and more.
Installation and Setup
Installation Options
macOS (Homebrew)
brew install postgresql
brew services start postgresqlUbuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
sudo systemctl enable postgresqlWindows
Download from postgresql.org
Basic Commands
# Connect
psql -U postgres
# With database
psql -U postgres myapp
# With host and port
psql -h localhost -p 5432 -U postgres myapppsql Commands
-- Meta commands (backslash commands)
\help -- SQL help
\? -- psql help
\l -- List databases
\c myapp -- Connect to database
\d -- List tables
\d users -- Describe table
\dt -- List tables only
\di -- List indexes
\du -- List users
\dv -- List views
\df -- List functions
\x -- Toggle expanded display
\i file.sql -- Execute file
\o output.txt -- Output to file
\timing -- Toggle query timingDatabase Basics
Creating Database
CREATE DATABASE myapp;
CREATE DATABASE myapp WITH OWNER = myuser;
CREATE DATABASE myapp ENCODING = 'UTF8';
CREATE DATABASE myapp TEMPLATE = template0;Using Database
\c myappListing Databases
\l
\l+Deleting Database
DROP DATABASE myapp;
DROP DATABASE IF EXISTS myapp;Current Database
SELECT current_database();
SELECT pg_database.datname FROM pg_database WHERE datistemplate = false;Tables
Creating Tables
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10, 2),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Table Structure
\d users
\d+ usersModifying Tables
-- Add column
ALTER TABLE users ADD COLUMN age INTEGER;
ALTER TABLE users ADD COLUMN phone VARCHAR(20) UNIQUE;
-- Add constraint
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
-- Drop column
ALTER TABLE users DROP COLUMN age;
-- Modify column type
ALTER TABLE users ALTER COLUMN username TYPE VARCHAR(100);
-- Rename column
ALTER TABLE users RENAME COLUMN created_at TO registration_date;
-- Rename table
ALTER TABLE users RENAME TO customers;Deleting Table
DROP TABLE orders;
DROP TABLE IF EXISTS orders;
DROP TABLE orders CASCADE; -- Drop dependent objectsData Types
Numeric Types
-- Integers
SMALLINT -- -32768 to 32767
INTEGER -- -2.1B to 2.1B
BIGINT -- Very large numbers
SERIAL -- Auto-incrementing integer
BIGSERIAL -- Auto-incrementing bigint
-- Arbitrary precision
NUMERIC(10, 2) -- Exact decimal
DECIMAL(10, 2) -- Same as NUMERIC
-- Floating point
REAL -- Single precision (6 digits)
DOUBLE PRECISION -- Double precision (15 digits)Character Types
VARCHAR(n) -- Variable length with limit
CHAR(n) -- Fixed length, padded
TEXT -- Variable unlimited length
-- Examples
username VARCHAR(50)
postal_code CHAR(6)
content TEXTDate/Time Types
DATE -- Calendar date
TIME -- Time of day
TIME WITH TIME ZONE
TIMESTAMP -- Date and time
TIMESTAMP WITH TIME ZONE -- Timezone aware
INTERVAL -- Time span
-- Current values
SELECT NOW(); -- Current timestamp
SELECT CURRENT_DATE; -- Current date
SELECT CURRENT_TIME; -- Current time
SELECT CURRENT_TIMESTAMP; -- Current timestamp (timezone aware)Boolean
BOOLEAN -- true, false, NULL
-- Also: 't', 'f', 'true', 'false', 'yes', 'no', '1', '0'Array Types
CREATE TABLE products (
name TEXT,
tags TEXT[] -- Array of text
);
CREATE TABLE schedules (
days INTEGER[],
times TIME[]
);
-- Using arrays
INSERT INTO products VALUES ('Laptop', ARRAY['electronics', 'computer']);
INSERT INTO products VALUES ('Phone', '{"mobile", "electronics"}');
SELECT tags[1] FROM products; -- First element (1-indexed)
SELECT array_length(tags, 1) FROM products; -- Array length
SELECT UNNEST(tags) FROM products; -- Expand array to rowsJSON Types
JSON -- Validated JSON
JSONB -- Binary JSON, faster operations
CREATE TABLE events (
data JSONB
);
INSERT INTO events VALUES ('{"name": "John", "age": 30}');Other Types
UUID -- Universally unique identifier
ENUM -- Custom enumerated types
POINT -- Geometric point
ARRAY -- Dynamic arrays
XML -- XML data
INET -- IP address
CIDR -- Network address
MACADDR -- MAC address
BYTEA -- Binary dataCreating ENUM
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered');
CREATE TABLE orders (
status order_status DEFAULT 'pending'
);CRUD Operations
INSERT
-- Insert single row
INSERT INTO users (username, email) VALUES ('john', 'john@example.com');
-- Insert with default
INSERT INTO users (username) VALUES ('jane');
-- Insert multiple rows
INSERT INTO users (username, email) VALUES
('bob', 'bob@example.com'),
('alice', 'alice@example.com');
-- Insert from select
INSERT INTO users (username, email)
SELECT username, email FROM old_users;
-- INSERT ... ON CONFLICT (Upsert)
INSERT INTO users (username, email) VALUES ('john', 'johnnew@example.com')
ON CONFLICT (username) DO UPDATE SET email = EXCLUDED.email;
-- INSERT ... ON CONFLICT DO NOTHING
INSERT INTO users (username, email) VALUES ('john', 'john@example.com')
ON CONFLICT DO NOTHING;
-- RETURNING
INSERT INTO users (username, email) VALUES ('test', 'test@example.com')
RETURNING id, username;SELECT
SELECT * FROM users;
SELECT id, username FROM users;
SELECT username AS "User Name" FROM users;
SELECT DISTINCT country FROM users;
SELECT username, price * quantity AS total FROM orders;UPDATE
-- Update single column
UPDATE users SET email = 'new@example.com' WHERE id = 1;
-- Update multiple columns
UPDATE users
SET email = 'new@example.com',
username = 'newname'
WHERE id = 1;
-- Update with expression
UPDATE products SET price = price * 0.9 WHERE category = 'sale';
-- RETURNING
UPDATE users SET status = 'active' WHERE id = 1
RETURNING id, username, status;DELETE
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE status = 'banned';
DELETE FROM users WHERE id IN (SELECT user_id FROM banned_users)
RETURNING id, username;Querying Data
WHERE Clause
SELECT * FROM users WHERE age >= 18;
SELECT * FROM users WHERE age >= 18 AND country = 'USA';
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE status IN ('active', 'premium');
SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';Pattern Matching
-- LIKE
SELECT * FROM users WHERE name LIKE 'J%'; -- Starts with J
SELECT * FROM users WHERE name LIKE '%ohn%'; -- Contains 'ohn'
SELECT * FROM users WHERE name LIKE 'J_hn'; -- J + any + hn
-- ILIKE (case-insensitive)
SELECT * FROM users WHERE name ILIKE 'j%';
-- SIMILAR TO (regex)
SELECT * FROM users WHERE name SIMILAR TO '(John|Jane)%';
-- POSIX regex (PostgreSQL)
SELECT * FROM users WHERE name ~ '^J.*';NULL Handling
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM users WHERE phone IS NOT NULL;
-- COALESCE
SELECT COALESCE(phone, email, 'No contact') AS contact FROM users;
-- NULLIF
SELECT NULLIF(value, 0); -- Returns NULL if value is 0
-- NVL alias
SELECT COALESCE(phone, email);Filtering
AND/OR/NOT
SELECT * FROM users
WHERE (age >= 18 AND status = 'active')
OR role = 'admin';
SELECT * FROM users WHERE NOT status = 'banned';IN/BETWEEN
SELECT * FROM users WHERE country IN ('USA', 'Canada', 'Mexico');
SELECT * FROM users WHERE country NOT IN ('USA', 'Canada');
SELECT * FROM products WHERE price BETWEEN 10 AND 100;
SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31';EXISTS
SELECT username FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);Sorting
ORDER BY
SELECT * FROM users ORDER BY username ASC;
SELECT * FROM users ORDER BY created_at DESC;
SELECT * FROM users ORDER BY country ASC, username ASC;
-- NULLS positioning
SELECT * FROM users ORDER BY phone NULLS FIRST;
SELECT * FROM users ORDER BY phone DESC NULLS LAST;Complex Sorting
SELECT * FROM users
ORDER BY
CASE status
WHEN 'premium' THEN 1
WHEN 'active' THEN 2
ELSE 3
END,
username ASC;Limit and Offset
LIMIT/OFFSET
SELECT * FROM users LIMIT 10;
SELECT * FROM users LIMIT 10 OFFSET 20;
SELECT * FROM users ORDER BY id DESC LIMIT 5;
-- Pagination
SELECT * FROM users LIMIT 20 OFFSET 40;FETCH (SQL standard)
SELECT * FROM users FETCH FIRST 10 ROWS ONLY;
SELECT * FROM users OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;Aggregate Functions
COUNT/SUM/AVG/MIN/MAX
SELECT COUNT(*) FROM users;
SELECT COUNT(DISTINCT country) FROM users;
SELECT SUM(amount) FROM orders;
SELECT AVG(price) FROM products;
SELECT MIN(created_at) FROM users;
SELECT MAX(salary) FROM employees;GROUP BY
SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country;
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;HAVING
SELECT country, COUNT(*) AS cnt
FROM users
GROUP BY country
HAVING COUNT(*) > 10;
SELECT category, SUM(amount) AS total
FROM products
GROUP BY category
HAVING SUM(amount) > 10000;GROUP BY with Filters
SELECT
date_trunc('month', created_at) AS month,
COUNT(*) AS order_count,
SUM(total) AS revenue
FROM orders
GROUP BY date_trunc('month', created_at)
ORDER BY month;FILTER Clause
SELECT
department,
AVG(salary) AS avg_salary,
AVG(salary) FILTER (WHERE status = 'fulltime') AS avg_fulltime_salary
FROM employees
GROUP BY department;Joins
INNER JOIN
SELECT u.username, o.order_id, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- Multiple joins
SELECT u.username, o.order_id, p.product_name
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;LEFT JOIN
SELECT u.username, o.order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- Users without orders
SELECT u.username
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;RIGHT JOIN
SELECT u.username, o.order_id
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;FULL OUTER JOIN
SELECT u.username, o.order_id
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- Exclude matches from both sides
SELECT u.username, o.order_id
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.id IS NULL;CROSS JOIN
SELECT u.username, p.product_name
FROM users u
CROSS JOIN products p;Self Join
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;Subqueries
Subquery in WHERE
SELECT * FROM users WHERE id IN (
SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5
);Subquery in SELECT
SELECT
username,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;Subquery in FROM
SELECT * FROM (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_stats
WHERE avg_salary > 50000;EXISTS/NOT EXISTS
SELECT * FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);Lateral Joins
SELECT u.username, latest_order.order_id, latest_order.order_date
FROM users u
LEFT JOIN LATERAL (
SELECT order_id, order_date
FROM orders o
WHERE o.user_id = u.id
ORDER BY order_date DESC
LIMIT 1
) latest_order ON true;Views
Creating Views
CREATE VIEW active_users AS
SELECT id, username, email
FROM users
WHERE status = 'active';
CREATE OR REPLACE VIEW order_summary AS
SELECT
o.id AS order_id,
u.username,
COUNT(oi.id) AS item_count,
SUM(oi.quantity * p.price) AS total
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
GROUP BY o.id, u.username;Using Views
SELECT * FROM active_users;
SELECT * FROM order_summary WHERE total > 100;Materialized Views
-- Create materialized view
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT
date_trunc('month', order_date) AS month,
SUM(total) AS revenue
FROM orders
GROUP BY date_trunc('month', order_date);
-- Refresh
REFRESH MATERIALIZED VIEW monthly_sales;
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales;
-- Query
SELECT * FROM monthly_sales;Drop View
DROP VIEW active_users;
DROP VIEW IF EXISTS active_users;
DROP MATERIALIZED VIEW monthly_sales;Stored Procedures
PL/pgSQL Basics
CREATE OR REPLACE PROCEDURE update_user_status(
p_id INTEGER,
p_status VARCHAR(20)
)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE users SET status = p_status WHERE id = p_id;
END;
$$;
-- Call procedure
CALL update_user_status(1, 'active');
-- Transaction control
CREATE OR REPLACE PROCEDURE transfer_funds(
from_account INTEGER,
to_account INTEGER,
amount DECIMAL
)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE accounts SET balance = balance - amount WHERE id = from_account;
UPDATE accounts SET balance = balance + amount WHERE id = to_account;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
$$;Variables
CREATE OR REPLACE PROCEDURE example()
LANGUAGE plpgsql
AS $$
DECLARE
user_count INTEGER;
user_name VARCHAR(50);
BEGIN
SELECT COUNT(*) INTO user_count FROM users;
SELECT username INTO user_name FROM users LIMIT 1;
RAISE NOTICE 'User count: %, first user: %', user_count, user_name;
END;
$$;Conditionals
CREATE OR REPLACE FUNCTION get_discount(price DECIMAL)
RETURNS DECIMAL
LANGUAGE plpgsql
AS $$
BEGIN
IF price > 100 THEN
RETURN price * 0.20;
ELSIF price > 50 THEN
RETURN price * 0.10;
ELSE
RETURN 0;
END IF;
END;
$$;Loops
CREATE OR REPLACE PROCEDURE generate_numbers(count INTEGER)
LANGUAGE plpgsql
AS $$
DECLARE
i INTEGER := 1;
BEGIN
WHILE i <= count LOOP
RAISE NOTICE 'Number: %', i;
i := i + 1;
END LOOP;
END;
$$;
-- FOR loop over query
CREATE OR REPLACE PROCEDURE process_users()
LANGUAGE plpgsql
AS $$
BEGIN
FOR user_rec IN SELECT id, username FROM users LOOP
RAISE NOTICE 'Processing: %', user_rec.username;
END LOOP;
END;
$$;Functions
Basic Function
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
count INTEGER;
BEGIN
SELECT COUNT(*) INTO count FROM users;
RETURN count;
END;
$$;
SELECT get_user_count();Function with Parameters
CREATE OR REPLACE FUNCTION get_user_by_id(user_id INTEGER)
RETURNS TABLE (
id INTEGER,
username VARCHAR(50),
email VARCHAR(100)
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT u.id, u.username, u.email
FROM users u
WHERE u.id = user_id;
END;
$$;
SELECT * FROM get_user_by_id(1);Returns SETOF
CREATE OR REPLACE FUNCTION get_users_by_country(country_name VARCHAR)
RETURNS SETOF users
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT * FROM users WHERE country = country_name;
END;
$$;RETURNS TABLE
CREATE OR REPLACE FUNCTION calculate_stats()
RETURNS TABLE (
total_count BIGINT,
avg_value NUMERIC,
max_value NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*), AVG(price), MAX(price)
INTO total_count, avg_value, max_value
FROM products;
RETURN;
END;
$$;IMMUTABLE/STABLE/VOLATILE
-- Function always returns same result for same input
CREATE OR REPLACE FUNCTION add_numbers(a INTEGER, b INTEGER)
RETURNS INTEGER
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
RETURN a + b;
END;
$$;
-- Function might modify database
CREATE OR REPLACE PROCEDURE update_user_status(
p_id INTEGER,
p_status VARCHAR(20)
)
RETURNS VOID
LANGUAGE plpgsql
VOLATILE
AS $$
BEGIN
UPDATE users SET status = p_status WHERE id = p_id;
END;
$$;Indexes
Creating Indexes
-- Single column
CREATE INDEX idx_username ON users(username);
-- Composite
CREATE INDEX idx_country_status ON users(country, status);
-- Unique
CREATE UNIQUE INDEX idx_email ON users(email);
-- Expression index
CREATE INDEX idx_lower_email ON users(LOWER(email));
-- Partial index
CREATE INDEX idx_active_users ON users(last_login)
WHERE status = 'active';
-- Covering index (INCLUDE)
CREATE INDEX idx_covering ON orders(user_id, created_at)
INCLUDE (total, status);Index Types
-- B-tree (default)
CREATE INDEX idx_btree ON users(username);
-- Hash
CREATE INDEX idx_hash ON users USING HASH (email);
-- GiST (spatial)
CREATE INDEX idx_location ON locations USING GIST (coordinates);
-- GIN (full-text, JSON)
CREATE INDEX idx_gin_tags ON products USING GIN (tags);
-- BRIN (block range, large tables)
CREATE INDEX idx_brin ON orders USING BRIN (created_at);Using Indexes
-- Show indexes
\d users
\d+ users
-- Force index usage
SELECT * FROM users FORCE INDEX (idx_username) WHERE username = 'john';
-- Disable index
ALTER INDEX idx_username DISABLE;
ALTER INDEX idx_username ENABLE;Drop Index
DROP INDEX idx_username;
DROP INDEX IF EXISTS idx_username;
DROP INDEX CONCURRENTLY idx_username; -- No lockConstraints
Primary Key
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(50)
);
-- Composite primary key
CREATE TABLE order_items (
order_id INTEGER,
product_id 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 actions
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE SET NULL
);Unique Constraint
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(100) UNIQUE,
username VARCHAR(50),
UNIQUE (username)
);NOT NULL
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
);CHECK Constraint
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price DECIMAL(10, 2) CHECK (price > 0),
quantity INTEGER CHECK (quantity >= 0)
);
-- Named constraint
CREATE TABLE employees (
salary DECIMAL(10, 2) CONSTRAINT positive_salary CHECK (salary > 0)
);
-- Table-level check
CREATE TABLE orders (
subtotal DECIMAL(10, 2),
tax DECIMAL(10, 2),
total DECIMAL(10, 2),
CHECK (subtotal + tax = total)
);Exclusion Constraint
-- Prevent overlapping reservations
CREATE TABLE room_reservations (
room_id INTEGER,
time_range TSRANGE,
EXCLUDE USING GIST (
room_id WITH =,
time_range WITH &&
)
);Transactions
Basic Transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Or rollback on error
BEGIN;
INSERT INTO orders (user_id, total) VALUES (1, 100);
COMMIT;Savepoints
BEGIN;
INSERT INTO orders (user_id, total) VALUES (1, 100);
SAVEPOINT after_order;
INSERT INTO order_items (order_id, product_id) VALUES (1, 1);
-- If error:
ROLLBACK TO SAVEPOINT after_order;
COMMIT;Isolation Levels
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- At session level
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;JSON Data
JSON/JSONB
-- JSON (stored as text)
-- JSONB (binary, faster, indexed)
CREATE TABLE events (
id SERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO events (data) VALUES
('{"name": "John", "age": 30}'),
('{"name": "Jane", "city": "NYC"}');JSON Operators
-- Access operators
SELECT data->>'name' FROM events; -- As text
SELECT data->'name' FROM events; -- As JSON
-- Where clause
SELECT * FROM events WHERE data->>'name' = 'John';
SELECT * FROM events WHERE data->'age' > 25;
-- Containment
SELECT * FROM events WHERE data @> '{"name": "John"}';
SELECT * FROM events WHERE data <@ '{"name": "John", "age": 30}';JSON Functions
-- Create JSON
SELECT json_build_object('name', 'John', 'age', 30);
SELECT row_to_json(users.*) FROM users WHERE id = 1;
-- Parse JSON
SELECT json_array_elements('["a", "b", "c"]');
SELECT json_object_keys('{"name": "John", "age": 30}');
-- Extract values
SELECT json_extract_path(data, 'name') FROM events;
SELECT json_extract_path_text(data, 'name') FROM events;GIN Index on JSONB
CREATE INDEX idx_events_data ON events USING GIN (data);
-- Query using index
SELECT * FROM events WHERE data @> '{"name": "John"}';
SELECT * FROM events WHERE data ? 'name'; -- Has key 'name'
SELECT * FROM events WHERE data ?| array['name', 'age']; -- Has any key
SELECT * FROM events WHERE data ?& array['name', 'age']; -- Has all keysUsers and Permissions
Creating Users
-- PostgreSQL 14+
CREATE USER john WITH PASSWORD 'password123';
CREATE USER app WITH PASSWORD 'password' VALID UNTIL '2025-12-31';
-- Legacy
CREATE ROLE john LOGIN PASSWORD 'password123';Granting Permissions
-- Grant privileges
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO app;
GRANT ALL PRIVILEGES ON DATABASE myapp TO app;
GRANT USAGE ON SCHEMA public TO app;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app;
-- Default privileges
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO app;Revoking Permissions
REVOKE INSERT ON users FROM app;
REVOKE ALL PRIVILEGES ON DATABASE myapp FROM app;Role Membership
CREATE ROLE reader;
GRANT reader TO app;
-- Role with admin option
GRANT reader TO admin WITH ADMIN true;Drop User/Role
DROP USER IF EXISTS john;
DROP ROLE IF EXISTS john;PostgreSQL Specifics
Schemas
-- Default schema is 'public'
CREATE SCHEMA myschema;
CREATE TABLE myschema.users (...);
-- Search path
SHOW search_path;
SET search_path TO myschema, public;Sequences
CREATE SEQUENCE my_sequence START 100 INCREMENT 5;
SELECT nextval('my_sequence');
SELECT currval('my_sequence');
ALTER SEQUENCE my_sequence RESTART WITH 100;Window Functions
-- Running total
SELECT
order_date,
total,
SUM(total) OVER (ORDER BY order_date) AS running_total
FROM orders;
-- Rank
SELECT
username,
total,
RANK() OVER (ORDER BY total DESC) AS rank,
DENSE_RANK() OVER (ORDER BY total DESC) AS dense_rank,
ROW_NUMBER() OVER (ORDER BY total DESC) AS row_num
FROM orders;
-- Partition
SELECT
department,
username,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees;Common Table Expressions (CTE)
WITH active_users AS (
SELECT * FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, SUM(total) AS total
FROM orders
GROUP BY user_id
)
SELECT
au.username,
COALESCE(uo.total, 0) AS total_spent
FROM active_users au
LEFT JOIN user_orders uo ON au.id = uo.user_id;
-- Recursive CTE
WITH RECURSIVE subordinates AS (
SELECT id, username, manager_id
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.username, e.manager_id
FROM employees e
JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;DISTINCT ON
-- Get first order per user
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at;Performance Tuning
EXPLAIN
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...ANALYZE
ANALYZE users;
ANALYZE VERBOSE users;VACUUM
VACUUM users;
VACUUM FULL users;
VACUUM ANALYZE users;
-- Autovacuum
ALTER TABLE users SET (autovacuum_vacuum_scale_factor = 0.01);Configuration
-- Show configuration
SHOW all;
SHOW shared_buffers;
-- Set configuration
ALTER SYSTEM SET shared_buffers = '256MB';
SELECT pg_reload_conf();
-- Session level
SET work_mem = '64MB';
SET LOCAL statement_timeout = '30s';pg_stat_statements
-- Enable extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Query statistics
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;Next Steps
Now that you know PostgreSQL fundamentals:
- Learn PostGIS for geospatial data
- Explore full-text search capabilities
- Study database replication
- Learn about partitioning
- Explore time-series data with TimescaleDB
- Study database security best practices