MySQL Complete Guide | InnoDB Internals, MVCC, Optimizer, Replication & Production Patterns

MySQL Complete Guide | InnoDB Internals, MVCC, Optimizer, Replication & Production Patterns

이 글의 핵심

A practical and internals-aware tour of MySQL: how InnoDB stores data, how MVCC implements isolation, how the optimizer picks plans, how replication ships changes, and how to run it in production—with SQL and config you can use immediately.

What this post covers

This guide combines hands-on SQL with how MySQL actually works under the hood: InnoDB storage layout, MVCC and isolation, the query optimizer, replication, and production patterns (pooling, backups, safety knobs).

From experience: On a MySQL deployment handling on the order of 100M transactions per day, we improved hot-query latency by ~100× through indexing and gained roughly read capacity with replicas—after we also fixed a few “mysterious” slowdowns caused by long transactions and stale optimizer statistics.

Introduction: “MySQL feels slow”

Slowness is rarely “because MySQL is slow”; it is usually missing indexes, non-sargable predicates, lock contention, replication lag, or mis-sized InnoDB memory. This post gives you both the checklist and the mental model.

Real-world scenarios

Scenario 1: Queries take 10 seconds

On a 100M-row table, a full scan can be catastrophic. Correct composite indexes and sargable filters often bring that to milliseconds.

Scenario 2: Read load is too high

One primary cannot serve all reads. Replication (and eventually sharding or separate analytics stores) spreads load—while you accept replica lag semantics.

Scenario 3: The table is too large

Partitioning helps management and pruning; it is not a substitute for indexing, but it can shrink maintenance windows and speed up range deletes.


1. What is MySQL?

MySQL is a widely deployed open-source RDBMS. For most applications today, InnoDB is the storage engine that matters: it implements ACID transactions, row-level locking, foreign keys, and crash recovery via redo logging.

Strengths often cited in production:

  • Strong OLTP read performance when indexed well
  • Mature replication and operator tooling
  • Large ecosystem (drivers, hosts, managed services)
  • Operable defaults for many web workloads

2. InnoDB storage engine (internals)

Understanding InnoDB explains why primary key design, secondary indexes, and write amplification behave the way they do.

Buffer pool and pages

InnoDB caches data and indexes in the buffer pool (innodb_buffer_pool_size). Disk files are organized into pages (commonly 16 KiB). Pages belong to extents and tablespace segments. When a page is needed, InnoDB reads it into the buffer pool; dirty pages are flushed asynchronously.

Why it matters: If the working set does not fit in RAM, you will see random I/O and latency spikes. On a dedicated DB host, allocating ~70–80% of RAM to the buffer pool is a common starting point—always validated with monitoring, not slogans.

Redo log and crash recovery

InnoDB is durability-oriented: user transactions modify pages in memory first, while redo logs record physical changes so that after a crash, committed work can be replayed. The redo log is effectively a write-ahead log (WAL) for InnoDB.

innodb_flush_log_at_trx_commit tradeoff:

  • 1: flush redo to disk at commit (safest for single-instance durability; more sync I/O)
  • 2: write to OS cache at commit, flush periodically (better performance; small crash-window risk depending on infrastructure)

Pair this with sync_binlog when you care about binlog + InnoDB consistency on failure—especially for replication.

Doublewrite buffer

InnoDB uses a doublewrite buffer to protect against partial page writes during crashes. This adds some write overhead but prevents torn pages from corrupting tablespaces.

Undo logs and rollback

Undo stores older versions of rows so transactions can roll back and so consistent reads can be constructed (see MVCC). Long transactions that keep old versions needed can delay purge and hurt performance.

Clustered index (primary key)

InnoDB tables are stored as a clustered index on the primary key. Secondary indexes store primary key columns as pointers to the actual row.

Practical rule: Use a stable, narrow, monotonic primary key for insert-heavy tables (often BIGINT auto-increment). Random UUIDs as PK can cause fragmentation and hotspot behaviors unless you use a suitable variant (e.g., ordered UUID schemes)—tradeoffs apply.

Change buffer (formerly insert buffer)

Secondary index maintenance can be deferred in some cases via the change buffer, reducing random I/O for writes—until merges occur. Heavy write loads still require good index discipline.


3. Transaction isolation and MVCC

InnoDB uses Multi-Version Concurrency Control (MVCC) to let readers avoid blocking writers in many cases. Each row is tied to versioning information; undo chains reconstruct older row versions.

