Cheat Sheet

SQL Cheat Sheet — Complete Reference (Postgres, MySQL, SQLite)

Practical SQL cheat sheet: SELECT, JOIN, GROUP BY, window functions, CTEs, indexing. Works for Postgres, MySQL, SQLite. Free, dense, with worked examples.

A practical SQL reference for daily querying. Examples work in PostgreSQL; flagged where MySQL or SQLite differ. Format messy queries with the SQL formatter.

SELECT basics

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

-- All columns
SELECT * FROM users;

-- Filter
SELECT * FROM users WHERE active = true AND age >= 18;

-- Distinct values
SELECT DISTINCT country FROM users;

-- Limit + offset (pagination)
SELECT * FROM users ORDER BY created_at DESC LIMIT 20 OFFSET 40;

-- Top N (SQL Server)
SELECT TOP 20 * FROM users ORDER BY created_at DESC;

-- Conditional column
SELECT name, CASE WHEN age < 18 THEN 'minor' ELSE 'adult' END AS category FROM users;

WHERE — filter operators

OperatorExample
= != <>WHERE status = 'active'
> < >= <=WHERE age >= 18
BETWEENWHERE age BETWEEN 18 AND 65
INWHERE country IN ('US','UK','CA')
NOT INWHERE id NOT IN (1,2,3)
LIKEWHERE email LIKE '%@gmail.com'
ILIKE (Postgres)Case-insensitive LIKE
IS NULL / IS NOT NULLWHERE deleted_at IS NULL
AND / OR / NOTCombine conditions
EXISTSWHERE EXISTS (SELECT 1 FROM orders WHERE user_id = users.id)

JOINs

-- INNER JOIN: rows where match exists in both
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON o.user_id = u.id;

-- LEFT JOIN: all left rows, NULLs for missing right
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;

-- RIGHT JOIN: all right rows (rarely needed; flip the tables)
-- FULL OUTER JOIN: all rows from both, NULLs where no match (Postgres yes, MySQL no)
-- CROSS JOIN: cartesian product (every left × every right)

-- Self-join (find pairs)
SELECT a.name, b.name
FROM employees a
JOIN employees b ON b.manager_id = a.id;

Aggregation

SELECT
  country,
  COUNT(*) AS total_users,
  COUNT(DISTINCT email) AS unique_emails,
  AVG(age) AS avg_age,
  MIN(created_at) AS earliest,
  MAX(created_at) AS latest,
  SUM(balance) AS total_balance
FROM users
WHERE active = true
GROUP BY country
HAVING COUNT(*) > 100
ORDER BY total_users DESC;

HAVING vs WHERE: WHERE filters rows before aggregation; HAVING filters groups after.

Window functions

-- Rank within partition
SELECT name, department, salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank
FROM employees;

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

-- Compare to previous row
SELECT date, price,
  LAG(price, 1) OVER (ORDER BY date) AS prev_price,
  price - LAG(price, 1) OVER (ORDER BY date) AS daily_change
FROM stock_prices;

-- N-day moving average
SELECT date, price,
  AVG(price) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM stock_prices;

CTEs (WITH clauses)

-- Readable alternative to subqueries
WITH active_users AS (
  SELECT * FROM users WHERE active = true AND deleted_at IS NULL
),
recent_orders AS (
  SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '30 days'
)
SELECT u.name, COUNT(o.id) AS recent_count
FROM active_users u
LEFT JOIN recent_orders o ON o.user_id = u.id
GROUP BY u.name;

-- Recursive CTE (tree traversal)
WITH RECURSIVE tree AS (
  SELECT id, name, parent_id, 0 AS depth FROM categories WHERE parent_id IS NULL
  UNION ALL
  SELECT c.id, c.name, c.parent_id, t.depth + 1
  FROM categories c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree ORDER BY depth, name;

INSERT, UPDATE, DELETE

-- Insert one row
INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice');

-- Insert + return inserted row (Postgres)
INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice') RETURNING id, created_at;

-- Insert multiple
INSERT INTO users (email, name) VALUES
  ('[email protected]', 'Alice'),
  ('[email protected]', 'Bob');

-- Insert from select
INSERT INTO archive_users SELECT * FROM users WHERE created_at < '2020-01-01';

-- Upsert (Postgres)
INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;

-- UPDATE
UPDATE users SET active = false WHERE last_login < NOW() - INTERVAL '1 year';

-- UPDATE with JOIN (Postgres)
UPDATE orders o SET status = 'expired'
FROM users u
WHERE o.user_id = u.id AND u.deleted_at IS NOT NULL;

-- DELETE
DELETE FROM users WHERE active = false AND last_login < NOW() - INTERVAL '5 years';

Schema (DDL)

-- Create table
CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,                    -- Postgres; MySQL: BIGINT AUTO_INCREMENT
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  age INTEGER CHECK (age >= 0),
  active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(active) WHERE active = true;  -- partial (Postgres)
CREATE UNIQUE INDEX idx_users_email_lower ON users(LOWER(email));     -- expression

-- Foreign key
ALTER TABLE orders ADD COLUMN user_id BIGINT REFERENCES users(id) ON DELETE CASCADE;

-- Add column
ALTER TABLE users ADD COLUMN avatar_url TEXT;

-- Drop column
ALTER TABLE users DROP COLUMN avatar_url;

-- Rename
ALTER TABLE users RENAME COLUMN name TO full_name;

Performance — EXPLAIN

-- See the query plan
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';

-- Run + measure
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';

Look for Seq Scan (full-table read — usually means missing index) vs Index Scan (using an index — usually fast).

Date / time functions (Postgres)

FunctionReturns
NOW() / CURRENT_TIMESTAMPCurrent timestamp with timezone
CURRENT_DATEToday's date
EXTRACT(YEAR FROM created_at)Year as integer
DATE_TRUNC('month', created_at)Round down to month
created_at + INTERVAL '7 days'Add 7 days
AGE(created_at)Time since (interval)

JSON in Postgres (JSONB)

-- Access field
SELECT data->'name' FROM users;          -- returns JSONB
SELECT data->>'name' FROM users;         -- returns text

-- Nested path
SELECT data#>>'{address,city}' FROM users;

-- Filter
SELECT * FROM users WHERE data->>'role' = 'admin';

-- Index for JSONB queries
CREATE INDEX idx_users_data ON users USING GIN (data);

Related tools

Format messy queries: SQL formatter. Inspect JSONB query results: JSON formatter. Convert query results to JSON for an API: CSV to JSON. Comparing schema migrations side by side: diff checker.

Featured Tools

Try these free tools directly in your browser — no sign-up required.

sql cheat sheet sql commands sql reference sql joins sql window functions

Explore 300+ Free Tools

Utilko has tools for developers, writers, designers, students, and everyday users — all free, all browser-based.