M
MeshWorld.
Cheatsheet SQL Database Backend Developer Tools Query Optimization 13 min read

SQL Cheat Sheet: Queries, Joins, Aggregates & More

Vishnu
By Vishnu
| Updated: Mar 27, 2026

Quick reference tables

SELECT syntax

SyntaxWhat it does
SELECT * FROM usersSelect all columns
SELECT name, email FROM usersSelect specific columns
SELECT name AS full_name FROM usersColumn alias
SELECT DISTINCT country FROM usersUnique values only
SELECT * FROM users LIMIT 10Limit rows returned
SELECT * FROM users LIMIT 10 OFFSET 20Pagination (skip 20, take 10)
SELECT * FROM users ORDER BY name ASCSort ascending
SELECT * FROM users ORDER BY created_at DESCSort descending
SELECT * FROM users ORDER BY country ASC, name DESCMulti-column sort

Filtering — WHERE

SyntaxWhat it does
WHERE status = 'active'Equality
WHERE age != 18Not equal (<> also works)
WHERE age > 18 AND age < 65Range with AND
WHERE status = 'active' OR status = 'trial'OR condition
WHERE NOT status = 'banned'NOT
WHERE name LIKE 'Al%'Starts with (case-sensitive)
WHERE name LIKE '%son'Ends with
WHERE name LIKE '%lee%'Contains
WHERE id IN (1, 2, 3)Match any value in list
WHERE id NOT IN (4, 5)Exclude values
WHERE age BETWEEN 18 AND 65Inclusive range
WHERE email IS NULLNULL check
WHERE email IS NOT NULLNot null
WHERE created_at >= '2025-01-01'Date comparison

Conditional expressions

SyntaxWhat it does
CASE WHEN age < 18 THEN 'minor' ELSE 'adult' ENDIf/else in a column
COALESCE(nickname, name, 'Anonymous')First non-NULL value
NULLIF(value, 0)Returns NULL if value equals 0 (avoid divide-by-zero)
IIF(active, 'Yes', 'No')Shorthand CASE (SQL Server / SQLite)

Joins

JoinWhat it does
INNER JOIN orders ON orders.user_id = users.idOnly rows with matches on both sides
LEFT JOIN orders ON orders.user_id = users.idAll users; NULL for unmatched orders
RIGHT JOIN users ON orders.user_id = users.idAll orders; NULL for unmatched users
FULL OUTER JOINAll rows from both; NULL for non-matches
CROSS JOINEvery combination (cartesian product)
JOIN employees m ON e.manager_id = m.idSelf-join (same table twice)

Aggregations

FunctionWhat it does
COUNT(*)Count all rows (including NULLs)
COUNT(email)Count non-NULL values
COUNT(DISTINCT country)Count unique values
SUM(amount)Total
AVG(score)Average
MIN(price)Minimum value
MAX(price)Maximum value
GROUP BY countryGroup rows before aggregating
HAVING COUNT(*) > 5Filter groups (not rows — use after GROUP BY)

Set operations

SyntaxWhat it does
SELECT ... UNION SELECT ...Combine results, remove duplicates
SELECT ... UNION ALL SELECT ...Combine results, keep duplicates
SELECT ... INTERSECT SELECT ...Rows in both results
SELECT ... EXCEPT SELECT ...Rows in first but not second (MINUS in Oracle)

Data types

TypeExamplesNotes
INT / INTEGER42, -7Whole numbers
BIGINTLarge IDs64-bit integer
DECIMAL(10,2)99.99Fixed precision (use for money)
FLOAT / DOUBLE3.14Approximate — avoid for money
VARCHAR(255)'hello'Variable-length string with limit
TEXT'long text...'Unlimited string (PostgreSQL/MySQL)
CHAR(10)'ABCDE 'Fixed-length, padded with spaces
BOOLEANTRUE, FALSESome DBs use TINYINT(1)
DATE'2025-12-31'Date only
TIME'14:30:00'Time only
TIMESTAMP'2025-12-31 14:30:00'Date + time
JSON / JSONB'{"key": "val"}'Semi-structured data
NULLNULLUnknown/missing — not equal to anything

Constraints

ConstraintWhat it does
PRIMARY KEYUnique, not null, one per table
FOREIGN KEY (user_id) REFERENCES users(id)Referential integrity
UNIQUENo duplicate values in the column
NOT NULLColumn must have a value
CHECK (age >= 0)Custom validation rule
DEFAULT 'active'Value used when none is provided
ON DELETE CASCADEDelete child rows when parent is deleted
ON DELETE SET NULLNull out FK when parent is deleted

DDL — schema changes

