Bootcamp
A 60-minute MySQL bootcamp: scaffold schema, seed, query, index, optimise, back up.
A 60-minute MySQL bootcamp
EXAMPLE
# ===== Objectives =====
# 1. Stand up MySQL 8 in Docker
# 2. Design schema with proper types + indexes
# 3. Seed + query
# 4. Tune via EXPLAIN
# 5. Back up + restore
# ===== 0-5 min: stand up =====
docker run -d --name shop-db -p 3306:3306 \
-e MYSQL_DATABASE=shop -e MYSQL_ROOT_PASSWORD=devpw \
mysql:8.0
# Connect
docker exec -it shop-db mysql -u root -pdevpw shop
# ===== 5-20 min: schema =====
CREATE TABLE customers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL,
name VARCHAR(120) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED NOT NULL,
status ENUM('new','paid','shipped','cancelled') NOT NULL DEFAULT 'new',
total_cents BIGINT UNSIGNED NOT NULL,
payload JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY ix_customer_status_created (customer_id, status, created_at DESC),
CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(id)
) ENGINE=InnoDB;
# ===== 20-30 min: seed =====
INSERT INTO customers (email, name) VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob'),
('carol@example.com', 'Carol');
INSERT INTO orders (customer_id, total_cents, status, payload) VALUES
(1, 4995, 'paid', JSON_OBJECT('source','web')),
(1, 9900, 'new', JSON_OBJECT('source','web')),
(2, 1500, 'shipped', JSON_OBJECT('source','app'));
# ===== 30-40 min: queries =====
# top customers
SELECT c.id, c.name, SUM(o.total_cents) AS revenue
FROM customers c
JOIN orders o ON o.customer_id = c.id AND o.status IN ('paid','shipped')
GROUP BY c.id ORDER BY revenue DESC LIMIT 10;
# Window: rank orders per customer
SELECT id, customer_id, total_cents,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY total_cents DESC) AS rk
FROM orders;
# JSON
SELECT id, payload->>'$.source' AS source FROM orders WHERE payload->>'$.source' = 'web';
# ===== 40-50 min: EXPLAIN + tune =====
EXPLAIN SELECT * FROM orders WHERE customer_id = 1 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
# Verify 'type' is 'ref' (or better), key = ix_customer_status_created, rows small
# Generated column for indexable JSON
ALTER TABLE orders
ADD COLUMN source VARCHAR(32) GENERATED ALWAYS AS (payload->>'$.source') STORED,
ADD KEY ix_source (source);
# ===== 50-55 min: transactions =====
START TRANSACTION;
UPDATE customers SET name = 'Alicia' WHERE id = 1;
INSERT INTO orders (customer_id, total_cents, status) VALUES (1, 2500, 'new');
COMMIT;
# Test
SELECT * FROM customers WHERE id = 1;
SELECT * FROM orders WHERE customer_id = 1;
# ===== 55-60 min: backup + restore =====
docker exec shop-db mysqldump -u root -pdevpw shop > shop-$(date +%F).sql
# Restore
# docker exec -i shop-db mysql -u root -pdevpw shop < shop-2026-06-18.sql
# Production: physical backups via Percona xtrabackup, or RDS snapshots
# ===== Post-bootcamp checklist =====
# - Schema uses utf8mb4 + InnoDB
# - PKs + FKs + composite indexes on hot queries
# - EXPLAIN confirmed index usage
# - Backup taken + restore verified
# ===== Pitfalls =====
# - 'utf8' (3-byte) instead of utf8mb4
# - Missing indexes on FK columns -> JOIN performance
# - Heavy SELECT * in transactions -> long-running tx
# - Forgetting to back up before schema migrations
Why it matters
A 60-minute MySQL bootcamp that ends with a verified backup and a tuned query plan is the artifact that proves you can do production database work. Pair the bootcamp with a `EXPLAIN ANALYZE` reflex and most "MySQL is slow" tickets become a one-line index addition you saw coming.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…