Senior 6 min · March 05, 2026

MySQL 8.4 Install — Root Without Password, Bound to 0.0.0.0

MySQL root had no password and bound to 0.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • MySQL is an RDBMS with two pieces: a server (background process on port 3306) and a client (shell or GUI)
  • Install via apt (Linux), Homebrew (macOS), or winget (Windows) — then run mysql_secure_installation immediately
  • MySQL 8.4 is the current LTS release as of 2026 — avoid 8.0 for new projects, it reaches EOL in April 2026
  • Always create a dedicated user per application with least-privilege permissions — never use root for app connections
  • INSERT/UPDATE/DELETE require connection.commit() in Python — uncommitted changes silently vanish on connection close
  • Use DECIMAL(8,2) for money columns — FLOAT causes rounding errors like $12.98999977
  • The mysql_secure_installation script is not optional — skipping it leaves your database open to the internet
Plain-English First

Think of MySQL as a super-organised filing cabinet for your app. Instead of stuffing papers randomly into drawers, MySQL lets you store data in neat, labelled folders (tables) that you can search, sort, and update in milliseconds. Installing MySQL is like plugging in that filing cabinet — until you do, your app has nowhere to put its data. Once it's running, it works silently in the background, always ready to fetch exactly the file you ask for. The 2026 release of MySQL 8.4 LTS makes that cabinet sturdier and faster than ever, with authentication defaults that are actually secure out of the box — though you still need to do your part.

Every app you love — Instagram, Airbnb, Shopify — needs a place to remember things. User accounts, orders, messages, prices — all of it has to live somewhere when the server restarts. That 'somewhere' is a database, and MySQL is the most widely deployed open-source relational database on the planet. It powers roughly 40% of all websites and has been doing so reliably since 1995. If you're building anything on the web, chances are MySQL (or its cousin MariaDB) is under the hood.

The problem most beginners hit isn't the SQL language itself — it's getting MySQL installed, secured, and connected without breaking anything. A missed configuration step can leave your database wide open to the internet, locked behind a password you forgot, or refusing connections from your own app. Those errors are cryptic, confidence-shattering, and almost always avoidable.

One thing worth noting before we dive in: MySQL 8.0 reaches end-of-life in April 2026. If you're starting a new project today, install MySQL 8.4 LTS — it's the current long-term support release, supported through 2032, and it ships with caching_sha2_password as the default authentication plugin. That's actually a good thing for security, but it means a few connection steps look slightly different from older tutorials you may have found. Every command below is written for 8.4.

By the end of this article you'll have MySQL installed on your machine (Windows, Mac, or Linux), the root account secured, a real database and user created, and your first table populated with data you can query. No prior database knowledge needed. Every command is explained before you type it.

What MySQL Actually Is — And Why You Need a Server Running Locally

MySQL is a Relational Database Management System (RDBMS). 'Relational' just means data is stored in tables — rows and columns, like a spreadsheet — and those tables can be linked together. 'Management System' means there's a program that handles reading, writing, and protecting that data for you.

Here's the part beginners often miss: MySQL has two pieces, not one. There's the server — a background process (mysqld) that runs continuously, owns the data files on disk, and listens for connections on port 3306. Then there's the client — a command-line tool (or a GUI like MySQL Workbench) that you use to talk to that server. When you type a query, the client sends it over a network socket to the server, the server executes it against the data files, and sends the results back to your screen.

Why does this matter? Because 'MySQL is not running' and 'MySQL is not installed' are two completely different problems with two different fixes. The server must be started before any client can connect. On most systems, installation configures it to start automatically on boot — but that's worth verifying, and we will. It also matters when you're debugging connection errors from application code: the first thing to rule out is whether the server process is actually up, before you start questioning your connection string or credentials.

In 2026, MySQL 8.4 LTS is the version you want. MySQL 8.0 hits end-of-life in April 2026, and MySQL 9.x is the innovation track (new features, shorter support window). For anything you're building that needs to run reliably for the next few years, 8.4 is the stable choice.

check_mysql_status.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# ── Check whether the MySQL server process is running ──────────────────────
# On Linux (systemd-based — Ubuntu, Debian, RHEL, Fedora):
sudo systemctl status mysql
# On some distributions the service name is 'mysqld' instead of 'mysql':
sudo systemctl status mysqld

# On older Linux systems still using 'service':
sudo service mysql status

# On macOS (installed via Homebrew):
brew services list | grep mysql

# On Windows (PowerShell, run as Administrator):
Get-Service -Name 'MySQL*'

# ── Confirm what port MySQL is listening on ────────────────────────────────
# Linux/macOS — ss replaces the older netstat on modern systems:
sudo ss -tlnp | grep 3306

# macOS alternative:
sudo lsof -i :3306

# Windows (PowerShell):
netstat -ano | findstr :3306

