Cheat SheetsMySQLJoins & Set Ops

Joins & Set Ops — Cheat Sheet

MySQL · 5 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Joins & Set Ops
MySQL5 topicsQuick revision reference
1

JOINs — INNER, LEFT, RIGHT, CROSS

INNER JOIN returns matching rows; LEFT/RIGHT JOIN includes unmatched rows from one side; CROSS JOIN produces a Cartesian product; choose the right join to avoid silent data exclusion.

  • INNER JOIN returns only matched rows from both sides — it silently excludes rows with no match, which can lead to incorrect reports.
  • LEFT JOIN returns all rows from the left table; use it when the right side is optional (e.g., customers with or without orders).
  • MySQL does not support FULL OUTER JOIN natively; emulate it with LEFT JOIN UNION RIGHT JOIN.
  • Always index the columns used in ON clauses — especially FK columns on the child table — to avoid full table scans.
  • A missing ON condition in a JOIN produces a CROSS JOIN (Cartesian product) — every row times every row.
  • Use EXPLAIN to verify that joins use indexes (type = ref/eq_ref) rather than full table scans (type = ALL).
SQL — MySQL
-- Schema
-- customers: id, name, city
-- orders:    id, customer_id, total, created_at

-- INNER JOIN: only customers who have at least one order
SELECT c.name, o.id AS order_id, o.total
FROM   customers c
INNER  JOIN orders o ON c.id = o.customer_id;
-- Customers with no orders are excluded

-- LEFT JOIN: all customers, NULL for orders if none exist
SELECT c.name,
       COUNT(o.id)     AS order_count,
       SUM(o.total)    AS total_spent
FROM   customers c
LEFT   JOIN orders o ON c.id = o.customer_id
GROUP  BY c.id, c.name;
-- Returns every customer, 0 count and NULL sum for those with no orders
2

Subqueries

Scalar, row, and table subqueries appear in SELECT, FROM, and WHERE; correlated subqueries reference the outer query and execute per row — often replaceable with JOIN for better performance.

  • Correlated subqueries execute once per outer row — avoid in WHERE/SELECT on large tables; rewrite as JOIN.
  • EXISTS stops at the first matching row; IN fetches all matching values — EXISTS is usually faster for large subsets.
  • Derived tables in FROM must have an alias; MySQL may materialise them into a temp table.
  • MySQL 8.0 CTEs (WITH clause) are syntactically cleaner than derived tables with the same execution plan.
  • LATERAL joins (MySQL 8.0.14+) allow derived tables to reference outer query columns — useful for "latest N per group".
  • Always check EXPLAIN for correlated subqueries — look for "dependent subquery" in Extra which confirms O(n²) execution.
SQL — scalar, IN, and EXISTS subqueries
-- Scalar subquery in SELECT (executes once per output row)
SELECT
    o.id,
    o.total,
    (SELECT AVG(total) FROM orders) AS avg_order_total,
    o.total - (SELECT AVG(total) FROM orders) AS diff_from_avg
FROM orders o;

-- IN subquery: customers who placed an order this month
SELECT id, email
FROM customers
WHERE id IN (
    SELECT DISTINCT customer_id
    FROM orders
    WHERE created_at >= DATE_FORMAT(NOW(), '%Y-%m-01')
);

-- EXISTS (often faster than IN for large sets — stops at first match)
SELECT id, email
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id          -- correlated reference
      AND o.created_at >= '2024-01-01'
);
-- Note: EXISTS uses index on orders.customer_id — check EXPLAIN

-- NOT EXISTS — customers with NO orders (better than LEFT JOIN IS NULL for large tables)
SELECT id, email
FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
3

GROUP BY & Aggregation

GROUP BY collapses rows by distinct values; aggregate functions (COUNT, SUM, AVG, MIN, MAX) operate per group; ROLLUP and CUBE extend GROUP BY for hierarchical totals.

  • ONLY_FULL_GROUP_BY (default MySQL 5.7+): every SELECT column must be in GROUP BY or wrapped in an aggregate.
  • HAVING filters groups after aggregation; WHERE filters rows before — they are not interchangeable.
  • COUNT(col) counts non-NULL values; COUNT(*) counts all rows — they differ when the column has NULLs.
  • WITH ROLLUP adds subtotal and grand total rows; use GROUPING() to distinguish real NULLs from rollup markers.
  • GROUP_CONCAT aggregates values into a comma-separated string per group; max length controlled by group_concat_max_len.
  • Aggregation and window functions can be combined: aggregate with GROUP BY, then rank groups with window functions.