Isolation levels (typical behaviors)

MySQL/InnoDB supports READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ (InnoDB default), and SERIALIZABLE.

  • READ COMMITTED: Each statement sees committed data at statement start; fewer oddities than RR for some apps, but still watch locking for writes.
  • REPEATABLE READ: Consistent snapshot for the transaction; InnoDB uses next-key locks to reduce phantoms for locking reads.

Developer takeaway: Default RR is powerful but surprises teams who expect PostgreSQL-like behaviors everywhere—locking reads (SELECT ... FOR UPDATE) still interact with gap locks depending on predicates and indexes.

Read View

A Read View determines which transaction IDs are visible for a snapshot read. MVCC avoids most reader/writer blocking, but does not remove all serialization concerns: writes still lock rows, and DDL can still disrupt workloads.

Phantom reads and next-key locks

A phantom is when the same query twice returns a different set of rows because another transaction inserted matching rows. InnoDB reduces many phantom scenarios for locking reads using next-key locks (a record lock plus a gap lock on a range).

Operational symptom: Under-contended “range” updates without a proper index, locks can escalate to large ranges and stall traffic.

Purge lag and “history list length”

Old row versions must be purged. If transactions stay open a long time (or replicas lag in ways that retain needed versions), purge backlog grows. Symptoms include bloat, slow reads, and disk growth in undo tablespace.

Mitigation: keep transactions short; avoid SELECT * across huge ranges inside transactions; monitor history length and long-running queries.


4. Query optimizer

The optimizer chooses join order, index usage, access methods, and join algorithms (historically nested-loop oriented; hash joins appear for certain queries in MySQL 8).

Statistics and histograms

The optimizer relies on table statistics (and histograms in MySQL 8 for better cardinality estimates on skewed columns). Stale stats yield bad plans: index not used, wrong join order, or expensive nested loops.

Practical actions:

  • Run ANALYZE TABLE after large bulk loads
  • Use multi-column indexes that match real filter + sort patterns
  • Validate with EXPLAIN (and EXPLAIN ANALYZE where available) on real parameters

Sargable predicates

Functions on indexed columns often prevent index use:

-- Often cannot use an index on created_at
SELECT * FROM users WHERE YEAR(created_at) = 2026;

-- Prefer a range predicate
SELECT * FROM users
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';

EXPLAIN in practice

EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE email = '[email protected]';

Read type, key, rows, Extra. Huge rows estimates and Using filesort / Using temporary on big tables are red flags.

Join discipline

-- Avoid selecting wide rows unnecessarily
SELECT u.id, u.name, p.title
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.is_active = 1;

Optimizer hints (use sparingly)

MySQL supports optimizer hints for edge cases where stats are wrong and you have proven a stable plan—hints can ossify queries across versions, so treat them as temporary surgery, not default style.


5. Indexing strategy

Single-column and composite indexes

CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_user_created ON users(user_id, created_at);
CREATE UNIQUE INDEX idx_email_unique ON users(email);
SHOW INDEX FROM users;
DROP INDEX idx_email ON users;

Composite index column order should match equality filters first, then range, then sort—verify with EXPLAIN.

Full-text indexes

CREATE FULLTEXT INDEX idx_content ON articles(title, content);

SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('mysql tutorial' IN NATURAL LANGUAGE MODE);

SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+mysql -oracle' IN BOOLEAN MODE);

6. Partitioning

Partitioning helps pruning and operations at very large sizes; it is not a replacement for correct indexing.

Range partitioning

CREATE TABLE orders (
    id INT NOT NULL,
    user_id INT NOT NULL,
    amount DECIMAL(10, 2),
    created_at DATE NOT NULL
)
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p2026 VALUES LESS THAN (2027),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

List partitioning

CREATE TABLE users (
    id INT NOT NULL,
    name VARCHAR(100),
    country VARCHAR(2)
)
PARTITION BY LIST COLUMNS(country) (
    PARTITION p_us VALUES IN ('US'),
    PARTITION p_kr VALUES IN ('KR'),
    PARTITION p_jp VALUES IN ('JP'),
    PARTITION p_others VALUES IN (DEFAULT)
);

7. Replication mechanisms

Replication ships changes from a source (primary) to replicas using the binary log.

