Advanced Queries — Cheat Sheet
MySQL · 7 topics. Download the PDF or the Instagram carousel and share it.
Window Functions
ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, SUM OVER PARTITION — window functions compute a result across a sliding frame without collapsing rows like GROUP BY.
- ✓Window functions compute per-row results over a window without collapsing rows (unlike GROUP BY)
- ✓PARTITION BY divides rows into groups; ORDER BY orders rows within each partition
- ✓ROW_NUMBER: unique; RANK: gaps on ties; DENSE_RANK: no gaps on ties
- ✓LAG/LEAD access previous/next rows by offset — perfect for delta/trend calculations
- ✓ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW gives a cumulative running total
- ✓Window functions are evaluated after WHERE/GROUP BY but before ORDER BY/LIMIT
-- Top 3 orders per customer by amount
SELECT *
FROM (
SELECT
customer_id,
order_id,
amount,
ROW_NUMBER() OVER (PARTITION BY customer_id
ORDER BY amount DESC) AS rn
FROM orders
) ranked
WHERE rn <= 3;
-- RANK vs DENSE_RANK vs ROW_NUMBER on tied scores
SELECT
name,
score,
RANK() OVER (ORDER BY score DESC) AS rank_w_gaps,
DENSE_RANK() OVER (ORDER BY score DESC) AS rank_no_gaps,
ROW_NUMBER() OVER (ORDER BY score DESC) AS unique_row_num
FROM leaderboard;
-- score=100: rank=1, dense=1, row=1
-- score=95: rank=2, dense=2, row=2
-- score=95: rank=2, dense=2, row=3 ← same score, same rank
-- score=90: rank=4, dense=3, row=4 ← RANK skips 3Common Table Expressions (CTE)
WITH clause defines reusable named result sets; recursive CTEs traverse hierarchical data (org charts, bill-of-materials) using a base case and recursive member.
- ✓CTEs require MySQL 8.0+ — they do not exist in MySQL 5.x.
- ✓Regular CTEs improve readability and allow reuse within the same query; they do not automatically materialise.
- ✓Recursive CTEs use UNION ALL (not UNION) between anchor and recursive member; UNION would eliminate duplicates unnecessarily.
- ✓Recursion terminates when the recursive member returns zero rows — ensure your join condition narrows with each iteration.
- ✓cte_max_recursion_depth (default 1000) prevents infinite loops from data cycles; always validate input trees.
- ✓For large CTEs referenced multiple times, MySQL materialises them into a temp table — verify with EXPLAIN FORMAT=TREE.
-- Monthly revenue with CTE (vs nested subquery)
WITH monthly_orders AS (
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
SUM(total_amount) AS revenue,
COUNT(*) AS order_count
FROM orders
WHERE status = 'COMPLETED'
GROUP BY month
),
monthly_avg AS (
SELECT AVG(revenue) AS avg_revenue FROM monthly_orders
)
SELECT
mo.month,
mo.revenue,
mo.order_count,
ROUND(mo.revenue / ma.avg_revenue * 100, 1) AS pct_of_avg
FROM monthly_orders mo, monthly_avg ma
ORDER BY mo.month;
-- Multiple CTEs: chain them with commas
WITH
active_users AS (SELECT id FROM users WHERE status = 'active'),
recent_orders AS (
SELECT user_id, COUNT(*) AS cnt
FROM orders
WHERE user_id IN (SELECT id FROM active_users)
AND created_at > NOW() - INTERVAL 30 DAY
GROUP BY user_id
)
SELECT u.email, ro.cnt
FROM users u JOIN recent_orders ro ON u.id = ro.user_id
ORDER BY ro.cnt DESC;Stored Procedures
Stored procedures encapsulate multi-statement logic in the DB; use IN/OUT/INOUT parameters, cursors, loops, and conditional logic to build server-side reusable routines.
- ✓Use DELIMITER $$ to avoid ; conflicts when defining multi-statement procedures in MySQL CLI.
- ✓DECLARE CONTINUE HANDLER FOR NOT FOUND is required to detect cursor exhaustion — without it the loop runs indefinitely.
- ✓Stored procedures do not reduce query parsing overhead in MySQL (unlike some other databases) — benefit is fewer round-trips.
- ✓SIGNAL SQLSTATE '45000' raises a user-defined exception; RESIGNAL re-throws the caught exception to the caller.
- ✓Test stored procedures carefully: they run in the DB context, are harder to unit-test than application code.
- ✓Prefer application-layer logic with Flyway-managed SP scripts in production for testability and deployment control.
DELIMITER $$
CREATE PROCEDURE calculate_customer_ltv(
IN p_customer_id BIGINT,
OUT p_ltv DECIMAL(12, 2),
OUT p_order_count INT
)
BEGIN
-- Local variable declarations must come before logic
DECLARE v_total_revenue DECIMAL(12, 2) DEFAULT 0.00;
DECLARE v_count INT DEFAULT 0;
-- Query into local variables
SELECT SUM(total), COUNT(*)
INTO v_total_revenue, v_count
FROM orders
WHERE customer_id = p_customer_id
AND status NOT IN ('CANCELLED', 'REFUNDED');
-- Handle NULL (no orders)
SET p_ltv = COALESCE(v_total_revenue, 0.00);
SET p_order_count = COALESCE(v_count, 0);
END$$
DELIMITER ;
-- Call the procedure
CALL calculate_customer_ltv(42, @ltv, @orders);
SELECT @ltv AS lifetime_value, @orders AS total_orders;Triggers & User-Defined Functions
BEFORE/AFTER INSERT/UPDATE/DELETE triggers enforce cross-table logic; deterministic scalar UDFs encapsulate reusable computation safe for use in queries.
- ✓BEFORE triggers can modify NEW row values (validation, defaults); AFTER triggers cannot modify the row but can audit
- ✓Trigger errors roll back the parent DML transaction — never let a trigger silently fail in a AFTER trigger
- ✓DETERMINISTIC UDFs tell the optimiser inputs always produce the same output — required for use in generated column indexes
- ✓Triggers are invisible to application code — document them in schema migrations and test them explicitly
- ✓In microservices, prefer Hibernate @EntityListeners or application-layer logic over triggers for testability
- ✓SHOW TRIGGERS and information_schema.TRIGGERS are the primary tools for discovering existing trigger definitions
-- BEFORE INSERT trigger: set created_at and validate
DELIMITER $$
CREATE TRIGGER trg_orders_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
SET NEW.created_at = NOW();
SET NEW.updated_at = NOW();
IF NEW.total <= 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Order total must be positive';
END IF;
END$$
DELIMITER ;
-- AFTER INSERT trigger: write audit log
DELIMITER $$
CREATE TRIGGER trg_orders_after_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
INSERT INTO order_audit_log (order_id, action, new_status, changed_at)
VALUES (NEW.id, 'CREATED', NEW.status, NOW());
END$$
DELIMITER ;
-- AFTER UPDATE trigger: audit status changes
DELIMITER $$
CREATE TRIGGER trg_orders_after_update
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
IF OLD.status <> NEW.status THEN
INSERT INTO order_audit_log (order_id, action, old_status, new_status, changed_at)
VALUES (NEW.id, 'STATUS_CHANGE', OLD.status, NEW.status, NOW());
END IF;
END$$
DELIMITER ;Views
Views are named stored SELECT queries; updatable views allow DML on simple single-table views; use views to simplify complex joins and enforce column-level security.
- ✓Views are virtual tables backed by a stored SELECT — not materialised; every query re-executes the underlying SELECT
- ✓Grant permissions on the view, not the base table, to enforce column- and row-level access control for specific roles
- ✓Updatable views require single base table and no aggregates, DISTINCT, GROUP BY, UNION, or select-list subqueries
- ✓WITH CHECK OPTION blocks inserts/updates that would make the row invisible through the view's WHERE filter
- ✓ALGORITHM=MERGE inlines the view and allows index use on the outer query; ALGORITHM=TEMPTABLE blocks predicate pushdown
- ✓Views with GROUP BY, aggregates, or DISTINCT always use TEMPTABLE — check EXPLAIN for DERIVED to identify this
-- View exposing only non-sensitive order columns (column-level security) CREATE OR REPLACE VIEW v_orders_public AS SELECT order_id, customer_id, order_date, status, total_amount FROM orders WHERE deleted_at IS NULL; -- Grant SELECT on view only — reporting_user cannot access base table GRANT SELECT ON shop.v_orders_public TO 'reporting_user'@'%'; -- Stacked view — joins over another view CREATE OR REPLACE VIEW v_customer_orders AS SELECT c.name, c.email, o.order_id, o.total_amount FROM customers c JOIN v_orders_public o ON c.id = o.customer_id; -- Query view like a table SELECT * FROM v_customer_orders WHERE total_amount > 1000; -- Inspect view definition SHOW CREATE VIEW v_orders_public;
String Functions
CONCAT, SUBSTRING, LENGTH, REPLACE, UPPER/LOWER, TRIM, REGEXP_REPLACE, and FORMAT are core string functions; MySQL is case-insensitive for string comparison by default (COLLATION-dependent).
- ✓CONCAT_WS is safer than CONCAT when some arguments may be NULL (NULLs are skipped).
- ✓Use CHAR_LENGTH for character count in multibyte encodings; LENGTH returns byte count.
- ✓REGEXP_REPLACE and REGEXP_SUBSTR require MySQL 8.0+.
- ✓Most utf8mb4_unicode_ci comparisons are case-insensitive; use BINARY for case-sensitive search.
- ✓FORMAT(n, d) produces locale-formatted numbers; LPAD/RPAD produce fixed-width strings.
- ✓String function calls in WHERE clauses prevent index use — avoid on indexed columns.
-- CONCAT and CONCAT_WS
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customers;
SELECT CONCAT_WS(', ', city, state, country) AS address FROM locations;
-- CONCAT_WS skips NULL values; CONCAT returns NULL if any arg is NULL
-- SUBSTRING
SELECT SUBSTRING('Hello World', 1, 5); -- 'Hello' (1-indexed)
SELECT LEFT('Hello World', 5); -- 'Hello'
SELECT RIGHT('Hello World', 5); -- 'World'
SELECT MID('Hello World', 7, 5); -- 'World' (alias for SUBSTRING)
-- Length: bytes vs characters
SELECT LENGTH('こんにちは'); -- 15 (5 chars × 3 bytes each in utf8)
SELECT CHAR_LENGTH('こんにちは'); -- 5 (5 characters)
-- Always use CHAR_LENGTH for user-visible string length checks
-- Case and trim
SELECT UPPER('hello WORLD'); -- 'HELLO WORLD'
SELECT LOWER('HELLO world'); -- 'hello world'
SELECT TRIM(' hello '); -- 'hello'
SELECT LTRIM(' hello'); -- 'hello'
SELECT RTRIM('hello '); -- 'hello'Date & Time Functions
NOW(), CURDATE(), DATE_ADD(), DATEDIFF(), DATE_FORMAT(), UNIX_TIMESTAMP(), and STR_TO_DATE() are essential for date arithmetic, formatting, and timezone-aware applications.
- ✓NOW() is evaluated once per statement; SYSDATE() is evaluated at call time — matters inside loops
- ✓Never apply functions to indexed date columns in WHERE — use range comparisons instead
- ✓DATEDIFF() returns integer days; TIMESTAMPDIFF(UNIT, start, end) supports any unit
- ✓DATE_FORMAT uses % specifiers: %Y (4-digit year), %m (month), %d (day), %H:%i:%s (time)
- ✓TIMESTAMP is auto-converted from/to session timezone; DATETIME stores verbatim
- ✓Store all timestamps in UTC in the DB; apply timezone conversion in the application layer
-- Current date/time
SELECT NOW(), CURDATE(), CURTIME(), UTC_TIMESTAMP();
-- Date arithmetic
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY) AS next_week,
DATE_SUB(NOW(), INTERVAL 1 MONTH) AS last_month;
-- Difference
SELECT DATEDIFF('2025-12-31', '2025-01-01') AS days_diff, -- 364
TIMESTAMPDIFF(MONTH, '2024-01-01', NOW()) AS months_since;
-- Index-friendly range query (avoids function on column)
SELECT * FROM orders
WHERE created_at >= CURDATE()
AND created_at < CURDATE() + INTERVAL 1 DAY;