# What you're looking for:
# Linux/Mac: a line showing '0.0.0.0:3306' or '127.0.0.1:3306' with mysqld in the process column
# Windows: a line showing '0.0.0.0:3306' with state LISTENING
# If nothing appears on port 3306, the server is not running or is using a non-default port.
Output
# Linux systemctl output (server running):
● mysql.service - MySQL Community Server
Loaded: loaded (/lib/systemd/system/mysql.service; enabled)
Active: active (running) since Tue 2026-03-05 09:12:44 UTC; 2h 5min ago
Main PID: 1234 (mysqld)
Version: 8.4.4
# macOS Homebrew output:
mysql started homebrew.mxcl.mysql
# Windows PowerShell output:
Status Name DisplayName
------- ---- -----------
Running MySQL84 MySQL 8.4
# ss output (Linux) confirming port is open:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=1234,fd=23))
Port 3306 — Remember This Number
MySQL listens on TCP port 3306 by default. If a firewall, security group, or OS-level rule blocks this port, your application cannot reach the database even if MySQL is running perfectly — and the error you get ('Connection refused' or a timeout) looks identical to 'MySQL is not running'. When debugging any connection error, always confirm two things independently: (1) the server process is up, and (2) port 3306 is reachable from where you're connecting. On Linux use sudo ss -tlnp | grep 3306, on macOS use sudo lsof -i :3306, and on Windows use netstat -ano | findstr :3306.
Production Insight
MySQL is a server-client architecture — the server process (mysqld) must be running before any client or application can connect.
The server listens on port 3306 by default — firewall rules, OS-level packet filters, and cloud security groups can silently block connections even when the server is healthy.
MySQL 8.4 LTS is the target version for new projects in 2026 — MySQL 8.0 EOLs in April 2026.
Rule: always verify (1) server process status and (2) port 3306 reachability as the first two steps when debugging any MySQL connection error.
Key Takeaway
MySQL has two pieces: the server (background process mysqld, owns data files, listens on port 3306) and the client (shell or GUI you type queries into).
'MySQL not running' and 'MySQL not installed' are different problems — check server status first before debugging anything else.
MySQL 8.4 LTS is the current stable release for 2026 — use it for all new installations.

Installing MySQL on Windows, macOS, and Linux — Step by Step

MySQL installs differently on each operating system, but the goal is the same: get the server running and the client available in your terminal. Pick your OS below and follow every step — skipping even one (especially the secure installation script) is responsible for the majority of headaches beginners report.

Windows: winget is the cleanest path for developers in 2026 — it's built into Windows 10 and 11, handles the download and service registration silently, and doesn't require you to click through a wizard. If you prefer the graphical installer, it's still available at dev.mysql.com/downloads/installer — choose the 'Server only' variant unless you also want Workbench.

macOS: Homebrew remains the standard developer workflow. If you're on Apple Silicon (M1/M2/M3/M4), Homebrew installs to /opt/homebrew — Homebrew-managed MySQL works natively on ARM, no Rosetta needed.

Linux (Ubuntu/Debian): The apt package manager handles this cleanly on Ubuntu 22.04 and 24.04. Note that Ubuntu 24.04's default apt repository ships MySQL 8.0 — if you specifically want MySQL 8.4, you'll need to add Oracle's official APT repository first. On RHEL 9 and Fedora, use dnf.

After the install, regardless of your OS, you'll run mysql_secure_installation — a built-in script that locks down the default configuration. This is not optional. The default MySQL install (even 8.4) leaves certain things in a permissive state that you must harden. The script does it in about two minutes.

mysql_install_all_platforms.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# ════════════════════════════════════════════════════════
# OPTION A — macOS (using Homebrew)
# ════════════════════════════════════════════════════════

# Step 1: Install Homebrew if you don't have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# After install on Apple Silicon, follow the prompt to add Homebrew to your PATH.

# Step 2: Install MySQL 8.4 via Homebrew
brew install mysql
# As of 2026, 'brew install mysql' resolves to MySQL 8.4 LTS.
# Homebrew installs under /opt/homebrew/opt/mysql/ on Apple Silicon.

# Step 3: Start the MySQL server now AND on every future boot
brew services start mysql
# 'start' = launch now + register as a login item (auto-start on reboot).
# Use 'brew services run mysql' if you only want it running for this session.

# Verify it started correctly:
brew services list | grep mysql
mysql --version

# ════════════════════════════════════════════════════════
# OPTION B — Ubuntu 22.04 / 24.04 (MySQL 8.4 from Oracle repo)
# ════════════════════════════════════════════════════════

# Step 1: Add Oracle's official MySQL APT repository
# This ensures you get MySQL 8.4 LTS, not 8.0 from Ubuntu's default repo.
wget https://dev.mysql.com/get/mysql-apt-config_0.8.32-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.32-1_all.deb
# A dialog appears — select MySQL 8.4 Server, then OK.

# Step 2: Refresh the package list to include Oracle's repo
sudo apt update

# Step 3: Install MySQL 8.4 Server
sudo apt install mysql-server -y
# The service starts automatically after install.

# Step 4: Verify version and service status
mysql --version
sudo systemctl status mysql

# Step 5: Enable auto-start on system reboot (usually already enabled)
sudo systemctl enable mysql

# ════════════════════════════════════════════════════════
# OPTION C — Windows 10 / 11 (via winget)
# ════════════════════════════════════════════════════════

# Open PowerShell as Administrator:
winget install Oracle.MySQL
# winget downloads MySQL 8.4, installs silently, and registers a Windows Service.
# After install completes, verify:
Get-Service -Name 'MySQL*'
mysql --version

# ════════════════════════════════════════════════════════
# ALL PLATFORMSSecurity hardening script (run this next, always)
# ════════════════════════════════════════════════════════

sudo mysql_secure_installation
# On Windows, run this from the MySQL bin directory or add it to your PATH.
# The script will ask you five questions — answer as shown:
# 1. Set up VALIDATE PASSWORD component? → YES (enforces strong passwords)
# 2. Password strength level → 1 (MEDIUM) is fine for local dev, 2 for production
# 3. Set a root password? → YES — pick something strong, write it down
# 4. Remove anonymous users? → YES
# 5. Disallow root login remotely? → YES
# 6. Remove test database? → YES
# 7. Reload privilege tables now? → YES