CommandWhat it does
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL)Create table
DROP TABLE usersDelete table permanently
DROP TABLE IF EXISTS usersDelete only if it exists
TRUNCATE TABLE usersDelete all rows fast (no rollback in some DBs)
ALTER TABLE users ADD COLUMN age INTAdd a column
ALTER TABLE users DROP COLUMN ageRemove a column
ALTER TABLE users ALTER COLUMN name TYPE TEXTChange column type (PostgreSQL)
ALTER TABLE users RENAME COLUMN name TO full_nameRename column
ALTER TABLE users RENAME TO customersRename table
CREATE SCHEMA analyticsCreate a namespace/schema

DML — data changes

CommandWhat it does
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]')Insert one row
INSERT INTO users (name, email) VALUES (...), (...), (...)Insert multiple rows
INSERT INTO archive SELECT * FROM users WHERE active = falseInsert from query
UPDATE users SET name = 'Bob' WHERE id = 1Update one row
UPDATE users SET active = false WHERE last_login < '2024-01-01'Conditional update
DELETE FROM users WHERE id = 1Delete one row
DELETE FROM users WHERE active = falseDelete matching rows

Upsert (insert or update)

SyntaxDatabase
INSERT ... ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.namePostgreSQL
INSERT ... ON DUPLICATE KEY UPDATE name = VALUES(name)MySQL
INSERT OR REPLACE INTO ...SQLite
MERGE INTO target USING source ON (...) WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...SQL Server / Oracle

Indexes

CommandWhat it does
CREATE INDEX idx_users_email ON users(email)Basic index
CREATE UNIQUE INDEX ON users(email)Unique index
CREATE INDEX ON orders(user_id, created_at)Composite index
CREATE INDEX CONCURRENTLY ON users(email)Non-blocking (PostgreSQL)
DROP INDEX idx_users_emailRemove index

Detailed sections

SELECT — expressions and computed columns

-- Arithmetic in SELECT
SELECT price, quantity, price * quantity AS total FROM order_items;

-- CASE WHEN — if/else per row
SELECT
  name,
  score,
  CASE
    WHEN score >= 90 THEN 'A'
    WHEN score >= 80 THEN 'B'
    WHEN score >= 70 THEN 'C'
    ELSE 'F'
  END AS grade
FROM students;

-- COALESCE — first non-NULL wins
SELECT name, COALESCE(display_name, username, 'Anonymous') AS label FROM users;

-- NULLIF — avoid divide-by-zero
SELECT total_sales / NULLIF(num_orders, 0) AS avg_order_value FROM sales_summary;

-- String functions (standard SQL)
SELECT UPPER(name), LOWER(email), LENGTH(bio) FROM users;
SELECT TRIM('  hello  ');       -- 'hello'
SELECT SUBSTRING(name, 1, 3);  -- first 3 chars
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;

-- Date functions
SELECT CURRENT_DATE, CURRENT_TIMESTAMP;
SELECT EXTRACT(YEAR FROM created_at) AS year FROM orders;

JOIN patterns

-- INNER JOIN: only rows with a match on both sides
SELECT users.name, orders.total
FROM users
INNER JOIN orders ON orders.user_id = users.id;

-- LEFT JOIN: all users, even those with no orders
SELECT users.name, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id, users.name;

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

-- Self-join: employees and their managers
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- FULL OUTER JOIN: show unmatched rows from both sides
SELECT a.id AS a_id, b.id AS b_id
FROM table_a a
FULL OUTER JOIN table_b b ON a.key = b.key
WHERE a.id IS NULL OR b.id IS NULL;  -- show only non-matching rows

Subqueries

-- Scalar subquery: returns one value
SELECT name, salary,
  (SELECT AVG(salary) FROM employees) AS company_avg
FROM employees;

-- Subquery in WHERE
SELECT name FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- IN subquery
SELECT name FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders WHERE total > 1000);

-- EXISTS — often faster than IN for large sets
SELECT name FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > 1000
);

