SQL Cheat Sheet

Use this SQL cheat sheet as a quick reference for querying, filtering, sorting, joining, grouping, modifying data, creating tables, and writing common database patterns.

Basic SQL Query

Use SELECT to read data from a table.

SELECT column_name
FROM table_name;

Example:

SELECT first_name, last_name
FROM customers;

Basic Query Parts

  • SELECT chooses the columns you want.
  • FROM chooses the table.
  • WHERE filters rows.
  • ORDER BY sorts results.
  • LIMIT controls how many rows come back.

Select All Columns

Use * to return every column.

SELECT *
FROM customers;

Use this for quick checks. In production queries, select only the columns you need.

SELECT id, email, created_at
FROM customers;

Select Unique Values

Use DISTINCT to remove duplicate values.

SELECT DISTINCT country
FROM customers;

This returns each country once.

Filter Rows with WHERE

Use WHERE to return rows that match a condition.

SELECT *
FROM customers
WHERE country = 'Spain';

Common Conditions

Equal to:

SELECT *
FROM products
WHERE category = 'Books';

Not equal to:

SELECT *
FROM products
WHERE category <> 'Books';

Greater than:

SELECT *
FROM orders
WHERE total > 100;

Greater than or equal to:

SELECT *
FROM orders
WHERE total >= 100;

Less than:

SELECT *
FROM orders
WHERE total < 100;

Less than or equal to:

SELECT *
FROM orders
WHERE total <= 100;

Combine Conditions

AND

All conditions must be true.

SELECT *
FROM customers
WHERE country = 'Germany'
  AND status = 'active';

OR

At least one condition must be true.

SELECT *
FROM customers
WHERE country = 'Germany'
   OR country = 'Austria';

NOT

Reverses a condition.

SELECT *
FROM customers
WHERE NOT status = 'inactive';

Group Conditions with Parentheses

SELECT *
FROM orders
WHERE status = 'paid'
  AND (country = 'Germany' OR country = 'Austria');

Use parentheses when mixing AND and OR to avoid confusing results.

Match Multiple Values with IN

Use IN instead of repeating many OR conditions.

SELECT *
FROM customers
WHERE country IN ('Germany', 'Austria', 'Switzerland');

Use NOT IN to exclude values.

SELECT *
FROM customers
WHERE country NOT IN ('Germany', 'Austria');

Match Ranges with BETWEEN

Use BETWEEN for inclusive ranges.

SELECT *
FROM orders
WHERE total BETWEEN 50 AND 200;

This includes 50 and 200.

Date range example:

SELECT *
FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-01-31';

Match Text with LIKE

Use LIKE for pattern matching.

Starts With

SELECT *
FROM customers
WHERE email LIKE 'admin%';

Ends With

SELECT *
FROM customers
WHERE email LIKE '%@example.com';

Contains

SELECT *
FROM products
WHERE name LIKE '%phone%';

One Character Match

SELECT *
FROM products
WHERE code LIKE 'A_1';

Wildcards

  • % matches any number of characters.
  • _ matches one character.

Work with NULL

NULL means a missing or unknown value. Do not compare it with =.

Find Missing Values

SELECT *
FROM customers
WHERE phone IS NULL;

Find Non-Missing Values

SELECT *
FROM customers
WHERE phone IS NOT NULL;

Replace NULL with a Default

SELECT
    name,
    COALESCE(phone, 'No phone') AS phone_display
FROM customers;

COALESCE() returns the first non-null value.

Sort Results with ORDER BY

Sort ascending:

SELECT *
FROM products
ORDER BY price ASC;

Sort descending:

SELECT *
FROM products
ORDER BY price DESC;

Sort by multiple columns:

SELECT *
FROM customers
ORDER BY country ASC, created_at DESC;

If you omit ASC or DESC, most databases sort ascending by default.

Limit Results

Use LIMIT to return fewer rows.

SELECT *
FROM customers
LIMIT 10;

Use OFFSET for pagination.

SELECT *
FROM customers
ORDER BY id
LIMIT 10 OFFSET 20;

This skips the first 20 rows and returns the next 10.

Some databases use different syntax. SQL Server often uses TOP:

SELECT TOP 10 *
FROM customers;

Rename Output with Aliases

