Server Layers and Storage Engines
MySQL is split into three logical layers:
- Connection & Authentication – handles client hand-shake, privilege checks, SSL, thread pooling.
- SQL Layer – parses, optimizes, rewrites and executes statements. All built-in functions, views, triggers, stored procedures live here. The query cache was removed in 8.0.
- Storage Engine Layer – pluggable engines (InnoDB, MyISAM, RocksDB, …) that store and retrieve rows. Engines do not parse SQL; they only respond to low-level requests such as "fetch row with PK = 42".
Locking Granularity
| Lock Level | Cost | Deadlock Possible | Concurrency |
|---|---|---|---|
| Table | very low | no | low |
| Page | medium | yes | medium |
| Row | high | yes | high |
InnoDB defaults to row-level locking and supports:
- Record Lock – locks the exact index record.
- Gap Lock – locks the gap between index records to prevent phantom inserts.
- Next-Key Lock – record + gap; used for range scans under REPEATABLE READ.
Transaction & Logging
ACID is enforced via:
- Redo Log (InnoDB) – physical log that guarantees durability after crash.
- Undo Log (InnoDB) – stores before-images for rollback and MVCC snapshots.
- Binary Log (MySQL Server) – logical log for replication and point-in-time recovery.
MVCC under REPEATABLE READ
Each row has two hidden columns: DB_TRX_ID (creation txn) and DB_ROLL_PTR (pointer to undo). A snapshot is created at the first consistent read; latter reads use the same snapshot, eliminating read locks for repeatable read isolation.
B+Tree Indexes
InnoDB uses a clustered B+Tree for the primary key. Secondary indexes store the primary-key value as the logical pointer.
- All data lives in the leaf nodes of the clustered index.
- Secondary index leaves contain (index-columns, PK) pairs; accessing the row requires a second lookup ("double read").
- Covering indexes avoid the second lookup by including all requested columns in the secondary index.
Index Design Tips
- Favor monotonically increasing keys (e.g.,
BIGINT AUTO_INCREMENT) to reduce page splits. - Avoid random UUIDs as primary key; inserts become scattered and cause fragmentation.
- Composite indexes obey left-prefix matching; put the most sleective column first.
- Cardinality shown by
SHOW INDEXhelps the optimizer choose the correct index.
Query Life Cycle
- Client connects via TCP / Unix socket.
- Parser checks syntax and builds a parse tree.
- Preprocessor resolves names, checks privileges.
- Optimizer chooses join order, indexes, access methods.
- Executor opens tables, applies WHERE, GROUP BY, HAVING, ORDER BY, LIMIT.
- Rows are returned to the cliant.
Execution Order in a Single SELECT
FROM & JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Join Types
INNER JOIN -- intersection
LEFT JOIN -- left + matched right
RIGHT JOIN -- right + matched left
FULL OUTER JOIN -- MySQL emulates with UNION of LEFT & RIGHT
Typical Slow-Query Fixes
- Check
EXPLAIN FORMAT=JSONfortype=ALL(full scan). - Ensure predicates are sargable (no
WHERE YEAR(col) = 2023). - Add or extend composite indexes to match filter order.
- Use covering indexes to eliminate "Using where; Using filesort".
- Rewrite OR-chained predicates as UNION ALL.
- Avoid
OFFSET Nfor large N; use "seek method" (keyset pagination) instead.
Deadlock Example
-- session 1
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- session 2
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 2;
-- session 1
UPDATE account SET balance = balance + 100 WHERE id = 2; -- waits
-- session 2
UPDATE account SET balance = balance + 100 WHERE id = 1; -- deadlock detected
MyBatis Quick Notes
#{}– prepared statement parameter, SQL-injection safe.${}– literal substitution, useful for dynamic column/table names.- First-level cache is per-SqlSession; second-level cache is global and pluggable (Redis, Ehcache).
Common SQL Recipes
-- Pagination (keyset style, faster than OFFSET)
SELECT * FROM posts
WHERE (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- Time diff
SELECT TIMESTAMPDIFF(SECOND, start_ts, end_ts) AS duration_sec
FROM events;
-- Conditional aggregation
SELECT
SUM(CASE WHEN status = 'PAID' THEN amount ELSE 0 END) AS paid_amount,
SUM(CASE WHEN status = 'REFUND' THEN amount ELSE 0 END) AS refund_amount
FROM orders;
-- Distinct count
SELECT COUNT(DISTINCT user_id) FROM logins WHERE login_date = CURDATE();