# After it finishes, verify you can log in with the new password:
mysql -u root -p
# You should reach the 'mysql>' prompt after entering your root password.
Output
# macOS: brew services list
mysql started homebrew.mxcl.mysql
# mysql --version output (all platforms):
mysql Ver 8.4.4 Distrib 8.4.4, for Linux (x86_64) using EditLine wrapper
# Ubuntu apt install (tail of output):
Setting up mysql-server-8.4 (8.4.4-1ubuntu22.04) ...
Created symlink /etc/systemd/system/multi-user.target.wants/mysql.service →
/lib/systemd/system/mysql.service.
Starting MySQL Community Server ...
# mysql_secure_installation — full interactive session:
Securing the MySQL server deployment.
Enter password for user root:
VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security.
Estimated strength of the password: 100
Do you wish to continue with the password provided? (Press y|Y for Yes) : y
Remove anonymous users? (Press y|Y for Yes): y
... Success!
Disallow root login remotely? (Press y|Y for Yes): y
... Success!
Remove test database and access to it? (Press y|Y for Yes): y
- Dropping test database...
... Success!
Reload privilege tables now? (Press y|Y for Yes): y
... Success!
All done!
Watch Out: macOS 'Access Denied' After Homebrew Install
On a fresh Homebrew MySQL 8.4 install, the root user authenticates via the caching_sha2_password plugin — which on a brand-new install with no password set means you connect with sudo mysql (no -p flag) rather than mysql -u root -p. If the latter gives 'Access denied', try sudo mysql first. Once inside, run: ALTER USER 'root'@'localhost' IDENTIFIED BY 'YourNewPassword'; FLUSH PRIVILEGES; — this sets the password while keeping the modern auth plugin. Then you can connect with mysql -u root -p going forward.
Production Insight
Default MySQL installs — even 8.4 — leave anonymous users and the test database in place until you remove them.
mysql_secure_installation is a two-minute investment that closes the most commonly exploited default weaknesses.
On Ubuntu 24.04, apt installs MySQL 8.0 by default — add Oracle's APT repo to get 8.4 LTS.
Rule: run mysql_secure_installation immediately after install on every machine, every time — no exceptions for dev environments.
Key Takeaway
Install method varies by OS but the goal is the same: MySQL 8.4 server running, client in your PATH.
mysql_secure_installation removes anonymous users, sets root password, enables password validation, and deletes the test database — it is not optional.
On Ubuntu 24.04, explicitly add Oracle's APT repo to get MySQL 8.4 instead of the older 8.0 from Ubuntu's default packages.

Creating Your First Database, User, and Table — Then Querying It

With MySQL running and secured, let's do something concrete. We'll create a database for a bookstore, add a dedicated MySQL user (never use root for application connections — more on that in the common mistakes section), create a table for books, insert some rows, and run SELECT queries to prove it all works.

Think of a database as a named folder on your filing cabinet. A table is a spreadsheet inside that folder. A row is one record — one book. A column is one field present in every record — title, author, price. The structure of the columns is defined when you create the table, and MySQL enforces it on every row you insert.

One decision worth making consciously before you create any table: character set. We use utf8mb4 below, not the older utf8. MySQL's 'utf8' is actually a broken three-byte subset of real UTF-8 that cannot store emoji or certain Asian scripts. utf8mb4 is proper four-byte UTF-8 — it handles everything. In MySQL 8.4, utf8mb4 is the default, so you may not need to specify it explicitly, but being explicit is a good habit when your data will cross environments.

The SQL commands below work identically on MySQL 8.4 and MariaDB 10.11+. Type them one at a time in your MySQL shell, or save them as a .sql file and run them with mysql -u root -p < bookstore_setup.sql.

bookstore_setup.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
-- ── STEP 1: Connect to MySQL as root ──────────────────────────────────────
-- Run this in your terminal (outside MySQL):
-- mysql -u root -p
-- Enter the root password you set during mysql_secure_installation.
-- You'll see the 'mysql>' prompt when you're inside.

-- ── STEP 2: Create a dedicated database ───────────────────────────────────
CREATE DATABASE IF NOT EXISTS bookstore_db
    CHARACTER SET utf8mb4        -- full Unicode including emoji
    COLLATE utf8mb4_unicode_ci;  -- case-insensitive string comparison
-- IF NOT EXISTS prevents an error if you run this script twice.

-- Verify it was created:
SHOW DATABASES;

-- ── STEP 3: Create a dedicated application user ────────────────────────────
-- Replace 'secure_password_here' with a real password (16+ chars, mixed case,
-- numbers, symbols). The user only has access from this machine ('localhost').
CREATE USER IF NOT EXISTS 'bookstore_app'@'localhost'
    IDENTIFIED BY 'secure_password_here';
-- IF NOT EXISTS prevents an error if you accidentally run this twice.

-- ── STEP 4: Grant only the permissions the app actually needs ──────────────
GRANT SELECT, INSERT, UPDATE, DELETE
    ON bookstore_db.*
    TO 'bookstore_app'@'localhost';
-- The app gets no DROP, CREATE, GRANT, or SHOW DATABASES.
-- If the app is ever compromised, the attacker cannot destroy the schema
-- or create backdoor users.

-- Apply the privilege changes immediately:
FLUSH PRIVILEGES;

-- ── STEP 5: Switch into the bookstore database ────────────────────────────
USE bookstore_db;