Use AS to rename a column in the result.

SELECT
    first_name AS name,
    email AS contact_email
FROM customers;

Use aliases for calculated columns:

SELECT
    price,
    quantity,
    price * quantity AS line_total
FROM order_items;

You can also alias tables:

SELECT c.email
FROM customers AS c;

Basic Calculations

Add

SELECT price + tax AS total_price
FROM products;

Subtract

SELECT original_price - discount AS final_price
FROM products;

Multiply

SELECT price * quantity AS line_total
FROM order_items;

Divide

SELECT revenue / orders AS average_order_value
FROM sales_summary;

Be careful when dividing by zero.

SELECT
    revenue / NULLIF(orders, 0) AS average_order_value
FROM sales_summary;

NULLIF(orders, 0) returns NULL when orders is zero, which avoids a division error.

Aggregate Functions

Aggregate functions summarize multiple rows.

Count Rows

SELECT COUNT(*) AS total_customers
FROM customers;

Count Non-Null Values

SELECT COUNT(phone) AS customers_with_phone
FROM customers;

Sum

SELECT SUM(total) AS total_revenue
FROM orders;

Average

SELECT AVG(total) AS average_order_value
FROM orders;

Minimum

SELECT MIN(total) AS smallest_order
FROM orders;

Maximum

SELECT MAX(total) AS largest_order
FROM orders;

Group Results with GROUP BY

Use GROUP BY with aggregate functions.

SELECT
    country,
    COUNT(*) AS customer_count
FROM customers
GROUP BY country;

Group by multiple columns:

SELECT
    country,
    status,
    COUNT(*) AS customer_count
FROM customers
GROUP BY country, status;

Every selected column that is not aggregated should usually appear in the GROUP BY.

Filter Groups with HAVING

Use HAVING to filter grouped results.

SELECT
    customer_id,
    SUM(total) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(total) > 500;

WHERE vs HAVING

  • WHERE filters rows before grouping.
  • HAVING filters groups after aggregation.

Example with both:

SELECT
    customer_id,
    SUM(total) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
HAVING SUM(total) > 500;

Join Tables

Joins combine rows from multiple tables.

Inner Join

Returns rows with matches in both tables.

SELECT
    customers.name,
    orders.total
FROM customers
INNER JOIN orders
    ON customers.id = orders.customer_id;

Left Join

Returns all rows from the left table and matching rows from the right table.

SELECT
    customers.name,
    orders.total
FROM customers
LEFT JOIN orders
    ON customers.id = orders.customer_id;

Use LEFT JOIN when you want to keep rows even if the second table has no match.

Right Join

Returns all rows from the right table and matching rows from the left table.

SELECT
    customers.name,
    orders.total
FROM customers
RIGHT JOIN orders
    ON customers.id = orders.customer_id;

Many developers prefer rewriting this as a LEFT JOIN by switching table order.

Full Outer Join

Returns rows from both tables, even when there is no match.

SELECT
    customers.name,
    orders.total
FROM customers
FULL OUTER JOIN orders
    ON customers.id = orders.customer_id;

Not every database supports FULL OUTER JOIN.

Join with Table Aliases

Aliases make joins shorter and easier to read.

SELECT
    c.name,
    o.total,
    o.order_date
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id;

Use clear aliases, especially when joining several tables.

Join Multiple Tables

SELECT
    c.name,
    o.id AS order_id,
    p.name AS product_name
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id
INNER JOIN order_items AS oi
    ON o.id = oi.order_id
INNER JOIN products AS p
    ON oi.product_id = p.id;

This query connects customers, orders, order items, and products.

Find Rows Without a Match

Use LEFT JOIN with IS NULL.

SELECT c.*
FROM customers AS c
LEFT JOIN orders AS o
    ON c.id = o.customer_id
WHERE o.id IS NULL;

This returns customers who have no orders.

Subqueries

A subquery is a query inside another query.

Subquery in WHERE

SELECT *
FROM products
WHERE price > (
    SELECT AVG(price)
    FROM products
);

This returns products with prices above the average.

Subquery with IN

SELECT *
FROM customers
WHERE id IN (
    SELECT customer_id
    FROM orders
    WHERE total > 500
);

This returns customers who placed large orders.

