PostgreSQL commands, data types, queries, indexes, constraints, and administration
Essential psql meta-commands for navigating databases
\l -- list all databases
\c dbname -- connect to database
\dt -- list tables in current DB
\d tablename -- describe table structure
\di -- list indexes
\du -- list roles/users
\q -- quit psqlDisplay and output settings
\x -- toggle expanded output
\timing -- toggle query timing
\e -- open query in editor
\i file.sql -- execute SQL file
\o file.txt -- send output to fileCommon data types for columns
integer / int4 -- 32-bit integer
bigint / int8 -- 64-bit integer
serial -- auto-increment integer
bigserial -- auto-increment bigint
numeric(p,s) -- exact decimal
real / float4 -- 32-bit float
varchar(n) -- variable-length string (max n)
text -- unlimited string
char(n) -- fixed-length stringDate/time, boolean, JSON, and UUID types
boolean -- true/false
date -- YYYY-MM-DD
timestamp -- date and time
timestamptz -- with time zone (preferred)
interval -- time duration
jsonb -- binary JSON (indexed)
json -- text JSON
uuid -- universally unique ID
array -- e.g. integer[]Create a new table with columns and constraints
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name TEXT NOT NULL,
active BOOLEAN DEFAULT true,
created TIMESTAMPTZ DEFAULT NOW()
);Modify an existing table structure
ALTER TABLE users ADD COLUMN age INTEGER;
ALTER TABLE users DROP COLUMN age;
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users ALTER COLUMN email TYPE TEXT;Create indexes for faster queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE UNIQUE INDEX idx_unique_slug ON posts(slug);
CREATE INDEX idx_posts_created ON posts(created_at DESC);Query data with filtering, sorting, and pagination
SELECT * FROM users WHERE active = true
ORDER BY created DESC
LIMIT 20 OFFSET 40;
SELECT id, name, email FROM users
WHERE name ILIKE '%alice%'
AND created > NOW() - INTERVAL '7 days';Insert one or many rows
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO users (name, email)
SELECT name, email FROM old_users
WHERE active = true
RETURNING id, email;Modify or remove existing rows
UPDATE users SET active = false
WHERE last_login < NOW() - INTERVAL '90 days'
RETURNING id;
DELETE FROM users WHERE id = 123
RETURNING *;Different join types and when to use each
-- INNER JOIN (only matching rows)
SELECT u.name, p.title
FROM users u
INNER JOIN posts p ON p.user_id = u.id;
-- LEFT JOIN (all users, even without posts)
SELECT u.name, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id;Group and summarize data
SELECT user_id, COUNT(*) AS total_posts
FROM posts
GROUP BY user_id
HAVING COUNT(*) > 5
ORDER BY total_posts DESC;
-- Common aggregates
COUNT(*), COUNT(DISTINCT col)
SUM(col), AVG(col)
MIN(col), MAX(col)
ARRAY_AGG(col), STRING_AGG(col, ',')Compute values across related rows without collapsing groups
SELECT name, salary,
RANK() OVER (ORDER BY salary DESC) AS rank,
AVG(salary) OVER (PARTITION BY dept) AS dept_avg
FROM employees;Enforce data integrity rules
PRIMARY KEY -- unique, not null identifier
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
UNIQUE (email)
NOT NULL
CHECK (age >= 0 AND age < 150)
DEFAULT NOW()Well-structured table with foreign keys, indexes, and defaults
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
slug VARCHAR(255) NOT NULL UNIQUE,
title TEXT NOT NULL,
content TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes for common queries
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status) WHERE status = 'published';
CREATE INDEX idx_posts_created ON posts(created_at DESC);Retrieve related data with aggregates
-- Get users with their post count and latest post date
SELECT
u.id,
u.name,
u.email,
COUNT(p.id) AS post_count,
MAX(p.created_at) AS last_post_at
FROM users u
LEFT JOIN posts p ON p.user_id = u.id AND p.status = 'published'
WHERE u.active = true
GROUP BY u.id, u.name, u.email
HAVING COUNT(p.id) > 0
ORDER BY last_post_at DESC NULLS LAST
LIMIT 10;Insert or update atomically using ON CONFLICT
-- Upsert: insert or update if conflict
INSERT INTO user_settings (user_id, key, value)
VALUES (123, 'theme', 'dark')
ON CONFLICT (user_id, key)
DO UPDATE SET
value = EXCLUDED.value,
updated_at = NOW();
-- Ignore conflict (do nothing)
INSERT INTO user_follows (follower_id, followee_id)
VALUES (1, 2)
ON CONFLICT (follower_id, followee_id)
DO NOTHING;Use BIGSERIAL or GENERATED ALWAYS AS IDENTITY for auto-increment primary keys
Always index foreign key columns — PostgreSQL does not auto-index them like MySQL
Use EXPLAIN ANALYZE to diagnose slow queries and find missing indexes
Prefer timestamptz over timestamp to avoid timezone-related bugs
Use jsonb (not json) for JSON columns — it is faster and can be indexed