🐬 MySQL Setup and Administration
Every dynamic web app needs a place to keep its data, and MySQL is the workhorse relational database that powers a huge slice of the web. In this lesson you'll install MySQL 8, lock it down, tune the settings that actually matter, create least-privilege users, design a real schema, and learn to back up and speed up your data.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Install MySQL 8 on Windows, macOS, or Linux and verify it is running
- Secure and configure the server through
mysql_secure_installationand keymy.cnfsettings - Create databases, tables, and users with correct character sets and least-privilege grants
- Back up and restore a database with
mysqldump, including an automated script - Diagnose query performance using indexes and the
EXPLAINstatement
Estimated Time: 45–60 minutes • Difficulty: Intermediate
Hands-on: Stand up a blog database with users, posts, and comments, then back it up and profile a join.
In This Lesson
What MySQL Is and Why It Matters
MySQL is an open-source relational database management system (RDBMS): software that stores data in structured tables of rows and columns, keeps those tables related to one another, and answers questions about them using SQL (Structured Query Language). It is the "M" in the classic LAMP stack (Linux, Apache, MySQL, PHP) and remains one of the most deployed databases on the planet.
💡 A useful analogy — the digital library. Picture MySQL as a well-run library. The server is the building that controls who gets in. A database is a wing of the library; a table is a themed shelf; a row is one book; and columns are the fields printed on each book's spine (title, author, year). An index is the card catalog that lets you find a book without walking every aisle, and SQL is how you ask the librarian to fetch, add, or reshelve books. As a developer you are both the architect who designs the library and the patron who queries it.
Why developers reach for MySQL
- Mature and reliable — decades of production use across every industry
- Fast — the default InnoDB storage engine is tuned for mixed read/write web workloads
- Scales with you — from a hobby project to replicated multi-server clusters
- Cross-platform & free — the Community Edition runs everywhere at no cost
- Huge ecosystem — first-class support in PHP, Node.js, Python, and every major ORM
📖 Know the variants
MySQL Community Edition — the free, open-source version we use in this course.
MySQL Enterprise Edition — Oracle's paid tier with extra monitoring, security, and support tools.
MariaDB — a drop-in compatible fork started by MySQL's original creators; the default in many Linux distros.
Percona Server — a performance-focused, fully compatible replacement.
Everything in this lesson applies to all four with only minor differences.
Installing MySQL
Pick the path for your operating system. On all three you are installing the MySQL Server 8.0+ and, ideally, a client to talk to it.
Windows
- Download the MySQL Installer from dev.mysql.com.
- Choose the Developer Default setup (server, Workbench, and connectors) or Server only if you just want the engine.
- During configuration, set the root password, keep the default port
3306, and register MySQL as a Windows service so it starts on boot.
macOS (Homebrew — recommended)
# Install Homebrew if you don't have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install and start MySQL
brew install mysql
brew services start mysql
# Lock it down (see next section)
mysql_secure_installation
Linux (Ubuntu / Debian)
# Refresh the package index and install the server
sudo apt update
sudo apt install mysql-server
# Start it now and enable it at boot
sudo systemctl start mysql
sudo systemctl enable mysql
# Lock it down
sudo mysql_secure_installation
Verify the install
# Confirm the client version
mysql --version
# Connect as root (you'll be prompted for the password)
mysql -u root -p
Once connected you'll see the mysql> prompt. Try a couple of commands:
SHOW DATABASES;
SELECT VERSION();
EXIT;
Expected output
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
⚠️ Common install snags
- "Access denied for user 'root'" — the password is wrong, or on fresh Ubuntu root uses
auth_socket, so runsudo mysqlthe first time. - "Can't connect to MySQL server" — the service isn't running (
sudo systemctl status mysql) or you're pointing at the wrong host/port. - Check the error log when stuck: Linux
/var/log/mysql/error.log, macOS/opt/homebrew/var/mysql/*.err, WindowsC:\ProgramData\MySQL\MySQL Server 8.0\Data\*.err.
Securing & Configuring the Server
Run the security script first
Before anything else, run mysql_secure_installation. It walks you through setting a strong root password, removing anonymous accounts, disabling remote root login, and dropping the test database — the four settings that cause the most beginner breaches.
The configuration file
MySQL reads its startup options from my.cnf (Linux/macOS) or my.ini (Windows). Common locations are /etc/mysql/my.cnf, /opt/homebrew/etc/my.cnf, or C:\ProgramData\MySQL\MySQL Server 8.0\my.ini.
[mysqld]
# Network
port = 3306
bind-address = 127.0.0.1 # local-only; use 0.0.0.0 to accept remote connections
# Character set — utf8mb4 stores full Unicode incl. emoji
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# InnoDB storage engine
innodb_buffer_pool_size = 512M # cache; 50-70% of RAM on a dedicated DB server
innodb_redo_log_capacity = 128M # MySQL 8.0.30+ replaces innodb_log_file_size
# Connections
max_connections = 150
wait_timeout = 600 # drop idle web connections after 10 min
[client]
default-character-set = utf8mb4
| Parameter | What it controls | Sensible default |
|---|---|---|
bind-address | Which network interfaces accept connections | 127.0.0.1 for local-only safety |
character-set-server | Default encoding for new databases | Always utf8mb4 |
innodb_buffer_pool_size | Memory used to cache data & indexes | 50–70% of RAM on a dedicated server |
max_connections | Max simultaneous client connections | Raise for high-traffic apps |
wait_timeout | Seconds an idle connection is kept | 300–600 for web apps |
⚠️ After every config change
Back up the file, restart the server (sudo systemctl restart mysql), then confirm the value took effect from inside MySQL:
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
Users & the Privilege System
Never let your application connect as root. Instead, create a dedicated user with exactly the privileges it needs. MySQL's privilege model is hierarchical — grants can apply globally, per-database, per-table, or even per-column.
Create a least-privilege application user
-- Create the account (localhost = only local connections)
CREATE USER 'webuser'@'localhost' IDENTIFIED BY 'ChangeMe_Str0ng!';
-- Grant only the data operations the app needs on ONE database
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_db.* TO 'webuser'@'localhost';
-- Apply the changes
FLUSH PRIVILEGES;
Inspect, modify, and remove users
-- Who exists?
SELECT user, host FROM mysql.user;
-- What can this user do?
SHOW GRANTS FOR 'webuser'@'localhost';
-- Take a privilege away
REVOKE DELETE ON myapp_db.* FROM 'webuser'@'localhost';
-- Rotate a password
ALTER USER 'webuser'@'localhost' IDENTIFIED BY 'NewStr0ng_Pass!';
-- Remove an account entirely
DROP USER 'webuser'@'localhost';
✅ Security do's and don'ts
Do: follow least privilege, use 'localhost' instead of '%' when the app is on the same box, use long random passwords, and audit accounts periodically.
Don't: connect apps as root, grant ALL PRIVILEGES "just to be safe", or commit credentials into your code repository.
Creating Databases & Tables
Create a database
-- Always specify the character set explicitly
CREATE DATABASE myapp_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE myapp_db; -- make it the active database
SELECT DATABASE(); -- confirm which one is active
Design related tables
Here is a two-table schema linked by a foreign key: each post belongs to one user, and deleting a user cascades to delete their posts.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
posts.user_id references users.id, creating a one-to-many relationship.Modify and inspect tables
ALTER TABLE users ADD COLUMN last_login DATETIME; -- add a column
ALTER TABLE posts MODIFY title VARCHAR(300) NOT NULL; -- change a column
CREATE INDEX idx_posts_user_id ON posts(user_id); -- speed up joins
DESCRIBE users; -- column summary
SHOW CREATE TABLE posts; -- the exact DDL
SHOW TABLES; -- list everything
Backup & Recovery
A database you can't restore is a disaster waiting to happen. The everyday tool for logical backups (a file of SQL statements that rebuilds your data) is mysqldump.
# Back up one database
mysqldump -u root -p myapp_db > myapp_backup.sql
# Back up and compress in one step
mysqldump -u root -p myapp_db | gzip > myapp_backup.sql.gz
# Back up specific tables only
mysqldump -u root -p myapp_db users posts > tables.sql
# Restore into an (already created) database
mysql -u root -p myapp_db < myapp_backup.sql
# Restore from a compressed backup
gunzip < myapp_backup.sql.gz | mysql -u root -p myapp_db
Automate it
On Linux/macOS, a tiny script plus a cron job keeps rolling backups and prunes old ones:
#!/bin/bash
# nightly-backup.sh
DB_USER="backup_user"
DB_NAME="myapp_db"
BACKUP_DIR="/var/backups/mysql"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILE="$BACKUP_DIR/$DB_NAME-$DATE.sql.gz"
mkdir -p "$BACKUP_DIR"
# Read the password from an env var, never hardcode it
if mysqldump -u "$DB_USER" -p"$MYSQL_PWD" --single-transaction "$DB_NAME" | gzip > "$FILE"; then
echo "Backup OK: $FILE"
else
echo "Backup FAILED" >&2
exit 1
fi
# Delete backups older than 30 days
find "$BACKUP_DIR" -name "$DB_NAME-*.sql.gz" -mtime +30 -delete
💡 Backups you can trust
- Use
--single-transactionfor InnoDB so the dump is consistent without locking the site. - Test your restores. An untested backup is only a hope, not a plan.
- Keep at least one copy off the server (another host or cloud bucket).
Indexing & Query Analysis
The single biggest performance lever you control is indexing. An index is a sorted lookup structure that lets MySQL jump straight to matching rows instead of scanning the whole table — the card catalog from our library analogy.
CREATE INDEX idx_username ON users(username); -- single column
CREATE INDEX idx_name ON users(last_name, first_name); -- composite
CREATE UNIQUE INDEX idx_email ON users(email); -- also enforces uniqueness
SHOW INDEX FROM users; -- what indexes exist?
DROP INDEX idx_username ON users;
⚠️ Index with intention
Good candidates: columns used in WHERE, JOIN, ORDER BY, and GROUP BY. But every index costs disk space and slows down INSERT/UPDATE/DELETE, because the index must be maintained too. Don't index everything — index what your queries actually filter on.
Write queries that can use indexes
| Slower | Faster | Why |
|---|---|---|
SELECT * FROM users | SELECT id, username FROM users | Fetch only the columns you need |
WHERE YEAR(created_at) = 2026 | WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01' | A function on the column disables the index |
WHERE name LIKE '%smith%' | WHERE name LIKE 'smith%' | A leading wildcard can't use an index |
EXPLAIN: see how MySQL runs a query
Prefix any query with EXPLAIN to get the execution plan without running it.
EXPLAIN SELECT u.username, p.title
FROM users u
JOIN posts p ON u.id = p.user_id
WHERE p.created_at > '2026-01-01';
Reading the plan — the fields that matter
- type — join strategy.
eq_ref,ref, andrangeare good;ALLmeans a full table scan (a red flag on big tables). - key — which index was chosen;
NULLmeans none was usable. - rows — estimated rows examined. Lower is better.
- Extra — watch for Using filesort or Using temporary, which hint at missing indexes.
Hands-on Exercise
🏋️ Build, back up, and profile a blog database
Objective: Practice the full admin loop — schema, user, backup, and query analysis.
Instructions:
- Create a database
web_dev_coursewith theutf8mb4character set. - Create three related tables:
users,posts(FK to users), andcomments(FK to both posts and users). - Create a least-privilege user
course_userwith onlySELECT, INSERT, UPDATE, DELETEon that database. - Add an index on
comments.post_id, then runEXPLAINon a join of posts and comments. - Take a compressed
mysqldumpbackup, then restore it into a second databaseweb_dev_restoreto prove it works.
💡 Hint
A comment references two tables, so it needs two foreign keys: FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE and FOREIGN KEY (user_id) REFERENCES users(id). To restore into a new database, you must CREATE DATABASE web_dev_restore; first, then pipe the dump into it.
✅ Sample solution
CREATE DATABASE web_dev_course
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE web_dev_course;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE comments (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE USER 'course_user'@'localhost' IDENTIFIED BY 'S0me_Str0ng!';
GRANT SELECT, INSERT, UPDATE, DELETE ON web_dev_course.* TO 'course_user'@'localhost';
FLUSH PRIVILEGES;
EXPLAIN SELECT p.title, c.content
FROM posts p JOIN comments c ON p.id = c.post_id;
# Backup and restore
mysqldump -u root -p web_dev_course | gzip > course.sql.gz
mysql -u root -p -e "CREATE DATABASE web_dev_restore CHARACTER SET utf8mb4;"
gunzip < course.sql.gz | mysql -u root -p web_dev_restore
🎯 Quick Quiz
Question 1: Why should a web application connect to MySQL with a dedicated user rather than root?
Question 2: Which character set should you choose for full Unicode support, including emoji?
Question 3: In EXPLAIN output, which type value warns you of a full table scan?
Summary & Quiz
🎉 Key Takeaways
- MySQL is a mature relational database organized as server → databases → tables → rows/columns.
- Install for your OS, then always run
mysql_secure_installationand setutf8mb4. - Tune only the settings that matter:
bind-address, buffer pool size, connections, and timeouts. - Give apps a least-privilege user, never
root. - Model data with tables linked by foreign keys; back up with
mysqldumpand test the restore. - Speed up reads with well-chosen indexes, and diagnose slow queries with
EXPLAIN.
📚 Further Reading
🚀 What's Next?
Your database is running and secured. Next we'll connect it to code: in PHP and MySQL Integration you'll open connections with MySQLi and PDO, run queries, and process results from PHP.
🎉 Well done!
You can now install, secure, populate, back up, and tune a MySQL server — the foundation every backend rests on.