Subquery in FROM

SELECT
    country,
    AVG(customer_count) AS average_count
FROM (
    SELECT
        country,
        COUNT(*) AS customer_count
    FROM customers
    GROUP BY country
) AS country_counts
GROUP BY country;

Subqueries can make complex logic easier to split into steps.

Common Table Expressions

Use WITH to create a named temporary result inside a query.

WITH customer_totals AS (
    SELECT
        customer_id,
        SUM(total) AS total_spent
    FROM orders
    GROUP BY customer_id
)
SELECT *
FROM customer_totals
WHERE total_spent > 500;

CTEs often read better than deeply nested subqueries.

Insert Data

Insert One Row

INSERT INTO customers (name, email, country)
VALUES ('Alex Smith', 'alex@example.com', 'Germany');

Insert Multiple Rows

INSERT INTO customers (name, email, country)
VALUES
    ('Alex Smith', 'alex@example.com', 'Germany'),
    ('Sam Lee', 'sam@example.com', 'Austria'),
    ('Taylor Kim', 'taylor@example.com', 'Switzerland');

Always list the column names. This makes inserts safer if the table changes later.

Update Data

Use UPDATE to change existing rows.

UPDATE customers
SET status = 'active'
WHERE id = 1;

Update multiple columns:

UPDATE customers
SET
    status = 'active',
    updated_at = CURRENT_TIMESTAMP
WHERE id = 1;

Always include a WHERE clause unless you truly want to update every row.

Delete Data

Use DELETE to remove rows.

DELETE FROM customers
WHERE id = 1;

Delete rows that match a condition:

DELETE FROM orders
WHERE status = 'cancelled';

Always check the rows first:

SELECT *
FROM orders
WHERE status = 'cancelled';

Then run the delete only after confirming the result.

Create a Table

Use CREATE TABLE to define a new table.

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE,
    country VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Common Column Types

  • INTEGER stores whole numbers.
  • DECIMAL stores exact decimal values, often for money.
  • VARCHAR(n) stores text with a maximum length.
  • TEXT stores longer text.
  • DATE stores a date.
  • TIMESTAMP stores a date and time.
  • BOOLEAN stores true or false values.

Exact type names vary between databases.

Constraints

Constraints protect data quality.

Primary Key

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100)
);

A primary key uniquely identifies each row.

Not Null

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);

NOT NULL requires a value.

Unique

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    email VARCHAR(255) UNIQUE
);

UNIQUE prevents duplicate values.

Default

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    status VARCHAR(20) DEFAULT 'active'
);

DEFAULT sets a value when none is provided.

Foreign Key

CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER,
    total DECIMAL(10, 2),
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

A foreign key links one table to another.

Alter a Table

Use ALTER TABLE to change an existing table.

Add a Column

ALTER TABLE customers
ADD phone VARCHAR(50);

Rename a Column

ALTER TABLE customers
RENAME COLUMN phone TO phone_number;

Drop a Column

ALTER TABLE customers
DROP COLUMN phone_number;

Support for ALTER TABLE differs between databases.

Drop a Table

Use DROP TABLE to remove a table.

DROP TABLE customers;

Use this carefully. It removes the table structure and its data.

Safer version:

DROP TABLE IF EXISTS customers;

Create an Index

Indexes can make queries faster.

CREATE INDEX idx_customers_email
ON customers(email);

Index multiple columns:

CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);

Use indexes on columns often used in:

  • WHERE
  • JOIN
  • ORDER BY
  • GROUP BY

Do not add indexes everywhere. They can slow down inserts and updates.

Views

A view saves a query as a reusable virtual table.

CREATE VIEW active_customers AS
SELECT
    id,
    name,
    email
FROM customers
WHERE status = 'active';

Query the view:

SELECT *
FROM active_customers;

Use views for repeated reporting logic or simplified access to complex joins.

Transactions

Transactions group changes together.

BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;

If something goes wrong, roll back:

ROLLBACK;

Use transactions when multiple changes must succeed or fail together.

Case Expressions

Use CASE to create conditional values.

SELECT
    name,
    total,
    CASE
        WHEN total >= 500 THEN 'High value'
        WHEN total >= 100 THEN 'Medium value'
        ELSE 'Low value'
    END AS order_value_group