-- ── STEP 6: Create the books table ───────────────────────────────────────
CREATE TABLE IF NOT EXISTS books (
    book_id      INT           NOT NULL AUTO_INCREMENT,
    title        VARCHAR(255)  NOT NULL,
    author_name  VARCHAR(150)  NOT NULL,
    genre        VARCHAR(80)   NOT NULL,
    price_usd    DECIMAL(8,2)  NOT NULL,   -- exact decimal, never FLOAT for money
    in_stock     BOOLEAN       NOT NULL DEFAULT TRUE,
    created_at   TIMESTAMP     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at   TIMESTAMP     NOT NULL DEFAULT CURRENT_TIMESTAMP
                               ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (book_id),
    INDEX idx_genre (genre),               -- speeds up WHERE genre = '...'
    INDEX idx_price (price_usd)            -- speeds up ORDER BY price_usd
);
-- AUTO_INCREMENT: MySQL assigns book_id automatically — 1, 2, 3, ...
-- DECIMAL(8,2): stores values up to 999999.99 exactly — right for money.
-- ON UPDATE CURRENT_TIMESTAMP: updated_at refreshes automatically on any change.
-- The two INDEX definitions are small now but matter when the table has millions of rows.

-- ── STEP 7: Insert sample books ───────────────────────────────────────────
INSERT INTO books (title, author_name, genre, price_usd) VALUES
    ('The Pragmatic Programmer', 'David Thomas',      'Technology', 49.95),
    ('Clean Code',               'Robert C. Martin',  'Technology', 35.00),
    ('Dune',                     'Frank Herbert',     'Sci-Fi',     14.99),
    ('Project Hail Mary',        'Andy Weir',         'Sci-Fi',     16.50),
    ('The Psychology of Money',  'Morgan Housel',     'Finance',    18.00);
-- We omit book_id, in_stock, created_at, updated_at — MySQL fills these automatically.

-- ── STEP 8: Query the data ────────────────────────────────────────────────
-- All books, cheapest first:
SELECT book_id, title, author_name, price_usd
FROM   books
ORDER  BY price_usd ASC;

-- Only Sci-Fi books:
SELECT title, author_name, price_usd
FROM   books
WHERE  genre = 'Sci-Fi';

-- Count and average price per genre:
SELECT
    genre,
    COUNT(*)              AS total_books,
    AVG(price_usd)        AS avg_price,
    MIN(price_usd)        AS cheapest,
    MAX(price_usd)        AS most_expensive
FROM   books
GROUP  BY genre
ORDER  BY total_books DESC, avg_price DESC;

-- ── STEP 9: Verify the table structure ────────────────────────────────────
DESCRIBE books;
-- Shows column names, types, nullability, defaults, and key assignments.
-- Good habit: always describe a table after creating it to confirm it looks right.
Output
-- SHOW DATABASES:
+--------------------+
| Database |
+--------------------+
| bookstore_db |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
-- SELECT all books (ORDER BY price_usd ASC):
+---------+--------------------------+--------------------+-----------+
| book_id | title | author_name | price_usd |
+---------+--------------------------+--------------------+-----------+
| 3 | Dune | Frank Herbert | 14.99 |
| 4 | Project Hail Mary | Andy Weir | 16.50 |
| 5 | The Psychology of Money | Morgan Housel | 18.00 |
| 2 | Clean Code | Robert C. Martin | 35.00 |
| 1 | The Pragmatic Programmer | David Thomas | 49.95 |
+---------+--------------------------+--------------------+-----------+
5 rows in set (0.002 sec)
-- GROUP BY genre:
+------------+-------------+-----------+----------+----------------+
| genre | total_books | avg_price | cheapest | most_expensive |
+------------+-------------+-----------+----------+----------------+
| Technology | 2 | 42.475000 | 35.00 | 49.95 |
| Sci-Fi | 2 | 15.745000 | 14.99 | 16.50 |
| Finance | 1 | 18.000000 | 18.00 | 18.00 |
+------------+-------------+-----------+----------+----------------+
3 rows in set (0.001 sec)
-- DESCRIBE books:
+-------------+--------------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+-------------------+-------------------+
| book_id | int | NO | PRI | NULL | auto_increment |
| title | varchar(255) | NO | | NULL | |
| author_name | varchar(150) | NO | | NULL | |
| genre | varchar(80) | NO | MUL | NULL | |
| price_usd | decimal(8,2) | NO | MUL | NULL | |
| in_stock | tinyint(1) | NO | | 1 | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED
on update |
+-------------+--------------+------+-----+-------------------+-------------------+
Use DECIMAL for Money — Always. No Exceptions.
Storing prices as FLOAT is one of the most common beginner mistakes and one of the most expensive to fix later. Floating-point arithmetic cannot exactly represent most decimal fractions — $12.99 stored as FLOAT may come back as $12.98999977111816 in your application. Multiply that across thousands of transactions and you have a genuine accounting error. DECIMAL(8,2) stores exact values with exactly 2 decimal places — it works the way money actually works. The '8' is the total digit count, so DECIMAL(8,2) handles up to $999,999.99. Need bigger? Use DECIMAL(12,2) for values up to $9,999,999,999.99.
Production Insight
Never use root for application connections — if the app is compromised, root access means total server takeover.
Use IF NOT EXISTS in CREATE DATABASE and CREATE USER — makes your setup scripts safe to re-run without errors.
Index columns you filter or sort on from day one (genre, price_usd) — adding indexes to a table with 10 million rows later causes a full table lock in older MySQL versions.
Rule: one dedicated user per application, least-privilege GRANT, utf8mb4 character set, DECIMAL for money.
Key Takeaway
Create a dedicated MySQL user per application with minimum GRANT permissions — SELECT, INSERT, UPDATE, DELETE only.
Use IF NOT EXISTS on CREATE DATABASE and CREATE USER so your scripts are idempotent.
Use DECIMAL(8,2) for money — FLOAT introduces silent rounding errors that compound across transactions.
Add indexes on columns you know you'll filter or sort on — far easier to do at creation time than at scale.