SQL — GROUP BY with standard aggregate functions
-- Revenue and order count by status
SELECT
    status,
    COUNT(*)                            AS order_count,
    SUM(total)                          AS revenue,
    AVG(total)                          AS avg_order_value,
    MIN(total)                          AS min_order,
    MAX(total)                          AS max_order,
    GROUP_CONCAT(id ORDER BY id LIMIT 5) AS sample_ids
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY status
HAVING COUNT(*) >= 10          -- filter groups with at least 10 orders
ORDER BY revenue DESC;

-- Multi-column grouping: revenue by month and category
SELECT
    DATE_FORMAT(created_at, '%Y-%m') AS month,
    category,
    COUNT(*)                          AS orders,
    SUM(total)                        AS revenue
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY month, category
ORDER BY month, revenue DESC;

-- COUNT(DISTINCT): unique customer count per day
SELECT
    DATE(created_at)            AS day,
    COUNT(DISTINCT customer_id) AS unique_customers,
    COUNT(*)                    AS total_orders
FROM orders
GROUP BY day
ORDER BY day;
4

HAVING Clause

HAVING filters groups after aggregation (unlike WHERE which filters rows before); it can reference aggregate expressions that are not valid in WHERE.

  • WHERE filters rows before GROUP BY; HAVING filters groups after aggregation.
  • Always push non-aggregate conditions into WHERE for better performance.
  • HAVING can reference aggregate expressions (COUNT(*), SUM()) that are invalid in WHERE.
  • COUNT(*) includes NULL rows; COUNT(column) excludes NULLs.
  • GROUP_CONCAT concatenates values within a group — useful for producing comma-separated lists.
  • WITH ROLLUP adds subtotals and grand total rows to GROUP BY results.
SQL — HAVING with GROUP BY and aggregates
-- Find customers with more than 5 orders and total spend > $1000
SELECT
    customer_id,
    COUNT(*)            AS order_count,
    SUM(total)          AS total_spent,
    AVG(total)          AS avg_order_value
FROM orders
WHERE status <> 'CANCELLED'          -- ✓ filter rows BEFORE grouping (WHERE)
GROUP BY customer_id
HAVING order_count > 5               -- ✓ filter groups AFTER aggregating (HAVING)
   AND total_spent > 1000
ORDER BY total_spent DESC;

-- Products with average rating below 3 and at least 10 reviews
SELECT
    product_id,
    AVG(rating)   AS avg_rating,
    COUNT(*)      AS review_count
FROM reviews
GROUP BY product_id
HAVING avg_rating < 3.0 AND review_count >= 10;

-- HAVING without GROUP BY — rare but valid (entire table is one group)
SELECT COUNT(*) AS total FROM orders HAVING total > 1000;
5

UNION & UNION ALL

UNION deduplicates results from two SELECT statements; UNION ALL is faster (no dedup); both require matching column count and compatible types.

  • UNION deduplicates combined rows (expensive); UNION ALL appends without dedup (fast).
  • Always use UNION ALL unless you explicitly need duplicate removal.
  • All SELECT statements must have the same number of columns with compatible types.
  • Column names in the result come from the first SELECT statement.
  • ORDER BY and LIMIT apply to the full combined result — place them at the end.
  • UNION is useful for querying sharded tables, time-partitioned archives, or multi-status reports.
SQL — UNION vs UNION ALL
-- UNION — deduplicates (sorts combined result)
SELECT id, name, 'current' AS source FROM products
UNION
SELECT id, name, 'archive' AS source FROM products_archive;
-- Removes exact duplicate rows across both queries

-- UNION ALL — no deduplication (faster)
SELECT id, name, 'UK' AS region FROM products_uk
UNION ALL
SELECT id, name, 'EU' AS region FROM products_eu
UNION ALL
SELECT id, name, 'US' AS region FROM products_us;
-- All rows returned including any duplicates

-- Performance tip:
-- UNION:     requires sort/hash to detect duplicates → O(N log N)
-- UNION ALL: simple append → O(N)
-- Always prefer UNION ALL unless duplicate removal is required

-- Column aliases — only first SELECT's aliases appear in result
SELECT id AS product_id, name AS product_name FROM products
UNION ALL
SELECT id, name FROM archived_products;  -- aliases from first SELECT are used
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/mysql