Skip to main content

🐬 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_installation and key my.cnf settings
  • 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 EXPLAIN statement

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.

graph TD M[MySQL] --> C["Community Edition (free)"] M --> E["Enterprise Edition (paid)"] C --> MDB[MariaDB fork] C --> P[Percona Server] E --> CT["Commercial tools and support"]

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

  1. Download the MySQL Installer from dev.mysql.com.
  2. Choose the Developer Default setup (server, Workbench, and connectors) or Server only if you just want the engine.
  3. 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 run sudo mysql the 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, Windows C:\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
ParameterWhat it controlsSensible default
bind-addressWhich network interfaces accept connections127.0.0.1 for local-only safety
character-set-serverDefault encoding for new databasesAlways utf8mb4
innodb_buffer_pool_sizeMemory used to cache data & indexes50–70% of RAM on a dedicated server
max_connectionsMax simultaneous client connectionsRaise for high-traffic apps
wait_timeoutSeconds an idle connection is kept300–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.

MySQL privilege hierarchy Privileges cascade from global, to database level, to table level, to column level, each scope narrower than the last. Global (all databases) Database level Table level Column level narrower scope ↓
Figure 1 — Grant privileges at the narrowest scope that still lets the app do its job.

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;
One-to-many relationship between users and posts The users table connects to the posts table via a foreign key; one user has many posts. users id (PK) username email password_hash created_at posts id (PK) user_id (FK) title content created_at 1 : N
Figure 2 — 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-transaction for 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

SlowerFasterWhy
SELECT * FROM usersSELECT id, username FROM usersFetch only the columns you need
WHERE YEAR(created_at) = 2026WHERE 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, and range are good; ALL means a full table scan (a red flag on big tables).
  • key — which index was chosen; NULL means 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:

  1. Create a database web_dev_course with the utf8mb4 character set.
  2. Create three related tables: users, posts (FK to users), and comments (FK to both posts and users).
  3. Create a least-privilege user course_user with only SELECT, INSERT, UPDATE, DELETE on that database.
  4. Add an index on comments.post_id, then run EXPLAIN on a join of posts and comments.
  5. Take a compressed mysqldump backup, then restore it into a second database web_dev_restore to 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_installation and set utf8mb4.
  • 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 mysqldump and 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.