Connecting MySQL to Your Application — And Verifying Everything Works

A database that only lives in the MySQL shell isn't much use. Let's connect it from real application code. We'll use Python with the mysql-connector-python library as the example — the connection concepts (host, port, user, password, database, commit) apply identically in Node.js, PHP, Go, Java, and every other language. The library is the translator: your Python code calls Python functions, the library converts them into MySQL's wire protocol, sends them to the MySQL server over a TCP socket, gets results back, and hands them to Python as normal objects.

Notice we connect as bookstore_app — not root. This is a habit you want to build from day one, not something you bolt on later. If someone finds a SQL injection vulnerability in your code (it happens to experienced engineers too), a connection with only SELECT/INSERT/UPDATE/DELETE on one database cannot drop your tables, read your system tables, or create backdoor users. Root can do all of that. The scope of a security incident is directly determined by the privilege level of the compromised connection.

For mysql-connector-python in 2026, make sure you're on version 9.x — it has native support for MySQL 8.4's caching_sha2_password authentication plugin, which is the default in 8.4. Older versions of the connector (8.0.x) may throw an authentication error against MySQL 8.4 servers. The fix is simply upgrading: pip install --upgrade mysql-connector-python.

The connection credentials — host, port, user, password, database — should always come from environment variables in real projects. Never hardcode passwords in source files. Source files end up in Git. Git history is forever.

bookstore_db_connect.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# Install the connector first (run in your terminal, not in Python):
# pip install mysql-connector-python
# Verify you have a 9.x version for MySQL 8.4 compatibility:
# pip show mysql-connector-python

import mysql.connector
import os
from decimal import Decimal

# ── Connection configuration — from environment variables ─────────────────
# Before running locally, set these in your shell:
#   export DB_HOST=127.0.0.1
#   export DB_PASSWORD=secure_password_here
# In production, inject these from a secrets manager or CI/CD secret variables.
db_config = {
    "host":     os.getenv("DB_HOST",     "127.0.0.1"),  # 127.0.0.1 is safer than 'localhost'
    "port":     int(os.getenv("DB_PORT", "3306")),
    "user":     os.getenv("DB_USER",     "bookstore_app"),
    "password": os.getenv("DB_PASSWORD", ""),           # empty default forces explicit env var
    "database": os.getenv("DB_NAME",     "bookstore_db"),
}
# Note: we use 127.0.0.1 rather than 'localhost' deliberately.
# On Linux, 'localhost' routes through a Unix socket, 127.0.0.1 uses TCP.
# The bookstore_app user was created for 'localhost' (socket) but if your
# driver resolves differently, the host spec in mysql.user may not match.
# Using 127.0.0.1 consistently avoids this ambiguity.


def get_connection():
    """
    Returns a new MySQL connection. Call this at the start of each operation.
    In a real application, replace this with a connection pool
    (e.g., mysql.connector.pooling.MySQLConnectionPool).
    """
    return mysql.connector.connect(**db_config)


def fetch_books_under_price(max_price: Decimal) -> list[dict]:
    """
    Returns all books priced strictly below max_price, sorted cheapest first.
    Uses a parameterised query — NEVER concatenate user input into SQL strings.
    """
    with get_connection() as connection:
        # dictionary=True: each row comes back as {'title': 'Dune', 'price_usd': Decimal('14.99')}
        # rather than a plain tuple — much easier to work with in application code.
        with connection.cursor(dictionary=True) as cursor:
            query = """
                SELECT book_id, title, author_name, price_usd
                FROM   books
                WHERE  price_usd < %s
                ORDER  BY price_usd ASC
            """
            # The %s placeholder is replaced safely by the connector library.
            # Parameters are always passed as a tuple — even a single parameter.
            # This prevents SQL injection because user input never becomes SQL syntax.
            cursor.execute(query, (max_price,))
            return cursor.fetchall()


def add_new_book(title: str, author: str, genre: str, price: Decimal) -> int:
    """
    Inserts a new book row and returns the auto-generated book_id.
    Demonstrates the commit() pattern required for all write operations.
    """
    with get_connection() as connection:
        with connection.cursor() as cursor:
            insert_query = """
                INSERT INTO books (title, author_name, genre, price_usd)
                VALUES (%s, %s, %s, %s)
            """
            cursor.execute(insert_query, (title, author, genre, price))

            # ── CRITICAL: commit() writes the transaction to disk ─────────
            # Without this line, the INSERT is held in a pending InnoDB
            # transaction. When the 'with' block exits and the connection
            # closes, MySQL silently rolls back the pending transaction.
            # No error. No warning. The row simply will not exist.
            connection.commit()

            # lastrowid gives us the AUTO_INCREMENT value MySQL assigned.
            return cursor.lastrowid


def update_book_price(book_id: int, new_price: Decimal) -> bool:
    """
    Updates the price of a book by ID.
    Returns True if a row was updated, False if book_id was not found.
    """
    with get_connection() as connection:
        with connection.cursor() as cursor:
            cursor.execute(
                "UPDATE books SET price_usd = %s WHERE book_id = %s",
                (new_price, book_id)
            )
            connection.commit()  # required — this is a write operation
            return cursor.rowcount > 0  # rowcount = number of rows actually changed