Binary log formats

  • STATEMENT: log SQL statements; compact but risky for non-deterministic statements
  • ROW: log row images; larger log volume, safer for many workloads (common default in modern setups)
  • MIXED: hybrid policy

Why row-based matters: Triggers fewer surprises with functions, but increases binlog size and IO.

GTID

Global Transaction Identifiers (GTID) simplify failover: each transaction has a unique ID, making source promotion and replica positioning less error-prone than file/position alone.

Asynchronous replication and lag

Most MySQL replication is asynchronous: a replica can fall behind under load. Applications reading replicas must decide whether stale reads are acceptable.

Semi-synchronous replication

Semi-sync waits for at least one replica to acknowledge receipt before commit returns—reducing data loss risk at the cost of latency.

Parallel appliers

Modern replicas can apply transactions in parallel (when dependencies allow), reducing apply lag—especially important for high write primaries.

Group Replication / InnoDB Cluster (brief)

For automatic membership and stronger HA, MySQL offers Group Replication and InnoDB Cluster. These add operational complexity but reduce manual failover toil—evaluate with your SLOs.

Replication setup (classic)

Primary configuration:

[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_do_db = mydb

Replica configuration:

[mysqld]
server-id = 2
relay-log = /var/log/mysql/mysql-relay-bin

SQL (older syntax shown; MySQL 8.0+ prefers CHANGE REPLICATION SOURCE TO):

CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
SHOW BINARY LOG STATUS;

CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='primary-host',
  SOURCE_USER='repl',
  SOURCE_PASSWORD='password',
  SOURCE_LOG_FILE='mysql-bin.000001',
  SOURCE_LOG_POS=157;
START REPLICA;
SHOW REPLICA STATUS\G

8. Transactions (ACID in practice)

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- or
ROLLBACK;

Isolation inspection:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@transaction_isolation;

9. Production MySQL patterns

Connection pooling

Open a new connection per HTTP request at scale will exhaust MySQL. Use pools in the app server or middleware such as ProxySQL, RDS Proxy, or connection poolers appropriate to your platform.

Read scaling with replicas

Route read-mostly queries to replicas, but handle replication lag: user-after-write flows may need primary read or consistency tokens—don’t pretend replicas are always fresh.

Backups: logical vs physical

  • mysqldump: logical, portable, slow for huge datasets; good for small/medium or selective restores
  • Physical hot backups (e.g., Percona XtraBackup): fast for large datasets; more moving parts

Always test restore regularly; an untested backup is a rumor.

Monitoring that actually matters

Watch InnoDB buffer pool hit rate, redo log waits, history length, locks, replication lag, slow queries, and disk saturation. Alert on sustained lag, growing locks, and failing replicas.

Configuration sketch

[mysqld]
innodb_buffer_pool_size = 8G
max_connections = 500
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5

Note: Query cache existed in older versions but was removed in MySQL 8.0—do not enable it on 8.0+.

Slow query analysis

mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

10. Backups

mysqldump -u root -p --all-databases > backup.sql
mysqldump -u root -p mydb > mydb.sql
mysql -u root -p mydb < mydb.sql
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
mysqldump -u root -p"${MYSQL_PASSWORD}" mydb > /backups/mydb_${DATE}.sql
find /backups -name "*.sql" -mtime +7 -delete

Summary and checklist

Key takeaways

  • InnoDB is page-based, WAL-backed, clustered-on-PK storage; memory sizing and write durability knobs matter.
  • MVCC enables snapshot reads, but locks, long transactions, and purge still dominate real incidents.
  • The optimizer needs accurate stats and sargable SQL; verify with EXPLAIN.
  • Replication is powerful but introduces lag and consistency semantics; choose ROW/GTID thoughtfully.
  • Production success is pooling, monitoring, backups, and disciplined schema/query design—not “one magic config”.

Production checklist

  • Size InnoDB buffer pool and validate with real traffic
  • Keep transactions short; hunt long-running queries
  • Maintain statistics after bulk changes
  • Use replicas with explicit staleness rules
  • Automate backups and test restores
  • Monitor replication lag, locks, and InnoDB metrics

  • PostgreSQL performance and comparison posts on this blog
  • Dedicated EXPLAIN and slow-query guides (mysql-query-optimization-guide-en)

Keywords covered in this post

MySQL, InnoDB, MVCC, Isolation, Binary Log, Replication, GTID, Query Optimizer, Indexing, Performance