MySQL cheat sheet: the commands you use every day
AdvertisementThis is a quick reference for the MySQL you actually type: connecting, reading, summarising, changing and tuning data. It is a cheat sheet, so it is short on explanation and long on working examples. Every query that shows output was run on MySQL 8.4 against a small customers and orders dataset (defined in the SQL joins cheat sheet), so the output is real. For the reasoning behind each command, the free MySQL course has a lesson on each.
Connect and look around
-- from the shell
mysql -u root -p -- prompt for the password
mysql -h 127.0.0.1 -P 3306 -u app -p shopdb
SHOW DATABASES;
USE shopdb;
SHOW TABLES;
DESCRIBE customers; -- columns and types
SHOW CREATE TABLE customers\G -- the full CREATE statement
SELECT VERSION(), DATABASE(), CURRENT_USER();
Create and change structure
CREATE DATABASE shopdb CHARACTER SET utf8mb4;
CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
label VARCHAR(20) UNIQUE,
uses INT DEFAULT 1
);
ALTER TABLE tags ADD COLUMN note TEXT;
ALTER TABLE tags MODIFY label VARCHAR(40) NOT NULL;
ALTER TABLE tags DROP COLUMN note;
CREATE INDEX idx_orders_customer ON orders (customer_id);
DROP TABLE IF EXISTS tags; -- gone for good
Common data types: INT, BIGINT, DECIMAL(10,2) (money), VARCHAR(n), TEXT, DATE, DATETIME, TIMESTAMP, BOOLEAN (alias of TINYINT(1)), JSON.
SELECT, filter, sort, page
SELECT name, city FROM customers
WHERE city IN ('Pune','Goa')
ORDER BY name DESC
LIMIT 2 OFFSET 1;
| name | city |
|---|---|
| Chen | Pune |
| Asha | Pune |
| Goal | Syntax |
|---|---|
| Range | WHERE total BETWEEN 100 AND 300 |
| Several values | WHERE city IN ('Pune','Goa') |
| Pattern | WHERE name LIKE 'A%' (% any text, _ one character) |
| Regex | WHERE name REGEXP '^[A-C]' |
| Missing value | WHERE customer_id IS NULL |
| Unique values | SELECT DISTINCT city FROM customers |
| Combine | AND, OR, NOT (use brackets) |
LIKE 'A%' OR LIKE '_e%' returned Asha, Ben and Dev on our data. REGEXP '^[A-C]' returned Asha, Ben and Chen.
NULL: the rules
SELECT NULL = NULL AS eq, NULL IS NULL AS isnull, 1 + NULL AS plus,
COUNT(customer_id) AS cnt_col, COUNT(*) AS cnt_all
FROM orders;
| eq | isnull | plus | cnt_col | cnt_all |
|---|---|---|---|---|
| NULL | 1 | NULL | 4 | 5 |
- Comparing with
NULLgivesNULL, so useIS NULL/IS NOT NULL. - Arithmetic with
NULLgivesNULL. COUNT(column)skipsNULLs;COUNT(*)counts rows.
Group and aggregate
SELECT status, COUNT(*) AS n, SUM(total) AS sum_total,
ROUND(AVG(total),2) AS avg_total, MIN(total) AS lo, MAX(total) AS hi
FROM orders
GROUP BY status
HAVING COUNT(*) >= 1
ORDER BY n DESC, status;
| status | n | sum_total | avg_total | lo | hi |
|---|---|---|---|---|---|
| paid | 3 | 430.50 | 143.50 | 60.00 | 250.00 |
| cancelled | 1 | 500.00 | 500.00 | 500.00 | 500.00 |
| pending | 1 | 80.00 | 80.00 | 80.00 | 80.00 |
WHERE filters rows before grouping; HAVING filters groups after. WITH ROLLUP adds a grand-total row:
SELECT status, COUNT(*) n FROM orders GROUP BY status WITH ROLLUP;
| status | n |
|---|---|
| cancelled | 1 |
| paid | 3 |
| pending | 1 |
| NULL | 5 |
Functions you reach for
Text
SELECT name, UPPER(name) u, LENGTH(name) len, CONCAT(name,' - ',city) label,
SUBSTRING(name,1,2) s2, REPLACE(city,'Pune','Pune City') r
FROM customers LIMIT 1;
| name | u | len | label | s2 | r |
|---|---|---|---|---|---|
| Asha | ASHA | 4 | Asha - Pune | As | Pune City |
Also: TRIM(), LEFT(), RIGHT(), LOWER(), CHAR_LENGTH() (characters; LENGTH counts bytes), LOCATE().
Numbers
SELECT ROUND(2.567,1) r, CEIL(2.1) c, FLOOR(2.9) f, MOD(17,5) m, ABS(-4) a, TRUNCATE(2.789,2) t;
| r | c | f | m | a | t |
|---|---|---|---|---|---|
| 2.6 | 3 | 2 | 2 | 4 | 2.78 |
Dates
SELECT DATE_FORMAT('2026-10-24','%d %b %Y') f, DATE_ADD('2026-10-24', INTERVAL 30 DAY) plus30,
DATEDIFF('2026-12-25','2026-10-24') days_left, YEAR('2026-10-24') y,
DAYNAME('2026-10-24') dn, LAST_DAY('2026-02-10') ld;
| f | plus30 | days_left | y | dn | ld |
|---|---|---|---|---|---|
| 24 Oct 2026 | 2026-11-23 | 62 | 2026 | Saturday | 2026-02-28 |
Also: NOW(), CURDATE(), DATE_SUB(), TIMESTAMPDIFF(unit, a, b), STR_TO_DATE().
Conditions
SELECT id, total,
IF(total >= 200,'big','small') size,
CASE WHEN status='paid' THEN 'ok' WHEN status='pending' THEN 'wait' ELSE 'no' END state,
IFNULL(customer_id,0) cid, COALESCE(customer_id,-1) c2
FROM orders;
| id | total | size | state | cid | c2 |
|---|---|---|---|---|---|
| 101 | 250.00 | big | ok | 1 | 1 |
| 102 | 120.50 | small | ok | 1 | 1 |
| 103 | 80.00 | small | wait | 2 | 2 |
| 104 | 60.00 | small | ok | 0 | -1 |
| 105 | 500.00 | big | no | 3 | 3 |
Joins and subqueries (short form)
SELECT c.name, o.total FROM customers c JOIN orders o ON o.customer_id = c.id; -- matches only
SELECT c.name, o.total FROM customers c LEFT JOIN orders o ON o.customer_id = c.id; -- keep all customers
SELECT name FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE total > 200);
SELECT name FROM customers c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
The first subquery returns Asha and Chen. The details and traps are in the SQL joins cheat sheet.
CTEs and window functions
A CTE names a subquery so the main query reads top to bottom:
WITH big AS (SELECT customer_id, SUM(total) s FROM orders GROUP BY customer_id)
SELECT c.name, big.s FROM customers c JOIN big ON big.customer_id = c.id ORDER BY big.s DESC;
| name | s |
|---|---|
| Chen | 500.00 |
| Asha | 370.50 |
| Ben | 80.00 |
A window function calculates over related rows without collapsing them:
SELECT id, total,
ROW_NUMBER() OVER (ORDER BY total DESC) AS rn,
RANK() OVER (ORDER BY status) AS rk,
SUM(total) OVER () AS grand
FROM orders;
| id | total | rn | rk | grand |
|---|---|---|---|---|
| 105 | 500.00 | 1 | 1 | 1010.50 |
| 101 | 250.00 | 2 | 2 | 1010.50 |
| 102 | 120.50 | 3 | 2 | 1010.50 |
| 104 | 60.00 | 5 | 2 | 1010.50 |
| 103 | 80.00 | 4 | 5 | 1010.50 |
Notice RANK(): three rows tie at rank 2 (status paid), and the next rank jumps to 5. DENSE_RANK() would give 3 instead. ROW_NUMBER() never ties. Add PARTITION BY to restart the numbering per group.
Insert, update, delete, upsert
INSERT INTO tags (label) VALUES ('php'),('sql');
INSERT INTO tags (label) VALUES ('php')
ON DUPLICATE KEY UPDATE uses = uses + 1; -- upsert
UPDATE tags SET uses = uses * 10 WHERE label = 'sql';
DELETE FROM tags WHERE label = 'php';
After the upsert, php had uses = 2; after the update and delete only sql remained, with uses = 10. A golden rule: write the WHERE first, and test it with a SELECT before running an UPDATE or DELETE. Without WHERE, every row changes.
One detail worth knowing: the upsert used up an AUTO_INCREMENT value even though it inserted nothing. SHOW CREATE TABLE tags afterwards showed AUTO_INCREMENT=4 with only ids 1 and 2 in the table. Auto-increment numbers are unique, not gap-free.
Transactions
START TRANSACTION;
UPDATE orders SET status = 'paid' WHERE id = 103;
ROLLBACK; -- or COMMIT;
SELECT status FROM orders WHERE id = 103; -- still 'pending'
A transaction makes several statements succeed or fail together. It needs a transactional engine (InnoDB, the default).
Indexes and EXPLAIN
CREATE INDEX idx_orders_customer ON orders (customer_id);
EXPLAIN SELECT * FROM orders WHERE customer_id = 1;
EXPLAIN showed type = ref, key = idx_orders_customer and rows = 2: MySQL used the index and expected to read two rows. Without an index you would see type = ALL, a full table scan. Index columns used in WHERE, JOIN and ORDER BY; every index slows writes a little and uses space.
Users, backup and restore
CREATE USER 'app'@'localhost' IDENTIFIED BY 'a-long-secret';
GRANT SELECT, INSERT, UPDATE ON shopdb.* TO 'app'@'localhost';
SHOW GRANTS FOR 'app'@'localhost';
mysqldump -u root -p shopdb > shopdb.sql # backup
mysql -u root -p shopdb < shopdb.sql # restore
Give applications the fewest privileges they need, and never connect a website as root.
Order of a SELECT
Written order: SELECT … FROM … JOIN … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT. MySQL evaluates roughly FROM/JOIN, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. This is why a column alias works in ORDER BY but not in WHERE.
More
Practise with the lessons in the MySQL course, follow the 60-day plan, test yourself with the MySQL interview questions, and prepare for the exam with MySQL certificate: how the final exam works. From code, connect with PDO as shown in the PHP course, and never build queries by joining user input into strings.