# ── Demo: run when the script is executed directly ───────────────────────
if __name__ == "__main__":
    print("=== Books under $20 ===")
    affordable = fetch_books_under_price(max_price=Decimal("20.00"))
    for book in affordable:
        print(f"  [{book['book_id']}] {book['title']} by {book['author_name']} — ${book['price_usd']}")

    print("\n=== Adding a new book ===")
    new_id = add_new_book(
        title="Atomic Habits",
        author="James Clear",
        genre="Self-Help",
        price=Decimal("16.99")
    )
    print(f"  New book inserted — book_id = {new_id}")

    print("\n=== Updating a price ===")
    updated = update_book_price(book_id=new_id, new_price=Decimal("14.99"))
    print(f"  Price update {'succeeded' if updated else 'failed — book_id not found'}")
Output
=== Books under $20 ===
[3] Dune by Frank Herbert — $14.99
[4] Project Hail Mary by Andy Weir — $16.50
[5] The Psychology of Money by Morgan Housel — $18.00
=== Adding a new book ===
New book inserted — book_id = 6
=== Updating a price ===
Price update succeeded
Watch Out: Forgetting connection.commit() Silently Loses Your Data
This is the single most common mistake I see from developers who are new to MySQL — and it is absolutely maddening to debug because there is no error. You call cursor.execute(), it returns without complaint, cursor.rowcount says '1 row affected', and then you query the table and the row is not there. What happened: MySQL's InnoDB storage engine (the default since MySQL 5.5) uses transactions. Every INSERT, UPDATE, or DELETE starts a transaction implicitly. That transaction stays pending until you explicitly call connection.commit(). When the connection closes — either at the end of your 'with' block or when the script exits — MySQL sees an uncommitted transaction and rolls it back. Silently. The fix is always the same: add connection.commit() after every write. If you want MySQL to commit automatically after each statement without explicit calls, set autocommit=True in your connection config — but be aware this disables the ability to roll back a failed batch of writes.
Production Insight
Use mysql-connector-python 9.x for MySQL 8.4 — older 8.0.x versions may fail against caching_sha2_password auth.
Use 127.0.0.1 rather than 'localhost' in connection strings to avoid socket vs. TCP ambiguity on Linux.
Parameterised queries (%s placeholders) are the only correct way to include variable data in SQL — string concatenation is never acceptable.
Rule: commit() after every write, environment variables for every credential, parameterised queries for every user input.
Key Takeaway
Use mysql-connector-python 9.x for MySQL 8.4 compatibility — upgrade with pip install --upgrade mysql-connector-python.
Never hardcode passwords in source code — use environment variables or a secrets manager.
Parameterised queries (%s) prevent SQL injection — never concatenate user input into SQL strings.
connection.commit() is required after every INSERT, UPDATE, and DELETE — InnoDB holds writes in a pending transaction until you explicitly commit.
● Production incidentPOST-MORTEMseverity: high

Production Database Exposed to the Internet Because mysql_secure_installation Was Skipped

Symptom
Customer data appeared on a public paste site. The application was still functioning normally — no visible errors, no alerts fired. The breach was discovered by a third-party security researcher who found the server responding to connections on port 3306 from arbitrary IPs. By the time the team was notified, the attacker had been inside for an estimated 11 hours.
Assumption
The team assumed MySQL was secure by default after installation and that only the application server could reach the database because they shared the same VPC. No one had explicitly verified the bind-address setting or tested port 3306 reachability from outside the VPC.
Root cause
The MySQL root account had no password (default behaviour on Ubuntu apt install prior to 8.4). The server was bound to 0.0.0.0 — accepting connections from any IP — instead of 127.0.0.1. The test database and anonymous users were still present from the default install. A port scan from the public internet discovered port 3306 open because a security group rule had been left overly permissive during a debugging session two weeks earlier and never reverted. The attacker connected as root with no password and dumped all databases in under four minutes.
Fix
1. Immediately terminated all active MySQL connections and revoked the exposed security group rule at the cloud provider level 2. Ran mysql_secure_installation to set a strong root password and remove anonymous users 3. Changed bind-address in /etc/mysql/mysql.conf.d/mysqld.cnf from 0.0.0.0 to 127.0.0.1 and restarted the service 4. Added an explicit deny firewall rule: ufw deny 3306 from any 5. Created dedicated users per application with least-privilege permissions and rotated all application credentials 6. Enabled MySQL audit logging (using the Community audit plugin) for future detection 7. Added automated port-scan checks to the CI/CD pipeline to catch accidental 3306 exposure before it reaches staging
Key lesson
  • Always run mysql_secure_installation immediately after installing MySQL — no exceptions, no 'we'll do it before go-live'
  • Never bind MySQL to 0.0.0.0 unless you specifically need remote access and have verified, tested firewall rules protecting it
  • Block port 3306 at the firewall or security group level independently of the bind-address setting — defense in depth
  • Default MySQL installs (especially pre-8.4) are intentionally permissive by design — hardening is your responsibility, not the installer's
  • Audit your security group and firewall rules on a schedule — one-off 'temporary' debug changes have a way of becoming permanent