-- Correlated subquery: references outer query
SELECT name,
  (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;

-- Derived table (subquery as a FROM source)
SELECT dept, avg_sal FROM (
  SELECT department AS dept, AVG(salary) AS avg_sal
  FROM employees
  GROUP BY department
) dept_stats
WHERE avg_sal > 60000;

CTEs — Common Table Expressions

-- Basic CTE: readable named subquery
WITH active_users AS (
  SELECT id, name FROM users WHERE active = true
),
user_order_counts AS (
  SELECT user_id, COUNT(*) AS orders
  FROM orders
  GROUP BY user_id
)
SELECT u.name, COALESCE(o.orders, 0) AS total_orders
FROM active_users u
LEFT JOIN user_order_counts o ON o.user_id = u.id
ORDER BY total_orders DESC;

-- Recursive CTE: walk a hierarchy (org chart, category tree)
WITH RECURSIVE category_tree AS (
  -- Base: top-level categories
  SELECT id, name, parent_id, 0 AS depth
  FROM categories
  WHERE parent_id IS NULL

  UNION ALL

  -- Recursive: children of each row
  SELECT c.id, c.name, c.parent_id, t.depth + 1
  FROM categories c
  JOIN category_tree t ON c.parent_id = t.id
)
SELECT REPEAT('  ', depth) || name AS indented_name, depth
FROM category_tree
ORDER BY depth, name;

Window functions

Window functions compute values across a set of rows related to the current row — without collapsing them like GROUP BY does.

-- ROW_NUMBER: unique sequential rank per partition
SELECT
  name,
  department,
  salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept
FROM employees;

-- RANK vs DENSE_RANK
-- RANK skips numbers after ties (1,2,2,4); DENSE_RANK does not (1,2,2,3)
SELECT name, score,
  RANK() OVER (ORDER BY score DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM students;

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

-- Moving average (last 7 rows)
SELECT
  day,
  revenue,
  AVG(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7
FROM daily_revenue;

-- LAG / LEAD: access previous/next row
SELECT
  month,
  revenue,
  LAG(revenue) OVER (ORDER BY month) AS prev_month,
  revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly_revenue;

-- Get the most recent order per user
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
  FROM orders
) ranked
WHERE rn = 1;

Transactions

-- Basic transaction
BEGIN;
  UPDATE accounts SET balance = balance - 500 WHERE id = 1;
  UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;

-- Rollback on error
BEGIN;
  DELETE FROM orders WHERE user_id = 99;
  -- something went wrong...
ROLLBACK;

-- Savepoint: partial rollback
BEGIN;
  INSERT INTO log (event) VALUES ('step 1');
  SAVEPOINT step1;

  INSERT INTO log (event) VALUES ('step 2');
  -- oops, undo only step 2
  ROLLBACK TO SAVEPOINT step1;

  INSERT INTO log (event) VALUES ('step 2 corrected');
COMMIT;

Isolation levels (from lowest to highest isolation):

LevelDirty ReadNon-repeatable ReadPhantom Read
READ UNCOMMITTEDYesYesYes
READ COMMITTED (default)NoYesYes
REPEATABLE READNoNoYes
SERIALIZABLENoNoNo
-- Set isolation level (PostgreSQL / SQL Server)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
  -- ...
COMMIT;

Schema design patterns

-- One-to-many: users → orders
CREATE TABLE users (
  id      SERIAL PRIMARY KEY,
  name    VARCHAR(100) NOT NULL,
  email   VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
  id        SERIAL PRIMARY KEY,
  user_id   INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  total     DECIMAL(10, 2) NOT NULL,
  status    VARCHAR(50) DEFAULT 'pending',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Many-to-many: posts ↔ tags (junction table)
CREATE TABLE posts (
  id    SERIAL PRIMARY KEY,
  title TEXT NOT NULL
);

CREATE TABLE tags (
  id   SERIAL PRIMARY KEY,
  name VARCHAR(50) UNIQUE NOT NULL
);

CREATE TABLE post_tags (
  post_id INT REFERENCES posts(id) ON DELETE CASCADE,
  tag_id  INT REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (post_id, tag_id)
);

-- Query: all tags for a post
SELECT t.name
FROM tags t
JOIN post_tags pt ON pt.tag_id = t.id
WHERE pt.post_id = 42;

Cross-dialect notes

Key syntax differences between the major SQL databases:

FeaturePostgreSQLMySQLSQLiteSQL Server
Auto-increment PKSERIAL or GENERATED ALWAYS AS IDENTITYAUTO_INCREMENTINTEGER PRIMARY KEYIDENTITY(1,1)
String concat|| or CONCAT()CONCAT()||+ or CONCAT()
Limit rowsLIMIT n OFFSET mLIMIT n OFFSET mLIMIT n OFFSET mOFFSET m ROWS FETCH NEXT n ROWS ONLY
Boolean typeBOOLEANTINYINT(1)INTEGER (0/1)BIT
Current timestampNOW()NOW()datetime('now')GETDATE()
UpsertON CONFLICT DO UPDATEON DUPLICATE KEY UPDATEON CONFLICT DO UPDATEMERGE
Regex match~ (case-sensitive), ~* (i)REGEXPREGEXP (requires extension)LIKE only (no native regex)
JSON supportJSON / JSONB with rich operatorsJSON type + functionsJSON functions (3.38+)NVARCHAR + OPENJSON()
Case-sensitive LIKELIKE (case-sensitive)LIKE (case-insensitive by default)LIKE (case-insensitive for ASCII)LIKE (collation-dependent)
Full-text searchtsvector / tsqueryFULLTEXT INDEXFTS5 extensionCONTAINS / FREETEXT

Related: PostgreSQL Cheat Sheet | Debug a Slow SQL Query