FROM orders;

CASE works well for labels, categories, and conditional calculations.

String Functions

Function names vary slightly across databases, but these are common patterns.

Convert to Lowercase

SELECT LOWER(email) AS normalized_email
FROM customers;

Convert to Uppercase

SELECT UPPER(country) AS country_code
FROM customers;

Get String Length

SELECT LENGTH(name) AS name_length
FROM customers;

Some databases use LEN() instead of LENGTH().

Join Strings

SELECT CONCAT(first_name, ' ', last_name) AS full_name
FROM customers;

Some databases also support ||:

SELECT first_name || ' ' || last_name AS full_name
FROM customers;

Number Functions

Round

SELECT ROUND(total, 2) AS rounded_total
FROM orders;

Absolute Value

SELECT ABS(balance) AS positive_balance
FROM accounts;

Ceiling

SELECT CEIL(price) AS rounded_up
FROM products;

Some databases use CEILING() instead of CEIL().

Floor

SELECT FLOOR(price) AS rounded_down
FROM products;

Date and Time

Date functions vary between databases, but the core ideas are similar.

Current Date

SELECT CURRENT_DATE;

Current Timestamp

SELECT CURRENT_TIMESTAMP;

Filter by Date

SELECT *
FROM orders
WHERE order_date >= '2026-01-01';

Sort by Date

SELECT *
FROM orders
ORDER BY order_date DESC;

Group by Date

SELECT
    order_date,
    COUNT(*) AS order_count
FROM orders
GROUP BY order_date;

For production reporting, you may need database-specific functions to extract day, month, or year.

Window Functions

Window functions calculate values across related rows without collapsing results.

Row Number

SELECT
    customer_id,
    order_date,
    total,
    ROW_NUMBER() OVER (
        PARTITION BY customer_id
        ORDER BY order_date DESC
    ) AS order_rank
FROM orders;

This ranks each customer’s orders from newest to oldest.

Running Total

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

Rank Rows

SELECT
    customer_id,
    total,
    RANK() OVER (
        ORDER BY total DESC
    ) AS sales_rank
FROM orders;

Use window functions for rankings, running totals, moving averages, and grouped comparisons.

Set Operations

Set operations combine results from multiple queries.

UNION

Combines results and removes duplicates.

SELECT email
FROM customers
UNION
SELECT email
FROM newsletter_signups;

UNION ALL

Combines results and keeps duplicates.

SELECT email
FROM customers
UNION ALL
SELECT email
FROM newsletter_signups;

INTERSECT

Returns values found in both queries.

SELECT email
FROM customers
INTERSECT
SELECT email
FROM newsletter_signups;

EXCEPT

Returns values from the first query that are not in the second.

SELECT email
FROM customers
EXCEPT
SELECT email
FROM unsubscribed_users;

Database support for INTERSECT and EXCEPT can vary.

Common SQL Query Order

SQL is written in one order but processed in another.

Writing Order

SELECT
    country,
    COUNT(*) AS customer_count
FROM customers
WHERE status = 'active'
GROUP BY country
HAVING COUNT(*) > 10
ORDER BY customer_count DESC
LIMIT 5;

Logical Processing Order

  • FROM
  • WHERE
  • GROUP BY
  • HAVING
  • SELECT
  • ORDER BY
  • LIMIT

Knowing this helps explain why aliases may not work in some parts of a query.

Common SQL Patterns

Find Duplicate Emails

SELECT
    email,
    COUNT(*) AS email_count
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

Find Latest Order per Customer

WITH ranked_orders AS (
    SELECT
        customer_id,
        id AS order_id,
        order_date,
        total,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY order_date DESC
        ) AS row_num
    FROM orders
)
SELECT *
FROM ranked_orders
WHERE row_num = 1;

Count Orders by Status

SELECT
    status,
    COUNT(*) AS order_count
FROM orders
GROUP BY status;

Calculate Revenue by Month

SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(total) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;

DATE_TRUNC() works in PostgreSQL. Other databases use different date functions.

Find Customers with No Orders

SELECT c.*
FROM customers AS c
LEFT JOIN orders AS o
    ON c.id = o.customer_id
WHERE o.id IS NULL;