Production debug guideWhen your app cannot connect to MySQL or queries behave unexpectedly5 entries
Symptom · 01
Can't connect to MySQL server on '127.0.0.1' (errno 111 — Connection refused)
Fix
The MySQL server is not running or not listening on port 3306. Check with: sudo systemctl status mysql. If inactive, start it with: sudo systemctl start mysql. If it starts and immediately stops, something is wrong with the configuration — check /var/log/mysql/error.log for the actual reason. On MySQL 8.4, a common cause is a datadir permissions issue after an upgrade from 8.0.
Symptom · 02
Access denied for user 'app_user'@'localhost' (using password: YES)
Fix
The user exists but the password is wrong, or the user was created for a different host specifier. On MySQL 8.4, also check that the client library supports caching_sha2_password — older versions of mysql-connector-python (below 8.0.28) may fail silently against 8.4's default auth plugin. Verify with: SELECT user, host, plugin FROM mysql.user; and try connecting as root to confirm the server is reachable.
Symptom · 03
INSERT rows disappear — no error, SELECT returns nothing
Fix
Missing connection.commit(). InnoDB uses transactions — uncommitted changes roll back when the connection closes. Add connection.commit() after every write operation. If you want writes to auto-commit without explicit calls, set autocommit=True in the connection config, but understand you lose the ability to roll back a failed batch.
Symptom · 04
Too many connections error (max_connections exceeded)
Fix
Check the current setting: SHOW VARIABLES LIKE 'max_connections'; — the default is 151 on most installs. Also check how many connections are currently open: SHOW STATUS LIKE 'Threads_connected'; If you are using a connection pool in your application, verify the pool size is not exceeding max_connections across all app instances. Look for connection leaks — connections that are opened but never closed, especially in error paths.
Symptom · 05
Authentication plugin 'caching_sha2_password' cannot be loaded
Fix
This appears when an older client library connects to MySQL 8.4, which uses caching_sha2_password by default instead of mysql_native_password. Update your client library first: pip install --upgrade mysql-connector-python. If you cannot upgrade the library, you can change a specific user's auth plugin: ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; — but treat this as a temporary workaround, not a long-term fix.
★ MySQL Setup Debugging Cheat SheetWhen MySQL installation or connection goes wrong — quick commands to find the problem fast
MySQL server not responding
Immediate action
Check if the server process is actually running before touching anything else
Commands
sudo systemctl status mysql
sudo lsof -i :3306
Fix now
Start the server: sudo systemctl start mysql. If it starts then immediately stops, the error is in the config or datadir — check: sudo journalctl -u mysql --no-pager -n 50 and /var/log/mysql/error.log for the real reason.
Access denied for root user+
Immediate action
Try socket-based auth first — on Ubuntu and macOS Homebrew installs, root uses socket auth by default, not password
Commands
sudo mysql
SELECT user, host, plugin FROM mysql.user WHERE user='root';
Fix now
If plugin is auth_socket or caching_sha2_password and you need password login, run: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'NewPass123!'; FLUSH PRIVILEGES; — note that MySQL 8.4 deprecates mysql_native_password so prefer caching_sha2_password with an updated client.
Connection refused from application code+
Immediate action
Verify MySQL is listening on the address and port your application code is targeting
Commands
sudo ss -tlnp | grep 3306
grep bind-address /etc/mysql/mysql.conf.d/mysqld.cnf
Fix now
If bind-address is 127.0.0.1, connect to 127.0.0.1 in your application code — not 'localhost', which on Linux resolves to a Unix socket path, not the TCP loopback interface. This is a surprisingly common mismatch.
MySQL 8.4 LTS (Community) vs MySQL Workbench (GUI)
AspectMySQL 8.4 LTS (Community)MySQL Workbench (GUI)
What it isThe actual database server (mysqld) plus the mysql CLI clientA visual desktop application for managing MySQL via point-and-click
Required?Yes — this IS the database. Nothing works without it.No — entirely optional, but genuinely useful for beginners exploring data visually
How you interactTerminal commands and SQL queries at the mysql> promptDrag, click, and write queries in a colour-coded visual editor
Best forProduction use, scripting, automation, CI/CD pipelinesSchema design, one-off exploratory queries, visual data browsing, ER diagrams
Resource usageLightweight server process — roughly 200-400MB RAM depending on buffer pool configHeavier desktop app — roughly 400-600MB RAM, Java-based, slower to start
MySQL 8.4 compatibilityThis is MySQL 8.4 — fully compatibleWorkbench 8.0.36+ supports MySQL 8.4 — verify version before connecting
Install methodapt / Homebrew / winget — covered in this guideSeparate download from dev.mysql.com/downloads/workbench
Needed to follow this guide?Yes — install this firstNo — install it once you're comfortable with the CLI, as a complement not a replacement

Key takeaways

1
MySQL has two pieces
the server (background process mysqld, owns the data files, listens on port 3306) and the client (shell or GUI you type queries into). Both must be present; the server must be running before any client or application can connect.
2
MySQL 8.4 LTS is the current stable release for 2026. MySQL 8.0 reaches end-of-life in April 2026
use 8.4 for all new installations and plan upgrades for existing 8.0 deployments.
3
Always run mysql_secure_installation immediately after installing MySQL
it removes anonymous users, sets a root password, enables password validation, and deletes the test database. Skipping it leaves avoidable attack surface on every machine you touch.
4
Create a dedicated MySQL user per application with only the permissions it needs
GRANT SELECT, INSERT, UPDATE, DELETE on that app's database. Root is for administration only, never for application connections.
5
INSERT, UPDATE, and DELETE require connection.commit() in Python (and most languages) before changes are written to disk
InnoDB uses transactions by default, and uncommitted changes are silently rolled back when the connection closes.
6
Use DECIMAL(8,2) for money columns
FLOAT uses binary floating-point arithmetic that cannot exactly represent most decimal fractions, causing rounding errors like $12.98999977 that accumulate across transactions.
7
Never hardcode database credentials in source code
use environment variables for local development and a secrets manager in production. Credentials that appear in Git history should be treated as permanently compromised and rotated immediately.
8
Update mysql-connector-python to version 9.x when using MySQL 8.4
older versions do not support the caching_sha2_password authentication plugin that MySQL 8.4 uses by default.

