SQL Cheat Sheet: Queries, Joins, Functions, and More

6 min read
Beginner SQL Database MySQL PostgreSQL Cheat Sheet

Quick Answer: SELECT * FROM users WHERE age > 18 ORDER BY name LIMIT 10; gets 10 adult users sorted by name. INSERT INTO users (name, age) VALUES ('Sam', 30); adds a row. UPDATE users SET age = 31 WHERE name = 'Sam'; updates it. DELETE FROM users WHERE id = 5; removes it.

SELECT (Reading Data)

-- All columns
SELECT * FROM users;

-- Specific columns
SELECT name, email FROM users;

-- With alias
SELECT name AS full_name, email AS contact FROM users;

-- Unique values
SELECT DISTINCT country FROM users;

-- Count rows
SELECT COUNT(*) FROM users;

WHERE (Filtering)

-- Comparison
SELECT * FROM users WHERE age > 18;
SELECT * FROM users WHERE name = 'Sam';
SELECT * FROM users WHERE age != 30;
SELECT * FROM users WHERE age >= 18 AND age <= 65;

-- BETWEEN
SELECT * FROM users WHERE age BETWEEN 18 AND 65;

-- IN (list of values)
SELECT * FROM users WHERE country IN ('US', 'UK', 'CA');

-- LIKE (pattern matching)
SELECT * FROM users WHERE name LIKE 'S%';      -- Starts with S
SELECT * FROM users WHERE email LIKE '%@gmail%'; -- Contains @gmail
SELECT * FROM users WHERE name LIKE '_am';       -- 3 chars ending in "am"

-- NULL checks
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM users WHERE phone IS NOT NULL;

-- NOT
SELECT * FROM users WHERE country NOT IN ('CN', 'RU');
SELECT * FROM users WHERE name NOT LIKE '%test%';

ORDER BY and LIMIT

-- Sort ascending (default)
SELECT * FROM users ORDER BY name;

-- Sort descending
SELECT * FROM users ORDER BY created_at DESC;

-- Multiple sort columns
SELECT * FROM users ORDER BY country ASC, name ASC;

-- Limit results
SELECT * FROM users LIMIT 10;

-- Pagination (offset)
SELECT * FROM users LIMIT 10 OFFSET 20;    -- Page 3 (skip 20, get 10)
-- MySQL shorthand:
SELECT * FROM users LIMIT 20, 10;

INSERT (Adding Data)

-- Single row
INSERT INTO users (name, email, age)
VALUES ('Sam', '[email protected]', 30);

-- Multiple rows
INSERT INTO users (name, email, age) VALUES
    ('Alice', '[email protected]', 25),
    ('Bob', '[email protected]', 35),
    ('Charlie', '[email protected]', 28);

-- Insert from select
INSERT INTO archive (name, email)
SELECT name, email FROM users WHERE active = false;

UPDATE (Modifying Data)

-- Update specific rows (always use WHERE!)
UPDATE users SET age = 31 WHERE name = 'Sam';

-- Update multiple columns
UPDATE users SET name = 'Samuel', age = 31 WHERE id = 1;

-- Update with calculation
UPDATE products SET price = price * 1.10;    -- WARNING: updates ALL rows!

-- Update with subquery
UPDATE users SET status = 'premium'
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);

DELETE (Removing Data)

-- Delete specific rows (always use WHERE!)
DELETE FROM users WHERE id = 5;

-- Delete with condition
DELETE FROM users WHERE last_login < '2025-01-01';

-- Delete all rows (keeps table structure)
DELETE FROM users;
-- Or faster:
TRUNCATE TABLE users;

JOINs

-- INNER JOIN (only matching rows from both tables)
SELECT users.name, orders.total
FROM users
INNER JOIN orders ON users.id = orders.user_id;

-- LEFT JOIN (all from left table, matching from right)
SELECT users.name, orders.total
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

-- RIGHT JOIN (all from right table, matching from left)
SELECT users.name, orders.total
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;

-- Multiple JOINs
SELECT u.name, o.total, p.name AS product
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;