Find Top Products by Revenue

SELECT
    p.name,
    SUM(oi.quantity * oi.price) AS revenue
FROM products AS p
INNER JOIN order_items AS oi
    ON p.id = oi.product_id
GROUP BY p.name
ORDER BY revenue DESC
LIMIT 10;

Common Mistakes

Forgetting the WHERE Clause in UPDATE

UPDATE customers
SET status = 'inactive';

This updates every row.

Better:

UPDATE customers
SET status = 'inactive'
WHERE id = 42;

Forgetting the WHERE Clause in DELETE

DELETE FROM customers;

This deletes every row.

Better:

DELETE FROM customers
WHERE id = 42;

Using = with NULL

SELECT *
FROM customers
WHERE phone = NULL;

Better:

SELECT *
FROM customers
WHERE phone IS NULL;

Use IS NULL and IS NOT NULL.

Filtering Aggregates with WHERE

SELECT
    customer_id,
    SUM(total) AS total_spent
FROM orders
WHERE SUM(total) > 500
GROUP BY customer_id;

Better:

SELECT
    customer_id,
    SUM(total) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(total) > 500;

Use HAVING for aggregate filters.

Selecting Columns Not in GROUP BY

SELECT
    country,
    email,
    COUNT(*) AS customer_count
FROM customers
GROUP BY country;

Better:

SELECT
    country,
    COUNT(*) AS customer_count
FROM customers
GROUP BY country;

Grouped queries should select grouped columns or aggregate values.

Creating Duplicate Rows with Joins

SELECT
    c.name,
    o.total
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id;

This is correct if you expect one row per order. It may look wrong if you expected one row per customer.

To summarize per customer:

SELECT
    c.name,
    SUM(o.total) AS total_spent
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id
GROUP BY c.name;

Debugging Tips

Start with a Small Query

SELECT *
FROM orders
LIMIT 10;

Check the data before writing complex filters or joins.

Check Join Keys

SELECT
    c.id AS customer_id,
    o.customer_id AS order_customer_id
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id
LIMIT 10;

Make sure the columns actually match.

Count Rows Before and After a Join

SELECT COUNT(*)
FROM customers;
SELECT COUNT(*)
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id;

A large row increase may mean one-to-many relationships are creating duplicates.

Test Filters One at a Time

Start simple:

SELECT *
FROM orders
WHERE status = 'paid';

Then add another condition:

SELECT *
FROM orders
WHERE status = 'paid'
  AND total > 100;

Use Aliases for Readability

SELECT
    c.name,
    o.total
FROM customers AS c
INNER JOIN orders AS o
    ON c.id = o.customer_id;

Aliases make long queries easier to scan.

Format Queries Clearly

SELECT
    customer_id,
    SUM(total) AS total_spent
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
ORDER BY total_spent DESC;

Line breaks make SQL easier to edit and review.

Quick Reference

Select Data

SELECT column_name
FROM table_name;

Select All Columns

SELECT *
FROM table_name;

Filter Rows

SELECT *
FROM table_name
WHERE column_name = 'value';

Sort Results

SELECT *
FROM table_name
ORDER BY column_name DESC;

Limit Results

SELECT *
FROM table_name
LIMIT 10;

Count Rows

SELECT COUNT(*)
FROM table_name;

Group Results

SELECT
    column_name,
    COUNT(*) AS row_count
FROM table_name
GROUP BY column_name;

Filter Groups

SELECT
    column_name,
    COUNT(*) AS row_count
FROM table_name
GROUP BY column_name
HAVING COUNT(*) > 5;

Join Tables

SELECT
    a.column_name,
    b.column_name
FROM table_a AS a
INNER JOIN table_b AS b
    ON a.id = b.table_a_id;

Insert Data

INSERT INTO table_name (column_one, column_two)
VALUES ('value one', 'value two');

Update Data

UPDATE table_name
SET column_name = 'new value'
WHERE id = 1;

Delete Data

DELETE FROM table_name
WHERE id = 1;

Create a Table

CREATE TABLE table_name (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

Create an Index

CREATE INDEX index_name
ON table_name(column_name);

Use a CTE

WITH result_name AS (
    SELECT *
    FROM table_name
)
SELECT *
FROM result_name;