Common mistakes to avoid

6 patterns
×

Using root for application database connections

Symptom
The app works fine locally. But any SQL injection vulnerability — even a minor one — gives an attacker root-level access to the entire MySQL server. They can DROP every table, CREATE backdoor users, and read the mysql system tables. The blast radius is total.
Fix
Always create a dedicated MySQL user per application with only the permissions that application needs: GRANT SELECT, INSERT, UPDATE, DELETE ON your_db.* TO 'app_user'@'localhost'; Root credentials belong in your password manager for administration tasks, never in application config files or environment variables that application processes read.
×

Forgetting connection.commit() after INSERT/UPDATE/DELETE

Symptom
Code runs without errors. cursor.rowcount reports '1 row affected'. But a subsequent SELECT returns nothing — the row has vanished. This is especially confusing when the script runs in development with autocommit enabled (some ORMs set this) but not in production where the connection pool has different defaults.
Fix
Add connection.commit() immediately after every write operation. Check your ORM or connection pool settings explicitly — don't assume autocommit is on or off. If you want MySQL to commit each statement automatically without explicit calls, set autocommit=True in the connection config and document that decision clearly.
×

Skipping mysql_secure_installation after setup

Symptom
MySQL root has no password. Anonymous users exist. The 'test' database is present and accessible to anyone who can reach port 3306. If the server is even briefly exposed to the internet — or if another user on the same machine is malicious — the database is wide open.
Fix
Run sudo mysql_secure_installation immediately after installing MySQL — before you do anything else. Answer yes to every prompt. If you already skipped it, you can run it again at any time on a running server — it is completely safe to re-run and does not affect existing databases or users you've already created.
×

Storing money as FLOAT instead of DECIMAL

Symptom
A price stored as $12.99 comes back from the database as $12.98999977111816 in your application. Calculations on financial values produce penny-level rounding errors that accumulate silently across thousands of transactions — sometimes in your favour, sometimes not, never correctly.
Fix
Use DECIMAL(8,2) for all money columns, no exceptions. DECIMAL stores exact values with a fixed number of decimal places, which is how money actually works. FLOAT uses IEEE 754 binary floating-point, which physically cannot represent most decimal fractions exactly. This is not a MySQL quirk — it's a fundamental property of binary arithmetic.
×

Hardcoding database credentials in source code

Symptom
The password appears in plain text in a .py, .js, or .php file that gets committed to a Git repository. Git history is permanent — even if you delete the file later, the password remains in history. If the repository is ever made public, or if an attacker gains read access, the database is immediately compromised.
Fix
Use environment variables: os.getenv('DB_PASSWORD') in Python, process.env.DB_PASSWORD in Node.js. Add .env to .gitignore before making your first commit. In production, use a proper secrets manager — AWS Secrets Manager, HashiCorp Vault, or your CI/CD platform's built-in secret variables. Treat any credential that has ever been committed to source control as permanently compromised and rotate it immediately.
×

Connecting to MySQL 8.4 with an outdated client library

Symptom
Application throws: 'Authentication plugin caching_sha2_password cannot be loaded' or 'MySQL Client does not support authentication protocol'. The server is running fine, credentials are correct, but the connection fails immediately.
Fix
Upgrade your client library: pip install --upgrade mysql-connector-python for Python (target 9.x). For Node.js, upgrade mysql2 to 3.x. If you absolutely cannot upgrade the library in the short term, you can change a specific user to the older plugin: ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; — but schedule the library upgrade, because mysql_native_password is deprecated in MySQL 8.4 and will be removed in a future version.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between the MySQL server and the MySQL client, an...
Q02JUNIOR
Why should you never use the root MySQL account for application database...
Q03SENIOR
A developer inserts 1,000 rows in a loop using Python's mysql-connector-...
Q04SENIOR
How would you secure a MySQL server that is already in production but wa...
Q05SENIOR
An application worked correctly against MySQL 8.0 but fails with an auth...
Q01 of 05JUNIOR

What is the difference between the MySQL server and the MySQL client, and why does that distinction matter when debugging a 'connection refused' error?

ANSWER
The MySQL server is the mysqld background process that owns the data files on disk and listens for incoming connections on port 3306. The MySQL client is the mysql CLI tool (or any GUI, or your application's database library) that connects to the server over a TCP socket or Unix socket. When debugging 'connection refused' (errno 111 on Linux), the distinction matters because the error means the client cannot reach the server — not that the credentials are wrong. Three things to verify in order: (1) is the server process running — sudo systemctl status mysql; (2) is something listening on port 3306 — sudo ss -tlnp | grep 3306; (3) is a firewall blocking the port — sudo ufw status or check cloud security groups. Only move on to credential debugging after you've confirmed all three.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How do I install MySQL 8.4 on Windows 11 without the graphical wizard?
02
What is the default MySQL root password after installation?
03
What is the difference between MySQL and MariaDB — can I follow this guide for both?
04
What is the difference between localhost and 127.0.0.1 when connecting to MySQL?
05
How do I reset the MySQL root password if I forgot it?
06
My application worked with MySQL 8.0 but now gives an authentication error after upgrading to MySQL 8.4. What changed?
🔥

That's MySQL & PostgreSQL. Mark it forged?

6 min read · try the examples if you haven't

Previous
MySQL vs PostgreSQL
2 / 13 · MySQL & PostgreSQL
Next
PostgreSQL Installation and Setup