-- Self JOIN
SELECT a.name AS employee, b.name AS manager
FROM employees a
JOIN employees b ON a.manager_id = b.id;
Join Type Returns
INNER JOIN Only rows with matches in both tables
LEFT JOIN All rows from left + matches from right (NULL if no match)
RIGHT JOIN All rows from right + matches from left
FULL OUTER JOIN All rows from both (NULL where no match)
CROSS JOIN Every combination of rows (cartesian product)

GROUP BY and Aggregates

-- Count per group
SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country;

-- Sum, average, min, max
SELECT category,
    COUNT(*) AS products,
    AVG(price) AS avg_price,
    MIN(price) AS cheapest,
    MAX(price) AS most_expensive,
    SUM(quantity) AS total_stock
FROM products
GROUP BY category;

-- Filter groups (HAVING, not WHERE)
SELECT country, COUNT(*) AS cnt
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

-- GROUP BY with JOIN
SELECT u.country, SUM(o.total) AS revenue
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.country
ORDER BY revenue DESC;

Subqueries

-- In WHERE
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 500);

-- In FROM (derived table)
SELECT avg_total FROM (
    SELECT user_id, AVG(total) AS avg_total
    FROM orders
    GROUP BY user_id
) AS user_avgs
WHERE avg_total > 100;

-- Correlated subquery
SELECT name, (
    SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id
) AS order_count
FROM users;

-- EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

Table Operations

-- Create table
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,     -- MySQL
    -- id SERIAL PRIMARY KEY,              -- PostgreSQL
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    age INT DEFAULT 0,
    active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Remove column
ALTER TABLE users DROP COLUMN phone;

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

-- Change column type
ALTER TABLE users MODIFY COLUMN age BIGINT;          -- MySQL
-- ALTER TABLE users ALTER COLUMN age TYPE BIGINT;    -- PostgreSQL

-- Drop table
DROP TABLE users;
DROP TABLE IF EXISTS users;

Indexes

-- Create index
CREATE INDEX idx_email ON users(email);

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

-- Composite index
CREATE INDEX idx_country_age ON users(country, age);

-- Drop index
DROP INDEX idx_email ON users;          -- MySQL
-- DROP INDEX idx_email;                -- PostgreSQL

-- View indexes
SHOW INDEX FROM users;                  -- MySQL
-- \d users                             -- PostgreSQL

Common Functions

String

CONCAT(first, ' ', last)          -- Combine strings
UPPER(name)                       -- UPPERCASE
LOWER(name)                       -- lowercase
LENGTH(name)                      -- Character count
TRIM(name)                        -- Remove whitespace
SUBSTRING(name, 1, 5)             -- First 5 chars
REPLACE(name, 'old', 'new')       -- Replace text

Date (MySQL Syntax)

NOW()                             -- Current datetime
CURDATE()                         -- Current date
DATE(created_at)                  -- Extract date from datetime
YEAR(created_at)                  -- Extract year
DATEDIFF(end, start)              -- Days between dates
DATE_ADD(date, INTERVAL 7 DAY)   -- Add 7 days
n-- PostgreSQL equivalents: CURRENT_DATE, end_date - start_date, date + INTERVAL '7 days'

Numeric

ROUND(price, 2)                   -- Round to 2 decimals
CEIL(4.2)                         -- 5
FLOOR(4.8)                        -- 4
ABS(-5)                           -- 5
MOD(10, 3)                        -- 1

Conditional

-- CASE
SELECT name,
    CASE
        WHEN age < 13 THEN 'Child'
        WHEN age < 18 THEN 'Teen'
        ELSE 'Adult'
    END AS age_group
FROM users;

-- COALESCE (first non-null)
SELECT COALESCE(phone, email, 'No contact') FROM users;

-- NULLIF
SELECT NULLIF(score, 0)    -- Returns NULL if score is 0
FROM tests;

Common Patterns

Upsert (Insert or Update)

-- MySQL
INSERT INTO users (id, name, email) VALUES (1, 'Sam', '[email protected]') AS new
ON DUPLICATE KEY UPDATE name = new.name, email = new.email;

-- PostgreSQL
INSERT INTO users (id, name, email) VALUES (1, 'Sam', '[email protected]')
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email;

Ranking

SELECT name, score,
    ROW_NUMBER() OVER (ORDER BY score DESC) AS rank,
    RANK() OVER (ORDER BY score DESC) AS rank_with_ties,
    DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM players;

Running Total

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

See Also