<?xml version="1.0" encoding="utf-8"?>
<ul>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本附录按生产场景组织常用命令和检查项。命令以 MySQL 8.0 为主线，部分 5.7 兼容命令会单独标注。1. 连接与客户端mysql -h 127.0.0.1 -P 3306 -u root -pmysql -h 127.0.0.1 -u root -p \      --database=shop --compressmysqldump -h 127.0.0.1 -u backup_user -p \  --single-transaction shop &gt; shop.sqlmysqlbinlog --no-defaults \  --start-position=157 --stop-position=1000 \  mysql-bin.000001 &gt; events.sql常用客户端命令：\h\sSHOW DATABASES;USE shop;SHOW TABLES;SHOW CREATE TABLE orders\GSELECT VERSION(), CURRENT_USER(), DATABASE();2. 服务与参数SELECT VERSION();SHOW VARIABLES LIKE 'datadir';SHOW VARIABLES LIKE 'port';SHOW VARIABLES LIKE 'socket';SHOW VARIABLES LIKE 'character_set_server';SHOW VARIABLES LIKE 'collation_server';SHOW VARIABLES LIKE 'transaction_isolation';SHOW VARIABLES LIKE 'innodb_buffer_pool_size';SHOW VARIABLES LIKE 'max_connections';SHOW VARIABLES LIKE 'log_bin';SHOW VARIABLES LIKE 'gtid_mode';修改会话参数：SET SESSION transaction_isolation = 'READ-COMMITTED';SET SESSION lock_wait_timeout = 10;SET SESSION max_execution_time = 5000;修改全局参数必须评估持久化、重启和回滚：SET GLOBAL max_connections = 2000;SET PERSIST max_connections = 2000;3. 日常巡检会话与连接：SHOW FULL PROCESSLIST;SHOW STATUS LIKE 'Threads_connected';SHOW STATUS LIKE 'Threads_running';SHOW STATUS LIKE 'Max_used_connections';SHOW STATUS LIKE 'Aborted_connects';事务与锁：SELECT trx_id, trx_state, trx_started,       trx_rows_locked, trx_rows_modified,       trx_mysql_thread_idFROM information_schema.innodb_trxORDER BY trx_started;SELECT * FROM sys.innodb_lock_waits;SELECT * FROM performance_schema.data_locks;SHOW ENGINE INNODB STATUS\G容量：SELECT  table_schema,  ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mbFROM information_schema.tablesGROUP BY table_schemaORDER BY size_mb DESC;大表：SELECT  table_schema,  table_name,  engine,  table_rows,  ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mbFROM information_schema.tablesORDER BY data_length + index_length DESCLIMIT 20;4. 性能诊断状态指标：SHOW GLOBAL STATUS LIKE 'Questions';SHOW GLOBAL STATUS LIKE 'Slow_queries';SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';SHOW GLOBAL STATUS LIKE 'Created_tmp%';SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';Buffer Pool 命中率计算：read_ahead = Innodb_buffer_pool_read_aheadlogical_read = Innodb_buffer_pool_read_requestsphysical_read = Innodb_buffer_pool_reads命中率 ≈ 1 - physical_read / logical_read执行计划：EXPLAINSELECT order_no, amountFROM ordersWHERE user_id = 10001  AND status = 'PAID';EXPLAIN ANALYZESELECT order_no, amountFROM ordersWHERE user_id = 10001  AND status = 'PAID';索引使用：SELECT object_schema, object_name, index_name, count_starFROM performance_schema.table_io_waits_summary_by_index_usageWHERE object_schema = 'shop'ORDER BY count_star DESC;表统计：ANALYZE TABLE shop.orders;SHOW TABLE STATUS LIKE 'orders'\G5. 事务与隔离查看隔离级别：SELECT @@transaction_isolation;SHOW VARIABLES LIKE 'transaction_isolation';事务控制：BEGIN;UPDATE accounts SET balance = balance - 100 WHERE id = 1;UPDATE accounts SET balance = balance + 100 WHERE id = 2;COMMIT;ROLLBACK;保存点：SAVEPOINT after_order;ROLLBACK TO SAVEPOINT after_order;RELEASE SAVEPOINT after_order;当前读：SELECT * FROM orders WHERE id = 1 FOR UPDATE;SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;MySQL 8.0 也可以使用：SELECT * FROM orders WHERE id = 1 FOR SHARE;6. 复制与高可用主库：SHOW MASTER STATUS;SHOW BINARY LOGS;SHOW PROCESSLIST;较新版本中推荐使用更清晰的状态命令，具体可用性以当前版本文档为准：SHOW BINARY LOG STATUS;从库：SHOW REPLICA STATUS\GSELECT * FROM performance_schema.replication_applier_status_by_worker;SELECT * FROM performance_schema.replication_connection_status;GTID：SHOW VARIABLES LIKE 'gtid_mode';SHOW VARIABLES LIKE 'enforce_gtid_consistency';SELECT @@GLOBAL.gtid_executed;复制错误处理前必须确认业务和数据一致性，不能盲目跳过：STOP REPLICA;SET GLOBAL sql_slave_skip_counter = 1;START REPLICA;GTID 空事务方式跳过：STOP REPLICA;SET GTID_NEXT = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1';BEGIN;COMMIT;SET GTID_NEXT = 'AUTOMATIC';START REPLICA;7. 备份与恢复逻辑备份：mysqldump --single-transaction --set-gtid-purged=off \  --triggers --routines --events \  -h 127.0.0.1 -u backup_user -p shop &gt; shop.sql恢复：mysql -h 127.0.0.1 -u root -p shop &lt; shop.sql查看 Binlog：mysqlbinlog --no-defaults mysql-bin.000001mysqlbinlog --no-defaults \  --start-datetime='2026-08-25 10:00:00' \  --stop-datetime='2026-08-25 11:00:00' \  mysql-bin.000001 &gt; restore.sql清理 Binlog：PURGE BINARY LOGS BEFORE '2026-08-18 00:00:00';清理前必须确认：  所有从库已消费；  备份链路可用；  时间点恢复窗口允许；  磁盘压力不是因为复制中断堆积。8. 用户与权限账号：SELECT user, host, plugin, account_lockedFROM mysql.userORDER BY user, host;CREATE USER 'shop_app'@'10.0.%'IDENTIFIED BY 'StrongPassword_2026';GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*TO 'shop_app'@'10.0.%';SHOW GRANTS FOR 'shop_app'@'10.0.%';角色：CREATE ROLE shop_readonly;GRANT SELECT ON shop.* TO shop_readonly;GRANT shop_readonly TO 'report_user'@'10.20.%';SET DEFAULT ROLE shop_readonlyTO 'report_user'@'10.20.%';回收与锁定：REVOKE DROP ON shop.* FROM 'shop_app'@'10.0.%';ALTER USER 'shop_app'@'10.0.%' ACCOUNT LOCK;DROP USER 'shop_app'@'10.0.%';强制 TLS：ALTER USER 'shop_app'@'10.0.%' REQUIRE SSL;ALTER USER 'shop_app'@'10.0.%' REQUIRE X509;9. DDL 与在线变更Instant 加列：ALTER TABLE shop.orders  ADD COLUMN channel VARCHAR(16) NOT NULL DEFAULT 'APP',  ALGORITHM=INSTANT;在线加索引：ALTER TABLE shop.orders  ADD INDEX idx_merchant_created (merchant_id, created_at),  ALGORITHM=INPLACE,  LOCK=NONE;隐藏索引：ALTER TABLE shop.orders ALTER INDEX idx_old INVISIBLE;ALTER TABLE shop.orders ALTER INDEX idx_old VISIBLE;gh-ost：gh-ost \  --host=127.0.0.1 --port=3306 --user=change_user \  --database=shop --table=orders \  --alter="ADD INDEX idx_status_created(status, created_at)" \  --chunk-size=1000 \  --max-load=Threads_running=50 \  --critical-load=Threads_running=200 \  --cut-over=default \  --executept-osc：pt-online-schema-change \  --alter "ADD INDEX idx_status_created(status, created_at)" \  D=shop,t=orders \  --host=127.0.0.1 --port=3306 \  --user=change_user --ask-pass \  --chunk-size=1000 \  --execute变更前检查：1. 表大小和增长2. 长事务与 MDL3. 主从延迟4. 磁盘与 IO 余量5. 支持的 DDL 算法6. 测试环境演练结果7. 停止条件8. 回滚方案10. 常见故障处理10.1 连不上SHOW STATUS LIKE 'Threads_connected';SHOW STATUS LIKE 'Aborted_connects';SHOW FULL PROCESSLIST;检查：  MySQL 进程和端口；  安全组与防火墙；  账号 host；  密码和认证插件；  max_connections;  应用连接池；  被锁定或 blocked 的账号。10.2 CPU 高SELECT * FROM sys.sessionORDER BY statement_latency DESCLIMIT 10;处理：KILL QUERY &lt;processlist_id&gt;;KILL &lt;processlist_id&gt;;杀连接前确认事务和操作人。止血还要考虑限流、回滚发布、停止批任务和切流。10.3 锁等待SELECT * FROM sys.innodb_lock_waits;SELECT * FROM performance_schema.data_locks;SELECT * FROM information_schema.innodb_trx;判断阻塞源头、事务年龄、锁对象和等待 SQL，再决定是否终止事务。10.4 主从延迟SHOW REPLICA STATUS\GSELECT * FROM performance_schema.replication_applier_status_by_worker;检查大事务、Worker 状态、从库负载、表索引、网络和磁盘。10.5 磁盘满SHOW VARIABLES LIKE 'datadir';SHOW VARIABLES LIKE 'log_bin_basename';SHOW BINARY LOGS;常见来源：Binlog、临时表、Relay Log、备份残留、错误日志、Undo 膨胀和大表空间。不要直接删除 MySQL 正在管理的文件。11. 推荐参数起点以下不是万能配置，必须结合机器规格、工作集和业务峰值调整：[mysqld]character_set_server = utf8mb4collation_server = utf8mb4_0900_ai_cidefault_storage_engine = InnoDBmax_connections = 2000thread_cache_size = 100innodb_buffer_pool_size = 40Ginnodb_log_file_size = 2Ginnodb_flush_log_at_trx_commit = 1sync_binlog = 1transaction_isolation = READ-COMMITTEDslow_query_log = ONlong_query_time = 0.5log_queries_not_using_indexes = OFFbinlog_format = ROWbinlog_row_image = FULLgtid_mode = ONenforce_gtid_consistency = ON强持久性组合：innodb_flush_log_at_trx_commit = 1sync_binlog = 112. 上线检查清单表设计：[ ] 显式主键[ ] 金额使用 DECIMAL[ ] 时间类型和精度明确[ ] 字符集和排序规则统一[ ] 唯一约束覆盖业务规则[ ] 高频查询有索引设计[ ] 大字段单独评估[ ] 表和字段有注释SQL：[ ] EXPLAIN 已评审[ ] 无 SELECT * 依赖[ ] UPDATE / DELETE 有 WHERE[ ] 大事务已拆分[ ] 深分页有方案[ ] 超时和重试明确[ ] 无隐式类型转换发布：[ ] 变更可回滚[ ] 备份点可用[ ] 低峰执行[ ] 停止条件明确[ ] 监控看板就绪[ ] 通知相关团队治理：[ ] 慢查询采集开启[ ] 主从延迟告警[ ] 连接水位告警[ ] 磁盘容量预测[ ] 权限最小化[ ] 恢复演练通过13. 版本差异速记            主题      MySQL 5.7      MySQL 8.0                  默认认证插件      mysql_native_password      caching_sha2_password              字符集      latin1      utf8mb4              数据字典      FRM 文件      InnoDB 数据字典              DDL      部分 Online      INSTANT / INPLACE 增强              角色管理      不支持      支持              CTE / 窗口函数      不支持      支持              EXPLAIN ANALYZE      不支持      支持              隐藏索引      不支持      支持              复制术语      MASTER / SLAVE      SOURCE / REPLICA 逐步推广      14. 学习地图第一阶段：SQL、类型、建表、基础查询第二阶段：EXPLAIN、B+Tree、索引、JOIN、子查询第三阶段：事务、MVCC、锁、Redo、Undo、Binlog第四阶段：Buffer Pool、复制、高可用、备份恢复第五阶段：分库分表、容量、在线变更、监控治理第六阶段：源码阅读、内核优化、大规模架构</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。读完前面的章节，只说明你已经建立了 MySQL 的完整知识地图。要成为真正能处理复杂问题的人，还需要把这些知识放进真实系统里反复锤炼：看得出问题，讲得清取舍，扛得住故障，改得动架构，也能带团队建立规范。本章给出一条从入门到专家的进阶路线、能力矩阵、项目训练、源码阅读方法和职业成长建议。35.1 五个成长阶段Level 1 使用者  会连接、会 CRUD、会建表      |      vLevel 2 优化者  会执行计划、索引设计、慢查询治理      |      vLevel 3 原理掌握者  懂事务、MVCC、锁、日志、Buffer Pool      |      vLevel 4 生产治理者  会高可用、备份、容量、监控、在线变更      |      vLevel 5 架构与内核专家  能设计大规模数据架构，能阅读源码定位疑难问题每个阶段的核心问题不同：            阶段      核心问题                  使用者      这条 SQL 怎么写？表应该怎么设计？              优化者      为什么慢？执行计划说明了什么？              原理掌握者      MySQL 为什么这样执行？崩溃后如何恢复？              生产治理者      如何在故障、变更和增长下保持稳定？              架构与内核专家      系统边界在哪里？能否修改或绕过内核限制？      35.2 能力矩阵35.2.1 SQL 与建模专家水平需要：  熟练使用 CTE、窗口函数、分层查询和复杂聚合；  能平衡范式与反范式；  理解主键、唯一键、外键、CHECK 和默认值；  掌握状态机、软删除、多租户和审计字段设计；  能评审金额、时间、JSON、大字段和字符集设计。推荐练习：1. 设计电商订单系统2. 设计账户余额和流水3. 设计 SaaS 多租户模型4. 设计活动库存和秒杀5. 设计审计日志和归档每个模型都问自己：查询路径是什么？写入热点在哪里？数据保留多久？如何归档？如何对账？未来如何分片？35.2.2 查询优化专家不是背索引规则，而是能建立成本视角：逻辑读取多少页？扫描多少行？回表多少次？是否排序？是否创建临时表？网络传输多少结果？锁住多少范围？训练方法：  每天分析一条真实慢 SQL；  先预测执行计划，再验证；  记录改写前后的扫描行数和耗时；  总结适用条件；  定期回看历史案例。35.2.3 事务与并发需要掌握：  隔离级别和异常现象；  快照读与当前读；  ReadView 可见性；  行锁、间隙锁、Next-Key Lock；  死锁分析和重试；  热点行拆分；  业务事务边界。训练题：1. 两个会话同时转账，观察锁等待2. RR 下演示幻读场景3. 无索引条件更新导致锁范围扩大4. 唯一键并发插入导致死锁5. 外部调用放在事务中导致长事务35.2.4 高可用与数据安全必须能回答：            问题      关键点                  RPO 是多少      半同步策略、MGR 多数派、备份和 Binlog 保留              RTO 是多少      探测、切换、客户端重连、预热              如何防脑裂      仲裁、fencing、多数派              如何验证数据      校验工具、抽样、对账              如何恢复      全量、增量、Binlog、时间点恢复              如何演练      定期故障注入和恢复演练      专家的标志不是“我们用了 MGR”，而是能说清故障场景下的数据丢失窗口和恢复步骤。35.2.5 容量与架构规模增长会依次考验：单表变大  -&gt; 索引和 DDL 成本上升     -&gt; 读写分离        -&gt; 冷热分离           -&gt; 归档和分析下线              -&gt; 垂直拆分                 -&gt; 水平分片                    -&gt; 单元化 / 多活分片前先评估：  分片键是否稳定；  查询是否能路由；  跨片查询比例；  分布式事务频率；  扩容方式；  数据倾斜；  全局唯一 ID；  运维和监控成本。不要为了技术先进性引入复杂度。能通过索引、归档和读写分离解决的问题，不一定要分库分表。35.3 项目训练项目一：电商订单库目标：训练建模、事务、索引和查询。功能：  用户、商品、购物车、订单、订单项；  支付流水和状态机；  订单列表按用户和商家查询；  库存扣减；  订单取消回补；  历史订单归档。验收：1. 核心 SQL 都有执行计划说明2. 下单事务不会超卖3. 能模拟锁等待和死锁4. 能展示深分页优化5. 能做冷热归档项目二：高可用集群目标：训练复制、切换和恢复。拓扑：主库  |-- 从库 1：常规读  |-- 从库 2：延迟读  +-- 备份实例功能：  搭建 GTID 复制；  配置半同步；  采集主从延迟监控；  执行模拟切换；  恢复误删表；  做数据校验。验收：能写清 RPO / RTO能处理复制中断能完成时间点恢复能解释切换时的连接处理项目三：慢查询治理平台目标：把优化过程产品化。功能：  采集慢日志；  解析 SQL 指纹；  聚合耗时和次数；  存储执行计划；  对比优化前后；  输出周报；  与发布和变更关联。技术要点：pt-query-digestPerformance SchemaEXPLAINSQL 指纹趋势图这个项目能把 MySQL 能力和工程平台能力结合起来，价值高于只背参数。项目四：在线变更演练目标：训练大表治理。任务：  构造一亿行测试表；  演练 INSTANT、INPLACE 和 COPY；  使用 gh-ost 增加索引；  记录磁盘、IO、延迟和耗时；  模拟长事务阻塞 MDL；  制定回滚和停止条件。输出一份真实数据报告，比“了解 Online DDL”更有说服力。35.4 源码阅读路线如果希望进入内核或深度排障阶段，可以按模块阅读 MySQL 源码：第一阶段：搭建与调试  编译 MySQL  运行单元测试  gdb 断点  跟踪一条 SQL第二阶段：Server 层  连接管理  Parser  Resolver  Optimizer  Executor第三阶段：InnoDB  B+Tree  Page 和 Record  Buffer Pool  Lock System  MVCC / ReadView  Transaction  Redo / Undo  Purge第四阶段：复制  Binlog 写入  GTID  Dump Thread  Relay Log  Applier第五阶段：工具  mysqlbinlog  mysqldump  performance_schema  information_schema阅读方法：  从问题出发，不从第一行代码出发；  先读数据结构和注释；  用测试用例定位入口；  gdb 单步验证；  画调用链；  写笔记和最小复现；  关注版本差异。好问题包括：一条 UPDATE 如何加锁？ReadView 什么时候创建？两阶段提交如何恢复？索引页分裂发生在哪里？并行复制如何分配 Worker？35.5 90 天进阶计划第 1 到 14 天：补基础1. 安装 MySQL 8.02. 完成基础 SQL 练习3. 设计订单表4. 理解字符集、类型和约束5. 使用 EXPLAIN 分析 20 条 SQL产出：一套表设计文档和 20 条执行计划笔记。第 15 到 30 天：索引与查询1. 复习 B+Tree2. 做联合索引实验3. 分析回表和覆盖索引4. 优化排序、分页和临时表5. 构造慢查询并修复产出：慢查询优化案例集。第 31 到 50 天：事务与日志1. 演示四个隔离级别2. 分析 MVCC 版本链3. 复习行锁、间隙锁和死锁4. 观察 Redo、Undo、Binlog5. 模拟崩溃恢复产出：事务锁实验报告。第 51 到 70 天：复制与高可用1. 搭建一主两从2. 使用 GTID3. 配置半同步4. 模拟主从延迟和复制中断5. 做备份和时间点恢复产出：高可用方案和恢复演练记录。第 71 到 90 天：生产治理1. 建立监控告警2. 制定容量报表3. 演练 Online DDL4. 使用 gh-ost 或 pt-osc5. 完成一次故障复盘产出：数据库治理手册。35.6 学习资源官方资源：  MySQL Reference Manual；  MySQL Shell；  MySQL Workbench；  MySQL Performance Schema 文档；  MySQL Source Code 文档。推荐书籍：            书      用途                  《高性能 MySQL》      性能、架构和生产实践              《MySQL 技术内幕：InnoDB 存储引擎》      InnoDB 内部机制              《数据库系统概念》      关系模型和事务理论              《数据结构与算法分析》      B+Tree 和复杂度基础              《Site Reliability Engineering》      生产治理和故障管理      常用工具：mysql / mysqlbinlog / mysqldumpmysqlslap / sysbenchpt-query-digestgh-ostpt-online-schema-changePercona ToolkitOrchestrator / MySQL RouterPrometheus + mysqld_exporter + Grafana35.7 方法论35.7.1 问题分类把问题先归类：资源问题：CPU、内存、IO、网络、磁盘并发问题：连接、锁、事务、热点查询问题：索引、执行计划、SQL 写法复制问题：延迟、中断、数据不一致架构问题：容量、拆分、读写、多活变更问题：DDL、参数、发布、数据修复安全问题：权限、注入、审计、备份分类以后，就不会在事故里随机尝试。35.7.2 建立证据链            结论      证据                  查询慢      慢日志、耗时、执行计划、扫描行数              锁等待      data_locks、innodb_trx、innodb_lock_waits              复制延迟      Seconds_Behind_Source、Worker 状态、Binlog 位点              磁盘不足      空间趋势、Binlog 保留、表大小、备份目录              内存不足      OS 内存、Buffer Pool、连接数、错误日志      没有证据链的优化，很容易变成“改了但不知道为什么变好”。35.7.3 沉淀知识库建议维护四类文档：  表设计和索引规范；  常见故障手册；  变更审批模板；  事故复盘库。文档要短、可执行、常更新。写没人看的几百页规范，不如维护十个能自动检查的上线规则。35.8 职业成长后端工程师重点：  表设计；  事务边界；  SQL 质量；  连接池；  数据一致性。差异化：能主动分析慢 SQL，而不是把问题全部交给 DBA。DBA / SRE重点：  高可用；  备份恢复；  容量规划；  监控告警；  权限治理；  故障响应。差异化：用平台和自动化减少重复运维，用演练证明恢复能力。数据工程师重点：  CDC；  数据同步；  数据校验；  大表归档；  MySQL 与 OLAP 系统分工。差异化：理解 Binlog、事务边界和乱序处理。架构师重点：  分库分表；  多活；  数据一致性；  成本；  演进路线；  团队规范。差异化：不追求复杂方案，而能在业务阶段、团队能力和风险之间做取舍。35.9 长期原则  事实优先：先拿监控和日志，再下结论；  最小变更：一次只改一个关键变量；  可回滚：没有回滚方案的变更不要上线；  可观测：无法度量就无法治理；  自动化：把规则放进流程和工具；  面向故障设计：假设磁盘会坏、节点会挂、人会误操作；  成本意识：性能不是免费的；  持续学习：MySQL 小版本行为可能变化；  团队协作：数据库稳定是系统性结果；  尊重数据：先保事实，再谈性能。35.10 最后的建议MySQL 的学习曲线不是一条直线。你会先觉得 SQL 很简单，然后被执行计划、锁和复制打击，再在真实故障里重新理解它们，最后发现所有高级技巧都回到几个基础问题：数据怎么组织、变更如何持久化、并发如何隔离、系统如何恢复。成为大师的关键，不是记住所有参数，而是能在压力下建立正确的判断顺序：先保护数据，再恢复服务，再优化成本，最后沉淀机制。本章小结大师之路由五个阶段组成：会用、会优化、懂原理、会治理、能设计架构和阅读内核。进阶训练应围绕真实项目展开，包括订单系统、高可用集群、慢查询平台和在线变更演练。源码阅读要从问题和调用链出发，逐步进入 Server 层、InnoDB 和复制模块。长期成长依赖方法论和知识沉淀：问题分类、证据链、可回滚变更、自动化治理和持续复盘。思考题  评估你当前处在五个阶段中的哪一层，并列出三个短板。  为团队设计一份 MySQL 上线检查清单。  选择一个源码问题，制定两周的阅读计划。  设计一次主库故障切换演练，包括成功和失败标准。  写下你未来 90 天的 MySQL 学习目标和可验证产出。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。MySQL 面试通常不是考背诵，而是从一道基础题开始不断追问，直到暴露你对索引、事务、日志、复制和故障处理的理解边界。本章把高频问题整理成“问题、核心答案、追问、易错点”的形式，帮助你把前 33 章的知识转成自己的表达。34.1 答题框架回答技术问题建议使用四步：1. 定义：它是什么2. 原理：为什么这样工作3. 场景：什么时候有效，什么时候失效4. 实践：生产中怎么验证和治理例如被问到索引为什么快，不要只回答“B+Tree”。更好的回答是：InnoDB 主键是聚簇 B+Tree，数据按主键顺序存放；查询从根节点到叶子节点，访问次数与树高相关；二级索引叶子保存主键，必要时回表；索引能减少扫描，但写入和维护有代价；生产上要看执行计划、扫描行数、回表和排序成本。这种回答既展示知识，也展示工程判断。34.2 基础与架构问题 1：一条 SELECT 语句的执行流程是什么？客户端连接  -&gt; 连接器认证与权限检查  -&gt; 解析器生成语法树  -&gt; 预处理做语义检查  -&gt; 优化器选择执行计划  -&gt; 执行器调用存储引擎接口  -&gt; InnoDB 读取 Buffer Pool 或磁盘页  -&gt; 返回结果集常见追问：  权限在哪里检查？连接时会做全局权限检查，语句执行前还会按对象检查；  Server 层和存储引擎层怎么分工？Server 层负责解析、优化和执行流程，InnoDB 负责数据、事务、锁、MVCC 和持久化；  MySQL 8.0 为什么移除 Query Cache？命中率不稳定，锁竞争和失效代价高，收益有限。问题 2：InnoDB 和 MyISAM 有什么区别？            维度      InnoDB      MyISAM                  事务      支持      不支持              锁粒度      行级锁，并有表级意向锁等      表锁              外键      支持      不支持              崩溃恢复      Redo / Undo 支持      依赖修复工具              MVCC      支持      不支持              存储组织      主键聚簇 B+Tree      索引和数据分离              现代默认      MySQL 5.5 后默认      不建议新业务使用      加分点：不要说 InnoDB “只有行锁没有表锁”。DDL、AUTO-INC 锁、意向锁和元数据锁仍可能影响整表。问题 3：为什么新表使用 utf8mb4？历史 utf8 实际是最长三字节的 utf8mb3，无法完整保存 Emoji、部分生僻字和补充字符；utf8mb4 才是完整 UTF-8 编码。实践建议：  新表统一 utf8mb4；  显式指定排序规则；  关注排序规则混用导致的隐式转换；  老表迁移前评估索引长度和存储变化。34.3 索引与执行计划问题 4：为什么 InnoDB 选择 B+Tree？B+Tree 的特点：  非叶子节点只保存键和指针；  叶子节点保存数据或主键；  叶子节点形成链表；  等值和范围查询都友好；  树高低，减少磁盘页访问次数。哈希索引适合等值查询，但不适合范围查询、排序和最左前缀匹配。InnoDB 的自适应哈希索引是内部机制，不是用户手工创建的普通哈希索引。问题 5：什么是回表、覆盖索引和最左前缀？示例：CREATE INDEX idx_user_status_createdON orders (user_id, status, created_at);SELECT order_no, amountFROM ordersWHERE user_id = 10001  AND status = 'PAID';回答：  二级索引叶子节点保存 user_id、status、created_at 和主键；  查询还需要的 order_no、amount 不在索引中，需要用主键回表；  如果查询只选择索引列和主键，就是覆盖索引；  联合索引遵循最左前缀原则；  单独按 status 查询无法完整使用该索引。追问：以下条件分别怎么使用索引？WHERE user_id = 1 AND status = 'PAID';WHERE user_id = 1 AND created_at &gt; '2026-08-01';WHERE status = 'PAID' AND created_at &gt; '2026-08-01';WHERE user_id &gt; 100;前三个可能使用索引的不同部分，最后一个只能按 user_id 做范围访问。最终以执行计划和统计信息为准。问题 6：EXPLAIN 最应该看什么？优先看：  type：const、eq_ref、ref、range、index、ALL 等；  key 和 possible_keys；  rows 预估扫描行数；  filtered 过滤比例；  Extra 是否出现 Using filesort、Using temporary、Using index；  id 与 select_type，判断多表和子查询顺序。MySQL 8.0 可以使用：EXPLAIN ANALYZESELECT order_no, amountFROM ordersWHERE user_id = 10001  AND status = 'PAID';EXPLAIN ANALYZE 会真实执行并展示每步耗时和行数，适合验证优化效果，但不要在高峰对大查询随意执行。问题 7：索引失效的常见原因？常见原因：  对索引列做函数或表达式计算；  隐式类型转换；  隐式字符集或排序规则转换；  前导模糊匹配；  不满足联合索引最左前缀；  OR 连接了无索引条件；  优化器认为全表扫描代价更低；  统计信息过期；  分区裁剪失效；  索引被隐藏。示例改写：-- 可能无法使用 created_at 索引WHERE DATE(created_at) = '2026-08-01';-- 更容易使用范围索引WHERE created_at &gt;= '2026-08-01 00:00:00'  AND created_at &lt;  '2026-08-02 00:00:00';不要把规则绝对化。不同版本和写法下优化器行为可能不同，最终必须看执行计划。34.4 事务、MVCC 与锁问题 8：ACID 分别靠什么实现？            特性      含义      主要机制                  Atomicity      全部成功或全部回滚      Undo Log              Consistency      数据满足约束和业务规则      事务、约束、锁、应用逻辑              Isolation      并发事务互不异常干扰      MVCC、锁、隔离级别              Durability      提交后不丢      Redo Log、Binlog、Doublewrite      不要说一致性只靠数据库。数据库能保证主键、唯一、外键和 CHECK 等约束，业务不变量仍需要正确的事务边界。问题 9：四个隔离级别分别解决什么问题？            隔离级别      脏读      不可重复读      幻读                  READ UNCOMMITTED      可能      可能      可能              READ COMMITTED      避免      可能      可能              REPEATABLE READ      避免      避免      InnoDB 在当前读下通过 Next-Key Lock 避免大部分幻读              SERIALIZABLE      避免      避免      避免      InnoDB 默认是 REPEATABLE READ。很多其他数据库默认 READ COMMITTED，不要混淆。追问为什么互联网业务常用 RC：RC 通常锁冲突更少，更容易支撑高并发；代价是同一事务内多次快照读可能看到不同版本，需要业务接受这个语义。问题 10：MVCC 怎么工作？核心要素：  行隐藏字段：DB_TRX_ID、DB_ROLL_PTR；  Undo Log 版本链；  ReadView；  活跃事务列表；  ReadView 创建时机。可见性判断可以概括为：版本事务 ID 小于 ReadView 低水位：可见版本事务 ID 大于高水位：不可见版本事务仍在活跃列表中：不可见版本事务不在活跃列表且已提交：可见RR 在事务第一次快照读时创建 ReadView 并复用；RC 每次快照读都创建新的 ReadView。问题 11：当前读和快照读的区别？快照读：SELECT * FROM orders WHERE id = 1;当前读：SELECT * FROM orders WHERE id = 1 FOR UPDATE;SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;UPDATE orders SET status = 'PAID' WHERE id = 1;DELETE FROM orders WHERE id = 1;INSERT INTO orders (...);快照读走 MVCC；当前读读取最新版本并加锁。讨论 RR 下的幻读，必须先区分这两种读。问题 12：行锁、间隙锁和 Next-Key Lock 是什么？Record Lock：锁索引记录Gap Lock：锁索引记录之间的间隙，不包含记录本身Next-Key Lock：Record Lock + Gap LockInsert Intention Lock：插入前表达插入意图示例：SELECT * FROM usersWHERE age BETWEEN 20 AND 30FOR UPDATE;RR 下可能锁住满足条件的记录和相关间隙，阻止其他事务插入导致幻读的新行。如果 age 没有索引，锁范围可能扩大，这是必须强调的易错点。问题 13：死锁怎么排查和预防？排查命令：SHOW ENGINE INNODB STATUS\GSELECT * FROM sys.innodb_lock_waits;SELECT * FROM performance_schema.data_locks;要看四件事：  两个事务分别执行了什么；  持有和等待的锁；  加锁顺序；  是否存在无索引条件、唯一冲突、间隙锁或大事务。预防：  缩短事务；  按相同顺序访问表和行；  为过滤条件建立合适索引；  避免在事务中等待外部调用；  减少热点行竞争；  使用合理重试策略；  必要时评估隔离级别和锁范围。34.5 日志、复制与高可用问题 14：Redo、Undo、Binlog 的区别？            日志      层级      作用      特点                  Redo Log      InnoDB      崩溃恢复，保证持久性      物理日志，顺序写              Undo Log      InnoDB      回滚和 MVCC 版本链      逻辑反操作              Binlog      Server      复制、恢复、CDC      逻辑日志，追加写      两阶段提交简化流程：1. InnoDB 写 Redo Log，进入 prepare2. 写 Binlog3. InnoDB 将事务置为 commit如果 Redo 已 prepare 而 Binlog 未成功，崩溃恢复时回滚；如果 Binlog 完整，则提交。这样避免主库、Binlog 和从库数据不一致。问题 15：主从复制流程是什么？主库事务提交  -&gt; 写 Binlog  -&gt; Dump Thread 发送 Binlog  -&gt; 从库 IO Thread 接收  -&gt; 写 Relay Log  -&gt; SQL Thread / Worker 回放  -&gt; 从库数据更新延迟常见原因：  主库大事务；  从库回放瓶颈；  从库硬件或磁盘差；  从库承担重查询；  表缺少主键或合适索引；  网络带宽不足；  DDL 回放慢。处理：  拆小事务；  开并行复制；  下线重查询；  补索引；  临时读主要谨慎并限流；  无法收敛时重建或切换。问题 16：异步、半同步和 MGR 怎么选？            方案      一致性      延迟      复杂度                  异步复制      可能丢事务      低      低              半同步      至少一个从库确认      中      中              MGR      多数派一致      中高      高      加分点：不要只说方案名称，要继续分析 RPO、RTO、客户端重试、切换工具、脑裂保护和数据校验。34.6 性能与生产治理问题 17：慢 SQL 怎么优化？标准流程：1. 确认 SQL、参数、耗时、扫描行数2. 查看执行计划3. 判断访问类型、索引、回表、排序、临时表4. 检查表结构和统计信息5. 改写 SQL 或调整索引6. 用同量级数据验证7. 观察线上耗时和资源8. 固化慢查询治理流程不要只回答“加索引”。索引解决扫描问题，但不能解决大结果集传输、锁等待、网络往返、N+1 查询和不合理数据模型。问题 18：Buffer Pool 命中率低怎么办？先看：SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';再判断：  工作集是否大于内存；  是否有大扫描污染缓存；  是否刚重启导致冷缓存；  SQL 是否缺索引导致随机读；  实例规格是否不足；  其他进程是否占用内存。处理：  优化扫描 SQL；  报表切到从库或分析库；  扩容 Buffer Pool；  拆分冷热数据；  滚动重启后预热；  继续评估内存与连接模型。问题 19：深分页怎么优化？慢查询：SELECT id, order_noFROM ordersORDER BY idLIMIT 1000000, 20;游标分页：SELECT id, order_noFROM ordersWHERE id &gt; 1000000ORDER BY idLIMIT 20;延迟关联：SELECT o.id, o.order_noFROM orders oJOIN (  SELECT id  FROM orders  WHERE user_id = 10001  ORDER BY created_at DESC, id DESC  LIMIT 1000000, 20) AS t ON t.id = o.id;连续列表更适合游标，后台跳页可以限制范围或使用缓存。问题 20：高并发库存如何防超卖？先用缓存或内存做预扣限流，MySQL 保存事实库存，并用条件更新兜底：UPDATE sku_stockSET available = available - 1WHERE sku_id = 1001  AND available &gt;= 1;应用检查影响行数：int affected = statement.executeUpdate();if (affected == 0) {    throw new SoldOutException();}不要先 SELECT 再无条件 UPDATE，并发下会覆盖彼此结果。热点商品还可以拆行、合并写或异步化，但必须保留对账补偿。问题 21：大表加字段要注意什么？回答层次：  先确认 MySQL 版本和 DDL 类型；  判断能否 INSTANT；  INPLACE 要评估磁盘、IO、耗时和锁；  大表考虑 gh-ost 或 pt-osc；  执行前处理长事务，设置 lock_wait_timeout；  监控 Threads_running、主从延迟和磁盘；  准备停止条件和回滚方案；  变更后验证表结构和业务 SQL。易错点：Online DDL 不等于无锁、无 IO、无复制延迟。34.7 场景题场景 1：核心接口 P99 从 50ms 变成 2s排查：1. 看应用错误率和调用链2. 确认是否只有数据库依赖慢3. 查看 Threads_running、锁等待、慢日志4. 找到 Top SQL 和执行计划变化5. 检查近期发布、DDL、索引删除、参数修改6. 检查主从延迟和读从库策略7. 检查磁盘、CPU、网络和云资源限流止血：  限流；  终止异常 SQL；  回滚发布；  切换实例；  暂停批任务；  恢复索引。场景 2：主从延迟 10 分钟先定位：SHOW REPLICA STATUS\GSELECT * FROM performance_schema.replication_applier_status_by_worker;判断是否为单个大事务、Worker 不均、从库 SQL 慢、IO 高、Binlog 拉取阻塞或磁盘性能下降。处理上可以停主库大批任务、拆分事务、增加并行 Worker、下线从库重查询、补索引，无法收敛时重建或切换。场景 3：误执行 UPDATE 少了 WHERE正确流程：1. 停止相关写入，保留现场2. 记录误操作时间和 SQL3. 不在原表直接乱改4. 恢复最近备份到临时实例5. 重放 Binlog 到误操作前6. 比对受影响数据7. 生成修复 SQL 和回滚 SQL8. 双人确认后执行9. 验证并复盘面试重点不是背命令，而是体现“先止血、可回滚、可验证”的意识。34.8 高频易错点  REPEATABLE READ 能避免所有幻读：不准确，要区分快照读和当前读；  InnoDB 只有行锁：不准确，还有间隙锁、意向锁、MDL 和 DDL 场景；  加索引一定变快：不一定，要看选择性、写放大和回表；  EXPLAIN 是最终事实：它是估算，必要时用 EXPLAIN ANALYZE 和真实耗时验证；  半同步绝不丢数据：要区分确认策略、超时退化和故障场景；  分库分表解决一切：会引入跨片查询、分布式事务和扩容复杂度；  连接数越大越好：会增加内存、调度和锁竞争压力；  Binlog 只用于主从：还用于备份恢复、CDC 和审计分析；  读写分离没有成本：会带来复制延迟、会话一致性和事务路由问题；  数据库问题只在数据库：也可能是调用次数、网络、事务边界和数据模型问题。34.9 面试准备清单必会命令：SHOW FULL PROCESSLIST;SHOW ENGINE INNODB STATUS\GSHOW REPLICA STATUS\GSELECT * FROM sys.innodb_lock_waits;SELECT * FROM performance_schema.data_locks;EXPLAIN SELECT ...;EXPLAIN ANALYZE SELECT ...;必画图：B+Tree 结构MVCC 版本链与 ReadViewRedo/Binlog 两阶段提交主从复制链路一条 SQL 执行流程半同步提交链路必讲项目：  业务背景；  问题指标；  分析证据；  方案取舍；  实施步骤；  线上验证；  长期治理。本章小结MySQL 面试的核心是把概念讲准、把原理讲深、把场景讲活。索引题要落到执行计划、回表和扫描成本；事务题要区分隔离级别、快照读和当前读；日志题要能画两阶段提交；复制题要能分析延迟和切换风险；性能题不能只回答加索引，要给出排查闭环。最有说服力的不是标准答案，而是你能讲出一次真实问题的证据链和取舍。思考题  为什么“MySQL 默认 RR 能避免所有幻读”这句话不准确？  为“订单列表接口变慢”设计一套完整排查脚本。  解释半同步复制在 AFTER_SYNC 下的提交顺序。  深分页有哪些方案？各自适合什么产品形态？  把你最近一次 MySQL 故障整理成面试项目，包含指标、证据、方案和验证。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。容量规划和在线变更决定数据库能否长期稳定运行。很多故障不是 SQL 写错，而是数据量进入新阶段、连接模型改变、磁盘增长没有被预测、DDL 没有评估锁和复制影响。本章把容量、表结构变更、数据归档和大规模治理放到同一个生产流程里。33.1 容量规划总览容量规划要同时看四类资源：计算资源  |-- CPU：解析、优化、执行、锁等待重试  +-- 并发：连接数、活跃线程、事务并发内存资源  |-- Buffer Pool：数据页和索引页  |-- 连接内存：sort buffer、join buffer、临时表  +-- 其他全局内存存储资源  |-- 数据和索引文件  |-- Binlog / Redo / Undo  +-- 临时空间与备份链路资源  |-- 磁盘 IOPS / 吞吐  |-- 网络带宽  +-- 主从复制延迟容量规划不是一次性估一个磁盘大小，而是持续回答：  当前水位是多少？  峰值是多少？  增长速度是多少？  什么时候到达阈值？  到阈值前如何扩容？  扩容期间是否影响可用性？33.2 数据容量评估33.2.1 查看库表大小SELECT  table_schema,  ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb,  ROUND(SUM(data_length) / 1024 / 1024 / 1024, 2) AS data_gb,  ROUND(SUM(index_length) / 1024 / 1024 / 1024, 2) AS index_gb,  SUM(table_rows) AS estimated_rowsFROM information_schema.tablesWHERE table_schema NOT IN      ('mysql', 'information_schema', 'performance_schema', 'sys')GROUP BY table_schemaORDER BY size_gb DESC;查看大表：SELECT  table_schema,  table_name,  engine,  table_rows,  ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS total_gb,  ROUND(data_free / 1024 / 1024 / 1024, 2) AS free_gbFROM information_schema.tablesWHERE table_schema NOT IN      ('mysql', 'information_schema', 'performance_schema', 'sys')ORDER BY data_length + index_length DESCLIMIT 20;TABLE_ROWS 是估算值，精确行数需要 COUNT(*)。容量评估通常不需要精确到每一行，但要以趋势数据为依据。33.2.2 单行大小估算示例表：CREATE TABLE orders (  id BIGINT UNSIGNED NOT NULL,  order_no VARCHAR(32) NOT NULL,  user_id BIGINT UNSIGNED NOT NULL,  status VARCHAR(16) NOT NULL,  amount DECIMAL(12, 2) NOT NULL,  remark VARCHAR(500) NULL,  created_at DATETIME(3) NOT NULL,  PRIMARY KEY (id),  UNIQUE KEY uk_order_no (order_no),  KEY idx_user_created (user_id, created_at)) ENGINE=InnoDB;粗略估算：行记录：约 80 到 150 字节主键 B+Tree 页开销：约 15% 到 30%uk_order_no：约 40 字节/行，再加页开销idx_user_created：约 20 字节/行，再加页开销假设 1 亿行，逻辑数据可能是几十 GB，加索引后可能达到上百 GB。实际大小必须以表统计和监控为准，估算只用于提前设计。33.2.3 增长预测建议每天记录：日期库大小表大小行数Binlog 增量备份大小QPS / TPS连接数磁盘使用率简单预测：日均增长 = 最近 30 天数据增量 / 30可用天数 = (容量上限 - 当前使用) / 日均增长示例：磁盘上限：2000 GB当前使用：1200 GB安全阈值：80%，即 1600 GB剩余可用空间：400 GB最近 30 天日均增长：8 GB到达阈值天数：400 / 8 = 50 天不要等磁盘到 90% 才行动。扩容、重建、备份、在线变更和回滚都需要提前预留空间。33.3 连接与内存容量33.3.1 连接数估算实际最大连接 = 应用实例数 × 每实例连接池上限示例：应用实例：40 台每台最大连接：50理论最大连接：2000MySQL max_connections：3000这仍不是安全设计，还要考虑：  滚动发布时新旧实例并存；  运维工具和临时任务；  主从切换后的连接集中；  故障后的重试风暴；  从库可能挂载更多只读服务；  max_connections 不是越高越好。相关配置：max_connections：覆盖正常峰值和可控故障重试thread_cache_size：减少线程创建开销应用连接池：统一治理最大值语句超时：避免查询长期占用连接事务边界：尽早提交，不要等待外部调用33.3.2 内存估算常用简化模型：可用内存  ≈ 服务器内存    - 操作系统预留    - MySQL 进程基础开销    - 每连接缓冲峰值    - 其他进程innodb_buffer_pool_size  ≈ 可用内存的 50% 到 70%示例：服务器内存：64 GB操作系统和监控预留：8 GB复杂连接按 4 MB 估算如果允许 3000 个复杂连接同时到达，理论峰值可达 12 GB这说明不能简单把 Buffer Pool 设置为 48 GB 后仍允许无限复杂查询。要么控制活跃连接，要么限制排序和临时查询，要么提高机器规格并重新分配。查看变量：SHOW VARIABLES LIKE 'innodb_buffer_pool_size';SHOW VARIABLES LIKE 'sort_buffer_size';SHOW VARIABLES LIKE 'join_buffer_size';SHOW VARIABLES LIKE 'tmp_table_size';SHOW VARIABLES LIKE 'max_heap_table_size';SHOW VARIABLES LIKE 'max_connections';会话级缓冲按需分配，连接建立时不会一次性全部分配，但极端 SQL 仍可能放大内存风险。33.4 磁盘与 IO 规划33.4.1 关键指标            指标      含义                  使用率      磁盘空间水位              IOPS      每秒 IO 次数              吞吐      每秒传输数据量              延迟      单次 IO 耗时              读放大      一次逻辑读带来的物理 IO              写放大      逻辑写入带来的实际落盘      观察：SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';SHOW GLOBAL STATUS LIKE 'Innodb_data_pending%';SHOW GLOBAL STATUS LIKE 'Innodb_log_writes';SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';33.4.2 空间组成除表数据外，还要预留：  Binlog 保留期空间；  Redo Log 空间；  Undo 表空间；  临时表空间；  备份临时目录；  DDL 或在线变更工具的磁盘副本；  慢日志和错误日志；  云盘快照和性能抖动余量。示例：数据与索引：800 GBBinlog 保留 7 天，每天 50 GB，需要 350 GB在线变更工具可能额外需要 1 到 1.5 倍表空间安全水位：70% 到 80%如果机器只有 1 TB 磁盘，就无法同时承担这张表的大变更，需要临时扩容、分批处理或更换方案。33.5 Online DDL 基础MySQL 8.0 的 ALTER TABLE 支持不同算法：            算法      典型场景      特点                  INSTANT      加列、修改默认值等      只改元数据，秒级              INPLACE      加二级索引等      引擎内执行，减少锁表              COPY      修改列类型等      复制表数据，代价高      显式指定算法：ALTER TABLE shop.orders  ADD COLUMN channel VARCHAR(16) NOT NULL DEFAULT 'APP',  ALGORITHM=INSTANT;如果 MySQL 不支持指定算法，会报错，而不是悄悄降级为高风险方式：ALTER TABLE shop.orders  MODIFY COLUMN channel VARCHAR(32) NOT NULL DEFAULT 'APP',  ALGORITHM=INSTANT;改变列类型通常不能 INSTANT，应评估 INPLACE 或 COPY。33.5.1 常见操作分类通常 INSTANT：  末尾增加列  增加或删除默认值  修改列名（受版本和条件限制）  设置或删除虚拟列通常 INPLACE：  增加二级索引  删除二级索引  重命名索引  增加 FULLTEXT 索引（有额外限制）可能 COPY 或复杂重建：  修改主键  修改列类型  改变字符集  转换存储引擎  增加 AUTO_INCREMENT 属性不同 MySQL 8.0 小版本对 Instant DDL 的支持范围有增强。执行前必须查阅当前版本文档，并在同版本测试环境验证。33.5.2 锁语义ALTER TABLE shop.orders  ADD INDEX idx_merchant_created (merchant_id, created_at),  ALGORITHM=INPLACE,  LOCK=NONE;LOCK=NONE 表示不允许阻塞 DML；如果当前操作做不到，会失败。这比线上意外锁表更安全。Online DDL 的“在线”不等于“零影响”：  重建占用 CPU、IO 和内存；  需要记录在线变更日志；  大表会拉长 DDL 时间；  主库执行时间长，从库回放也可能延迟；  DDL 开始前后可能需要短暂元数据锁；  长事务会导致 DDL 等待 MDL；  DDL 等待又可能阻塞后续查询。33.5.3 Metadata Lock 风险典型事故链：长查询持有表 MDL 读锁  -&gt; DDL 等待 MDL 写锁     -&gt; 后续所有查询排队        -&gt; 连接耗尽           -&gt; 业务不可用设置 DDL 会话锁等待：SHOW VARIABLES LIKE 'lock_wait_timeout';SET SESSION lock_wait_timeout = 10;执行 DDL 前检查长事务和长查询：SELECT trx_id, trx_state, trx_started,       trx_mysql_thread_idFROM information_schema.innodb_trxORDER BY trx_started;SELECT * FROM sys.sessionWHERE conn_id IS NOT NULLORDER BY statement_latency DESC;建议：  DDL 会话设置较短 lock_wait_timeout；  低峰执行；  先在测试环境估算时间；  监控 MDL 等待；  拿不到锁时快速失败，而不是无限排队。33.6 第三方在线变更工具33.6.1 为什么需要工具当表很大时，ALTER TABLE 可能面临：  主库执行时间不可控；  从库延迟明显；  磁盘空间不足；  失败后回滚代价高；  无法限速；  无法暂停和续跑；  云平台或主从架构有限制。常用工具：            工具      思路                  gh-ost      基于 Binlog 的无触发器在线变更              pt-online-schema-change      创建影子表和触发器，同步增量      33.6.2 gh-ost 基本流程1. 创建幽灵表和相关控制表2. 按主键分批读取原表数据3. 写入应用变更后的幽灵表4. 同时消费 Binlog 增量5. 校验、限速、观察水位6. 原子切换原表与幽灵表示例：gh-ost \  --host=127.0.0.1 --port=3306 --user=change_user \  --database=shop --table=orders \  --alter="ADD COLUMN channel VARCHAR(16) NOT NULL DEFAULT 'APP'" \  --chunk-size=1000 \  --max-load=Threads_running=50 \  --critical-load=Threads_running=200 \  --cut-over=default \  --allow-on-master \  --execute生产建议：  先在从库或非高峰演练；  保留执行前的 dry run 输出；  配置 max-load 和 critical-load；  确认磁盘可容纳影子表；  确认 Binlog 配置和工具权限；  明确切流窗口；  准备失败终止和重跑方案。33.6.3 pt-osc 基本流程1. 创建影子表2. 在影子表执行 ALTER3. 在原表创建 INSERT / UPDATE / DELETE 触发器4. 分批拷贝存量数据5. 原子 rename 切换示例：pt-online-schema-change \  --alter "ADD COLUMN channel VARCHAR(16) NOT NULL DEFAULT 'APP'" \  D=shop,t=orders \  --host=127.0.0.1 --port=3306 \  --user=change_user --ask-pass \  --chunk-size=1000 \  --max-load=Threads_running=50 \  --critical-load=Threads_running=200 \  --progress=time,30 \  --execute触发器方案要额外关注写入放大和高并发下的触发器开销。工具选择应结合版本、云平台限制、团队经验和表写入模式。33.7 在线变更流程33.7.1 变更前检查1. 表大小、行数、每日增长2. 当前 QPS / TPS 和峰值时间3. 是否有长事务和长 SQL4. 主从延迟水位5. 磁盘空间和 IOPS 余量6. 索引是否已被现有查询使用7. 是否存在同名临时表或工具残留8. 测试环境同版本演练结果9. 回滚方案10. 停止条件检查索引使用：SELECT object_schema, object_name, index_name,       count_starFROM performance_schema.table_io_waits_summary_by_index_usageWHERE object_schema = 'shop'  AND object_name = 'orders'ORDER BY count_star DESC;MySQL 8.0 可以先隐藏索引：ALTER TABLE shop.orders ALTER INDEX idx_old INVISIBLE;观察无影响后删除：DROP INDEX idx_old ON shop.orders;33.7.2 变更中监控必须监控：Threads_runningCPU / IO 利用率磁盘空间主从延迟锁等待错误日志工具复制进度业务接口 P99 和错误率停止条件示例：Threads_running 连续 5 分钟高于 100主从延迟高于 30 秒磁盘剩余低于 20%核心接口错误率高于 0.5%工具进度长期不推进出现无法解释的锁等待33.7.3 变更后验证SHOW CREATE TABLE shop.orders\GEXPLAINSELECT *FROM shop.ordersWHERE user_id = 10001  AND created_at &gt;= '2026-08-01';业务验证：  核心写入接口正常；  新字段默认值符合预期；  查询计划使用新索引；  无新增慢 SQL；  主从延迟恢复；  监控指标回落；  清理临时表和工具进程。33.8 大表数据治理33.8.1 归档策略            策略      适用场景                  按时间归档      订单、日志、流水              按状态归档      已关闭、已取消数据              按租户拆分      大客户独立存储              冷热分层      热表保近期，冷表保历史              直接过期      明确保留期的行为日志      归档前必须确认：  法规和业务保留期；  报表是否依赖历史数据；  审计是否需要明细；  用户是否还能查询；  关联表是否同步归档；  归档存储是否可恢复。33.8.2 分批删除不推荐一次性删除：DELETE FROM audit_logsWHERE created_at &lt; '2025-01-01';推荐分批：DELETE FROM audit_logsWHERE id IN (  SELECT id  FROM (    SELECT id    FROM audit_logs    WHERE created_at &lt; '2025-01-01'    ORDER BY id    LIMIT 1000  ) AS t);SELECT ROW_COUNT();循环执行时每批之间可暂停，并观察延迟和锁等待。更稳妥的方案是按主键范围或时间分区删除整个分区。33.8.3 分区表示例：CREATE TABLE audit_logs (  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,  created_at DATETIME(3) NOT NULL,  level VARCHAR(10) NOT NULL,  message TEXT NOT NULL,  PRIMARY KEY (id, created_at),  KEY idx_created (created_at)) ENGINE=InnoDBPARTITION BY RANGE (TO_DAYS(created_at)) (  PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),  PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),  PARTITION p202609 VALUES LESS THAN (TO_DAYS('2026-10-01')),  PARTITION pmax VALUES LESS THAN MAXVALUE);删除历史分区：ALTER TABLE audit_logs DROP PARTITION p202607;分区表不是万能优化。查询条件应能裁剪分区，否则可能扫描更多分区；主键和唯一键也必须满足分区表达式约束。33.9 评审模板变更评审模板：变更标题：负责人：评审人：目标库表：表大小：当前 QPS / TPS：预计执行时间：执行窗口：使用算法或工具：影响 SQL：锁风险：复制风险：磁盘余量：停止条件：回滚方案：验证 SQL：通知对象：容量报表模板：实例：角色：主库 / 从库CPU 峰值：内存峰值：磁盘使用：IOPS 峰值：IO 延迟：最大连接：活跃连接：QPS / TPS：最大表：90 天增长：预计达到阈值日期：扩容建议：负责人：本章小结容量规划要覆盖 CPU、内存、连接、磁盘空间、IO 和复制链路，并用增长趋势预测阈值。Online DDL 的算法和锁语义决定变更风险，INSTANT 不适用于所有场景，INPLACE 也不等于零影响。大表变更前必须评估长事务、MDL、磁盘、主从延迟和回滚方案，必要时使用 gh-ost 或 pt-osc 分批迁移和限速。历史数据要通过归档、分区和分批删除治理，所有变更都应有评审模板、停止条件和验证 SQL。思考题  为什么 max_connections 很高反而可能带来风险？  给一张 500 GB、日增 5 GB 的表做 DDL，需要评估哪些资源？  Online DDL 的 LOCK=NONE 意味着什么？它为什么不能消除所有影响？  长查询、DDL 和后续查询之间为什么会形成排队链？  为一次“给订单表增加索引”的变更写完整评审单。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。数据库安全不是“设置一个复杂密码”，而是一套持续运行的体系：账号有边界，权限可回收，网络有入口控制，敏感数据有分级，操作有审计，变更可追溯。很多拖库、误授权、SQL 注入和内部滥用，都来自治理缺口，而不是单点漏洞。本章以 MySQL 8.0 为主线，讲清楚账号与权限模型、角色管理、网络安全、传输加密、审计、敏感数据保护和生产权限治理。32.1 安全模型总览入口层  |-- 网络分区 / 安全组 / 防火墙  |-- 只暴露代理或网关  +-- VPN / 堡垒机       |       v账号层  |-- 最小权限账号  |-- 按应用拆分账号  +-- 密码策略与轮换       |       v权限层  |-- 库 / 表 / 列 / 存储例程权限  |-- 角色  +-- 定期审计与回收       |       v数据层  |-- 敏感字段识别  |-- 加密与脱敏  +-- 备份同样受控       |       v行为层  |-- SQL 审计  |-- 高危操作审批  +-- 异常访问告警安全设计遵循三个原则：  最小权限：只授予当前任务必需的权限；  职责分离：开发、DBA、运维、数据分析不共用超级账号；  默认拒绝：先不能访问，再按需求精确授权。32.2 账号与认证32.2.1 查看账号SELECT user, host, plugin, account_locked,       password_last_changedFROM mysql.userORDER BY user, host;MySQL 账号由 user 和 host 共同决定。同一个用户名来自不同主机可以是不同账号，权限也可能不同：app_user@10.0.%.%app_user@192.168.1.10report_user@10.20.%.%32.2.2 创建应用账号CREATE USER 'shop_app'@'10.0.%'  IDENTIFIED BY 'StrongPassword_2026'  PASSWORD EXPIRE INTERVAL 90 DAY  FAILED_LOGIN_ATTEMPTS 5  PASSWORD_LOCK_TIME 1;GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*TO 'shop_app'@'10.0.%';应用账号通常不需要 DROP、ALTER、CREATE、GRANT OPTION 和 SUPER。发布系统或 DBA 工具可以使用单独的变更账号。32.2.3 认证插件MySQL 8.0 默认认证插件是 caching_sha2_password：SELECT user, host, plugin FROM mysql.user;兼容旧客户端或旧驱动时，可能需要临时使用 mysql_native_password：ALTER USER 'legacy_app'@'10.0.%'  IDENTIFIED WITH mysql_native_password  BY 'StrongPassword_2026';兼容方案应有退出时间。长期方案是升级驱动和客户端，而不是让数据库永久停留在旧认证体系。32.2.4 禁止匿名账号与共享账号检查匿名账号：SELECT user, host FROM mysql.user WHERE user = '';删除：DROP USER ''@'localhost';共享账号的问题：  无法判断真实操作人；  权限只会越来越大；  密码轮换困难；  审计日志失去价值；  离职和项目下线无法回收。32.3 权限体系32.3.1 权限层级            层级      示例                  全局      *.*              数据库      shop.*              表      shop.orders              列      shop.users(phone)              存储例程      PROCEDURE shop.close_order              代理      PROXY 权限      查看权限：SHOW GRANTS FOR 'shop_app'@'10.0.%';32.3.2 常见账号模板普通业务写入账号：CREATE USER 'shop_app'@'10.0.%'  IDENTIFIED BY 'StrongPassword_2026';GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*TO 'shop_app'@'10.0.%';只读服务账号：CREATE USER 'shop_read'@'10.0.%'  IDENTIFIED BY 'StrongPassword_2026';GRANT SELECT ON shop.* TO 'shop_read'@'10.0.%';报表账号只给需要的表：CREATE USER 'report_user'@'10.20.%'  IDENTIFIED BY 'StrongPassword_2026';GRANT SELECT ON shop.orders TO 'report_user'@'10.20.%';GRANT SELECT ON shop.order_items TO 'report_user'@'10.20.%';备份账号：CREATE USER 'backup_user'@'127.0.0.1'  IDENTIFIED BY 'StrongPassword_2026';GRANT SELECT, LOCK TABLES, PROCESS, EVENT, TRIGGERON *.* TO 'backup_user'@'127.0.0.1';GRANT RELOAD ON *.* TO 'backup_user'@'127.0.0.1';不同 MySQL 版本和备份工具对权限要求略有差异，应按工具文档确认，但原则不变：不使用 root 做日常备份。32.3.3 危险权限清单            权限      风险                  ALL PRIVILEGES      权限边界失控              SUPER      终止会话、绕过只读、修改全局变量              DROP      删除库表和数据              GRANT OPTION      权限二次扩散              FILE      读写服务器文件              CREATE USER      创建后门账号              REPLICATION SLAVE      可拉取数据变更              EVENT      定时执行 SQL      授权前先问三个问题：  这个权限解决什么问题？  能否用更小范围替代？  到期后如何回收？32.3.4 角色管理MySQL 8.0 支持角色，适合把权限按职责打包：CREATE ROLE 'shop_readonly';CREATE ROLE 'shop_app_rw';CREATE ROLE 'shop_dba_change';GRANT SELECT ON shop.* TO 'shop_readonly';GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*TO 'shop_app_rw';GRANT ALTER, CREATE, INDEX, DROP ON shop.*TO 'shop_dba_change';GRANT 'shop_readonly' TO 'report_user'@'10.20.%';GRANT 'shop_app_rw' TO 'shop_app'@'10.0.%';启用默认角色：SET DEFAULT ROLE 'shop_app_rw'TO 'shop_app'@'10.0.%';查看权限：SHOW GRANTS FOR 'shop_app'@'10.0.%';SHOW GRANTS FOR 'shop_app'@'10.0.%'USING 'shop_app_rw';角色应表达职责，例如只读、写入、变更、备份，而不是把大权限换个名字继续扩散。32.4 网络与传输安全32.4.1 入口控制生产建议：  MySQL 不直接暴露公网；  应用通过内网或专线访问；  运维通过堡垒机或 VPN 访问；  安全组按源 IP 精确放行；  从库和备份机遵守同样规则；  开发、测试、生产网络隔离。查看监听与解析配置：SHOW VARIABLES LIKE 'bind_address';SHOW VARIABLES LIKE 'skip_name_resolve';bind_address 限制监听地址，账号 host 限制来源，两者配合使用。skip_name_resolve 可避免 DNS 解析带来的连接延迟，但账号应使用 IP 网段。32.4.2 TLS 加密查看连接加密状态：SHOW STATUS LIKE 'Ssl_cipher';SHOW VARIABLES LIKE 'have_ssl';生产至少要求：  跨机房、跨云和公网链路启用 TLS；  应用与数据库之间启用 TLS；  主从复制启用 TLS；  客户端校验服务端证书；  高安全场景启用双向认证。强制账号使用 TLS：ALTER USER 'shop_app'@'10.0.%'REQUIRE SSL;双向认证：ALTER USER 'shop_app'@'10.0.%'REQUIRE X509;32.5 SQL 注入防护错误示例：String sql = "SELECT id FROM users WHERE name = '" + name + "'";Statement st = conn.createStatement();ResultSet rs = st.executeQuery(sql);攻击者传入 ` ‘ OR ‘1’=’1 ，就可能读取超出预期的数据，甚至通过 UNION`、堆叠语句和时间盲注扩大影响。正确做法是参数化查询：String sql = "SELECT id FROM users WHERE name = ?";try (PreparedStatement ps = conn.prepareStatement(sql)) {    ps.setString(1, name);    try (ResultSet rs = ps.executeQuery()) {        while (rs.next()) {            long id = rs.getLong("id");        }    }}动态排序字段不能直接拼接用户输入：List&lt;String&gt; allowedColumns =        List.of("created_at", "amount", "status");if (!allowedColumns.contains(sortField)) {    throw new IllegalArgumentException("invalid sort field");}String sql = "SELECT id, amount FROM orders ORDER BY "        + sortField + " LIMIT 100";防护清单：  所有值使用参数绑定；  动态表名、列名、排序字段使用白名单；  ORM 中的原生 SQL 同样检查；  应用账号没有 FILE、DROP、SUPER 权限；  错误信息不回传 SQL 和堆栈；  管理后台做二次鉴权；  WAF 和扫描器只能作为辅助，不能替代参数化。32.6 敏感数据保护32.6.1 数据分级            级别      示例      要求                  公开      商品名、公告      完整性保护              内部      订单金额、用户 ID      访问控制与审计              敏感      手机号、地址、身份证      加密或脱敏              高敏      密码、密钥、支付凭证      不可明文存储      32.6.2 密码存储密码必须使用单向自适应哈希算法，不能使用 MD5 或 SHA1：Argon2idbcryptscryptPBKDF2应用层示例：String hash = passwordEncoder.encode(rawPassword);boolean ok = passwordEncoder.matches(rawPassword, storedHash);数据库只保存哈希值，不保存明文和可逆加密密码。找回密码时应生成一次性令牌，而不是把原密码发给用户。32.6.3 加密与脱敏            方案      适用场景                  应用层加密      少数高敏字段，数据库只见密文              透明数据加密 TDE      防磁盘或备份文件丢失              传输加密 TLS      防网络链路窃听              查询脱敏      报表、客服、测试环境              数据掩码      日志和页面展示      应用层加密后，等值查询要考虑密文随机性；范围查询和模糊查询会更困难。常见做法是单独保存 HMAC 检索值或维护脱敏索引，但不能为此降低安全边界。32.6.4 测试数据生产数据不应直接导入测试环境。应使用：  合成数据生成器；  静态脱敏工具；  按比例抽样并重命名；  删除或替换手机号、身份证、银行卡；  保留格式，不保留真实值。32.7 审计与高危操作治理32.7.1 审计方案MySQL Enterprise Edition 提供 Audit Log 插件，可以记录连接、查询、DDL、DML 和管理操作：INSTALL PLUGIN audit_log SONAME 'audit_log.so';SHOW VARIABLES LIKE 'audit_log%';社区版可以使用：  ProxySQL 或数据库网关审计；  应用层操作审计；  Binlog 分析变更；  general_log 短期抓取；  云厂商数据库审计能力。general_log 可能带来明显性能和磁盘开销，只适合短时间诊断，不适合作为长期审计方案。32.7.2 高危操作审批以下操作必须审批：DROP TABLE / DROP DATABASETRUNCATE TABLE不带 WHERE 的 UPDATE / DELETE大批量 UPDATE / DELETE账号创建和授权权限回收关闭 Binlog修改全局参数主从切换直接修改线上数据审批单应包含：  业务原因；  影响表和行数；  执行 SQL；  备份点；  回滚方案；  执行窗口；  验证 SQL；  负责人。32.7.3 权限审计找出高权限账号：SELECT user, host,       Grant_priv, Super_priv, Drop_priv, File_priv,       Create_user_priv, Repl_slave_privFROM mysql.userWHERE Grant_priv = 'Y'   OR Super_priv = 'Y'   OR Drop_priv = 'Y'   OR File_priv = 'Y'   OR Create_user_priv = 'Y'ORDER BY user, host;找出拥有 GRANT OPTION 的授权：SELECT grantee, privilege_type, is_grantableFROM information_schema.schema_privilegesWHERE is_grantable = 'YES'UNION ALLSELECT grantee, privilege_type, is_grantableFROM information_schema.table_privilegesWHERE is_grantable = 'YES';回收：REVOKE DROP ON shop.* FROM 'shop_app'@'10.0.%';REVOKE ALL PRIVILEGES, GRANT OPTIONFROM 'old_project'@'10.0.%';DROP USER 'old_project'@'10.0.%';32.8 备份与导出安全备份文件常常是安全治理的盲区。要求：  备份存储私有化，禁止公开读写；  传输使用 TLS 或内网专线；  备份文件加密；  恢复实例也受网络和账号控制；  导出数据必须审批；  临时导出文件到期删除；  备份账号不共享；  记录备份访问日志。示例：mysqldump --single-transaction --set-gtid-purged=off \  --triggers --routines --events \  -h 127.0.0.1 -u backup_user -p shop &gt; shop.sql如果导出包含敏感字段，应先写入受控目录，再执行脱敏，不能在个人电脑或共享目录中留存明文副本。32.9 安全基线清单            类别      检查项                  账号      无匿名账号，无共享账号，无默认弱密码              权限      应用、报表、备份、变更账号分离              网络      不暴露公网，安全组最小放行              传输      客户端、复制、备份链路启用 TLS              数据      密码不可逆哈希，敏感字段分级治理              审计      高危操作、授权、导出可追溯              备份      加密存储，访问受控，定期恢复演练              变更      DDL 和数据修复有审批与回滚              监控      异常登录、权限变化、大批量导出告警              应急      拖库、误授权、数据泄露预案      32.10 事故场景演练场景一：误授权 ALLSHOW GRANTS FOR 'shop_app'@'10.0.%';REVOKE ALL PRIVILEGES, GRANT OPTIONFROM 'shop_app'@'10.0.%';GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*TO 'shop_app'@'10.0.%';复盘时要确认误授权期间是否发生过 DROP、FILE 写入、账号创建或数据导出。场景二：账号泄露处理顺序：  锁定账号；  保留审计和连接日志；  通知安全团队；  修改凭据并轮换相关密钥；  检查异常 SQL 和导出；  收紧来源 IP；  确认数据影响；  输出改进项。锁定账号：ALTER USER 'shop_app'@'10.0.%' ACCOUNT LOCK;场景三：发现 SQL 注入止血：  下线受影响接口；  修复参数化查询；  收缩应用账号权限；  检查是否写入异常数据或文件；  分析访问日志确定影响范围；  重置可能泄露的凭据；  加固后台和上传入口。本章小结MySQL 安全治理覆盖账号、权限、网络、传输、数据、审计、备份和高危操作。应用账号必须最小权限并按职责拆分，MySQL 8.0 的角色可以让权限模板更清晰。SQL 注入的第一防线是参数化和白名单，而不是依赖 WAF。敏感数据要分级，密码必须使用单向自适应哈希，备份和导出同样需要纳入安全边界。权限审计和高危操作审批应常态化，才能让安全规则在长期迭代中不失效。思考题  为什么账号由 user 和 host 共同组成？  应用账号为什么不应该拥有 DROP、FILE 和 SUPER 权限？  设计一套报表、业务、备份、发布四类账号的权限模板。  参数化查询为什么不能直接处理动态排序字段？  如果发现生产账号泄露，写出前 30 分钟的处理动作。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。故障排查的原则是：先止血，再定位，后复盘。不要在事故中追求完美根因，而让业务持续不可用。31.1 通用排查流程1. 确认影响面：接口、用户、机房、错误率；2. 判断是否数据库：应用日志、连接错误、SQL 延迟；3. 保存现场：SHOW PROCESSLIST、错误日志、监控截图；4. 寻找变更：发布、DDL、参数、活动、流量；5. 定位资源：CPU、IO、锁、连接、复制、磁盘；6. 止血：限流、杀会话、切读、扩容、回滚；7. 恢复验证：核心接口、数据校验、监控回落；8. 根因分析和改进项；31.2 常用命令集会话与状态：SHOW PROCESSLIST;SHOW FULL PROCESSLIST;SHOW GLOBAL STATUS;SHOW ENGINE INNODB STATUS\G事务：SELECT  trx_id,  trx_state,  trx_started,  trx_rows_locked,  trx_rows_modified,  trx_mysql_thread_idFROM information_schema.INNODB_TRXORDER BY trx_started;锁：SELECT * FROM sys.innodb_lock_waits;SELECT * FROM performance_schema.data_locks;复制：SHOW REPLICA STATUS\GSELECT * FROM performance_schema.replication_applier_status_by_worker;容量：SELECT TABLE_NAME, TABLE_ROWS,       ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mbFROM information_schema.TABLESWHERE TABLE_SCHEMA = 'shop';31.3 场景一：应用连不上典型报错：Communications link failureToo many connectionsAccess deniedConnection refused排查：SHOW STATUS LIKE 'Threads_connected';SHOW STATUS LIKE 'Aborted_connects';SHOW PROCESSLIST;可能原因：  MySQL 停止；  端口不可达；  安全组或防火墙；  超过 max_connections；  账号来源限制；  密码或认证插件；  连接池耗尽；  主机被 blocked。止血：  恢复进程；  放行网络；  杀掉空闲异常连接；  应用限流；  临时提高连接数；  回滚连接池配置。31.4 场景二：CPU 高常见原因：  慢 SQL 扫描；  并发查询暴涨；  排序聚合；  锁等待后重试风暴；  复制应用压力；  SQL 解析过多；  备份和监控任务；  机器规格不足。查看：SELECT *FROM sys.sessionORDER BY statement_latency DESCLIMIT 10;处理：  终止明显异常查询；  应用限流；  优化索引；  报表下线或切走；  停止非核心任务；  扩容或切换；  回滚发布。只终止查询：KILL QUERY &lt;processlist_id&gt;;终止连接：KILL &lt;processlist_id&gt;;杀之前必须确认连接身份和事务状态。31.5 场景三：IO 高或磁盘延迟高常见原因：  Buffer Pool 命中率低；  大查询读大量页；  脏页刷盘；  Redo 空间不足；  Binlog 洪峰；  备份任务；  从库追复制；  磁盘故障或云盘限流；  临时表落盘。查看：SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';SHOW GLOBAL STATUS LIKE 'Created_tmp%';SHOW GLOBAL STATUS LIKE 'Innodb_data_pending%';处理：  停大查询；  延迟备份；  限制导入速率；  拆分大事务；  增大 Buffer Pool 需评估内存；  云盘升配；  只读流量切到其他从库。31.6 场景四：锁等待查看：SELECT * FROM sys.innodb_lock_waits;判断：  阻塞源头事务；  等待 SQL；  锁住的表和索引；  事务年龄；  是否长事务或外部调用。处理：  请求业务确认；  终止阻塞事务；  回滚异常会话；  限流热点接口；  修复索引；  优化锁顺序。不要盲目杀最大事务，可能是正在正常执行的批任务。杀错会触发回滚并进一步占用资源。31.7 场景五：死锁激增查看：SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';SHOW ENGINE INNODB STATUS\G排查：  死锁事务 SQL；  加锁顺序；  是否 RR 间隙锁；  唯一键冲突；  大事务；  应用是否重试。止血：  限流冲突接口；  暂停批任务；  回滚近期发布；  临时调整隔离级别需评审；  增加重试；  统一更新顺序。31.8 场景六：主从延迟查看：SHOW REPLICA STATUS\GSELECT * FROM performance_schema.replication_applier_status_by_worker;常见原因：  主库大事务；  从库单线程应用；  从库硬件差；  从库重查询；  表结构缺索引；  网络瓶颈。处理：  停止主库大批任务；  从库下线报表查询；  增加并行 worker；  暂停非关键 DDL；  临时读主库；  无法收敛时重建或切到其他从库。31.9 场景七：磁盘满检查：SHOW VARIABLES LIKE 'datadir';SHOW VARIABLES LIKE 'log_bin_basename';SHOW VARIABLES LIKE 'tmpdir';SHOW BINARY LOGS;常见来源：  Binlog 增长；  大表空间；  临时表落盘；  Relay Log 堆积；  备份残留；  错误日志暴涨；  Undo 膨胀；  快照恢复残留。安全处理：PURGE BINARY LOGS BEFORE '2026-08-18 00:00:00';不要直接 rm MySQL 正在管理的文件。清理前必须确认从库已消费和备份链路可恢复。31.10 场景八：重启后变慢典型现象：  Buffer Pool 冷；  命中率低；  磁盘读高；  P99 尖刺。处理：  降低流量；  预热核心索引；  逐步放量；  观察命中率；  保留更长监控窗口；  滚动重启避免同时冷启动。31.11 场景九：误更新或误删除立即动作：1. 停止写入或下线相关功能；2. 保留当前 Binlog；3. 记录误操作时间和 SQL；4. 不要在主库继续尝试修复；5. 恢复备份到临时实例；6. 用 Binlog 找到目标数据；7. 校验后导出修复 SQL；8. 双人确认后执行；修复期间要防止新数据覆盖，并为修复脚本准备回滚语句。31.12 事故复盘模板标题：时间：影响：处理时间线：监控表现：根因：触发条件：为什么没有提前发现：为什么没有被测试覆盖：止血动作：长期改进：负责人：截止时间：改进项必须可执行，例如“上线前必须用影子库执行 EXPLAIN”，而不是“大家以后小心”。本章小结故障排查先确认影响面和变更历史，再用会话、事务、锁、复制、资源和日志定位。常见故障包括连接耗尽、CPU 高、IO 高、锁等待、死锁、主从延迟、磁盘满和冷启动。事故中优先止血和保存现场，事后用可执行改进项闭环。思考题  为什么事故中要先保存现场？  CPU 高和 IO 高的处理顺序有什么不同？  杀阻塞事务前需要确认什么？  磁盘满时为什么不能直接删除 Binlog 文件？  为误更新场景写一份止血和恢复流程。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。没有监控的数据库等于黑盒。监控的目标不是收集所有指标，而是在问题影响用户前发现异常，并在事故中快速回答“哪里坏了、影响多大、该怎么处置”。30.1 监控分层            层级      指标                  业务      下单量、支付成功率、核心接口延迟              应用      SQL 次数、错误、连接池等待、事务耗时              MySQL      QPS、TPS、慢查询、连接、锁、临时表              InnoDB      Buffer Pool、脏页、Redo、Undo、行锁              复制      线程状态、延迟、GTID、错误              主机      CPU、内存、磁盘、网络、文件系统              存储      IOPS、吞吐、延迟、云盘限流      业务指标要和数据库指标放在同一时间轴上，否则容易误判。30.2 黄金信号            信号      数据库映射                  延迟      SQL P95 / P99、慢查询耗时              流量      QPS、TPS、网络进出              错误      SQL 错误、死锁、复制错误              饱和度      CPU、IO、连接、锁等待、磁盘水位      告警应该优先围绕用户影响，而不是单个瞬时指标。30.3 mysqld_exporterPrometheus 常用 mysqld_exporter 采集 MySQL。exporter 示例配置：[client]user=exporterpassword=Exporter123456!创建账号：CREATE USER 'exporter'@'127.0.0.1' IDENTIFIED BY 'Exporter123456!';GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.*  TO 'exporter'@'127.0.0.1';启动后 Prometheus 抓取：scrape_configs:  - job_name: mysql    static_configs:      - targets: ['mysql-exporter:9104']生产建议：  exporter 独立账号；  只授监控权限；  采集频率 15 到 30 秒；  多实例统一标签；  监控采集本身不能拖垮数据库。30.4 连接与线程常用查询：SHOW GLOBAL STATUS LIKE 'Threads%';SHOW GLOBAL STATUS LIKE 'Connections';SHOW GLOBAL STATUS LIKE 'Aborted%';SHOW GLOBAL STATUS LIKE 'Max_used_connections';重点指标：            指标      含义                  Threads_connected      当前连接              Threads_running      当前活跃线程              Threads_created      新建线程数              Max_used_connections      历史最大连接              Aborted_clients      客户端异常断开              Aborted_connects      连接失败      告警方向：  连接使用率超过 80%；  Threads_running 持续异常；  Aborted_connects 快速增长；  大量长时间 Sleep；  连接池等待超时。30.5 QPS / TPSQPS 可用 Questions 估算，TPS 可用 Com_commit 与 Com_rollback 估算：SHOW GLOBAL STATUS LIKE 'Questions';SHOW GLOBAL STATUS LIKE 'Com_commit';SHOW GLOBAL STATUS LIKE 'Com_rollback';Prometheus 常用 rate 计算：rate(mysql_global_status_questions[5m])rate(mysql_global_status_commands_total{command="commit"}[5m])解读：  QPS 高不代表压力大；  TPS 更能反映写事务压力；  必须与延迟、锁等待、IO 一起看；  突增要关联发布和活动；  基线按星期和小时建模。30.6 慢查询配置：[mysqld]slow_query_log = ONlong_query_time = 0.5log_queries_not_using_indexes = ONslow_query_log_file = /var/log/mysql/slow.log指标：  每分钟慢查询数；  慢查询总耗时；  扫描行数；  返回行数；  Top SQL 指纹；  新增慢 SQL；  锁等待时间；  Rows examined / Rows sent。推荐工具：  pt-query-digest；  应用 APM；  Elasticsearch / Loki 日志分析；  云厂商数据库洞察。分析：pt-query-digest /var/log/mysql/slow.log &gt; slow_report.txt30.7 InnoDB 指标关键项：            指标      说明                  Buffer Pool hit rate      缓存命中              dirty pages      脏页数量              Innodb_log_waits      Redo 等待              Innodb_row_lock_waits      行锁等待              Innodb_row_lock_time      行锁等待时间              Innodb_buffer_pool_read_requests      逻辑读              Innodb_buffer_pool_reads      磁盘读              History list length      Undo 积压      查看：SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';SHOW ENGINE INNODB STATUS\G告警：  命中率持续下降；  行锁等待时间增长；  Redo 等待非零增长；  脏页比例异常；  History list 持续增长。30.8 复制监控基础 SQL：SHOW REPLICA STATUS\G重点：Replica_IO_RunningReplica_SQL_RunningSeconds_Behind_SourceLast_IO_ErrnoLast_SQL_ErrnoRetrieved_Gtid_SetExecuted_Gtid_SetPerformance Schema：SELECT *FROM performance_schema.replication_applier_status_by_worker;建议监控：  IO / SQL 线程状态；  位点差或 GTID 差；  心跳延迟；  复制错误码；  Worker 队列；  Relay Log 堆积；  主从切换事件。Seconds_Behind_Source 不是唯一真相，推荐使用心跳表或 GTID 差值。30.9 主机与存储必须采集：            指标      说明                  CPU user / iowait      计算和 IO 等待              memory available      可用内存              disk usage      数据、Binlog、临时目录              disk IOPS / throughput      磁盘负载              IO latency      读写延迟              network in / out      流量              TCP retransmit      网络质量              fsync 延迟      提交延迟关键      MySQL 特殊目录：datadirlog_bintmpdirslow_logrelay_logbackup磁盘使用率建议在 70% 到 80% 前告警，为 Binlog、备份、临时表和恢复预留空间。30.10 事务与锁当前事务：SELECT  trx_id,  trx_started,  TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_s,  trx_rows_locked,  trx_rows_modifiedFROM information_schema.INNODB_TRXORDER BY trx_started;锁等待：SELECT *FROM sys.innodb_lock_waits;死锁：SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';SHOW ENGINE INNODB STATUS\G告警：  事务年龄超过 60 秒；  单事务锁行数异常；  死锁速率增长；  锁等待队列增长；  MDL 等待增长。30.11 告警设计告警分级：            级别      示例                  P0      主库不可写、主从全断、磁盘满              P1      复制延迟超阈值、连接耗尽、死锁激增              P2      慢查询增长、Buffer Pool 命中率下降              P3      容量趋势异常、配置漂移      好告警特征：  有明确动作；  有阈值和持续时间；  关联业务影响；  不频繁误报；  包含实例、角色、机房标签；  指向 Runbook；  能区分主从角色。Prometheus 示例：mysql_global_status_threads_connected  / on(instance) mysql_global_variables_max_connections &gt; 0.85rate(mysql_global_status_innodb_row_lock_waits[5m]) &gt; 1030.12 容量趋势每周评估：  数据增长率；  Binlog 增长率；  QPS / TPS 趋势；  P99 延迟趋势；  连接峰值；  Buffer Pool 命中率；  磁盘剩余天数；  CPU 和 IO 水位；  Top 表增长；  备份大小和恢复时间。估算剩余天数：剩余天数 = (总容量 - 当前使用量) / 日均增长至少在磁盘、连接、CPU、IO、网络、备份窗口六项进入阈值前扩容。本章小结监控要覆盖业务、SQL、MySQL、InnoDB、复制、主机和存储，并把指标放在同一时间轴。告警应优先反映用户影响和饱和度，慢查询、锁等待、复制延迟、磁盘水位和长事务是数据库健康的核心信号。容量趋势要提前预测，而不是在磁盘满时救火。思考题  数据库监控的黄金信号如何映射？  为什么 Seconds_Behind_Source 可能不可靠？  哪些指标适合 P0 / P1 告警？  如何监控长事务和 Undo 膨胀？  为你们 MySQL 集群设计一个核心仪表盘。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。性能调优不是改参数比赛，而是围绕用户可感知指标，找到当前系统的主要约束，并用可验证的方式消除它。数据库慢只是系统慢的一个可能环节。29.1 调优目标先定义指标：            指标      示例                  延迟 P95 / P99      订单创建 P99 &lt; 200ms              吞吐      峰值 TPS 5000              错误率      数据库错误 &lt; 0.01%              可用性      99.99%              主从延迟      P99 &lt; 1s              慢查询数      每分钟 &lt; 10              资源水位      CPU 常态 &lt; 60%      没有目标和基线的优化，很容易变成随机修改。29.2 分层定位端到端耗时：用户  -&gt; 前端 / 网关  -&gt; 应用逻辑  -&gt; 连接池  -&gt; MySQL     -&gt; SQL 执行     -&gt; InnoDB     -&gt; 操作系统     -&gt; 磁盘 / 网络先回答：  是所有接口慢，还是少数接口慢？  是读慢、写慢，还是锁等待？  是持续慢，还是尖刺？  是主库慢，还是从库慢？  是 CPU、IO、锁、复制，还是连接数瓶颈？  从什么时候开始？  发布、流量、数据量、参数是否变化？29.3 查询调优优先处理：  Top 10 耗时 SQL；  Top 10 扫描行 SQL；  Top 10 次数最多 SQL；  新增慢 SQL；  锁等待 SQL；  大事务；  报表 SQL。流程：1. 获取完整 SQL 和参数；2. 查看执行计划；3. 估算扫描行与返回行；4. 检查索引和过滤条件；5. 消除函数、隐式转换、SELECT *；6. 优化深分页和临时表；7. 验证结果一致；8. 记录收益；优先改 SQL 和索引，再考虑数据库参数。应用层错误调用一千次接口，数据库参数无法解决。29.4 Schema 调优检查：  主键是否趋势递增；  大字段是否拆分；  索引是否冗余；  唯一约束是否完整；  状态值是否收敛；  历史数据是否归档；  表是否长期增长无界；  字符集是否统一。查看容量：SELECT  TABLE_NAME,  TABLE_ROWS,  ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,  ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mbFROM information_schema.TABLESWHERE TABLE_SCHEMA = 'shop'ORDER BY DATA_LENGTH + INDEX_LENGTH DESC;大表治理往往比参数更能改变长期性能。29.5 连接与线程查看：SHOW STATUS LIKE 'Threads%';SHOW STATUS LIKE 'Max_used_connections';SHOW VARIABLES LIKE 'max_connections';SHOW PROCESSLIST;常见问题：            现象      原因                  连接暴涨      应用无连接池或重试风暴              大量 Sleep      连接未释放或超时过长              Threads_running 高      并发执行过多              Threads_cached 低      连接频繁新建              Connection refused      超过 max_connections 或主机 blocked      处理：  应用连接池设置合理；  服务熔断和退避；  事务缩短；  避免单请求并发打多条 SQL；  只读流量隔离；  拆分大查询；  监控连接等待。29.6 内存与临时区重要参数：            参数      作用      风险                  innodb_buffer_pool_size      缓存数据页      过大导致 OOM              sort_buffer_size      排序内存      每会话可能分配              join_buffer_size      JOIN 缓冲      每会话可能分配              read_buffer_size      顺序读缓冲      会话相关              tmp_table_size      内部临时表上限      过大消耗内存              max_heap_table_size      内存表上限      与 tmp_table_size 共同生效      不要盲目调大 sort/join buffer。它们可能按会话或执行阶段分配，并发高时放大内存压力。查看临时表：SHOW GLOBAL STATUS LIKE 'Created_tmp%';29.7 InnoDB 关键参数            参数      调优方向                  innodb_buffer_pool_size      根据专用内存设置              innodb_log_file_size / innodb_redo_log_capacity      控制 Redo 容量和写入抖动              innodb_flush_log_at_trx_commit      持久性与性能取舍              sync_binlog      Binlog 持久化              innodb_io_capacity      匹配磁盘能力              innodb_flush_method      通常 O_DIRECT              innodb_page_size      初始化后不可改，需提前评估      核心主库不要为了压测分数放松 sync_binlog=1 和 innodb_flush_log_at_trx_commit=1。29.8 写入热点热点表现：  少数行锁等待高；  TPS 不升但延迟升；  单表写入集中；  自增页或计数行集中；  活动库存、直播间、热门帖子异常。处理：  合并写请求；  异步化；  Redis 预扣或缓冲；  计数拆分为多行；  按时间分桶；  消息队列削峰；  业务限流；  拆分热点租户；  定期落库。29.9 缓存策略常见模式：1. 读请求 -&gt; Redis2. 未命中 -&gt; MySQL3. 写 MySQL 成功4. 删除或更新 Redis注意事项：  缓存失效可能短暂不一致；  先写库后删缓存更常用；  缓存要有过期时间；  防穿透、防击穿、防雪崩；  热点 key 监控；  缓存只能降低读压力，不能替代索引；  对账修复不可少。29.10 参数调优流程1. 确认问题和目标；2. 记录当前参数和指标；3. 在预发压测；4. 一次只改一个关键参数；5. 观察延迟、吞吐、错误、资源；6. 检查主从延迟；7. 灰度生产；8. 保留回滚配置；9. 记录结论和适用条件；严禁把网上“秒杀调优模板”直接套到核心库。29.11 基准测试常用工具：  sysbench；  mysqlslap；  业务回放；  影子库压测；  云厂商压测服务。sysbench 准备：sysbench oltp_read_write \  --mysql-host=127.0.0.1 \  --mysql-port=3306 \  --mysql-user=root \  --mysql-password='...' \  --mysql-db=sbtest \  --tables=10 \  --table-size=1000000 \  prepare运行：sysbench oltp_read_write \  --mysql-host=127.0.0.1 \  --mysql-port=3306 \  --mysql-user=root \  --mysql-password='...' \  --mysql-db=sbtest \  --tables=10 \  --table-size=1000000 \  --threads=64 \  --time=300 \  --report-interval=10 \  run清理：sysbench oltp_read_write \  --mysql-host=127.0.0.1 \  --mysql-db=sbtest \  cleanup压测必须模拟真实读写比、数据分布、事务大小和连接模型。29.12 常见反模式            反模式      后果                  只看 QPS      忽略延迟和错误              盲目加索引      写入和 DDL 变慢              大事务      锁、Undo、复制延迟              所有读走从库      一致性问题              无连接池上限      连接风暴              用缓存掩盖全表扫描      缓存失效后雪崩              深分页      扫描大量行              一次改十个参数      无法归因              没有回滚      故障扩大              压测不预热      结论失真      本章小结性能调优应从业务指标和执行计划开始，优先解决 SQL、索引、数据模型和热点问题，再评估连接、内存、InnoDB、磁盘和架构。每个优化都要有基线、验证和回滚。参数只是调优的一部分，不是银弹。思考题  为什么 P99 延迟比平均值更适合描述用户体验？  如何判断慢在应用、数据库还是网络？  为什么不能盲目调大 sort buffer？  热点行更新有哪些拆分方案？  为一次上线设计完整的性能验证报告。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。当单实例容量、写入吞吐、连接数或维护窗口无法满足业务时，需要拆分数据。分库分表是最后的水位手段，必须先穷尽索引、SQL、架构和读写分离优化。28.1 什么时候需要分判断信号：            维度      信号                  容量      单表或单实例磁盘持续增长              写入      主库 CPU、IO、Redo、Binlog 达到上限              DDL      索引维护和大表变更窗口不足              备份      全量备份和恢复时间超过目标              复制      大事务造成长期延迟              热点      单表或单行写入不可拆              运维      单表行数影响统计和归档      不建议只按“单表超过 2000 万行”这类绝对值决策。行宽、索引数、访问模式、Buffer Pool 和磁盘能力都影响实际边界。28.2 先做垂直拆分垂直拆分按业务或字段拆：订单库：orders、order_items用户库：users、profiles商品库：products、skus营销库：coupons、activities字段垂直拆分：orders：高频核心字段order_details：低频详情、大字段优点：  边界清晰；  故障域隔离；  权限和安全更容易；  大字段不拖慢核心表；  适合微服务治理。局限：  不能解决单业务无限增长；  跨库 JOIN 变复杂；  需要服务边界治理；  分布式事务问题出现。28.3 水平分片水平分片把同一张表按规则拆到多个库或表：orders_00 -&gt; db_00orders_01 -&gt; db_01orders_02 -&gt; db_02orders_03 -&gt; db_03常见分片方式：            方式      规则      特点                  Hash      user_id % N      分布均匀，扩容迁移多              Range      按时间或 ID 区间      便于归档，易热点              一致性 Hash      环形映射      减少迁移              基因法      ID 中嵌入分片基因      避免二次查询              查表      保存分片映射表      灵活，映射表自身要高可用              地理位置      按城市 / 区域      本地化，跨区查询复杂      28.4 分片键选择分片键应满足：  大多数核心查询都带它；  写入分布均匀；  业务生命周期内稳定；  不容易变更；  能表达数据归属；  与交易边界一致。常见选择：            业务      通常分片键                  电商订单      user_id 或 seller_id              支付流水      payment_no / merchant_id              聊天消息      conversation_id              IoT 时序      device_id + time              SaaS 多租户      tenant_id              日志检索      time + source      矛盾场景：订单既要按买家查，又要按卖家查。常见方案：  以买家为主分片；  卖家订单异步同步到卖家维度表；  后台运营查询走分析库；  全局索引服务；  双写投影，接受同步延迟。不要把两个维度都作为强一致实时查询路径。28.5 全局 ID分片后自增 ID 不能直接保证全局唯一。常见方案：            方案      说明      风险                  号段模式      数据库批量发号      中心依赖              雪花 ID      时间 + 机器 + 序列      时钟回拨              UUID      全局唯一      空间大、无序              Redis INCR      高性能      持久化和中心依赖              分片前缀      ID 带分片信息      泄露拓扑      推荐：内部主键：趋势递增 BIGINT业务单号：带日期、类型、机器位或随机位对外 ID：随机 public_id雪花 ID 必须处理时钟回拨、机器分配、监控和告警。28.6 跨片查询按非分片键查询：SELECT *FROM ordersWHERE order_no = '20260825000001';如果分片键是 user_id，系统需要先知道 order_no 在哪个分片。方案：  订单号嵌入用户 ID 基因；  保存全局索引表；  使用搜索引擎；  异步写投影表；  广播所有分片；  限制后台广播查询。广播查询代价：1 条查询 -&gt; N 个分片 -&gt; N 份执行计划 -&gt; N 份网络往返必须设置超时、并发限制和结果归并上限。28.7 跨片事务尽量避免跨片事务。设计原则：  一个交易边界内只操作一个分片；  其他动作使用本地状态表；  事务消息驱动最终一致；  幂等消费；  定时对账；  人工补偿入口。本地消息表示例：CREATE TABLE local_message_outbox (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    message_id VARCHAR(64) NOT NULL,    aggregate_type VARCHAR(32) NOT NULL,    aggregate_id VARCHAR(64) NOT NULL,    payload JSON NOT NULL,    status VARCHAR(16) NOT NULL,    retry_count INT UNSIGNED NOT NULL DEFAULT 0,    next_retry_at DATETIME(3) NOT NULL,    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_message_id (message_id),    KEY idx_status_next_retry (status, next_retry_at)) ENGINE=InnoDB;业务写入和 outbox 同库同事务，发送器读取 outbox 投递消息。28.8 中间件路由常见方式：            方式      代表      特点                  客户端 SDK      ShardingSphere-JDBC      少一层代理，语言绑定              代理层      ShardingSphere-Proxy、MyCat      语言无关，链路长              服务层路由      微服务自行路由      灵活，业务侵入              云原生分发      云厂商分布式数据库      托管，锁定能力不同      中间件要评估：  SQL 兼容度；  事务语义；  读写分离；  连接数；  元数据管理；  监控追踪；  DDL 支持；  社区和版本。28.9 迁移总体流程1. 明确目标：容量、性能、成本、安全、运维；2. 设计目标结构和路由；3. 评估 SQL 和事务改造成本；4. 搭建目标集群；5. 全量迁移；6. 增量同步；7. 数据校验；8. 灰度读；9. 双写或写切换；10. 观察和回滚窗口；11. 下线旧链路；每个阶段都要有 checkpoints、指标和回滚方案。28.10 全量与增量迁移全量按分片键或主键分片读取：SELECT *FROM ordersWHERE id &gt; :last_idORDER BY idLIMIT 1000;要求：  任务可断点续跑；  每批记录水位；  目标端幂等插入；  限速；  失败可重试。增量使用 Binlog CDC：旧库 Binlog -&gt; 解析 -&gt; 按新分片键写入目标库需要处理：  DDL；  INSERT / UPDATE / DELETE；  事务边界；  乱序；  重复；  位点持久化；  延迟；  大事务；  主从切换。28.11 双写切流双写流程：阶段一：写旧库，CDC 同步新库；阶段二：写旧库为主，同时写新库，新库失败记录；阶段三：校验一致，读灰度到新库；阶段四：写新库为主，反向同步旧库；阶段五：确认稳定，下线旧库；关键点：  以旧库为准或以新库为准必须唯一；  双写失败要有补偿队列；  不能让两个都成功的路径分叉；  写请求带版本或更新时间；  删除和状态回退要能同步；  保留反向回滚链路。28.12 数据校验校验维度：            类型      示例                  结构      表、列、索引、字符集              数量      总行数、分片行数              聚合      SUM 金额、COUNT 状态              抽样      主键详情对比              业务      订单和明细关联              时效      增量延迟              约束      唯一键冲突              性能      核心接口耗时      示例：SELECT  COUNT(*) AS row_count,  SUM(total_amount) AS amount_sum,  MAX(updated_at) AS max_updatedFROM ordersWHERE created_at &gt;= '2026-08-01';两边同口径执行，不能直接比较未对齐时间窗口的数据。28.13 切换与回滚切换前检查：1. 增量延迟低于阈值；2. 数据校验通过；3. 核心接口压测通过；4. 回滚开关可用；5. 监控和告警就绪；6. 值班人员确认；7. 业务低峰窗口确认；回滚方式：  配置中心切回旧库；  新库写入反向同步旧库；  修复分叉数据；  冻结新增数据；  人工校验后再放量。回滚必须在设计时验证过，不能在事故现场第一次尝试。本章小结分库分表先从业务边界和字段垂直拆分开始，再按稳定高频维度水平分片。分片键决定查询、事务和扩容成本，跨片查询应通过投影、基因或全局索引治理。数据迁移必须包含全量、增量、校验、灰度、切换和反向回滚，不能只做一次性复制。思考题  为什么不能只用行数判断是否分表？  分片键应该满足哪些条件？  买家维度和卖家维度查询冲突时如何设计？  双写切流如何避免数据分叉？  写一个分片迁移项目的里程碑和回滚清单。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。备份是最后一道防线。高可用可以处理机器故障，却无法自动处理误删表、错误更新、逻辑 Bug、勒索加密和版本缺陷。备份的价值只有在成功恢复后才存在。27.1 备份目标设计备份前先明确：            指标      问题                  RPO      最多接受丢多久数据              RTO      多久必须恢复业务              保留周期      天、周、月、年分别保留多久              恢复粒度      实例、库、表、行              恢复位置      原实例、临时实例、新集群              加密要求      备份和传输如何加密              演练频率      多久验证一次      没有恢复时间目标的备份方案只是文件堆积。27.2 备份分类            类型      说明      优点      缺点                  逻辑备份      导出 SQL 或数据行      可读、跨版本较灵活      恢复慢、体积可能较大              物理备份      复制数据文件和日志      恢复快、贴近生产      版本和平台要求严格              快照备份      存储层一致性快照      速度快      依赖存储语义              增量备份      只备份变化      体积小      恢复链路复杂              Binlog      记录增量变更      支持时间点恢复      不能单独作为全量      常见组合：每日物理全量 + Binlog 持续归档每周核心表逻辑备份每月恢复演练每年归档备份离线保存27.3 mysqldump适合中小库、表级恢复和跨环境迁移。全库备份：mysqldump -h127.0.0.1 -P3306 -uroot -p \  --single-transaction \  --source-data=2 \  --routines \  --triggers \  --events \  --all-databases \  &gt; full_backup.sql单库备份：mysqldump -h127.0.0.1 -uroot -p \  --single-transaction \  --source-data=2 \  --routines --triggers --events \  shop &gt; shop_backup.sql只备份结构：mysqldump -h127.0.0.1 -uroot -p --no-data shop &gt; shop_schema.sql只备份数据：mysqldump -h127.0.0.1 -uroot -p --no-create-info shop &gt; shop_data.sql关键参数：            参数      说明                  --single-transaction      使用一致性快照，避免 InnoDB 长锁表              --source-data=2      输出备份位点为注释              --routines      备份存储过程和函数              --triggers      备份触发器              --events      备份事件              --set-gtid-purged=OFF      恢复到独立实例时可避免 GTID 干扰      注意：备份时不要加入无关业务流量；大库逻辑备份会拉长备份窗口并影响复制。27.4 恢复 mysqldump创建目标库：CREATE DATABASE shop_recovery  DEFAULT CHARACTER SET utf8mb4;恢复：mysql -h127.0.0.1 -P3307 -uroot -p shop_recovery &lt; shop_backup.sql推荐恢复到临时实例，而不是直接覆盖生产库。恢复后验证：SELECT TABLE_NAME, TABLE_ROWSFROM information_schema.TABLESWHERE TABLE_SCHEMA = 'shop_recovery';抽样对比：SELECT COUNT(*), SUM(total_amount)FROM shop_recovery.ordersWHERE created_at &gt;= '2026-08-01';27.5 mydumper / myloader大逻辑备份可以使用 mydumper：mydumper \  --host=127.0.0.1 \  --user=root \  --password='...' \  --database=shop \  --threads=8 \  --trx-consistency-only \  --outputdir=/backup/shop恢复：myloader \  --host=127.0.0.1 \  --user=root \  --password='...' \  --directory=/backup/shop \  --threads=8优点：  多线程导出导入；  表级文件分离；  便于压缩和并行恢复；  支持大库分片处理。27.6 物理备份XtraBackupPercona XtraBackup 是常见开源物理备份工具：xtrabackup \  --backup \  --target-dir=/backup/base \  --user=root \  --password='...' \  --socket=/var/lib/mysql/mysql.sock准备备份：xtrabackup --prepare --target-dir=/backup/base恢复：systemctl stop mysqlxtrabackup --copy-back --target-dir=/backup/basechown -R mysql:mysql /var/lib/mysqlsystemctl start mysql物理备份必须与 MySQL 版号、页大小和平台兼容，恢复前阅读对应版本文档。Clone PluginMySQL 8.0 官方克隆插件适合搭建从库：INSTALL PLUGIN clone SONAME 'mysql_clone.so';CREATE USER 'clone_user'@'10.0.0.11' IDENTIFIED BY 'Clone123456!';GRANT BACKUP_ADMIN ON *.* TO 'clone_user'@'10.0.0.11';CLONE INSTANCE FROM  'clone_user'@'10.0.0.11':3306  IDENTIFIED BY 'Clone123456!';查看状态：SELECT * FROM performance_schema.clone_status;SELECT * FROM performance_schema.clone_progress;存储快照云盘快照必须保证：  InnoDB 处于一致性状态；  文件系统刷盘语义正确；  快照包含 Redo 和 Undo；  Binlog 位点可追踪；  恢复后通过表校验。不能假设任意时刻拷贝 .ibd 文件就是可用备份。27.7 时间点恢复流程：1. 恢复全量备份到临时实例；2. 找到备份对应 Binlog 位点或 GTID；3. 重放 Binlog 到目标时间前；4. 校验数据；5. 导出需要恢复的表；6. 人工确认后写回；示例：mysqlbinlog --no-defaults \  --start-position=154 \  --stop-position=987654 \  mysql-bin.000010 mysql-bin.000011 \  | mysql -h127.0.0.1 -P3307 -uroot -p shop_recovery误操作恢复思路：-- 在临时实例建反向表CREATE TABLE orders_recovered LIKE orders;从 Binlog 解析误删前的 Write_rows，或导出误删表快照，再按业务主键合并。27.8 备份验证每次恢复演练至少验证：            验证项      方法                  实例可用      SELECT 1              表结构      SHOW CREATE TABLE              行数      核心表 COUNT              金额      SUM / 对账              索引      SHOW INDEX              关键数据      抽样对比              Binlog 位点      SHOW BINARY LOG STATUS              应用连通      应用影子环境连接              恢复耗时      全流程计时              备份完整性      校验和或压缩测试      自动化验证脚本应输出：备份文件大小开始结束时间源实例位点恢复实例版本核心表行数校验结果恢复耗时负责人27.9 备份安全  传输使用 TLS；  存储使用对象存储服务端加密和客户自主密钥；  备份账号只授予必要权限；  生产库凭据放密钥系统；  访问审计；  防止恢复脚本泄漏密码；  异地保存；  设置对象版本和防删锁；  定期清理但仍满足保留策略；  测试密钥可用性。备份密码、云 KMS 权限和数据库凭据必须分开管理，避免一个账号同时能删数据和删备份。27.10 备份调度示例策略：            频率      内容                  每 15 分钟      Binlog 归档              每日      物理全量或快照              每周      核心库逻辑备份              每月      恢复演练              每季度      异地恢复演练              每年      合规归档      调度必须避开：  业务高峰；  大型批处理；  DDL 窗口；  备份从库正在追赶复制；  其他备份任务重叠。27.11 常见故障            问题      原因      处理                  备份期间主从延迟      IO 压力或大事务      低峰执行、限速、换专用从库              恢复到一半失败      SQL 错误、字符集、权限      保存日志，先在临时实例重放              Binlog 缺失      被提前清理      检查保留策略，寻找归档              备份文件损坏      传输或磁盘错误      校验和、多副本              GTID 冲突      恢复实例已有事务      使用干净实例和正确参数              恢复后数据不一致      位点错误      重新确认备份位点              压缩无法解压      工具版本      保留工具版本记录      本章小结备份方案应围绕 RPO 和 RTO 设计，常见组合是全量物理备份加 Binlog 归档。逻辑备份适合小库和表级恢复，物理备份适合快速重建实例。所有备份都必须恢复到临时环境并验证行数、金额、结构和位点，否则不能称为可用备份。思考题  mysqldump --single-transaction 的作用是什么？  为什么不能直接拷贝运行中的 .ibd 文件作为备份？  时间点恢复需要哪些输入？  如何验证一个备份真的可恢复？  设计一个每日备份和月度演练的自动化流程。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。高可用不是安装一个工具，而是围绕故障检测、数据边界、流量切换、应用重试和演练建立的一整套能力。目标是让故障可预测、可控制、可恢复。26.1 明确目标上线前必须明确：            指标      问题                  RPO      最多接受丢多少数据              RTO      最多接受多久不可写              可用性目标      99.9%、99.99% 分别对应多少停机时间              故障域      机架、机房、区域如何隔离              读写一致性      哪些读必须强一致              运维边界      自动切换还是人工确认      可用性时间参考：            目标      年停机预算                  99.9%      约 8 小时 46 分钟              99.99%      约 52 分钟              99.999%      约 5 分钟      目标越高，越要求自动检测、自动隔离和成熟演练，而不是更复杂的口头流程。26.2 典型架构一主一从App -&gt; Primary         | async replication       Replica适合：  小规模系统；  RPO 可容忍少量丢失；  故障时可人工切换；  备份和报表走从库。一主两从半同步App -&gt; Primary         | semi-sync       Replica A（另一个故障域）       Replica B（延迟副本 / 备份）适合核心在线业务，RPO 要求明显高于普通异步。InnoDB ClusterApp -&gt; MySQL Router         -&gt; MGR Primary         -&gt; MGR Replica x2适合自动故障切换、读写端口分离和较少人工介入的场景。分片高可用App -&gt; Shard Proxy       -&gt; shard 1 HA group       -&gt; shard 2 HA group       -&gt; shard N HA group适合写入超过单机容量、可以按业务键拆分的系统。26.3 故障检测检测信号包括：  MySQL 进程存活；  端口连通性；  SELECT 1；  只读状态；  复制状态；  GTID 位点；  磁盘空间；  主从延迟；  延迟分布；  事务错误率。仅检测端口存活不够。进程存在但磁盘满、只读、锁死或复制断裂时，服务已经不可用。推荐健康检查：SELECT 1;SELECT @@read_only, @@super_read_only;SHOW REPLICA STATUS;26.4 故障切换流程安全切换流程：1. 检测原主异常；2. 确认异常不是网络分区；3. 阻断旧主写入，避免脑裂；4. 选择数据最新的候选节点；5. 等候选节点追平或达到 RPO 决策；6. 提升候选节点为主；7. 打开写流量；8. 其他节点指向新主；9. 恢复旧主并重新加入；10. 校验数据和应用状态；最关键的是第 3 步：必须先隔离旧主，再提升新主。隔离方式：  修改防火墙；  交换机端口禁用；  云安全组；  分布式锁和运维平台；  STONITH / 电源控制；  MySQL Router 下线目标；  应用配置中心摘除。26.5 候选节点选择评估候选节点：  GTID 集合是否最新；  复制延迟；  已接收未应用的 Relay Log；  机器规格；  剩余容量；  机房位置；  历史稳定性；  是否承载特殊流量。查看 GTID：SELECT @@GLOBAL.gtid_executed;比较集合是否包含其他候选节点。数据旧的节点不能因为网络更容易访问而被优先提升。26.6 路由层常见方式：            方式      特点                  MySQL Router      官方，适合 InnoDB Cluster              ProxySQL      规则丰富，读写分离强              HAProxy + Keepalived      简单稳定，需外部感知主从              服务发现      灵活，依赖平台能力              DNS      有缓存延迟，不适合快速切换              应用配置中心      可控，需发布或动态刷新      应用还必须处理：  连接池重建；  事务中断重试；  读写路由区分；  半开连接；  切换期间的熔断；  幂等写入。高可用的最后一公里在应用代码里。26.7 防止脑裂脑裂：旧主认为自己可写；新主也被提升为可写；两个主同时接收写入；后果：  数据分叉；  主键冲突；  复制无法继续；  修复成本极高。防护：  切换前强制隔离旧主；  候选节点必须只读等待提升；  依赖多数派仲裁；  应用路由和数据库状态双确认；  人工紧急切换也必须双人复核；  定期演练。26.8 洪泛保护和过载保护主库故障恢复后可能遇到：  积压连接；  缓存失效导致读洪峰；  从库追复制占用 IO；  Buffer Pool 冷；  重试风暴。策略：  连接池限流；  应用退避和抖动；  逐步放量；  关闭非核心任务；  先预热再放量；  对只读故障降级；  保护交易主链路。26.9 备份与高可用高可用副本不能替代备份：            场景      高可用是否有用                  机器故障      有              网络故障      有              误删表 / 误更新      副本会快速重放错误              逻辑 bug 写坏数据      副本同样写坏              勒索加密      副本可能同样受影响              版本升级缺陷      可能全部受影响      必须有独立备份、离线或对象存储副本、保留窗口和恢复演练。26.10 演练设计至少每季度演练：            演练      目标                  主库进程崩溃      自动检测和切换              网络分区      防脑裂和多数派行为              从库延迟      读写路由降级              磁盘满      预警和快速扩容              误删表      时间点恢复              机房级故障      异地接管              应用重试风暴      限流和熔断      演练必须记录：  检测时间；  决策时间；  切换时间；  恢复时间；  丢事务数量；  错误率变化；  人工介入点；  改进项和负责人。26.11 故障切换 Runbook事故名称：MySQL 主库不可用1. 确认现象   - 应用错误率、连接失败、慢查询   - 主库进程、端口、健康检查2. 通告   - 值班群、业务负责人、事故频道3. 初步决策   - 只读故障：切读或降级   - 不可写：准备切换   - 网络抖动：观察短窗口4. 切换前检查   - 候选 GTID 最新   - 延迟范围   - 磁盘和容量   - 只读状态5. 隔离旧主   - 路由摘除   - 防火墙阻断写6. 提升新主   - 关闭 read_only   - 修改路由   - 应用验证7. 恢复复制   - 其他从库指向新主   - 观察延迟8. 事后   - 检查数据   - 分析根因   - 补齐监控   - 更新演练本章小结高可用设计先明确 RPO 和 RTO，再选择异步、半同步、组复制或分片架构。切换必须先隔离旧主，再提升数据最新的候选节点。路由层和应用重试是切换能否真正生效的关键。高可用不能替代备份，所有方案都要通过真实演练验证。思考题  RPO 和 RTO 分别决定什么架构选择？  为什么故障切换前必须隔离旧主？  如何判断候选从库的数据最新？  应用在主从切换时需要处理哪些问题？  写一份你们系统的数据库不可用 Runbook。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。异步复制性能好但可能丢事务；半同步复制让主库至少等待从库确认接收日志；组复制则通过协议多数派共识，提供更强的自动协调能力。25.1 复制一致性等级            模式      主库是否等待      RPO      复杂度                  异步      不等待      可能丢未传输事务      低              半同步      等待至少一个从库收到 Binlog      通常可避免 MySQL 崩溃丢接收前事务      中              组复制      事务提交依赖多数派认证      多数派存活时一致性更强      高              强一致分布式协议      多数派持久化      取决于协议和部署      最高      RPO 是目标恢复点，表示最多可接受丢多少数据；RTO 是恢复时间目标。选型前先回答这两个指标，而不是只看技术名词。25.2 半同步复制流程AFTER_SYNC 流程：1. 主库提交事务；2. 写 Binlog；3. 从库接收并写 Relay Log；4. 从库 ACK；5. 主库持久化提交并返回客户端；AFTER_SYNC 在从库应用前确认接收，可减少过期读，也称为 lossless semi-sync。AFTER_COMMIT 流程：1. 主库提交事务；2. 等待从库 ACK；3. 主库返回客户端；如果事务已在主库对其他会话可见而从库未 ACK，极端故障下可能出现读感知差异。25.3 半同步配置安装插件：INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';MySQL 8.0 插件名可能使用新的别名：INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';主库：SET GLOBAL rpl_semi_sync_source_enabled = ON;SET GLOBAL rpl_semi_sync_source_timeout = 3000;SET GLOBAL rpl_semi_sync_source_wait_point = 'AFTER_SYNC';从库：SET GLOBAL rpl_semi_sync_replica_enabled = ON;STOP REPLICA IO_THREAD;START REPLICA IO_THREAD;查看：SHOW STATUS LIKE 'Rpl_semi_sync_source_status';SHOW STATUS LIKE 'Rpl_semi_sync_source_yes_tx';SHOW STATUS LIKE 'Rpl_semi_sync_source_no_tx';SHOW STATUS LIKE 'Rpl_semi_sync_source_clients';25.4 半同步降级如果 timeout 内没有从库 ACK：主库可降级为异步；查看降级：SHOW STATUS LIKE 'Rpl_semi_sync_source_status';SHOW STATUS LIKE 'Rpl_semi_sync_source_no_tx';风险：  半同步状态可能是动态的；  网络抖动导致性能毛刺；  降级期间的事务仍可能丢；  应用无法感知复制模式；  故障切换仍需外部仲裁。生产必须告警 no_tx 增长和半同步状态变化。25.5 半同步的误区误区一：半同步代表从库已完成重放。实际上它通常只保证日志接收，不保证从库可读。误区二：有半同步就不会丢数据。主库磁盘损坏、ACK 后从库损坏、错误切换、降级窗口都可能影响 RPO。误区三：等待 ACK 一定能提升数据安全。如果 ACK 节点与主库同机房同时故障，价值有限。正确做法：  至少一个 ACK 从库在不同故障域；  使用 AFTER_SYNC；  设置合理超时；  监控降级；  切换前校验 GTID 集合；  定期演练故障。25.6 组复制概述MySQL Group Replication（MGR）是 MySQL 官方的高可用方案：应用  -&gt; 主节点     -&gt; Group Communication        -&gt; 多数派成员认证和复制核心机制：  事务在主节点执行；  通过组通信广播；  多数派成员达成一致；  冲突检测和认证；  各节点按相同顺序应用；  故障检测和成员变更。25.7 单主与多主            模式      说明      适合                  Single-Primary      组内只有一个主节点可写      大多数在线业务              Multi-Primary      所有成员可写      冲突少、多写需求明确的业务      单主优点：  语义接近传统主从；  冲突少；  运维简单；  应用改造成本低。多主风险：  事务冲突认证失败；  死锁跨节点；  热点写放大；  应用必须处理新错误；  需要严格控制表主键和业务分片。25.8 MGR 配置要求基础要求：  InnoDB 表必须有主键；  开启 GTID 和 Binlog；  ROW 格式；  所有成员版本兼容；  网络低延迟且稳定；  多数派成员可用；  服务器 UUID 唯一；  事务隔离级别推荐 RC；  外键和级联需谨慎；  停止复制时避免写不可认证数据。示例配置：[mysqld]server_id = 1gtid_mode = ONenforce_gtid_consistency = ONlog_bin = mysql-binbinlog_format = ROWlog_replica_updates = ONtransaction_isolation = READ-COMMITTEDplugin_load_add = 'group_replication.so'group_replication_group_name = '11111111-2222-3333-4444-555555555555'group_replication_start_on_boot = OFFgroup_replication_bootstrap_group = OFF首个节点启动组：SET GLOBAL group_replication_bootstrap_group = ON;START GROUP_REPLICATION;SET GLOBAL group_replication_bootstrap_group = OFF;其他节点加入：START GROUP_REPLICATION;查看：SELECT * FROM performance_schema.replication_group_members;SELECT * FROM performance_schema.replication_group_member_stats;25.9 MGR 故障检测组复制依赖多数派判断成员故障：  成员之间心跳；  超时后被怀疑；  多数派达成一致；  移除故障成员；  单主模式触发选主。关键参数：SHOW VARIABLES LIKE 'group_replication_member_expel_timeout';SHOW VARIABLES LIKE 'group_replication_unreachable_majority_timeout';脑裂防护：  多数派不可达时只读；  使用至少 3 个或 5 个节点；  跨机房按故障域分布；  避免偶数成员导致的多数派风险；  外部仲裁不能替代正确部署。25.10 InnoDB ClusterInnoDB Cluster 是官方组合方案：MySQL Shell  + MGR  + MySQL Router组件职责：            组件      职责                  MGR      数据复制、成员管理、故障检测              MySQL Router      读写端口、读端口、故障切换路由              MySQL Shell      集群创建、配置、状态检查      典型端口：6446：读写6447：只读查看集群：dba.getCluster().status();MySQL Router 自动感知拓扑，但应用仍要处理短暂连接失败和事务重试。25.11 方案对比            需求      推荐方案                  小规模、可人工介入      异步主从 + 备份              降低丢失窗口      半同步              自动高可用      MGR / InnoDB Cluster              超大规模写      业务分片 + 每分片高可用              异地容灾      独立集群 + 复制，避免低延迟共识跨地域              强一致多主      谨慎评估业务冲突模型      不要用多副本代替备份，也不要用高可用代替容量规划。本章小结半同步通过等待从库 ACK 降低丢失窗口，但可能降级且不保证从库已应用。组复制通过多数派协议和认证机制提供自动成员管理，InnoDB Cluster 在 MGR 上叠加 Router 和 Shell 管理能力。方案选择必须围绕 RPO、RTO、网络质量和应用冲突模型。思考题  半同步的 ACK 代表从库已应用事务吗？  AFTER_SYNC 和 AFTER_COMMIT 有什么区别？  MGR 为什么要求数据表必须有主键？  单主和多主分别适合什么业务？  设计一个跨机房 MGR 部署的故障域方案。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。主从复制让一份数据拥有多个副本，用于容灾、读写隔离、异地容灾和水平扩展读能力。它也带来延迟、一致性和运维复杂度。24.1 复制的基本流程异步复制流程：主库提交事务  -&gt; 写 Binlog  -&gt; Dump Thread 推送 Binlog  -&gt; 从库 Receiver Thread 写 Relay Log  -&gt; 从库 Applier Thread 重放 Relay Log  -&gt; 从库数据更新关键点：  主库提交不等待从库应用；  从库可能落后；  主从间传输的是 Binlog 事件；  重放顺序要保证事务一致性；  从库位点必须持久可靠。24.2 位点与 GTID文件位点mysql-bin.000010:123456查看主库：SHOW BINARY LOG STATUS;查看从库：SHOW REPLICA STATUS\G老版本使用：SHOW SLAVE STATUS\GGTID3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100GTID 让事务有全局身份，主从切换和重建更容易。配置示例：[mysqld]gtid_mode = ONenforce_gtid_consistency = ONlog_bin = mysql-binlog_replica_updates = ONread_only = ONsuper_read_only = ON24.3 搭建主从主库创建账号：CREATE USER 'repl'@'10.0.%' IDENTIFIED BY 'Repl123456!'  REQUIRE SSL;GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.%';备份主库：mysqldump -h10.0.0.11 -uroot -p \  --single-transaction \  --source-data=2 \  --routines --triggers --events \  --all-databases &gt; primary_backup.sql恢复到从库：mysql -h10.0.0.12 -uroot -p &lt; primary_backup.sqlGTID 自动定位：CHANGE REPLICATION SOURCE TO  SOURCE_HOST='10.0.0.11',  SOURCE_USER='repl',  SOURCE_PASSWORD='Repl123456!',  SOURCE_AUTO_POSITION=1,  SOURCE_SSL=1;START REPLICA;检查：SHOW REPLICA STATUS\G必须确认：Replica_IO_Running: YesReplica_SQL_Running: YesSeconds_Behind_Source: 0Last_IO_Error:Last_SQL_Error:24.4 复制线程            线程      职责                  Binlog Dump      主库推送 Binlog              Replica I/O      从库接收并写 Relay Log              Replica SQL      从库应用 Relay Log              Coordinator      并行复制协调器              Worker      并行应用事务      查看线程：SHOW PROCESSLIST;IO 停说明网络、权限或 Binlog 位点问题；SQL 停说明应用冲突、表结构差异或数据不一致。24.5 异步复制默认模式：主库提交完成即返回客户端；优点：  延迟低；  主库不依赖从库；  结构简单。风险：  主库崩溃时未传事务丢失；  自动切换需要额外仲裁；  不适合零 RPO 场景；  异地延迟会影响更强同步方案。24.6 并行复制传统单线程应用是复制延迟的常见瓶颈。MySQL 8.0 支持基于组提交的逻辑时钟并行：SHOW VARIABLES LIKE 'replica_parallel_type';SHOW VARIABLES LIKE 'replica_parallel_workers';SHOW VARIABLES LIKE 'binlog_transaction_dependency_tracking';配置示例：replica_parallel_type = LOGICAL_CLOCKreplica_parallel_workers = 8binlog_transaction_dependency_tracking = WRITESET适用：  主库并发高；  事务粒度小；  写入模型可并行；  从库 CPU 和 IO 有余量。局限：  单个大事务仍无法拆开；  热点行顺序限制并行度；  Worker 不是越大越好；  主库必须提供依赖信息。24.7 主从延迟常见原因：            来源      原因                  主库      大事务、高频写入、Binlog 洪峰              网络      带宽不足、跨地域 RTT              从库      单线程应用、机器规格差、IO 慢              查询      从库重查询、锁等待              表结构      从库缺少索引导致重放慢              资源      CPU、内存、磁盘瓶颈      排查：SHOW REPLICA STATUS\G重点：Seconds_Behind_SourceReplica_IO_RunningReplica_SQL_RunningLast_SQL_ErrorRetrieved_Gtid_SetExecuted_Gtid_Set更精细的 Performance Schema：SELECT *FROM performance_schema.replication_applier_status_by_worker;Seconds_Behind_Source 有局限：网络长时间断开、时钟差异或状态更新时机都可能让它不准确。需要结合位点差、GTID 差和心跳表监控。24.8 复制过滤过滤可以只复制指定库或表：replicate_do_db = shopreplicate_ignore_db = audit_log风险：  GTID 一致性管理复杂；  容易造成主从数据差异；  切换后拓扑混乱；  备份和恢复范围不完整；  应用和数据库过滤规则不完全等价。高可用集群内节点应尽量保持完整复制。过滤更适合同步特定数据的下游只读节点，并且要有明确标识。24.9 读写分离典型应用：写 -&gt; 主库强一致读 -&gt; 主库或延迟确认的从库可容忍旧数据读 -&gt; 从库一致性问题：1. 写主库；2. 立即读从库；3. 从库还没应用；4. 用户看到旧数据；处理方式：  写后固定时间读主库；  会话粘性；  关键路径读主库；  使用 GTID 等待从库追上；  版本号或缓存失效；  明确业务可接受的旧数据窗口。从库执行等待示例：SELECT WAIT_FOR_EXECUTED_GTID_SET('&lt;gtid_set&gt;', 1);不要把所有读都无条件打到从库，也不要把所有读都打到主库。24.10 主从一致性校验常用工具：  Percona pt-table-checksum；  pt-table-sync；  云厂商一致性校验；  自研抽样对账；  业务级指标对账。原则：  定期校验核心表；  低峰执行；  控制校验范围和频率；  发现差异先备份；  分析根因后修复；  禁止未确认的随意覆盖；  GTID 不代表数据一定一致。24.11 从库重建触发条件：  数据不一致；  位点丢失；  复制错误无法安全跳过；  从库磁盘损坏；  大范围 DDL 后需要重建；  主从延迟长期无法收敛。流程：1. 评估从库是否仍承载流量；2. 选择主库低峰备份；3. 使用物理备份或 Clone Plugin；4. 恢复到新节点；5. 配置 GTID 自动定位；6. 追平延迟；7. 校验一致性；8. 恢复流量；Clone Plugin 示例：INSTALL PLUGIN clone SONAME 'mysql_clone.so';CLONE INSTANCE FROM  'clone_user'@'10.0.0.11':3306  IDENTIFIED BY 'Clone123456!';24.12 常见复制错误1062 主键冲突原因可能是从库被写入、位点重复、数据损坏或双写。不要立刻跳过，先确认事务来源和影响。1032 记录不存在常见于 STATEMENT 复制、从库缺数据或手工修改。1396 权限错误检查复制账号、密码、SSL 和来源主机。网络中断确认 Replica_IO_Running 和错误日志，GTID 自动定位后通常可重连。处理原则：  保留日志和状态；  判断单事务差异还是整库差异；  能重建就重建；  跳过必须记录 GTID；  事后校验一致性；  补齐告警和防再发措施。本章小结主从复制通过 Binlog、Relay Log 和 Applier 线程传播数据。GTID 简化位点管理和切换，并行复制缓解应用瓶颈。读写分离必须明确一致性策略，延迟监控不能只依赖 Seconds_Behind_Source，核心数据要定期校验并具备快速重建能力。思考题  主从复制的三个关键线程分别做什么？  GTID 相比文件位点有什么优势？  主从延迟的常见来源有哪些？  读写分离后如何避免写后读旧数据？  设计一套从库延迟超阈值的应急处理流程。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Binlog 是 MySQL Server 层的逻辑变更日志，用于复制、时间点恢复、数据同步和审计。它和 InnoDB Redo 分工不同：Redo 服务崩溃恢复，Binlog 服务跨实例传播。23.1 Binlog 的用途            用途      说明                  主从复制      从库读取并重放主库变更              时间点恢复      全量备份 + Binlog 恢复到目标时刻              CDC      读取行变更同步 Redis、ES、Kafka、数仓              审计      分析谁在何时修改了什么              数据订阅      构建派生数据      Binlog 不属于单个存储引擎，因此 MyISAM 修改也会记录，但没有 InnoDB 事务保证。23.2 开启与查看查看：SHOW VARIABLES LIKE 'log_bin';SHOW VARIABLES LIKE 'binlog_format';SHOW VARIABLES LIKE 'server_id';SHOW BINARY LOGS;MySQL 8.0 默认开启 Binlog。如果是自建低版本环境，需要在配置文件中开启：[mysqld]server-id = 1log-bin = mysql-binbinlog_format = ROWbinlog_row_image = FULLbinlog_expire_logs_seconds = 604800max_binlog_size = 512M查看当前位点：SHOW BINARY LOG STATUS;老版本常使用：SHOW MASTER STATUS;23.3 Binlog 文件与事件Binlog 是追加式文件序列：mysql-bin.000001mysql-bin.000002mysql-bin.000003文件达到 max_binlog_size 或执行特定操作后切换。每个文件包含事件，事务事件按提交顺序写入。常用查看：SHOW BINLOG EVENTS IN 'mysql-bin.000001' LIMIT 20;命令行解析：mysqlbinlog --no-defaults mysql-bin.000001 &gt; events.sqlROW 格式可读化：mysqlbinlog --no-defaults \  --base64-output=decode-rows \  -vv mysql-bin.000001 &gt; decoded.sql按时间过滤：mysqlbinlog --no-defaults \  --start-datetime='2026-08-25 00:00:00' \  --stop-datetime='2026-08-25 12:00:00' \  mysql-bin.000001 &gt; recovery.sql23.4 三种格式            格式      记录内容      一致性      日志大小                  STATEMENT      SQL 语句      弱，受不确定函数影响      小              ROW      行前后镜像      强      较大              MIXED      自动选择      折中      中等      推荐：binlog_format = ROWROW 的行镜像模式：SHOW VARIABLES LIKE 'binlog_row_image';            值      行为                  FULL      记录所有列前后镜像              MINIMAL      只记录必要列和变更列              NOBLOB      尽量不记录未变 BLOB / TEXT      FULL 便于审计和下游同步；MINIMAL 减少日志量，但下游需要能理解镜像含义。23.5 Binlog 缓存事务写入 Binlog 前先使用内存缓存：SHOW VARIABLES LIKE 'binlog_cache_size';SHOW VARIABLES LIKE 'max_binlog_cache_size';SHOW GLOBAL STATUS LIKE 'Binlog_cache_disk_use';SHOW GLOBAL STATUS LIKE 'Binlog_cache_use';如果 Binlog_cache_disk_use 快速增长，说明大事务频繁落盘。优先拆分事务，不要只加大缓存。23.6 保留与清理查看保留策略：SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';SHOW VARIABLES LIKE 'expire_logs_days';MySQL 8.0 推荐使用 binlog_expire_logs_seconds。例如保留 7 天：binlog_expire_logs_seconds = 604800手工清理：PURGE BINARY LOGS TO 'mysql-bin.000010';PURGE BINARY LOGS BEFORE '2026-08-18 00:00:00';清理原则：  必须晚于最新备份和恢复所需位点；  必须保留所有从库尚未读取的日志；  保留故障切换可能需要的窗口；  不直接用操作系统命令删除文件；  磁盘告警要提前，不要等 100%。重置 Binlog 是高危操作，会改变复制和恢复上下文。MySQL 8.4 中相关语句为 RESET BINARY LOGS AND GTIDS，执行前必须确认整组实例和备份策略。23.7 Binlog 与 CDC典型链路：MySQL Binlog  -&gt; Canal / Debezium / Flink CDC     -&gt; Kafka        -&gt; Redis        -&gt; Elasticsearch        -&gt; ClickHouseROW 格式适合 CDC，因为可以拿到行级前后镜像。同步必须考虑：  DDL 变更顺序；  表结构版本；  事务边界；  乱序和重复；  下游幂等；  删除事件；  全量与增量衔接；  延迟监控；  主从切换后位点管理；  大事务导致的消息洪峰。23.8 时间点恢复场景：周一全量备份，周三 10:05 误删表。恢复流程：1. 恢复周一全量备份到临时实例；2. 找到全量备份对应 Binlog 位点或 GTID；3. 重放 Binlog 到误删前一刻；4. 校验临时实例数据；5. 导出误删表；6. 在人工确认后恢复生产表；示例：mysqlbinlog --no-defaults \  --start-position=154 \  --stop-position=100000 \  mysql-bin.000010 mysql-bin.000011 \  | mysql -h127.0.0.1 -P3307 -uroot -p shop_recovery注意：  先恢复到临时实例；  保存原始 Binlog；  记录目标时间前后的所有事件；  校验行数和抽样数据；  恢复动作本身也要可回滚；  演练过才算有恢复能力。23.9 Binlog 加密MySQL 8.0 支持 Binlog 加密：[mysqld]binlog_encryption = ON查看：SHOW VARIABLES LIKE 'binlog_encryption';同时必须管理：  keyring 组件；  密钥备份；  密钥轮换；  恢复环境可用性；  权限和审计。只加密文件不管理密钥，灾备时仍可能无法恢复。23.10 常见问题            问题      原因      处理                  Binlog 磁盘占满      保留策略过久、大事务      调整保留、拆事务、扩容              从库位点不存在      Binlog 被提前清理      重建从库或找其他源              CDC 丢事件      位点保存失败      事务性位点管理和重启校验              主从不一致      STATEMENT 不确定、手工写从库      ROW + 只读 + 校验              恢复慢      全量太大、Binlog 太多      分库、并行恢复、优化备份              大事务卡同步      单事务事件过大      拆分并限速      本章小结Binlog 是跨实例传播变更的核心日志，推荐 ROW 格式和明确的保留策略。它服务于复制、CDC 和时间点恢复，清理必须尊重备份链路和从库消费进度。大事务会放大 Binlog 缓存、日志体积和主从延迟。思考题  Binlog 和 InnoDB Redo 的定位有什么不同？  ROW 和 STATEMENT 分别适合什么场景？  Binlog_cache_disk_use 增长说明什么？  为什么不能随意删除 Binlog 文件？  写一份误删表的时间点恢复演练步骤。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Buffer Pool 是 InnoDB 的内存缓存区，也是 OLTP 性能的核心。数据页、索引页、Undo 页、Change Buffer 和自适应哈希都在这里协作，决定了请求是否需要访问磁盘。22.1 InnoDB 磁盘结构回顾InnoDB 逻辑对象包括：System Tablespace：系统元数据、Change Buffer、Doublewrite 等历史区域File-Per-Table Tablespace：每张表一个 .ibd 文件General Tablespace：多表共享表空间Undo Tablespace：回滚和 MVCC 版本Temporary Tablespace：临时表和内部排序聚合Redo Log：崩溃恢复日志查看：SELECT  space,  name,  file_typeFROM information_schema.INNODB_TABLESPACESLIMIT 20;现代部署通常开启：innodb_file_per_table = 1这样单表空间独立，DDL、迁移和空间治理更清晰。22.2 Buffer Pool 的作用InnoDB 以页为单位读写数据，默认 16KB：SHOW VARIABLES LIKE 'innodb_page_size';读取路径：SQL -&gt; 访问页 -&gt; Buffer Pool 命中 -&gt; 直接返回                 -&gt; 未命中 -&gt; 从磁盘读入 -&gt; 挂到 LRU修改路径：修改 Buffer Pool 中的页 -&gt; 生成 Redo -&gt; 页变成脏页 -&gt; 后台异步刷盘Buffer Pool 缓存的不只是业务数据：            页类型      说明                  Data / Index      数据和索引页              Undo      回滚和 MVCC              Change Buffer      二级索引写缓存              AHI      自适应哈希相关结构              System      数据字典等系统页      22.3 改进型 LRU朴素 LRU 的问题：  一次性全表扫描可能把热点页挤出去；  偶发查询污染缓存；  大报表影响在线查询；  缓存命中率抖动。InnoDB 将 LRU 分为两段：new sublist：热点区old sublist：冷区新读入的页先进入 old 区头部，满足以下条件才移动到 new 区：  第二次访问；  距第一次访问超过 innodb_old_blocks_time。查看：SHOW VARIABLES LIKE 'innodb_old_blocks_pct';SHOW VARIABLES LIKE 'innodb_old_blocks_time';这让短时间连续访问的大扫描仍可能停留在 old 区，减少对热点的冲击。22.4 Buffer Pool 配置查看大小：SHOW VARIABLES LIKE 'innodb_buffer_pool_size';SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';SHOW VARIABLES LIKE 'innodb_buffer_pool_chunk_size';在线调整：SET GLOBAL innodb_buffer_pool_size = 8 * 1024 * 1024 * 1024;大小建议：            场景      建议                  专用数据库服务器      可用内存的 50% 到 70%              混部服务器      根据其他组件内存上限评估              小规格容器      保守设置，避免 OOM              云托管      结合连接内存、排序和临时表限制      不要把机器内存全部给 Buffer Pool，还需要保留：  连接内存；  Sort Buffer；  Join Buffer；  临时表；  操作系统和文件系统缓存；  备份与监控进程；  内存增长余量。22.5 命中率与监控查看状态：SHOW STATUS LIKE 'Innodb_buffer_pool_read%';关键指标：            指标      含义                  Innodb_buffer_pool_read_requests      逻辑读请求              Innodb_buffer_pool_reads      必须从磁盘读页              Innodb_buffer_pool_pages_dirty      脏页数              Innodb_buffer_pool_pages_free      空闲页              Innodb_buffer_pool_pages_total      总页数      粗略命中率：WITH reads AS (  SELECT    MAX(CASE WHEN VARIABLE_NAME='Innodb_buffer_pool_reads'             THEN CAST(VARIABLE_VALUE AS DECIMAL(20,0)) END) AS disk_reads,    MAX(CASE WHEN VARIABLE_NAME='Innodb_buffer_pool_read_requests'             THEN CAST(VARIABLE_VALUE AS DECIMAL(20,0)) END) AS logical_reads  FROM performance_schema.global_status  WHERE VARIABLE_NAME IN (    'Innodb_buffer_pool_reads',    'Innodb_buffer_pool_read_requests'  ))SELECT  ROUND(100 * (1 - disk_reads / NULLIF(logical_reads, 0)), 2)    AS buffer_pool_hit_rate_pctFROM reads;解读：  命中率高不代表没有慢 SQL；  命中率突然下降要找大扫描；  新实例预热期命中率低是正常的；  重启后冷缓存需要预热；  读密集负载通常希望长期命中率高。22.6 脏页与刷盘查看：SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';SHOW GLOBAL STATUS LIKE 'Innodb_data_pending_fsyncs';SHOW GLOBAL STATUS LIKE 'Innodb_data_pending_writes';相关参数：SHOW VARIABLES LIKE 'innodb_io_capacity';SHOW VARIABLES LIKE 'innodb_io_capacity_max';SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';SHOW VARIABLES LIKE 'innodb_flush_neighbors';脏页过多的后果：  后台刷盘压力大；  用户线程可能被迫刷脏；  查询延迟尖刺；  checkpoint 推进受阻；  恢复时间变长。innodb_io_capacity 应与磁盘能力匹配，最终以压测和监控为准，而不是照抄模板。22.7 Change Buffer 与 AHIChange Buffer查看：SHOW VARIABLES LIKE 'innodb_change_buffering';SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';适合写多读少和非唯一二级索引。写后立即读、热点页常驻内存或唯一索引场景收益有限。Adaptive Hash Index查看：SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';SHOW STATUS LIKE 'Innodb_adaptive_hash%';AHI 由 InnoDB 自动维护，不能手动指定索引。高并发下可能成为争用点，是否关闭应通过对比测试决定。22.8 表空间与碎片查看表大小：SELECT  TABLE_NAME,  TABLE_ROWS,  ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,  ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb,  ROUND(DATA_FREE / 1024 / 1024, 2) AS free_mbFROM information_schema.TABLESWHERE TABLE_SCHEMA = 'shop'ORDER BY DATA_LENGTH + INDEX_LENGTH DESC;碎片常见来源：  随机主键；  大量删除；  可变长度频繁更新；  页分裂；  导入后重建索引。处理方式：OPTIMIZE TABLE orders;OPTIMIZE TABLE 会重建表并占用 IO，不要在高峰执行。更推荐容量规划、归档和新表迁移。22.9 存储引擎选择查看引擎：SELECT ENGINE, SUPPORT, COMMENTFROM information_schema.ENGINES;            引擎      特点      建议                  InnoDB      事务、MVCC、行锁、崩溃恢复      默认选择              MyISAM      表锁、无事务、全文历史方案      不建议新系统使用              Memory      内存表、重启丢失      临时性场景              CSV      CSV 文件      数据交换              Archive      压缩插入、查询受限      冷归档可评估              Blackhole      丢弃写入      复制测试      绝大多数互联网业务应默认 InnoDB，不要在核心链路混用非事务引擎。22.10 预热与重启重启后 Buffer Pool 是冷的，常见现象：  延迟升高；  CPU 等待 IO 增加；  命中率低；  磁盘读放大。预热方式：  逐步放开流量；  执行核心表主键和索引范围读取；  使用预热脚本加载热点数据；  云服务启用 Buffer Pool 预热能力；  主从滚动重启并等待追平；  避免同时重启多个节点。示例预热思路：SELECT id FROM orders WHERE id &gt; 0 ORDER BY id LIMIT 1000;应用按主键范围分批执行，并控制 QPS。本章小结Buffer Pool 是 InnoDB 的内存核心，通过改进型 LRU、脏页管理和后台刷盘平衡内存命中与持久化。专用实例应重点规划 Buffer Pool 大小，同时保留连接、排序、临时表和系统内存。存储引擎默认选择 InnoDB，它的事务、MVCC、行锁和崩溃恢复能力是生产可靠性的基础。思考题  InnoDB 为什么将 LRU 分为 new 和 old 两段？  命中率高是否代表系统没有慢查询？为什么？  脏页过多会带来哪些问题？  为什么 Buffer Pool 不应设置为机器全部内存？  设计一个主从滚动重启和预热方案。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。InnoDB Redo 和 Server 层 Binlog 是两份不同日志。如果一份成功、一份失败，主库和从库、恢复后的主库都可能不一致。内部两阶段提交就是为了解决这个问题。21.1 为什么需要两阶段提交假设事务提交时直接顺序执行，没有协调：崩溃场景一1. Redo 已持久化；2. Binlog 未写入；3. 实例崩溃；恢复后主库有该事务，Binlog 和从库没有，主从不一致。崩溃场景二1. Binlog 已持久化；2. InnoDB Redo 未持久化；3. 实例崩溃；恢复后主库没有该事务，从库重放了 Binlog，主从不一致。两阶段提交用状态和恢复规则保证 Redo 与 Binlog 对同一事务的结论一致。21.2 简化提交流程一个开启 Binlog 的 InnoDB 事务提交可以概括为：1. InnoDB Redo 写入 prepare 状态；2. 持久化 prepare Redo；3. 写 Binlog 并 fsync；4. InnoDB 写 commit Redo 并持久化；5. 清理事务状态和锁；不同版本实现细节有差异，但恢复规则稳定：prepare 事务：  Binlog 中存在该事务 -&gt; 提交；  Binlog 中不存在该事务 -&gt; 回滚；这样崩溃后主库结论与 Binlog 保持一致，从库也能得到相同结果。21.3 组提交 Group Commit多个事务同时提交时，可以合并刷盘：Leader 阶段：收集事务；Flush 阶段：写 Binlog / Redo；Sync 阶段：一次 fsync；Commit 阶段：各事务完成提交；收益：  多个事务共享一次 fsync；  提高写吞吐；  降低每次提交延迟；  延迟越高，合并效果越明显；  保持每个事务持久性语义。相关参数：SHOW VARIABLES LIKE 'binlog_group_commit_sync_delay';SHOW VARIABLES LIKE 'binlog_group_commit_sync_no_delay_count';SHOW VARIABLES LIKE 'sync_binlog';SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';binlog_group_commit_sync_delay 会主动等待一小段时间换取更多合并，可能增加单事务延迟，必须压测。21.4 双 1 配置生产核心库常见配置：sync_binlog = 1innodb_flush_log_at_trx_commit = 1含义：  每次提交 fsync Binlog；  每次提交 fsync InnoDB Redo；  MySQL 崩溃或操作系统崩溃后已提交事务不丢。代价：  每次提交至少触发关键刷盘；  对磁盘 fsync 能力敏感；  低延迟磁盘上收益明显；  高并发下依赖组提交摊薄成本。不建议直接关闭双 1 追求压测数字。若业务允许丢失少量事务，应明确 RPO，并只用于非核心实例。21.5 Binlog 格式查看：SHOW VARIABLES LIKE 'binlog_format';STATEMENT记录原始 SQL：UPDATE orders SET status='PAID' WHERE id=1;优点：  日志小；  可读性较好；  写放大较低。风险：  NOW()、UUID() 等不确定性结果；  LIMIT 无稳定排序时结果不确定；  依赖函数和触发器；  复制可能不一致。ROW记录行前后镜像：before: id=1, status=CREATEDafter:  id=1, status=PAID优点：  复制一致性最好；  对 SQL 语义依赖少；  支持更安全的并发复制；  适合 CDC。缺点：  大批量修改日志量大；  需要控制 binlog_row_image；  解析需要解析工具；  全镜像会放大存储和网络。查看行镜像：SHOW VARIABLES LIKE 'binlog_row_image';推荐在线业务默认 ROW + FULL 或按审计需求评估 MINIMAL。21.6 Binlog 事件查看日志：SHOW BINARY LOGS;SHOW MASTER STATUS;SHOW BINLOG EVENTS LIMIT 20;较新的 MySQL 8.x 版本推荐使用更清晰的状态命令；是否可用以当前版本文档为准：SHOW BINARY LOG STATUS;常见事件：            事件      说明                  Format_desc      文件格式描述              Query      STATEMENT SQL 或事务元信息              Table_map      ROW 格式表映射              Write_rows      插入行              Update_rows      更新行              Delete_rows      删除行              Xid      事务提交              GTID      GTID 事务标识      使用 mysqlbinlog：mysqlbinlog --no-defaults \  --base64-output=decode-rows \  -vv mysql-bin.000001 \  &gt; decoded.sql21.7 GTID开启 GTID 后，每个事务拥有全局唯一标识：3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100优点：  事务全局识别；  自动定位复制位点；  主从切换更简单；  防止重复重放同一事务；  故障排查更清晰。查看：SHOW VARIABLES LIKE 'gtid_mode';SHOW VARIABLES LIKE 'enforce_gtid_consistency';SELECT @@GLOBAL.gtid_executed;要求：  主从都正确配置；  禁止不安全事务；  升级需分步；  从库写入要严格控制；  运维工具要兼容 GTID。21.8 半同步与日志可靠性异步复制：主库提交后不等待从库；风险：主库故障时未接收 Binlog 的事务丢失。半同步复制：主库提交事务后，至少等待一个从库确认收到 Binlog；查看：SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled';SHOW STATUS LIKE 'Rpl_semi_sync_master_status';SHOW STATUS LIKE 'Rpl_semi_sync_master_no_tx';半同步保证“收到”，不等于“已经应用并对外可读”。它降低丢失窗口，但不是零 RPO 的完整方案。21.9 查看提交相关状态查看日志状态：SHOW GLOBAL STATUS LIKE 'Binlog%';SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';SHOW GLOBAL STATUS LIKE 'Innodb_log_write_requests';重点指标：            指标      含义                  Binlog_cache_disk_use      Binlog 缓存落盘次数              Binlog_cache_use      事务使用 Binlog 缓存次数              Innodb_log_waits      Redo 空间或刷盘等待              Innodb_os_log_written      Redo 写入量      如果 Binlog_cache_disk_use 高，说明大事务过多：SHOW VARIABLES LIKE 'binlog_cache_size';SHOW VARIABLES LIKE 'max_binlog_cache_size';优先拆分事务，而不是盲目加大缓存。21.10 大事务的问题大事务会放大：  Binlog 事件大小；  Binlog 缓存落盘；  Redo 写入；  锁持有时间；  Undo 版本；  主从延迟；  回滚时间。查看大事务：SELECT  trx_id,  trx_started,  trx_rows_modified,  trx_rows_lockedFROM information_schema.INNODB_TRXORDER BY trx_rows_modified DESC;治理：  按主键分批；  每批单独事务；  控制批大小；  避免一条 SQL 修改百万行；  监控单事务 Binlog 大小；  后台任务限速。21.11 故障恢复一致性案例崩溃时事务处于 prepare：恢复流程：1. 扫描 Redo；2. 找到 prepare 事务；3. 检查 Binlog 是否包含该 XID / GTID；4. 包含 -&gt; 提交；5. 不包含 -&gt; 回滚；因此，不要手工删除 Binlog 来“省空间”而不保留完整恢复链路。清理策略必须与备份、恢复和主从切换方案一致。本章小结内部两阶段提交让 InnoDB Redo 和 Binlog 在崩溃后保持一致。核心主库应默认使用双 1 配置，通过组提交摊薄 fsync 成本。Binlog 推荐 ROW 格式，GTID 简化复制和切换。大事务会放大日志、锁、Undo 和主从延迟，应始终拆分和限速。思考题  如果只有 Redo 成功而 Binlog 未写，会带来什么问题？  prepare 事务在崩溃恢复时如何决定提交或回滚？  双 1 配置的代价和收益是什么？  STATEMENT 和 ROW 格式各适合什么场景？  为什么大事务会显著增加 Binlog cache disk use？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。死锁不是异常玄学，而是并发事务以相反顺序请求对方持有的锁，形成等待环。分析死锁要还原 SQL、参数、索引、隔离级别和事务时序。20.1 死锁的定义最小模型：Session A 持有资源 1，等待资源 2；Session B 持有资源 2，等待资源 1；InnoDB 死锁检测发现等待环后，会选择一个代价较小的事务回滚，称为 victim。常见报错：ERROR 1213 (40001): Deadlock found when trying to get lock;try restarting transaction这不是业务 SQL 语法错误，而是需要重试或减少冲突。20.2 经典互相更新死锁准备：CREATE TABLE deadlock_accounts (    id INT PRIMARY KEY,    balance DECIMAL(12, 2) NOT NULL) ENGINE=InnoDB;INSERT INTO deadlock_accounts VALUES (1, 1000), (2, 1000);复现：-- Session ASTART TRANSACTION;UPDATE deadlock_accounts SET balance = balance - 10 WHERE id = 1;-- Session BSTART TRANSACTION;UPDATE deadlock_accounts SET balance = balance - 10 WHERE id = 2;-- Session A，等待 B 持有的 id=2UPDATE deadlock_accounts SET balance = balance + 10 WHERE id = 2;-- Session B，形成环UPDATE deadlock_accounts SET balance = balance + 10 WHERE id = 1;其中一个事务会回滚并收到 1213。修复：同一业务路径按固定顺序更新。所有转账先处理小 ID，再处理大 ID；或者按账户 + 流水号排序后逐个处理。20.3 间隙锁插入死锁准备：CREATE TABLE deadlock_gap (    id INT PRIMARY KEY,    value INT NOT NULL,    KEY idx_value (value)) ENGINE=InnoDB;INSERT INTO deadlock_gap VALUES (1, 100), (2, 300);RR 隔离级别下：-- Session ASTART TRANSACTION;SELECT * FROM deadlock_gap WHERE value = 200 FOR UPDATE;-- Session BSTART TRANSACTION;SELECT * FROM deadlock_gap WHERE value = 250 FOR UPDATE;两者都可能等待或锁定 (100, 300) 相关间隙：-- Session AINSERT INTO deadlock_gap VALUES (3, 200);-- 等待 Session B-- Session BINSERT INTO deadlock_gap VALUES (4, 250);-- 可能触发死锁原因：  两个事务都持有部分间隙锁；  双方都想向同一间隙插入；  插入意向锁互相等待；  等待环形成。处理：  评估是否使用 READ COMMITTED；  先插入唯一记录，再更新；  使用串行化任务或队列；  缩小事务；  用唯一键代替“先查是否存在再插入”；  对插入侧设置重试。20.4 唯一键冲突死锁三个事务同时尝试插入相同唯一键时，可能出现插入冲突和排他锁等待：-- Session AINSERT INTO users (username, nickname, mobile)VALUES ('dave', 'Dave', '13800000099');-- Session B 等待 AINSERT INTO users (username, nickname, mobile)VALUES ('dave', 'Dave2', '13800000098');-- Session C 等待 AINSERT INTO users (username, nickname, mobile)VALUES ('dave', 'Dave3', '13800000097');-- Session A 回滚ROLLBACK;B 或 C 可能收到锁等待或死锁。更稳方案：  使用固定幂等插入；  处理 Duplicate entry；  用 ON DUPLICATE KEY UPDATE 表达结果；  分布式锁只做削峰，不作为一致性唯一依据；  避免多路径写同一业务对象；  冲突后重新读取状态。20.5 查看死锁日志查看最近死锁：SHOW ENGINE INNODB STATUS\G找到：------------------------LATEST DETECTED DEADLOCK------------------------重点信息：            字段      说明                  TRANSACTION 1 / 2      参与事务              ACTIVE ... sec      事务持续时间              lock mode      锁模式              index ... of table ...      锁的索引和表              holds the lock      已持有锁              waiting for this lock      正在等待              ROLLBACK      被回滚的事务              SQL      触发语句      将所有死锁记录到错误日志：SET GLOBAL innodb_print_all_deadlocks = ON;生产建议开启，并接入日志采集和告警。20.6 结合 Performance Schema查看当前等待：SELECT  r.ENGINE_TRANSACTION_ID AS waiting_trx,  b.ENGINE_TRANSACTION_ID AS blocking_trx,  r.OBJECT_SCHEMA,  r.OBJECT_NAME,  r.INDEX_NAME,  r.LOCK_MODE,  r.LOCK_DATAFROM performance_schema.data_lock_waits wJOIN performance_schema.data_locks r  ON r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_IDJOIN performance_schema.data_locks b  ON b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;关联线程和 SQL：SELECT  p.PROCESSLIST_ID,  p.PROCESSLIST_USER,  p.PROCESSLIST_HOST,  p.PROCESSLIST_TIME,  p.PROCESSLIST_INFOFROM performance_schema.threads pWHERE p.PROCESSLIST_ID IS NOT NULLORDER BY p.PROCESSLIST_TIME DESC;MySQL 8.0 可以查看事务摘要：SELECT *FROM sys.innodb_lock_waits;sys.innodb_lock_waits 输出更接近人读的阻塞关系，适合快速定位。20.7 死锁分析流程1. 收集死锁时间、应用、接口、trace id；2. 找到两个事务的 SQL；3. 确认隔离级别；4. 查看涉及的索引和锁模式；5. 还原事务执行顺序；6. 判断是互相更新、间隙插入还是唯一冲突；7. 检查事务是否过长；8. 检查执行计划是否全表扫描；9. 设计统一加锁顺序；10. 增加重试、监控和回归验证；只看最后一条 SQL 通常无法得出结论，因为死锁是时序问题。20.8 应用重试策略死锁被数据库回滚后，应用可以安全重试整个事务。伪代码：int maxRetry = 3;for (int i = 1; i &lt;= maxRetry; i++) {    TransactionStatus tx = transactionManager.getTransaction(definition);    try {        doBusiness();        transactionManager.commit(tx);        return;    } catch (DeadlockLoserDataAccessException | MySQLTransactionRollbackException e) {        transactionManager.rollback(tx);        if (i == maxRetry) {            throw e;        }        sleepWithJitter(i * 50);    } catch (Exception e) {        transactionManager.rollback(tx);        throw e;    }}要求：  事务内不发送外部消息；  状态更新可重试；  非幂等外部动作放到提交后；  重试次数和退避有上限；  记录完整上下文；  死锁率高时优先修根因。20.9 死锁预防清单            风险      措施                  多行更新顺序不一致      按 ID 或固定业务顺序              大事务      拆小、减少扫描              无索引更新      补索引并检查执行计划              RR 间隙锁      评估 RC 或改写业务              先查再插      用唯一键和 Upsert              热点账户      异步汇总、拆分账户              外键级联      拆分操作、避免高峰              应用随机顺序      排序后处理              锁等待时间长      降低锁超时并快速失败              无重试      增加幂等重试      20.10 死锁与锁等待超时的区别            项目      Deadlock      Lock wait timeout                  本质      等待环      单向等待超时              检测      InnoDB 主动检测      到达超时时间              默认结果      回滚 victim      语句或事务行为取决于配置              处理      修复顺序和冲突，重试      找阻塞源并缩短事务      锁等待超时不一定回滚整个事务；死锁检测会回滚被选中的事务。本章小结死锁分析的核心是还原等待环。互相更新要统一加锁顺序；间隙插入要评估隔离级别和业务写入方式；唯一键冲突要用幂等写入处理。生产必须开启死锁日志采集，为应用实现有限次幂等重试，并通过缩短事务、精准索引和稳定顺序降低冲突概率。思考题  InnoDB 如何发现和处理死锁？  互相转账为什么会死锁？如何设计锁顺序？  RR 下间隙锁为什么可能导致插入死锁？  SHOW ENGINE INNODB STATUS 中死锁日志哪些信息最关键？  给一个高死锁业务设计修复和重试方案。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。InnoDB 常被称为行锁引擎，但严格来说，它锁的是索引记录和索引记录之间的间隙。理解这一点，才能解释为什么有些 UPDATE 锁住一行，有些查询却阻塞插入。19.1 实验准备创建表：CREATE TABLE lock_demo (    id INT PRIMARY KEY,    value INT NOT NULL,    KEY idx_value (value)) ENGINE=InnoDB;INSERT INTO lock_demo VALUES(10, 100),(20, 200),(30, 300);打开两个 MySQL 客户端，分别称为 Session A 和 Session B。19.2 Record Lock按主键等值更新存在的行：-- Session ASTART TRANSACTION;UPDATE lock_demoSET value = 101WHERE id = 10;查看锁：SELECT  INDEX_NAME,  LOCK_TYPE,  LOCK_MODE,  LOCK_DATAFROM performance_schema.data_locksWHERE OBJECT_NAME = 'lock_demo';尝试修改同一行：-- Session BSTART TRANSACTION;UPDATE lock_demoSET value = 102WHERE id = 10;-- 等待修改另一行：UPDATE lock_demoSET value = 201WHERE id = 20;-- 正常执行提交：-- Session ACOMMIT;-- Session BCOMMIT;这就是典型的记录锁。注意：二级索引列也被更新，因此锁不只涉及主键索引。19.3 Gap Lock间隙锁锁定索引记录之间的开区间，主要在 RR 隔离级别下防止幻影插入。索引 idx_value 当前值：100, 200, 300等值查询不存在的值：-- Session ASET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;START TRANSACTION;SELECT *FROM lock_demoWHERE value = 150FOR UPDATE;尝试插入落在 (100, 200) 的值：-- Session BSTART TRANSACTION;INSERT INTO lock_demo VALUES (15, 150);-- 等待原因：value=150 不存在；InnoDB 锁住 (100, 200) 间隙；B 的 150 正好落入该间隙；插入不落在该间隙的值通常不受影响：INSERT INTO lock_demo VALUES (40, 400);提交或回滚两个会话后释放锁。19.4 Next-Key LockNext-Key Lock 是记录锁和它前面间隙锁的组合，区间为左开右闭：(value=100, value=200]范围锁定：-- Session ASTART TRANSACTION;SELECT *FROM lock_demoWHERE value BETWEEN 100 AND 200FOR UPDATE;可能锁定：(前一个值, 100] 和 (100, 200]尝试插入：-- Session BINSERT INTO lock_demo VALUES (5, 50);    -- 可能不受影响INSERT INTO lock_demo VALUES (15, 150);  -- 等待INSERT INTO lock_demo VALUES (25, 250);  -- 取决于实际锁定边界真实边界与执行计划、索引、条件和隔离级别有关，必须通过 data_locks 查看确认。19.5 唯一索引等值命中主键或唯一索引等值命中已存在记录时，InnoDB 可以退化成记录锁，不需要锁间隙来防止同一唯一键再次插入。-- Session ASTART TRANSACTION;SELECT *FROM lock_demoWHERE id = 10FOR UPDATE;Session B：INSERT INTO lock_demo VALUES (15, 150);-- 通常可以执行，因为没有落入主键 10 的唯一记录锁但如果插入相同主键：INSERT INTO lock_demo VALUES (10, 999);-- 冲突或等待前提是优化器确实选择唯一索引执行等值匹配。19.6 非唯一索引等值查找在非唯一索引上，即使值已存在，也可能需要 Next-Key Lock，因为同一值可能有多条记录，要防止再插入相同二级索引值。准备：CREATE TABLE lock_demo2 (    id INT PRIMARY KEY,    value INT NOT NULL,    KEY idx_value (value)) ENGINE=InnoDB;INSERT INTO lock_demo2 VALUES(1, 100), (2, 100), (3, 200);执行：-- Session ASTART TRANSACTION;SELECT *FROM lock_demo2WHERE value = 100FOR UPDATE;Session B：INSERT INTO lock_demo2 VALUES (4, 100);-- 可能等待具体锁范围查看：SELECT  INDEX_NAME,  LOCK_MODE,  LOCK_DATAFROM performance_schema.data_locksWHERE OBJECT_NAME = 'lock_demo2';19.7 RC 下的间隙锁READ COMMITTED 通常不使用 Gap Lock / Next-Key Lock 来防止幻读，只锁匹配记录。切换：SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;实验：-- Session ASTART TRANSACTION;SELECT *FROM lock_demoWHERE value = 150FOR UPDATE;-- Session BINSERT INTO lock_demo VALUES (15, 150);-- 通常可以执行这是很多高并发系统选择 RC 的原因之一：锁范围更小，插入冲突和死锁更少。19.8 Insert Intention Lock插入意向锁是插入前的一种间隙锁请求。若目标间隙已被其他事务锁定，插入等待。Session A 锁住 (100, 200)；Session B 要插入 150；B 设置 insert intention lock 并等待；多个事务向不同间隙或同间隙不同值插入时，如果彼此不冲突，可以并发；但目标间隙已被 Gap Lock 占用时会被阻塞。19.9 锁与执行计划如果 WHERE 条件没有索引：UPDATE lock_demoSET value = 999WHERE id + 0 = 10;优化器可能全表扫描，导致扫描到的记录和相关间隙被锁定，锁范围远大于“一行”。查看计划：EXPLAINSELECT *FROM lock_demoWHERE id + 0 = 10;原则：  UPDATE / DELETE 必须走精准索引；  避免索引列函数化；  类型必须匹配；  事务前检查 rows；  大范围更新分批；  生产执行前在影子库验证锁。19.10 查看锁详情查看锁：SELECT  ENGINE_TRANSACTION_ID,  OBJECT_SCHEMA,  OBJECT_NAME,  INDEX_NAME,  LOCK_TYPE,  LOCK_MODE,  LOCK_DATAFROM performance_schema.data_locksWHERE OBJECT_NAME = 'lock_demo'ORDER BY ENGINE_TRANSACTION_ID, INDEX_NAME;字段解释：            字段      说明                  LOCK_TYPE      TABLE 或 RECORD              LOCK_MODE      X、S、X,GAP、X,REC_NOT_GAP 等              INDEX_NAME      锁住的索引              LOCK_DATA      索引记录或 supremum 值      常见 LOCK_MODE：            模式      含义                  X,REC_NOT_GAP      仅记录锁              X,GAP      仅间隙锁              X      Next-Key Lock              X,INSERT_INTENTION      插入意向              AUTO_INC      自增锁      查看等待关系：SELECT  r.ENGINE_TRANSACTION_ID AS waiting_trx,  b.ENGINE_TRANSACTION_ID AS blocking_trx,  r.OBJECT_NAME AS table_name,  r.INDEX_NAME,  r.LOCK_MODE,  r.LOCK_DATAFROM performance_schema.data_lock_waits wJOIN performance_schema.data_locks r  ON r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_IDJOIN performance_schema.data_locks b  ON b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;19.11 任务队列实践创建队列表：CREATE TABLE task_queue (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    task_no VARCHAR(64) NOT NULL,    status VARCHAR(16) NOT NULL DEFAULT 'PENDING',    owner VARCHAR(64) NULL,    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_task_no (task_no),    KEY idx_status_id (status, id)) ENGINE=InnoDB;多个 worker 抢任务：START TRANSACTION;SELECT id, task_noFROM task_queueWHERE status = 'PENDING'ORDER BY idLIMIT 10FOR UPDATE SKIP LOCKED;UPDATE task_queueSET status = 'RUNNING',    owner = 'worker-1'WHERE id IN (...);COMMIT;应用需要记录 SELECT 到的 ID 并替换 IN (...)。SKIP LOCKED 避免多个 worker 争抢同一批任务。更稳的流程：  唯一任务号防重复；  抢占和状态更新同事务；  设置执行超时；  支持重试和死信；  任务结果幂等写回；  监控积压和失败。本章小结InnoDB 行级锁作用在索引记录和间隙上。唯一索引等值命中可以退化为记录锁；非唯一索引和范围查询可能使用 Gap Lock 或 Next-Key Lock；RR 为防止幻读会使用更大范围，RC 通常更少使用间隙锁。生产写操作必须确保执行计划精准，否则“锁一行”可能变成锁一批。思考题  Record Lock、Gap Lock、Next-Key Lock 分别锁什么范围？  为什么唯一索引等值命中可能只加记录锁？  为什么 RR 下不存在的等值查询可能阻塞插入？  RC 为什么通常能减少间隙锁冲突？  用两个会话复现一个插入等待，并用 data_locks 解释锁范围。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Lock 保护事务之间的数据一致性，生命周期通常到提交或回滚；Latch 保护 MySQL 和 InnoDB 内部临界区，生命周期极短。两者都表现为“等待”，但排查方式完全不同。18.1 Lock 与 Latch 的区别            项目      Lock      Latch                  保护对象      表、行、区间、元数据      内存临界区、页、哈希表、互斥量              生命周期      事务级别或语句级别      微秒或毫秒级              是否参与死锁检测      InnoDB 行锁和表锁参与      通常以自旋等待为主              常见表现      lock wait timeout、deadlock      mutex wait、性能下降              排查工具      data_locks、innodb_status      Performance Schema wait events      用户日常说的“数据库锁”多数指 Lock，但性能毛刺常常来自 Latch。18.2 锁的模式常见锁模式：            锁      说明                  IS      意向共享锁              IX      意向排他锁              S      共享锁              X      排他锁              AUTO-INC      自增锁              RECORD      记录相关锁，包含记录、间隙、Next-Key              TABLE      表级锁              METADATA LOCK      元数据锁      兼容矩阵简化：            已持有 / 请求      S      X                  S      兼容      冲突              X      冲突      冲突      意向锁用于快速判断表级操作是否与行级锁冲突，不需要扫描所有行锁。18.3 共享锁与排他锁共享读：START TRANSACTION;SELECT *FROM ordersWHERE id = 1FOR SHARE;COMMIT;排他读：START TRANSACTION;SELECT *FROM ordersWHERE id = 1FOR UPDATE;COMMIT;区别：  FOR SHARE 允许其他事务也加 S 锁；  FOR SHARE 阻塞其他事务的 X 锁；  FOR UPDATE 阻塞其他 S 和 X 锁；  二者都是当前读；  都受执行计划和索引影响。如果 WHERE 没有有效索引，锁范围可能扩大，执行时间也可能变长。18.4 意向锁事务准备给行加 S 锁前，先在表上标记 IS；准备给行加 X 锁前，先标记 IX。作用：表级操作只需要检查意向锁不需要逐行判断是否存在行锁例如：LOCK TABLES orders WRITE;如果 orders 上存在 IX，则该请求需要等待。18.5 表级锁显式表锁：LOCK TABLES orders READ;UNLOCK TABLES;LOCK TABLES orders WRITE;UNLOCK TABLES;生产 OLTP 很少使用 LOCK TABLES，常见问题：  锁粒度过大；  阻塞面广；  与事务语义混淆；  备份工具或管理操作误用；  忘记 UNLOCK。如果需要一致性备份，InnoDB 应优先使用 MVCC 快照工具，如 mysqldump --single-transaction 或物理备份。18.6 元数据锁 MDLMDL 保护表结构稳定性：            操作      通常需要                  SELECT      MDL 读锁              DML      MDL 读锁              ALTER TABLE      MDL 写锁      风险场景：1. 长查询持有 MDL 读锁；2. ALTER 请求 MDL 写锁并排队；3. 后续所有新 SELECT 在 MDL 队列后排队；4. 表看起来完全不可访问。查看 MDL：SELECT *FROM performance_schema.metadata_locksWHERE OBJECT_SCHEMA = 'shop'  AND OBJECT_NAME = 'orders';MySQL 8.0 支持设置 DDL 等待时间：SET SESSION lock_wait_timeout = 10;ALTER TABLE orders  ADD COLUMN remark VARCHAR(255),  ALGORITHM=INPLACE,  LOCK=NONE;若超时失败，避免后续请求被长期堵塞。18.7 记录锁、间隙锁与 Next-Key LockInnoDB 行级锁实际作用对象是索引记录及其间隙：            锁      范围                  Record Lock      索引记录本身              Gap Lock      索引记录之间的开区间              Next-Key Lock      记录 + 前面的间隙，左开右闭              Insert Intention Lock      插入前等待间隙冲突释放      示例索引值：10, 20, 30可能存在：(-∞, 10), (10, 20), (20, 30), (30, +∞)第 19 章会用两个会话详细实验。18.8 自增锁查看模式：SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';            模式      含义                  0      传统模式，语句级 AUTO-INC 表锁              1      连续模式，简单插入用轻量互斥，批量插入可能用表锁              2      交错模式，并发最高，插入 ID 可能交错      MySQL 8.0 默认是 2。主从使用 STATEMENT Binlog 时，模式 2 可能导致复制不确定；ROW 格式下更安全。查看自增值：SHOW TABLE STATUS LIKE 'orders'\G自增 ID 不保证连续，也不保证不重用，业务不要依赖它表达顺序号或编号连续性。18.9 外键锁虽然互联网业务常不使用外键，但如果表存在外键，InnoDB 会执行引用检查，可能锁父表或子表相关行。查看外键：SELECT *FROM information_schema.REFERENTIAL_CONSTRAINTSWHERE CONSTRAINT_SCHEMA = 'shop';外键风险：  写入额外检查；  父子表锁交互；  批量导入变慢；  DDL 和迁移复杂；  死锁概率上升。不使用外键时，要通过应用校验和定时对账补偿。18.10 查看锁MySQL 8.0 使用 Performance Schema：SELECT  ENGINE_TRANSACTION_ID,  THREAD_ID,  OBJECT_SCHEMA,  OBJECT_NAME,  INDEX_NAME,  LOCK_TYPE,  LOCK_MODE,  LOCK_DATAFROM performance_schema.data_locks;查看锁等待：SELECT  requesting.ENGINE_TRANSACTION_ID AS requesting_trx,  blocking.ENGINE_TRANSACTION_ID AS blocking_trx,  requesting.OBJECT_NAME AS table_name,  requesting.LOCK_TYPE,  requesting.LOCK_MODE,  requesting.LOCK_DATAFROM performance_schema.data_lock_waits wJOIN performance_schema.data_locks requesting  ON requesting.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_IDJOIN performance_schema.data_locks blocking  ON blocking.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;查看 InnoDB 死锁：SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';SHOW ENGINE INNODB STATUS\G18.11 Latch 与互斥量常见 Latch：            对象      说明                  buffer pool mutex      保护缓冲池结构              page latch      保护数据页              lock system mutex      保护锁哈希表              dict mutex      保护数据字典              log mutex      保护日志写入结构              purge coordinator      Purge 调度相关      查看等待：SELECT  EVENT_NAME,  COUNT_STAR,  SUM_TIMER_WAIT / 1000000000 AS wait_msFROM performance_schema.events_waits_summary_global_by_event_nameWHERE EVENT_NAME LIKE 'wait/synch/%'ORDER BY SUM_TIMER_WAIT DESCLIMIT 20;常见原因：  高并发热点页；  Buffer Pool 过小；  自增或索引热点；  长时间持有内部锁；  高频元数据操作；  版本缺陷；  资源不足。处理方向：  升级小版本修复；  降低热点；  拆分写入位置；  调整 Buffer Pool；  收集 Performance Schema；  找数据库专家或厂商分析。18.12 锁等待参数查看超时：SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';会话级设置：SET SESSION innodb_lock_wait_timeout = 10;默认通常为 50 秒。在线应用应设置更短超时并做好重试，避免请求堆积。跳过锁等待：SELECT *FROM task_queueWHERE status = 'PENDING'LIMIT 1FOR UPDATE SKIP LOCKED;不等待：SELECT *FROM task_queueWHERE id = 1FOR UPDATE NOWAIT;SKIP LOCKED 适合任务队列抢占；NOWAIT 适合明确告诉用户资源忙。18.13 锁设计原则  锁范围尽量精确；  关联条件必须有索引；  多行更新使用稳定顺序；  事务尽量短；  避免锁内做外部调用；  热点数据拆分或异步化；  幂等键降低重复写入；  死锁要有重试策略；  管理操作要评估 MDL；  监控锁等待、长事务和死锁。本章小结Lock 保护事务数据，粒度包括表、元数据、记录、间隙和 Next-Key；Latch 保护数据库内部临界区。排查业务锁等待使用 data_locks、data_lock_waits 和 InnoDB 状态；排查性能毛刺则要关注 Performance Schema 的同步等待。锁优化的本质是缩小锁范围、缩短持有时间和消除热点冲突。思考题  Lock 和 Latch 有什么区别？  意向锁的作用是什么？  MDL 为什么可能让一个 ALTER 影响所有 SELECT？  SKIP LOCKED 和 NOWAIT 分别适合什么场景？  如何区分行锁等待和 Latch 等待？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。InnoDB 用 Redo Log 保证崩溃恢复，用 Undo Log 支持回滚和 MVCC。两者名字相似，方向相反：Redo 记录“如何重做已提交物理变更”，Undo 记录“如何撤销逻辑变更”。17.1 两份日志的分工            日志      方向      主要作用      内容特征                  Redo Log      前滚      崩溃后恢复已提交修改      物理页相关变更              Undo Log      回滚      回滚事务、MVCC 版本链      逻辑反操作      示例：UPDATE accountsSET balance = balance + 100WHERE id = 1;Undo 记录逻辑上的旧值：balance 旧值 -&gt; 500Redo 记录页面变更：某个数据页某偏移位置修改为某内容17.2 WAL：先写日志再写数据页InnoDB 遵循 Write-Ahead Logging：1. 修改 Buffer Pool 中的数据页；2. 生成 Redo；3. 事务提交前持久化 Redo；4. 数据页可以稍后刷盘；5. 崩溃后用 Redo 重放已提交变更。好处：  顺序写替代随机写；  提交时不必立刻刷所有数据页；  提交持久性由较小日志保证；  Buffer Pool 可以合并多次修改；  崩溃恢复有明确边界。17.3 Redo Log 结构查看参数：SHOW VARIABLES LIKE 'innodb_log_file_size';SHOW VARIABLES LIKE 'innodb_log_files_in_group';SHOW VARIABLES LIKE 'innodb_log_buffer_size';MySQL 8.0.30+ 更推荐：SHOW VARIABLES LIKE 'innodb_redo_log_capacity';循环写入：write pos：当前写入位置checkpoint：已刷盘数据页对应的水位write pos 追上 checkpoint 前必须推进 checkpoint如果 Redo 空间不足：  刷脏页加剧；  写入抖动；  checkpoint 阻塞；  Log file is nearly full；  响应时间尖刺。查看状态：SHOW ENGINE INNODB STATUS\G关注：LOG---Log sequence numberLog flushed up toLast checkpoint at17.4 innodb_flush_log_at_trx_commit参数取值：            值      行为      持久性      性能                  1      每次提交刷 Redo 到磁盘      最强      最低              2      提交写到 OS cache，每秒刷盘      MySQL 崩溃通常不丢，OS 崩溃可能丢      较高              0      每秒写并刷盘      崩溃可能丢事务      最高      查看：SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';订单、支付、账户主库建议：innodb_flush_log_at_trx_commit = 1sync_binlog = 1非核心日志库可以结合业务接受度评估 2 或 0，但必须明确故障窗口内可能丢数据。17.5 Redo 与 Binlog 的区别            项目      Redo Log      Binlog                  所属层      InnoDB      Server 层              内容      物理页变更      逻辑变更              用途      崩溃恢复      复制、归档、时间点恢复              写法      循环覆盖      追加文件              引擎      InnoDB      所有引擎              记录单位      页修改      SQL / 行变更      Binlog 常见格式：            格式      特点                  STATEMENT      记 SQL，体积小，但部分函数和不确定性语句有风险              ROW      记行镜像，一致性最好，大批量修改日志较大              MIXED      由 MySQL 选择，历史过渡方案      查看：SHOW VARIABLES LIKE 'binlog_format';SHOW BINARY LOGS;SHOW BINLOG EVENTS IN '&lt;log_file_name&gt;' LIMIT 10;17.6 Undo Log 类型            类型      场景                  insert undo      INSERT 产生，事务提交后可较快清理              update undo      UPDATE / DELETE 产生，需要服务 MVCC              purge undo      清理 delete-mark 记录和历史版本      INSERT 的旧版本通常不需要被其他事务读取，提交后清理成本较低。UPDATE / DELETE 的旧版本可能被长事务需要，因此保留更久。17.7 Undo 与回滚大事务回滚：START TRANSACTION;UPDATE ordersSET status = 'CANCELED'WHERE created_at &lt; '2025-01-01';ROLLBACK;回滚需要按 Undo 执行反向操作：  恢复旧值；  清理索引修改；  释放锁；  维护事务状态；  记录进度。大事务回滚可能比执行更久，期间锁和 Undo 仍在。千万不要以为 ROLLBACK 能瞬间撤销所有影响。查看回滚中的事务：SELECT  trx_id,  trx_state,  trx_started,  trx_rows_modified,  trx_mysql_thread_idFROM information_schema.INNODB_TRX;17.8 Undo 表空间治理MySQL 8.0 默认使用独立 undo 表空间：SHOW VARIABLES LIKE 'innodb_undo_directory';SHOW VARIABLES LIKE 'innodb_undo_tablespaces';SHOW VARIABLES LIKE 'innodb_undo_log_truncate';SHOW VARIABLES LIKE 'innodb_max_undo_log_size';查看：SELECT  space,  name,  file_typeFROM information_schema.INNODB_TABLESPACESWHERE name LIKE '%undo%';膨胀原因：  长事务；  高频 UPDATE；  大事务；  Purge 延迟；  机器 IO 瓶颈；  回滚大事务。处理顺序：1. 找最老事务；2. 确认业务是否可终止；3. 拆分大任务；4. 优化热点更新；5. 观察 History list；6. 最后才考虑空间参数和扩容。17.9 Buffer Pool 与脏页查看：SHOW VARIABLES LIKE 'innodb_buffer_pool_size';SHOW STATUS LIKE 'Innodb_buffer_pool_wait_free';SHOW STATUS LIKE 'Innodb_data_pending_writes';脏页来源：  数据页修改；  索引页修改；  Undo 页；  Change Buffer 合并；  Adaptive Hash 元数据。刷脏触发：  Redo 空间不足；  Buffer Pool 空闲页不足；  后台定期刷新；  checkpoint；  关闭实例；  手动刷新。手动触发：FLUSH TABLES;SET GLOBAL innodb_max_dirty_pages_pct = 75;生产不应频繁手动刷脏，需以监控和容量规划为主。17.10 Doublewrite BufferRedo 记录“对某页应用某变更”，而不是完整页面镜像。如果页本身发生部分写坏，仅靠 Redo 无法安全重放。Doublewrite 流程：1. 脏页先写入 doublewrite 区域；2. 刷盘；3. 再写入表空间对应位置；崩溃后发现页损坏时，可从 doublewrite 恢复完整页，再应用 Redo。查看：SHOW VARIABLES LIKE 'innodb_doublewrite';SHOW STATUS LIKE 'Innodb_dblwr_pages_written';关闭 Doublewrite 可能提升一点性能，但以牺牲部分写保护为代价。生产默认保持开启。17.11 崩溃恢复流程简化流程：1. 读取最近 checkpoint；2. 扫描 Redo；3. 重放必要页面变更；4. 恢复 Undo；5. 根据事务状态回滚未提交事务；6. 与 Binlog 协调 prepared 事务；7. 实例可对外服务。大事务、大量脏页、Redo 很长、IO 故障都会拉长恢复时间。高可用设计必须考虑重建时间，而不是只关心平时 QPS。17.12 生产实践  核心主库保持双 1；  控制事务大小；  避免大范围 UPDATE / DELETE；  监控 Redo 等待和 checkpoint；  监控 Undo 表空间和 History list；  大任务按主键或时间分批；  压测新参数；  备份和恢复演练必须覆盖崩溃场景；  磁盘必须使用掉电保护或可靠云盘；  不用非持久化参数换取虚假性能。本章小结Redo Log 通过 WAL 和崩溃恢复保证已提交修改的持久性，Undo Log 支持事务回滚和 MVCC。innodb_flush_log_at_trx_commit=1 提供最强持久性；Doublewrite 解决部分写坏页问题；长事务和大事务会拖累 Undo、Purge、锁和恢复时间。思考题  Redo Log 和 Undo Log 的方向有什么区别？  为什么提交时刷 Redo 而不是刷所有数据页？  innodb_flush_log_at_trx_commit 三个取值分别意味着什么？  Doublewrite Buffer 解决什么问题？  大事务回滚为什么可能很慢？如何避免？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。MVCC 是 InnoDB 在读写并发中的核心机制。它让普通读不加行锁，通过 Undo 版本链和 Read View 判断哪个版本对当前事务可见。16.1 为什么需要 MVCC锁_ONLY 方案：写事务持锁期间，读事务必须等待MVCC 方案：写事务创建新版本读事务按快照选择可见版本收益：  读不阻塞写；  写不阻塞一致性读；  查询能看到稳定快照；  减少锁等待；  支持不同隔离级别。MVCC 不代表没有锁。当前读、唯一性检查、外键检查和部分 DML 仍然需要锁。16.2 隐藏列InnoDB 行记录包含与 MVCC 相关的隐藏信息：            隐藏列      含义                  DB_TRX_ID      最近修改该行的事务 ID              DB_ROLL_PTR      指向 Undo Log 版本链              DB_ROW_ID      无主键且无合适唯一索引时的行 ID      逻辑示意：当前行：  balance=100, trx_id=80, roll_ptr -&gt; undo_record_2undo_record_2:  balance=90, trx_id=70, roll_ptr -&gt; undo_record_1undo_record_1:  balance=80, trx_id=60, roll_ptr=NULL更新流程：1. 写 Redo；2. 保存旧值到 Undo；3. 修改当前行；4. 当前行写新 trx_id；5. roll_ptr 指向旧版本；16.3 Undo 版本链每次修改都会形成旧版本。查询如果看不到最新版本，就沿 roll_ptr 向历史追溯。示例：CREATE TABLE mvcc_accounts (    id INT PRIMARY KEY,    balance DECIMAL(12, 2) NOT NULL) ENGINE=InnoDB;INSERT INTO mvcc_accounts VALUES (1, 100);UPDATE mvcc_accounts SET balance = 200 WHERE id = 1;UPDATE mvcc_accounts SET balance = 300 WHERE id = 1;逻辑版本：最新：300历史：200 -&gt; 100 -&gt; 插入初始版本MySQL 不提供直接查看隐藏列的普通 SQL。理解版本链主要通过行为实验和 Undo 日志指标。16.4 Read ViewRead View 是事务判断版本可见性的快照。关键信息包括：            名称      含义                  creator trx_id      创建该 Read View 的事务              active list      创建瞬间仍活跃的事务 ID              min limit      活跃列表最小事务 ID              next limit      创建瞬间下一个将要分配的事务 ID      简化判断规则：如果 DB_TRX_ID == creator：    可见，自己修改的数据如果 DB_TRX_ID &lt; min limit：    可见，创建快照前已提交如果 DB_TRX_ID &gt;= next limit：    不可见，快照创建后才开始如果 DB_TRX_ID 在活跃区间：    已不在活跃列表且小于 next limit：可见    仍在活跃列表或未提交：不可见不可见时，沿 Undo 版本链继续向前找，直到找到可见版本或无版本。16.5 RC 与 RR 的差别READ COMMITTED每条一致性读创建新的 Read View：SELECT 1 -&gt; Read View ASELECT 2 -&gt; Read View B其他事务提交后，后续 SELECT 可以看到新数据。REPEATABLE READ事务中第一次一致性读创建 Read View，后续普通 SELECT 复用：SELECT 1 -&gt; 创建 Read ViewSELECT 2 -&gt; 复用SELECT 3 -&gt; 复用因此同一行普通 SELECT 结果可重复。实验：-- Session A，RRSTART TRANSACTION;SELECT balance FROM mvcc_accounts WHERE id = 1;-- Session BUPDATE mvcc_accounts SET balance = 999 WHERE id = 1;-- Session A 再读SELECT balance FROM mvcc_accounts WHERE id = 1;-- RR：仍看到旧值-- RC：看到 99916.6 快照读与当前读并存RR 下：START TRANSACTION;SELECT balance FROM mvcc_accounts WHERE id = 1;-- 快照：100SELECT balance FROM mvcc_accounts WHERE id = 1 FOR UPDATE;-- 当前读：读取最新提交值并加锁COMMIT;这个设计能同时满足：  普通查询稳定；  修改基于最新数据；  关键路径防止丢失更新。也解释了“先普通 SELECT 再 UPDATE”可能不符合直觉：SELECT 看到旧快照，UPDATE 是当前读。16.7 MVCC 与幻读RR 的普通 SELECT 使用一致快照，多次读取不会看到新提交的行，因此不会出现快照意义上的幻影。但混合当前读时可能出现现象：-- Session ASTART TRANSACTION;SELECT COUNT(*) FROM mvcc_accounts WHERE id &gt; 0;-- Session BINSERT INTO mvcc_accounts VALUES (2, 500);COMMIT;-- Session ASELECT COUNT(*) FROM mvcc_accounts WHERE id &gt; 0; -- 仍是旧数量UPDATE mvcc_accounts SET balance = balance + 1 WHERE id &gt; 0; -- 当前读包含新行SELECT COUNT(*) FROM mvcc_accounts WHERE id &gt; 0; -- 可能看到被自己更新后的新行COMMIT;这不是简单的“MVCC 失效”，而是快照读和当前读规则不同。需要防止额外行被并发插入时，应使用锁定读或依赖唯一约束。16.8 长事务与 Undo 膨胀旧版本不能在仍有 Read View 需要时清理。长事务风险：  Undo Log 持续增长；  Undo 表空间膨胀；  历史版本扫描变慢；  查询可能沿长版本链回溯；  Purge 延迟；  主从延迟加剧；  回滚成本极高。查看历史列表长度：SHOW ENGINE INNODB STATUS\G关注：History list lengthPURGETRANSACTIONSMySQL 8.0 查看 undo：SELECT  space,  name,  file_typeFROM information_schema.INNODB_TABLESPACESWHERE name LIKE '%undo%';治理：  缩短事务；  外部调用移出事务；  大任务分批提交；  监控事务年龄；  避免连接池悬挂事务；  定期审计慢事务。16.9 Purge 机制Purge 负责清理不再需要的 Undo 版本：1. 找到最老 Read View；2. 判断哪些旧版本无人可见；3. 清理 delete-mark 记录；4. 回收 Undo 空间；相关变量：SHOW VARIABLES LIKE 'innodb_purge_threads';SHOW VARIABLES LIKE 'innodb_purge_batch_size';SHOW VARIABLES LIKE 'innodb_max_purge_lag';不要在没有瓶颈证据时随意调整。多数 Purge 延迟来自长事务和写入模式，而不是线程数。16.10 MVCC 性能影响MVCC 不是免费：  每次修改维护 Undo；  长版本链降低读性能；  大量更新产生更多历史版本；  删除只是标记，清理有延迟；  空间回收有滞后；  快照读可能扫描多个版本。优化建议：  短事务；  避免热点行反复更新；  计数类热点数据移到 Redis；  状态变化合并写入；  大量 UPDATE 分批；  历史数据归档；  监控 History list。本章小结MVCC 通过隐藏事务 ID、回滚指针、Undo 版本链和 Read View 实现一致性读。RC 每次读创建新快照，RR 复用第一次快照。普通 SELECT 是快照读，UPDATE 和锁定读是当前读。长事务会阻止旧版本清理，导致 Undo 膨胀和 Purge 延迟。思考题  DB_TRX_ID 和 DB_ROLL_PTR 分别有什么作用？  RC 和 RR 的可见性判断有什么区别？  为什么 RR 下普通 SELECT 结果稳定，UPDATE 却能影响新行？  长事务为什么会导致 History list 增长？  如何把一个包含外部调用的大事务拆成安全短事务？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。事务是 MySQL 成为交易系统事实来源的根基。它让一组操作要么全部可见，要么完全不可见，并在并发执行时为业务提供明确的一致性边界。15.1 事务的 ACID            特性      含义      主要依赖                  Atomicity 原子性      事务内操作全部成功或全部失败      Undo Log、事务管理              Consistency 一致性      数据满足约束和业务规则      约束、锁、应用逻辑              Isolation 隔离性      并发事务互不干扰      MVCC、锁              Durability 持久性      提交后的数据崩溃不丢      Redo Log、Binlog、Doublewrite      一致性是目的，原子性、隔离性和持久性是数据库提供的重要机制。应用把非法数据写入合法事务中，数据库无法替你保证业务一致性。15.2 显式事务开启事务：START TRANSACTION;等价写法：BEGIN;提交：COMMIT;回滚：ROLLBACK;示例：START TRANSACTION;UPDATE accountsSET balance = balance - 100WHERE user_id = 1  AND balance &gt;= 100;UPDATE accountsSET balance = balance + 100WHERE user_id = 2;COMMIT;如果第二条更新失败，应执行：ROLLBACK;不能让一半转账留在数据库里。15.3 autocommitMySQL 默认开启自动提交：SHOW VARIABLES LIKE 'autocommit';autocommit=1 时，单条 SQL 没有显式开启事务也会自动提交。显式 START TRANSACTION 后，需要执行 COMMIT 或 ROLLBACK。关闭自动提交：SET autocommit = 0;风险：  忘记提交会形成长事务；  连接归还连接池后事务状态可能残留；  锁和 Undo 版本长期保留；  主从延迟和性能问题难以定位。应用建议：显式管理事务边界，提交或回滚必须放在 finally 逻辑中；连接归还前确认没有未结束事务。15.4 SAVEPOINT保存点允许部分回滚：START TRANSACTION;SAVEPOINT after_order;INSERT INTO orders  (order_no, user_id, merchant_id, status, total_amount)VALUES  ('20260826000001', 1, 1, 'CREATED', 25.50);SAVEPOINT after_coupon;UPDATE couponsSET used = 1WHERE coupon_id = 100;ROLLBACK TO SAVEPOINT after_coupon;COMMIT;结果：  订单插入保留；  优惠券更新被撤销；  事务仍未结束，直到 COMMIT。删除保存点：RELEASE SAVEPOINT after_coupon;ROLLBACK TO SAVEPOINT 不是提交事务，只是撤销到某个内部位置。15.5 并发异常标准 SQL 定义了几个经典并发问题。            异常      描述                  Dirty Read 脏读      读到其他事务未提交的数据              Non-repeatable Read 不可重复读      同一事务两次读取同一行值不同              Phantom Read 幻读      同一条件两次读取出现或消失额外行              Lost Update 丢失更新      两个事务基于旧值互相覆盖      示例表：CREATE TABLE isolation_accounts (    id INT PRIMARY KEY,    balance DECIMAL(12, 2) NOT NULL) ENGINE=InnoDB;INSERT INTO isolation_accounts VALUES (1, 1000.00);不可重复读Session A：START TRANSACTION;SELECT balance FROM isolation_accounts WHERE id = 1;Session B：START TRANSACTION;UPDATE isolation_accountsSET balance = 900WHERE id = 1;COMMIT;Session A 再读：SELECT balance FROM isolation_accounts WHERE id = 1;COMMIT;如果两次结果不同，就是不可重复读。15.6 四个隔离级别查看级别：SELECT @@global.transaction_isolation,       @@session.transaction_isolation;设置会话级别：SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;设置全局级别：SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;            级别      脏读      不可重复读      幻读                  READ UNCOMMITTED      可能      可能      可能              READ COMMITTED      不可能      可能      可能              REPEATABLE READ      不可能      不可能      InnoDB 常规情况下基本避免              SERIALIZABLE      不可能      不可能      不可能      MySQL InnoDB 默认是 REPEATABLE READ。READ UNCOMMITTED可能读到未提交数据：SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;生产业务通常不使用。脏读会把中间状态、失败状态甚至回滚前的数据暴露给应用。READ COMMITTED每次一致性读都会生成新的 Read View，只能看到已提交数据。很多互联网系统将在线业务设置为 RC，原因是：  减少间隙锁使用；  并发写入冲突更少；  死锁概率下降；  业务多为单行短事务；  配合显式条件更新可满足一致性。代价：  同一事务内两次读可能不同；  复杂业务需要显式锁定读；  不能依赖快照做业务判断。REPEATABLE READ事务第一次一致性读时创建 Read View，后续普通 SELECT 复用同一快照。适合：  报表内多次读取希望口径一致；  默认 MySQL 行为；  需要减少不可重复读；  配合主键或唯一条件更新。注意：  普通 SELECT 是快照读，不锁行；  UPDATE 使用当前读；  先 SELECT 再 UPDATE 可能基于旧快照；  需要强一致判断时使用 SELECT ... FOR UPDATE；  幻读不是在所有场景下都绝对不存在。SERIALIZABLE普通 SELECT 会被隐式转换为锁定读，并发能力显著下降：SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;适合低并发、强隔离需求的任务。在线高并发交易系统很少使用。15.7 快照读与当前读快照读普通 SELECT：SELECT balanceFROM accountsWHERE user_id = 1;InnoDB 使用 MVCC 读取历史版本，不加行锁。当前读以下语句读取最新已提交或当前锁定版本，并涉及锁：SELECT * FROM accounts WHERE user_id = 1 FOR UPDATE;SELECT * FROM accounts WHERE user_id = 1 LOCK IN SHARE MODE;UPDATE accounts SET balance = balance - 1 WHERE user_id = 1;DELETE FROM accounts WHERE user_id = 1;INSERT INTO accounts VALUES (...);MySQL 8.0 中 FOR SHARE 替代 LOCK IN SHARE MODE，旧写法仍兼容。经典问题：START TRANSACTION;SELECT stock FROM products WHERE id = 1;-- 返回 10UPDATE products SET stock = 5 WHERE id = 1;如果另一个事务已经把库存改成 8，当前 UPDATE 会覆盖为 5。快照读不能防止丢失更新。安全写法：START TRANSACTION;SELECT stockFROM productsWHERE id = 1FOR UPDATE;UPDATE productsSET stock = 5WHERE id = 1;COMMIT;或直接条件更新：UPDATE productsSET stock = 5WHERE id = 1  AND stock = 10;15.8 丢失更新实验创建表：CREATE TABLE counters (    id INT PRIMARY KEY,    value INT NOT NULL) ENGINE=InnoDB;INSERT INTO counters VALUES (1, 0);错误流程：-- Session ASTART TRANSACTION;SELECT value FROM counters WHERE id = 1; -- 读到 0-- Session BSTART TRANSACTION;SELECT value FROM counters WHERE id = 1; -- 读到 0UPDATE counters SET value = 1 WHERE id = 1;COMMIT;-- Session AUPDATE counters SET value = 1 WHERE id = 1;COMMIT;B 的更新结果被 A 覆盖，等效丢失。正确方式：UPDATE countersSET value = value + 1WHERE id = 1;或：SELECT value FROM counters WHERE id = 1 FOR UPDATE;15.9 事务边界设计推荐：开启事务  -&gt; 校验和锁定必要数据  -&gt; 少量核心写入  -&gt; 提交不应放入事务：  远程 HTTP 调用；  文件上传下载；  消息发送等待；  复杂规则计算；  用户交互等待；  大批量数据清洗；  锁内调用外部服务。正确拆分：1. 前置校验；2. 开启本地事务；3. 锁定并更新核心数据；4. 提交事务；5. 事务外发送消息或调用外部系统；6. 通过状态机和对账处理外部失败。15.10 查看事务状态查看当前事务：SELECT  trx_id,  trx_state,  trx_started,  trx_requested_lock_id,  trx_wait_started,  trx_rows_locked,  trx_rows_modified,  trx_mysql_thread_idFROM information_schema.INNODB_TRXORDER BY trx_started;查看长事务：SELECT  trx_id,  trx_started,  TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,  trx_mysql_thread_idFROM information_schema.INNODB_TRXWHERE trx_started &lt; DATE_SUB(NOW(), INTERVAL 60 SECOND);终止会话：KILL &lt;thread_id&gt;;终止前必须确认影响，不要把业务正在执行的正常大事务随意杀掉。本章小结事务通过 ACID 机制保证一组操作的可靠性。InnoDB 默认使用 REPEATABLE READ，普通 SELECT 是基于 MVCC 的快照读，UPDATE、DELETE 和锁定读是当前读。隔离级别是并发能力和一致性之间的取舍：RC 更适合高并发短事务，RR 提供更稳定的快照口径，SERIALIZABLE 则明显降低并发。思考题  ACID 分别解决什么问题？分别依赖哪些机制？  RC 和 RR 的 Read View 创建时机有什么不同？  快照读和当前读的区别是什么？  为什么远程调用不应该放在数据库事务内？  设计一个余额转账事务，说明锁顺序、失败回滚和幂等处理。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。很多慢查询不是找不到数据，而是找到之后还要排序、去重、临时存储和传输。本章分析 filesort、GROUP BY、DISTINCT、UNION、深分页和磁盘临时表的治理方法。14.1 排序发生在哪里如果 ORDER BY 能复用索引顺序，MySQL 可以按索引读取并边读边输出：Index ordered scan -&gt; 直接返回如果不能复用索引，需要额外排序：读取数据 -&gt; 保存排序键和行指针 -&gt; 排序 -&gt; 返回结果执行计划通常显示：Using filesortfilesort 不一定真的使用磁盘文件，小结果可能在内存完成。真正要关注的是参与排序的行数和行大小。14.2 ORDER BY 使用索引的条件索引：KEY idx_user_created (user_id, created_at)可复用：SELECT id, user_id, created_atFROM ordersWHERE user_id = 1001ORDER BY created_at;不能稳定复用：SELECT *FROM ordersORDER BY created_at;SELECT *FROM ordersWHERE user_id &gt; 1000ORDER BY created_at;SELECT *FROM ordersWHERE user_id = 1001ORDER BY created_at, status;条件：  ORDER BY 列是索引连续后缀；  前导列有等值条件；  排序方向一致；  范围条件可能影响后续列复用；  查询列不能破坏可用路径；  需要唯一列补足稳定排序。14.3 filesort 过程查看排序参数：SHOW VARIABLES LIKE 'sort_buffer_size';SHOW VARIABLES LIKE 'max_length_for_sort_data';SHOW STATUS LIKE 'Sort%';常见状态：            状态      含义                  Sort_merge_passes      多路归并次数              Sort_range      范围扫描后排序次数              Sort_scan      全表或索引扫描后排序次数              Sort_rows      参与排序行数      如果 Sort_merge_passes 持续高，说明排序超过内存缓冲并发生归并。优先减少排序行数，再考虑调整 sort_buffer_size。不建议无依据地把 sort_buffer_size 设得极大：  每个会话可能分配；  占用内存；  影响并发；  收益取决于查询模式；  应通过压测确定。14.4 深分页问题慢 SQL：SELECT *FROM ordersORDER BY created_at DESC, id DESCLIMIT 20 OFFSET 1000000;MySQL 需要产生并丢弃前 100 万行。游标分页：SELECT *FROM ordersWHERE (created_at, id) &lt; ('2026-08-25 10:00:00', 123456)ORDER BY created_at DESC, id DESCLIMIT 20;行构造器写法等价于：SELECT *FROM ordersWHERE created_at &lt; '2026-08-25 10:00:00'   OR (       created_at = '2026-08-25 10:00:00'       AND id &lt; 123456   )ORDER BY created_at DESC, id DESCLIMIT 20;要求有排序索引：KEY idx_created_id (created_at, id)14.5 延迟关联当深分页无法使用游标时，可以先在索引中定位主键，再回表：SELECT o.*FROM orders oJOIN (  SELECT id  FROM orders  WHERE user_id = 1001  ORDER BY created_at DESC, id DESC  LIMIT 20 OFFSET 100000) p ON p.id = o.id;它仍然要扫描偏移行，但排序和偏移阶段使用更窄的索引行，减少回表和大字段传输。适合后台导出或兜底方案，不能替代游标分页。14.6 LIMIT 与 COUNTLIMIT 不改变扫描前的数据规模，只限制返回：SELECT *FROM ordersWHERE status = 'CREATED'LIMIT 10;如果符合条件的行很多，找到前 10 条可能很快；如果没有匹配，仍可能扫描完整个范围。统计数量：SELECT COUNT(*)FROM ordersWHERE status = 'CREATED';大表实时 COUNT 需要评估。常见方案：  分页展示“下一页”，不展示总数；  使用近似值；  预聚合计数表；  只统计最近时间窗；  历史统计进入报表库；  使用缓存并定期校准。14.7 GROUP BY 与临时表可能使用临时表：SELECT status, COUNT(*)FROM ordersGROUP BY status;是否能走松散索引扫描取决于索引和统计方式：KEY idx_status_created (status, created_at)相关算法：            算法      说明                  Loose Index Scan      利用索引跳跃读取分组起始点              Tight Index Scan      按索引范围扫描并顺序聚合              Temporary Table      无法按索引顺序分组时构建临时表      查看：EXPLAINSELECT status, COUNT(*)FROM ordersGROUP BY status;优化：  过滤条件前置；  分组列使用索引前缀；  减少 SELECT 字段；  拆分复杂统计；  预聚合；  迁移分析库。MySQL 8.0 的 GROUP BY 不再默认隐式排序。如果业务需要输出有序，必须显式 ORDER BY。14.8 DISTINCT 与去重示例：SELECT DISTINCT merchant_idFROM ordersWHERE status = 'PAID';优化器可能用索引或临时表去重。不要把 DISTINCT 当成数据质量工具。如果业务要求唯一，应通过唯一索引和清洗任务保证。窗口去重：WITH ranked AS (  SELECT t.*,         ROW_NUMBER() OVER (           PARTITION BY order_no           ORDER BY id DESC         ) AS rn  FROM payment_records t)SELECT *FROM rankedWHERE rn = 1;适合保留一条重复记录的查询，不能替代唯一约束。14.9 UNION 与临时表SELECT user_id FROM ordersUNIONSELECT id FROM users;UNION 需要去重，可能使用临时表。没有去重需求时使用：SELECT user_id, 'ORDER' AS source FROM ordersUNION ALLSELECT id, 'USER' AS source FROM users;外层 ORDER BY 作用于 UNION 结果，不是只作用于最后一个分支：SELECT order_no FROM ordersWHERE user_id = 1001UNION ALLSELECT payment_no FROM payment_recordsWHERE order_no IN (  SELECT order_no FROM orders WHERE user_id = 1001)ORDER BY order_noLIMIT 20;14.10 临时表机制查看临时表使用：SHOW GLOBAL STATUS LIKE 'Created_tmp%';常见来源：  GROUP BY 无法按索引顺序完成；  DISTINCT 去重；  UNION 去重；  派生表物化；  子查询物化；  IN 优化；  复杂表达式缓存；  窗口函数中间结果。内存限制：SHOW VARIABLES LIKE 'tmp_table_size';SHOW VARIABLES LIKE 'max_heap_table_size';SHOW VARIABLES LIKE 'temptable_max_ram';磁盘临时表增多的治理顺序：1. 减少参与行数；2. 减少参与列宽；3. 让 GROUP BY / ORDER BY 复用索引；4. 拆分复杂 SQL；5. 预聚合；6. 迁移分析负载；7. 再评估参数和硬件。14.11 排序分页案例案例一：列表接口原 SQL：SELECT *FROM ordersWHERE user_id = 1001ORDER BY created_at DESCLIMIT 20 OFFSET 5000;游标：SELECT *FROM ordersWHERE user_id = 1001  AND (created_at, id) &lt; (:last_created_at, :last_id)ORDER BY created_at DESC, id DESCLIMIT 20;索引：KEY idx_user_created (user_id, created_at)案例二：后台导出不要一次查询百万行。任务化处理：1. 按时间或主键分片；2. 每片 1000 到 5000 行；3. 使用游标条件；4. 写入文件或对象存储；5. 记录任务进度；6. 支持断点续跑；7. 控制对在线库的影响。案例三：复杂报表原 SQL 同时做：十表 JOIN + 多级 GROUP BY + DISTINCT + ORDER BY + OFFSET改造：  明确指标口径；  抽取基础明细宽表；  分钟级或小时级预聚合；  查询预聚合表；  历史数据进入 OLAP；  在线库只保留近期热点查询。本章小结排序优化的关键是让 ORDER BY 和 GROUP BY 复用索引顺序，无法复用时要减少参与排序的行数和列宽。深分页优先使用稳定游标，延迟关联只是兜底。UNION、DISTINCT、派生表和聚合都可能触发临时表，磁盘临时表持续增长时应先优化查询结构，再评估参数和硬件。思考题  什么条件下 ORDER BY 可以避免 filesort？  Using filesort 是否一定代表性能问题？  深分页为什么慢？游标分页为什么必须包含唯一列？  延迟关联适合什么场景？它有什么局限？  排查一个磁盘临时表增长问题，写出完整优化流程。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。子查询让 SQL 更接近业务语义，但也可能引入重复执行、临时表和不可下推条件。理解优化器如何改写、物化子查询，才能放心使用现代 MySQL 的表达能力。13.1 子查询类型回顾-- 标量子查询SELECT (SELECT MAX(price) FROM products) AS max_price;-- 列子查询SELECT *FROM usersWHERE id IN (SELECT user_id FROM orders);-- 行子查询SELECT *FROM productsWHERE (category, price) = ('BOOK', 79.00);-- 派生表SELECT t.categoryFROM (  SELECT category, COUNT(*) AS cnt  FROM products  GROUP BY category) t;-- 相关子查询SELECT u.usernameFROM users uWHERE EXISTS (  SELECT 1 FROM orders o WHERE o.user_id = u.id);执行方式取决于是否相关、结果大小、索引可用性和 MySQL 版本。13.2 相关子查询相关子查询引用外层列：EXPLAINSELECT o.order_noFROM orders oWHERE (  SELECT COUNT(*)  FROM order_items i  WHERE i.order_id = o.id) &gt; 2;概念上每条订单都要执行一次 COUNT。优化器可能改写或下推，但要重点看执行计划。更清晰的写法：WITH item_stats AS (  SELECT order_id, COUNT(*) AS item_count  FROM order_items  GROUP BY order_id)SELECT o.order_noFROM orders oJOIN item_stats s ON s.order_id = o.idWHERE s.item_count &gt; 2;13.3 EXISTS 与 IN查询有订单用户：SELECT *FROM users uWHERE EXISTS (  SELECT 1  FROM orders o  WHERE o.user_id = u.id);SELECT *FROM users uWHERE u.id IN (  SELECT o.user_id  FROM orders o);现代 MySQL 会尝试将符合条件的 IN / EXISTS 改写为 Semijoin。选择建议：  两边都写执行计划验证；  关联列必须有索引；  反向查询优先 NOT EXISTS；  NOT IN 必须排除 NULL；  不必执着于语法，看实际计划。13.4 NOT IN 的 NULL 陷阱以下查询结果为空：SELECT 1WHERE 1 NOT IN (2, NULL);因为 1 &lt;&gt; NULL 是 UNKNOWN。风险写法：SELECT *FROM users uWHERE u.id NOT IN (  SELECT o.user_id  FROM orders o);如果 orders.user_id 有 NULL，所有行都可能消失。修正：SELECT *FROM users uWHERE u.id NOT IN (  SELECT o.user_id  FROM orders o  WHERE o.user_id IS NOT NULL);推荐：SELECT *FROM users uWHERE NOT EXISTS (  SELECT 1  FROM orders o  WHERE o.user_id = u.id);13.5 派生表合并与物化派生表有两种处理方式：            方式      说明                  Merge      把派生表合并到外层查询，类似视图展开              Materialization      先执行派生表并生成临时表，再参与外层查询      可合并示例：SELECT t.user_idFROM (  SELECT user_id, status  FROM orders  WHERE status = 'PAID') tWHERE t.user_id = 1001;优化器可以直接下推 user_id=1001 和 status='PAID'。可能物化示例：SELECT t.user_id, t.order_countFROM (  SELECT user_id, COUNT(*) AS order_count  FROM orders  GROUP BY user_id) tWHERE t.order_count &gt;= 3;聚合必须先完成，外层 order_count 条件无法直接下推到聚合前。13.6 CTE 是否只执行一次非递归 CTE：WITH paid_orders AS (  SELECT id, user_id, total_amount  FROM orders  WHERE status = 'PAID')SELECT COUNT(*)FROM paid_ordersWHERE user_id = 1001;优化器可能合并 CTE 并下推条件。但当 CTE 被多处引用或包含聚合、窗口、LIMIT 时，可能物化。查看：EXPLAIN FORMAT=TREEWITH user_stats AS (  SELECT user_id, COUNT(*) AS cnt  FROM orders  GROUP BY user_id)SELECT *FROM user_statsWHERE cnt &gt; 3;不要假设 CTE 一定只执行一次，也不要假设一定只执行多次。以执行计划为准。13.7 递归 CTE组织树：CREATE TABLE org_nodes (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    parent_id BIGINT UNSIGNED NULL,    node_name VARCHAR(64) NOT NULL,    PRIMARY KEY (id),    KEY idx_parent (parent_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;INSERT INTO org_nodes (id, parent_id, node_name) VALUES(1, NULL, 'CEO'),(2, 1, 'Tech VP'),(3, 1, 'Sales VP'),(4, 2, 'DBA Team'),(5, 2, 'Backend Team');向下遍历：WITH RECURSIVE org_tree AS (  SELECT id, parent_id, node_name, 1 AS lvl,         CAST(CONCAT('/', id) AS CHAR(1000)) AS path  FROM org_nodes  WHERE parent_id IS NULL  UNION ALL  SELECT n.id, n.parent_id, n.node_name, t.lvl + 1,         CONCAT(t.path, '/', n.id)  FROM org_nodes n  JOIN org_tree t ON n.parent_id = t.id)SELECT *FROM org_treeORDER BY path;防环措施：  递归深度限制；  路径中检查是否重复；  写入前禁止环；  后台任务校验层级；  超过一定层级改用闭包表或图模型。设置递归深度：SET SESSION cte_max_recursion_depth = 100;13.8 LATERAL 子查询MySQL 8.0.22+ 支持 LATERAL 派生表：SELECT  u.username,  latest.order_no,  latest.created_atFROM users uJOIN LATERAL (  SELECT o.order_no, o.created_at  FROM orders o  WHERE o.user_id = u.id  ORDER BY o.created_at DESC, o.id DESC  LIMIT 1) latest ON TRUEWHERE u.status = 1;适合“每个外行取 Top N”。必须确保内层查询有高效索引，例如：KEY idx_user_created (user_id, created_at)如果外层用户很多且每个内层查询昂贵，LATERAL 会放大成本。13.9 子查询优化策略  能用 JOIN 表达的简单集合关系，优先写清晰 JOIN；  相关子查询看是否可用聚合表替代；  IN / EXISTS 用执行计划对比；  反向关系使用 NOT EXISTS；  CTE 命名复杂逻辑；  聚合派生表先过滤基础数据；  小心 LIMIT、窗口、UNION 导致物化；  大临时表要评估磁盘和内存；  报表子查询迁移到只读库或 OLAP；  确认结果集语义没有因改写改变。13.10 物化临时表观察查看临时表状态：SHOW GLOBAL STATUS LIKE 'Created_tmp%';SHOW SESSION STATUS LIKE 'Created_tmp%';相关参数：SHOW VARIABLES LIKE 'tmp_table_size';SHOW VARIABLES LIKE 'max_heap_table_size';临时表超过内存限制后会落盘。MySQL 8.0 使用 TempTable 引擎和 InnoDB 临时表空间：SHOW VARIABLES LIKE 'internal_tmp_mem_storage_engine';SHOW VARIABLES LIKE 'temptable_max_ram';发现大量磁盘临时表时：  检查 GROUP BY / DISTINCT / UNION；  减少 SELECT 字段；  增加过滤条件；  优化索引排序；  拆分 SQL；  引入预聚合；  迁移分析查询。13.11 子查询改写案例案例一：每用户最近订单相关子查询：SELECT o.*FROM orders oWHERE o.id = (  SELECT MAX(i.id)  FROM orders i  WHERE i.user_id = o.user_id);窗口函数：WITH ranked AS (  SELECT o.*,         ROW_NUMBER() OVER (           PARTITION BY o.user_id           ORDER BY o.created_at DESC, o.id DESC         ) AS rn  FROM orders o)SELECT *FROM rankedWHERE rn = 1;LATERAL：SELECT u.id, u.username, latest.order_noFROM users uJOIN LATERAL (  SELECT o.order_no  FROM orders o  WHERE o.user_id = u.id  ORDER BY o.created_at DESC, o.id DESC  LIMIT 1) latest ON TRUE;以数据量和执行计划选择。案例二：统计在群但未下单用户SELECT g.user_idFROM group_members gWHERE g.group_id = 100  AND NOT EXISTS (    SELECT 1    FROM orders o    WHERE o.user_id = g.user_id      AND o.created_at &gt;= '2026-08-01'  );案例三：巨大 IN 列表应用拼出十万元素的 IN 会导致：  SQL 过长；  解析成本高；  网络传输大；  优化器估算困难。改法：  分批查询；  使用临时表；  先落筛选结果；  使用 JOIN；  改为按维度范围查询。本章小结子查询的执行形态由优化器决定，可能是合并、物化、Semijoin 或相关执行。现代 MySQL 对 IN 和 EXISTS 的优化较好，但必须通过执行计划验证。NOT IN 的 NULL 陷阱、派生表物化、递归 CTE 防环和 LATERAL 索引依赖，是子查询实战中最需要关注的点。思考题  什么是相关子查询？它的风险是什么？  派生表 Merge 和 Materialization 有什么区别？  为什么 NOT EXISTS 通常比 NOT IN 更安全？  CTE 是否一定只执行一次？如何验证？  将一个三层嵌套子查询改写为 CTE 或 JOIN，并比较执行计划。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。JOIN 的性能取决于三件事：驱动表扫描多少行、被驱动表如何查找匹配行、结果是否需要额外排序或临时表。本章把 JOIN 的执行方式和优化路径讲清楚。12.1 JOIN 的基本执行模型简单嵌套循环：for row in 驱动表:    for row in 被驱动表:        if join_key 匹配:            输出结果如果被驱动表 Join Key 有索引，内层循环可以变成索引查找：for row in 驱动表:    通过索引直接查被驱动表匹配行这是 JOIN 优化的核心：让被驱动表走 eq_ref 或 ref。示例：EXPLAINSELECT o.order_no, u.usernameFROM orders oJOIN users u ON u.id = o.user_idWHERE o.user_id = 1001;理想计划：  orders 通过 idx_user_created 找到少量订单；  users 通过主键逐行查找，类型为 eq_ref。12.2 驱动表选择优化器通常倾向选择过滤后行数更小的表作为驱动表：SELECT COUNT(*)FROM ordersWHERE user_id = 1001;SELECT COUNT(*)FROM usersWHERE status = 1;影响选择的因素：  WHERE 条件选择性；  索引可用性；  统计信息；  JOIN 缓冲区大小；  ORDER BY 是否能复用某个表顺序；  派生表和子查询成本；  数据分布倾斜。不要固定认为小表一定是驱动表。过滤后更小、且能让另一边走索引的表才是更好的驱动表。12.3 Block Nested-Loop当被驱动表没有索引时，朴素嵌套循环代价很高。Block Nested-Loop 会把驱动表分批放入 Join Buffer：1. 读取一批驱动表行到 join_buffer；2. 扫描一次被驱动表；3. 对缓冲中的行做批量匹配；4. 清空缓冲，处理下一批。查看缓冲大小：SHOW VARIABLES LIKE 'join_buffer_size';如果执行计划出现：Using join buffer (Block Nested Loop)说明关联缺少合适索引。优先补关联键索引，而不是盲目调大 join_buffer_size。12.4 Hash JoinMySQL 8.0.18 起 InnoDB 支持 Hash Join，用于无可用索引的等值 JOIN：Build 阶段：  把较小的输入构建内存哈希表Probe 阶段：  扫描另一侧输入，探测哈希表并输出匹配结果示例：EXPLAIN FORMAT=TREESELECT COUNT(*)FROM orders oJOIN report_channels c  ON o.channel_code = c.channel_code;Hash Join 适合：  等值 JOIN；  关联列无索引；  分析型扫描；  大结果集构建代价低于反复查找；  内存能承载 Build 侧或可落盘分批处理。不适合或需谨慎：  非等值 JOIN；  极高并发在线核心链路；  内存压力大；  有更好索引查找路径；  小表主键查找场景。在线交易 JOIN 仍应优先设计好关联键索引。12.5 JOIN 算法对比            算法      条件      特点                  Index Nested-Loop      被驱动表有索引      在线业务首选              Block Nested-Loop      无索引时减少被驱动表扫描次数      仍需大扫描              Hash Join      等值条件，8.0.18+      分析查询常见              Straight Join      强制左表驱动      SQL 语义仍为 JOIN，但固定顺序      12.6 关联索引设计订单表：KEY idx_user_created (user_id, created_at)用户表：PRIMARY KEY (id)订单明细表：KEY idx_order (order_id)查询：SELECT o.order_no, oi.product_nameFROM orders oJOIN order_items oi ON oi.order_id = o.idWHERE o.user_id = 1001;好计划：  先按用户索引扫描订单；  再按明细表的 order_id 索引查找；  避免明细表全扫描。关联字段要求：  类型一致；  字符集一致；  排序规则一致；  两边语义一致；  被驱动表索引包含 Join Key；  尽量有 NOT NULL 约束。12.7 LEFT JOIN 优化保留左表：SELECT u.username, o.order_noFROM users uLEFT JOIN orders o  ON o.user_id = u.id AND o.status = 'PAID'WHERE u.status = 1;优化点：  左表过滤放 WHERE；  右表保留条件放 ON；  右表 Join Key 建索引；  需要 COUNT(o.id) 而不是 COUNT(*)；  小心一对多导致的汇总翻倍。示例：SELECT u.username, COUNT(o.id) AS paid_countFROM users uLEFT JOIN orders o  ON o.user_id = u.id AND o.status = 'PAID'GROUP BY u.username;12.8 派生表 JOIN先聚合再 JOIN：WITH order_stats AS (  SELECT    user_id,    COUNT(*) AS order_count,    SUM(total_amount) AS amount_sum  FROM orders  WHERE status = 'PAID'  GROUP BY user_id)SELECT u.username, s.order_count, s.amount_sumFROM users uJOIN order_stats s ON s.user_id = u.id;注意：  派生表可能物化为临时表；  WHERE 能下推时优化器会改写；  大派生表 JOIN 小表可能慢；  先过滤再聚合；  外层条件不一定都能推进 CTE。MySQL 8.0.22+ 支持 LATERAL 派生表，可按外行执行：SELECT  u.username,  latest.order_noFROM users uJOIN LATERAL (  SELECT o.order_no  FROM orders o  WHERE o.user_id = u.id  ORDER BY o.created_at DESC  LIMIT 1) AS latest ON TRUEWHERE u.status = 1;适合每个外行取少量 Top N 的场景，前提是 orders(user_id, created_at) 有索引。12.9 Semijoin 与 Antijoin存在匹配即返回：SELECT u.id, u.usernameFROM users uWHERE EXISTS (  SELECT 1  FROM orders o  WHERE o.user_id = u.id);MySQL 优化器可能将 EXISTS 改写为 Semijoin，不必扫描用户全部匹配订单，找到一条即可。不存在匹配：SELECT u.id, u.usernameFROM users uWHERE NOT EXISTS (  SELECT 1  FROM orders o  WHERE o.user_id = u.id);MySQL 8.0.17+ 对部分 Antijoin 场景有更好优化。12.10 JOIN 查询治理常见坏味道：  一个 SQL JOIN 十几张表；  无索引关联字段；  关联字段类型不一致；  JOIN 后再按大结果过滤；  报表 SQL 混入交易接口；  LEFT JOIN 右表条件放错位置；  一对多导致重复计数；  深分页叠加多表 JOIN。治理策略：            问题      策略                  表太多      拆接口、拆 SQL、异步组装              无索引      补关联索引              报表复杂      只读库、预聚合、OLAP              数据重复      先聚合再 JOIN              字段不一致      统一类型和字符集              大结果传输      减少字段、分页、游标              代码难维护      明确领域边界和数据归属      12.11 JOIN 优化案例案例一：被驱动表全扫描原 SQL：SELECT o.order_no, m.merchant_nameFROM orders oJOIN merchants m ON m.merchant_name = o.merchant_nameWHERE o.user_id = 1001;问题：  用名称关联；  名称可能修改，历史订单语义不稳定。优化：SELECT o.order_no, m.merchant_nameFROM orders oJOIN merchants m ON m.id = o.merchant_idWHERE o.user_id = 1001;案例二：一对多汇总翻倍错误：SELECT  SUM(o.total_amount) AS amountFROM orders oJOIN order_items i ON i.order_id = o.idWHERE o.status = 'PAID';正确：SELECT SUM(o.total_amount) AS amountFROM orders oWHERE o.status = 'PAID';或分别聚合后在订单粒度合并。案例三：报表大 JOIN原 SQL 在主库 JOIN 订单、用户、商户、商品、物流和售后表。优化方向：  移到只读实例；  使用预聚合宽表；  按 T+1 或分钟级更新；  大历史扫描进入 ClickHouse；  在线接口只查最近数据和固定维度。本章小结JOIN 优化的第一优先级是让被驱动表通过索引查找匹配行。选择驱动表时看过滤后的行数，而不是物理表大小。无索引等值 JOIN 可以利用 Hash Join，但在线交易仍应依赖良好索引。LEFT JOIN 条件位置、一对多重复统计、关联字段一致性和报表隔离，是生产 JOIN 问题的高频来源。思考题  Index Nested-Loop 为什么优于朴素嵌套循环？  Join Buffer 和 Hash Join 分别解决什么问题？  如何判断哪张表适合做驱动表？  LEFT JOIN 的右表条件为什么有时要放 ON？  优化一个四表 JOIN SQL，并记录执行计划变化。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。“加了索引还是慢”是 MySQL 排障的高频问题。索引失效并不总是语法层面完全不能用，更多时候是优化器评估代价后放弃，或者索引只能使用很少前缀，导致扫描量仍然很大。11.1 索引失效的判断方法不要凭感觉判断，先比较执行计划：EXPLAINSELECT *FROM ordersWHERE user_id = 1001;重点检查：  key 是否为 NULL；  possible_keys 是否包含预期索引；  key_len 是否只用了部分列；  rows 是否明显过大；  Extra 是否有临时表或排序；  是否有隐式转换提示。再用真实参数复现。同一条 SQL 不同参数可能选择不同执行计划，尤其是数据倾斜时。11.2 函数或表达式包裹索引列失效写法：SELECT *FROM ordersWHERE DATE(created_at) = '2026-08-25';B+Tree 按 created_at 原值排序，DATE(created_at) 的结果在树上是跳跃分布的，无法直接定位。改写：SELECT *FROM ordersWHERE created_at &gt;= '2026-08-25 00:00:00'  AND created_at &lt; '2026-08-26 00:00:00';常见类似问题：            失效写法      改写方向                  DATE(paid_at)='2026-08-25'      半开区间              SUBSTR(order_no,1,4)='2026'      前缀范围或额外查询列              CAST(user_id AS CHAR)='1001'      使用数字参数              amount + 0 &gt; 100      直接比较              id - 1 = 99      id = 100      如果业务必须使用表达式，MySQL 8.0.13+ 可以创建函数索引。11.3 隐式类型转换order_no 是 VARCHAR：SELECT *FROM ordersWHERE order_no = 20260825000001;应改为：SELECT *FROM ordersWHERE order_no = '20260825000001';数字与字符串比较时，MySQL 通常把字符串转换为数字，索引列被函数化后无法正常使用 B+Tree。常见类型问题：            列类型      错误参数      正确参数                  VARCHAR      数字      字符串              BIGINT      字符串数字通常仍可用      数字              DATETIME      非标准格式      标准时间或范围              DECIMAL      浮点字符串      精确数字              BINARY      字符集不一致      二进制或相同字符集      程序中必须使用参数绑定，并声明正确 JDBC 类型。11.4 前导模糊匹配可以走前缀索引：SELECT *FROM productsWHERE product_name LIKE 'MySQL%';普通 B+Tree 难以使用：SELECT *FROM productsWHERE product_name LIKE '%Book%';因为树按前缀有序，后缀匹配没有确定起点。可选方案：  限定前缀；  使用全文索引；  同步 Elasticsearch；  搜索词单独表；  限制扫描范围后模糊过滤；  业务上引导用户选择筛选条件。转义通配符：SELECT *FROM productsWHERE product_name LIKE '%50\\%%';11.5 联合索引缺少前导列索引：KEY idx_user_status_created (user_id, status, created_at)慢写法：SELECT *FROM ordersWHERE status = 'PAID'  AND created_at &gt;= '2026-08-01';改法一：保留 user_id 条件：SELECT *FROM ordersWHERE user_id = 1001  AND status = 'PAID'  AND created_at &gt;= '2026-08-01';改法二：为后台查询建立新索引：ALTER TABLE orders  ADD INDEX idx_status_created (status, created_at);MySQL 8.0 的 Skip Scan 对前导列取值很少的场景可能有帮助，但不是通用方案。11.6 OR 与 AND 的差异AND 通常能组合使用同一联合索引的多个列；OR 连接不同索引列时需要分别访问：SELECT *FROM ordersWHERE user_id = 1001   OR order_no = 'NO001';可能使用 index_merge，也可能退化为全表扫描。拆分为 UNION ALL 往往更可控：SELECT *FROM ordersWHERE user_id = 1001UNION ALLSELECT *FROM ordersWHERE order_no = 'NO001'  AND NOT (user_id = 1001);如果 OR 两边是同一列，用 IN 表达：SELECT *FROM ordersWHERE user_id IN (1001, 1002)  AND status = 'PAID';11.7 范围条件后的列索引：KEY idx_status_created_amount (status, created_at, amount)查询：SELECT *FROM ordersWHERE status = 'PAID'  AND created_at &gt;= '2026-08-01'  AND amount &gt; 100;status 精确匹配，created_at 走范围，amount 在 B+Tree 上无法继续作为定位前缀，但仍可能被索引条件下推过滤。改写方向：  缩小时间范围；  高频固定金额条件可调整索引顺序；  使用覆盖索引减少回表；  交给分析库；  增加预聚合或派生过滤字段。11.8 字符集与排序规则不一致JOIN 时如果两边字符集或排序规则不同，可能导致索引无法使用：SELECT COUNT(*)FROM orders oJOIN channels c ON o.channel = c.channel_code;检查：SELECT  TABLE_NAME,  COLUMN_NAME,  CHARACTER_SET_NAME,  COLLATION_NAMEFROM information_schema.COLUMNSWHERE TABLE_SCHEMA = 'shop'  AND COLUMN_NAME IN ('channel', 'channel_code');统一字符集：ALTER TABLE orders  MODIFY channel VARCHAR(32)  CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL;新旧表、临时表、跨库表都要统一，否则可能只在特定 SQL 上暴露。11.9 NOT、!= 和 IN 的选择性以下条件不是绝对不能用索引：SELECT *FROM ordersWHERE status &lt;&gt; 'CANCELED';但 &lt;&gt; 常常需要扫描大量值。如果非取消状态占比 95%，全表扫描可能是正确选择。改成 IN 并不一定更快，但语义更清晰：SELECT *FROM ordersWHERE status IN ('CREATED', 'PAID', 'SHIPPED');核心是选择性：排除 5% 不如直接定位要的 5%。任务类查询应显式列出待处理状态。11.10 ORDER BY 无法使用索引索引：KEY idx_user_created (user_id, created_at)可用：SELECT *FROM ordersWHERE user_id = 1001ORDER BY created_at;不能用该索引完成排序：SELECT *FROM ordersORDER BY created_at;SELECT *FROM ordersWHERE user_id = 1001ORDER BY updated_at;SELECT *FROM ordersWHERE user_id &gt; 1000ORDER BY created_at, user_id;排序优化原则：  ORDER BY 列顺序与联合索引一致；  前面的等值条件是索引前缀；  方向一致或使用降序索引；  范围条件可能破坏后续排序复用；  小结果集 filesort 未必是问题。MySQL 8.0 支持降序索引：ALTER TABLE orders  ADD INDEX idx_user_created_desc (user_id, created_at DESC);11.11 查询改写案例案例一：按天统计原写法：SELECT DATE(created_at) AS dt, COUNT(*)FROM ordersGROUP BY DATE(created_at);可改为生成列：ALTER TABLE orders  ADD COLUMN created_date DATE    GENERATED ALWAYS AS (DATE(created_at)) STORED,  ADD INDEX idx_created_date (created_date);SELECT created_date AS dt, COUNT(*)FROM ordersGROUP BY created_date;案例二：函数包裹状态原写法：SELECT *FROM ordersWHERE UPPER(status) = 'PAID';改写：SELECT *FROM ordersWHERE status = 'PAID';并规范数据写入，统一大小写。案例三：COUNT 多状态多个子查询：SELECT  (SELECT COUNT(*) FROM orders WHERE status='CREATED') AS created_count,  (SELECT COUNT(*) FROM orders WHERE status='PAID') AS paid_count,  (SELECT COUNT(*) FROM orders WHERE status='CANCELED') AS canceled_count;一次扫描：SELECT  SUM(status='CREATED') AS created_count,  SUM(status='PAID') AS paid_count,  SUM(status='CANCELED') AS canceled_countFROM orders;若表很大，实时 COUNT 本身仍可能是问题，应使用预聚合或近似统计。案例四：深分页原写法：SELECT *FROM ordersORDER BY idLIMIT 20 OFFSET 1000000;游标分页：SELECT *FROM ordersWHERE id &gt; 1000000ORDER BY idLIMIT 20;复合排序游标：SELECT *FROM ordersWHERE user_id = 1001  AND (created_at &lt; '2026-08-25 10:00:00'       OR (created_at = '2026-08-25 10:00:00' AND id &lt; 123))ORDER BY created_at DESC, id DESCLIMIT 20;必须使用唯一列做 tie-breaker，否则同一排序值可能重复或漏页。11.12 索引提示强制索引：SELECT *FROM orders FORCE INDEX (idx_user_created)WHERE user_id = 1001;建议索引：SELECT *FROM orders USE INDEX (idx_user_created)WHERE user_id = 1001;忽略索引：SELECT *FROM orders IGNORE INDEX (idx_status_created)WHERE user_id = 1001;使用原则：  先修复统计信息和 SQL；  只作为临时规避手段；  必须写注释说明原因；  设置到期复查；  数据分布变化后可能失效；  团队要知道哪些 SQL 使用了强制提示。11.13 查询改写流程1. 保留原始 SQL、参数和执行计划；2. 明确业务语义，不改结果；3. 找出扫描量和排序临时表来源；4. 消除隐式转换和列函数；5. 匹配联合索引前缀；6. 缩小时间和数据范围；7. 拆分 OR、深分页和巨型子查询；8. 评估覆盖索引；9. 对比返回行数和抽样结果；10. 记录上线指标和回滚方案。本章小结索引失效的常见原因是列被函数化、隐式类型转换、前导列缺失、字符集不一致、条件选择性差和排序顺序不匹配。处理时应先看执行计划和真实参数，再改写 SQL 或调整索引。索引提示只能临时救急，长期方案仍是统计信息、表结构和查询路径的治理。思考题  WHERE DATE(created_at)=... 为什么难用索引？如何改写？  VARCHAR 列用数字比较有什么风险？  OR 和 IN 在索引使用上有什么差异？  为什么 &lt;&gt; 可能不走索引但仍是合理执行计划？  把一条生产慢 SQL 从函数条件、深分页、SELECT * 三个角度完整改写一次。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。索引设计不是把所有 WHERE 字段都建一遍，而是从查询路径、数据分布、写入成本和业务演进出发，选择一组能覆盖核心访问模式的索引。10.1 索引设计原则核心原则：  先明确高频 SQL，再设计索引；  联合索引优先于多个单列索引；  等值条件放前面，范围条件放后面；  尽量让排序和分组复用索引顺序；  高频轻查询考虑覆盖索引；  唯一性交给唯一索引，不靠代码约定；  控制索引总数；  为索引建立负责人和验证记录；  上线前后对比执行计划；  定期清理无效索引。10.2 选择性评估查看表规模：SELECT TABLE_ROWS, DATA_LENGTH, INDEX_LENGTHFROM information_schema.TABLESWHERE TABLE_SCHEMA = 'shop'  AND TABLE_NAME = 'orders';查看单列选择性：SELECT  COUNT(*) AS total_rows,  COUNT(DISTINCT user_id) AS distinct_user,  COUNT(DISTINCT status) AS distinct_status,  COUNT(DISTINCT merchant_id) AS distinct_merchant,  COUNT(DISTINCT user_id) / COUNT(*) AS user_selectivity,  COUNT(DISTINCT status) / COUNT(*) AS status_selectivityFROM orders;查看组合选择性：SELECT  COUNT(*) AS total_rows,  COUNT(DISTINCT user_id, status) AS distinct_user_statusFROM orders;选择性的常见判断：            选择性      说明                  接近 1      大部分值唯一，适合索引              中等      可能适合与其他列组合              很低      单独索引未必有效              严重倾斜      平均值误导，需要按热门值分析      状态列基数低，但 (status, created_at) 对处理任务可能有效，因为任务只需扫描少量待处理状态。10.3 联合索引顺序假设订单表有三个高频查询：-- Q1SELECT *FROM ordersWHERE user_id = 1001ORDER BY created_at DESCLIMIT 20;-- Q2SELECT *FROM ordersWHERE user_id = 1001  AND status IN ('CREATED', 'PAID')ORDER BY created_at DESC;-- Q3SELECT *FROM ordersWHERE status = 'CREATED'  AND created_at &gt;= '2026-08-01'ORDER BY created_at;推荐索引：KEY idx_user_created (user_id, created_at),KEY idx_status_created (status, created_at)不建议一开始就设计：KEY idx_user_status_created (user_id, status, created_at)原因是 Q1 不能使用该索引完成 ORDER BY created_at，因为中间隔了 status。联合索引设计口诀：固定过滤 -&gt; 可选过滤 -&gt; 范围过滤 -&gt; 排序字段真实业务要结合查询频率和结果集大小调整，不能机械套用。10.4 覆盖索引设计订单列表接口只展示：order_no、user_id、status、total_amount、created_at创建索引：ALTER TABLE orders  ADD INDEX idx_user_created_cover (    user_id,    created_at,    order_no,    status,    total_amount  );查询：SELECT  id,  order_no,  status,  total_amount,  created_atFROM ordersWHERE user_id = 1001ORDER BY created_at DESCLIMIT 20;验证：EXPLAINSELECT id, order_no, status, total_amount, created_atFROM ordersWHERE user_id = 1001ORDER BY created_at DESC;如果 Extra 出现 Using index，说明无需回表。覆盖索引代价：  索引变宽；  写入成本增加；  磁盘和内存占用增加；  索引列更新更频繁时收益下降。适合核心高频接口，不适合盲目给所有查询加宽。10.5 前缀索引长字符串可以只索引前缀：ALTER TABLE users  ADD INDEX idx_nickname_prefix (nickname(20));评估前缀选择性：SELECT  COUNT(DISTINCT LEFT(nickname, 10)) / COUNT(DISTINCT nickname) AS p10,  COUNT(DISTINCT LEFT(nickname, 20)) / COUNT(DISTINCT nickname) AS p20,  COUNT(DISTINCT LEFT(nickname, 30)) / COUNT(DISTINCT nickname) AS p30FROM users;限制：  不能用于 ORDER BY 的完整排序优化；  不能作为覆盖索引；  唯一前缀可能误判，唯一约束应使用完整列或业务散列；  前缀过短会导致大量回表。长文本搜索应考虑全文索引、外部搜索系统或单独散列列。10.6 唯一索引唯一索引负责数据库层一致性：ALTER TABLE users  ADD UNIQUE KEY uk_mobile (mobile);插入冲突：INSERT INTO users (username, nickname, mobile)VALUES ('dave', 'Dave', '13800000001');报错 Duplicate entry 是数据库在保护业务约束。MySQL 允许唯一索引中多个 NULL 值。业务上如果不允许多个空手机号，应把列定义为 NOT NULL，或使用默认值加唯一约束。逻辑删除示例：ALTER TABLE users  DROP INDEX uk_username,  ADD UNIQUE KEY uk_username_deleted (username, deleted);该方案只能区分“未删”和“一次删除”，多次删除同值仍会冲突。更稳妥的是使用删除版本号：ALTER TABLE users  ADD COLUMN delete_version BIGINT UNSIGNED NOT NULL DEFAULT 0,  DROP INDEX uk_username_deleted,  ADD UNIQUE KEY uk_username_delver (username, delete_version);未删除行 delete_version=0，删除时更新为自增值。10.7 函数索引与表达式索引MySQL 8.0.13 支持函数索引：ALTER TABLE users  ADD INDEX idx_upper_username ((UPPER(username)));查询必须使用相同表达式：SELECT *FROM usersWHERE UPPER(username) = 'ALICE';也可以为 JSON 字段建立表达式索引：ALTER TABLE orders  ADD INDEX idx_extra_channel ((CAST(extra-&gt;&gt;'$.channel' AS CHAR(32))));原则：  优先改写 SQL 为普通索引可用的形式；  函数索引必须与应用表达式完全一致；  隐式转换和字符集差异会导致失效；  复杂表达式增加维护成本；  用注释记录用途。10.8 不可见索引MySQL 8.0 支持不可见索引：ALTER TABLE orders  ALTER INDEX idx_status_created INVISIBLE;优化器会忽略不可见索引，但写入仍维护它。适合删除索引前的灰度验证：1. 将索引设为 INVISIBLE；2. 观察慢日志和业务指标；3. 确认无影响后 DROP；4. 如出现劣化，立即 VISIBLE 恢复。恢复：ALTER TABLE orders  ALTER INDEX idx_status_created VISIBLE;注意：  不可见索引仍占用空间和写入成本；  主键不能设为不可见；  需要监控确认没有强制使用该索引的 SQL；  灰度周期要覆盖业务高峰和周期任务。10.9 冗余与重复索引以下索引存在冗余：KEY idx_user (user_id),KEY idx_user_created (user_id, created_at)通常 idx_user 可以删除，因为 idx_user_created 可支持按 user_id 前缀查询。以下不是简单冗余：KEY idx_user_created (user_id, created_at),KEY idx_user_status (user_id, status)两者支持不同排序和过滤路径，需要结合执行计划判断。查找重复和未使用索引：SELECT  OBJECT_SCHEMA,  OBJECT_NAME,  INDEX_NAME,  COUNT_READ,  COUNT_WRITEFROM performance_schema.table_io_waits_summary_by_index_usageWHERE OBJECT_SCHEMA = 'shop'  AND INDEX_NAME IS NOT NULLORDER BY COUNT_READ ASC, COUNT_WRITE DESC;判断索引是否可删除：  观察一个完整业务周期；  覆盖日常、周报、月报、活动、批处理；  搜索代码和配置中的 FORCE INDEX；  在预发或影子库验证；  先设为 INVISIBLE；  删除后保留恢复脚本。10.10 在线创建索引MySQL 8.0 InnoDB 支持 Online DDL 创建索引：ALTER TABLE orders  ADD INDEX idx_merchant_created (merchant_id, created_at),  ALGORITHM=INPLACE,  LOCK=NONE;Online DDL 不等于零影响：  需要扫描原表；  需要临时磁盘空间；  记录增量变更；  占用 IO 和 CPU；  末尾可能短暂持有元数据锁；  长事务会阻塞 DDL；  主从延迟可能上升。上线前检查长事务：SELECT trx_id, trx_state, trx_started, trx_mysql_thread_idFROM information_schema.INNODB_TRXORDER BY trx_started;避免在长事务、大流量和备份任务重叠时执行。10.11 索引设计案例案例一：订单查询原始 SQL：SELECT *FROM ordersWHERE merchant_id = 10  AND status IN ('PAID', 'SHIPPED')ORDER BY created_at DESCLIMIT 20;问题：  merchant_id 有索引但缺少时间排序；  SELECT * 无法覆盖；  回表后可能再排序。索引：ALTER TABLE orders  ADD INDEX idx_merchant_created (merchant_id, created_at);如果 IN 值较多，ORDER BY created_at 是否能完全避免排序取决于优化器执行方式，需用执行计划验证。案例二：后台任务扫描SELECT *FROM ordersWHERE status = 'TIMEOUT_PENDING'  AND created_at &lt; DATE_SUB(NOW(), INTERVAL 30 MINUTE)LIMIT 100;索引：ALTER TABLE orders  ADD INDEX idx_status_created (status, created_at);该索引虽然状态基数低，但待处理状态占比很小，扫描量极低，是典型有效设计。案例三：多租户表SELECT *FROM tenant_documentsWHERE tenant_id = 100  AND folder_id = 20  AND deleted = 0ORDER BY updated_at DESCLIMIT 50;索引：ALTER TABLE tenant_documents  ADD INDEX idx_tenant_folder_updated (    tenant_id,    folder_id,    deleted,    updated_at  );如果 deleted 只有 0/1 且必传，可以放在排序前。若多数查询不传 folder_id，应保留或设计另一条索引。案例四：活动库存扣减UPDATE activity_stockSET stock = stock - 1WHERE activity_id = 1001  AND sku_id = 2002  AND stock &gt; 0;索引：ALTER TABLE activity_stock  ADD UNIQUE KEY uk_activity_sku (activity_id, sku_id);唯一索引精确定位行，避免扫描大量库存行并降低锁范围。10.12 索引规范模板命名：            类型      前缀                  普通索引      idx_              唯一索引      uk_              全文索引      ft_              表达式索引      idx_表达式含义_      上线评审表：            项目      内容                  SQL      原始 SQL 和参数分布              表规模      当前行数、月增量              问题      扫描行、耗时、临时表、排序              新索引      完整 DDL              预期      新执行计划和扫描行变化              写入影响      预估 DML 延迟变化              验证      功能、性能、复制、监控              回滚      删除或不可见索引 DDL      本章小结索引设计要从真实查询出发。等值条件前置、范围条件后置、排序字段尽量复用索引顺序；高频接口可评估覆盖索引，唯一性必须由唯一索引保证。索引会增加写入、存储、内存和 DDL 成本，因此每个索引都应有明确用途、验证数据和下线机制。思考题  为什么 (a,b) 索引通常可以让 (a) 查询使用，反之不行？  覆盖索引适合什么场景？有哪些代价？  低基数索引在什么情况下仍然有效？  不可见索引如何用于安全删除索引？  为你们系统的一条核心慢 SQL 设计索引，并写出上线验证清单。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。MySQL 的 InnoDB 索引是 B+Tree。理解 B+Tree，就能理解为什么主键要递增、为什么联合索引遵守最左前缀、为什么二级索引会回表、为什么范围查询可能影响排序。9.1 为什么需要索引没有索引时，查找一个订单只能全表扫描：10 万行：最多比较 10 万次1000 万行：最多比较 1000 万次B+Tree 将有序数据按页组织，从根到叶逐层缩小范围。100 万行数据通常只需要几次页访问。真实次数取决于行大小、页填充率、缓冲命中和数据分布，但数量级差异非常明显。9.2 B+Tree 的结构InnoDB 默认页大小为 16KB：SHOW VARIABLES LIKE 'innodb_page_size';B+Tree 结构：Root Page  |  +-- Internal Page：只存键和子页指针        |        +-- Leaf Page：存索引键和行信息同一层叶子节点之间用双向链表连接特点：  非叶子节点只负责路由；  叶子节点保存全部键值；  叶子节点有序；  范围扫描可以沿链表继续读取；  树高通常为 2 到 4 层。9.3 聚簇索引InnoDB 表本身就是按主键组织的 B+Tree，称为聚簇索引。PRIMARY KEY B+Tree叶子节点：  主键值 + 完整行数据如果没有显式主键：  优先使用第一个非 NULL 唯一索引作为聚簇索引；  如果没有合适的唯一索引，InnoDB 生成隐藏的 ROW_ID。隐藏 ROW_ID 不利于治理，业务表应显式定义主键。主键查询SELECT *FROM ordersWHERE id = 1;沿着主键 B+Tree 定位叶子页，直接拿到完整行，不需要回表。9.4 二级索引二级索引也称为辅助索引。它的叶子节点保存索引列和主键值：INDEX(user_id, created_at)叶子节点：  user_id + created_at + id查询：SELECT *FROM ordersWHERE user_id = 1001;执行过程：1. 在 idx_user_created 中定位 user_id=1001；2. 从叶子节点拿到主键 id；3. 回到主键索引查完整行；4. 继续扫描直到 user_id 条件不满足。第 3 步就是回表。若结果很多，回表成本会明显上升。覆盖索引SELECT id, user_id, created_atFROM ordersWHERE user_id = 1001;二级索引叶子已经包含 user_id、created_at 和主键 id，无需回表，执行计划通常显示 Using index。覆盖索引不是一种独立语法，而是查询所需列恰好被索引包含的结果。9.5 最左前缀原则联合索引：KEY idx_user_status_created (user_id, status, created_at)可以支持：user_iduser_id + statususer_id + status + created_atuser_id + status + created_at 范围不能直接支持：statuscreated_atstatus + created_at因为 B+Tree 先按 user_id 排序；只有 user_id 相同，才按 status 排序；只有前两列相同，才按 created_at 排序。MySQL 8.0.13+ 有 Index Skip Scan，某些前导列取值很少的场景可以跳跃扫描，但不能把它当作通用设计依据。9.6 索引列顺序与范围条件联合索引 (a, b, c) 中：WHERE a = 1 AND b = 2 AND c &gt;= 10通常 a、b 可以精确匹配，c 走范围。WHERE a &gt;= 1 AND b = 2a 是范围后，b 在 B+Tree 全局有序性上无法继续精确定位。优化器仍可能使用 b 过滤，但效率取决于数据分布。设计建议：  等值条件列放前面；  范围条件列尽量放后面；  排序列放在范围列之后要谨慎；  高选择性列优先，但要结合固定查询；  不要只看单列区分度，要看组合查询路径。9.7 页分裂与页合并页分裂当一页写满后插入新行，InnoDB 会分配新页并移动部分记录：原页 100% 满  -&gt; 分裂成两个约 50% 满的页  -&gt; 父节点增加路由项随机主键会让新数据落在已有页之间，导致更多页分裂：  写入放大；  页空间利用率下降；  树高增长；  Buffer Pool 命中率下降；  二级索引同样受影响。趋势递增主键让新数据集中追加到最右侧页，分裂更少。页合并删除数据使页填充率低于阈值时，InnoDB 可能合并页：低填充页 -&gt; 与相邻页合并 -&gt; 释放空页删除大量数据后磁盘空间未必立刻返还给操作系统，通常以可复用页的形式留在表空间中。9.8 索引基数与统计信息查看索引基数：SHOW INDEX FROM orders;SELECT  table_name,  index_name,  column_name,  cardinalityFROM information_schema.STATISTICSWHERE table_schema = 'shop'  AND table_name = 'orders';基数表示索引列不同值的估算数量。基数越高，通常选择性越好。粗略选择性：SELECT  COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,  COUNT(DISTINCT user_id) / COUNT(*) AS user_selectivityFROM orders;注意：  基数是估算值；  低基数不等于索引一定没用；  状态列配合时间列可能有效；  高基数列也要匹配查询条件；  数据分布倾斜时平均值会误导判断。更新统计信息：ANALYZE TABLE orders;MySQL 8.0 支持直方图，帮助优化器了解非索引列分布：ANALYZE TABLE orders  UPDATE HISTOGRAM ON status, merchant_id WITH 100 BUCKETS;查看：SELECT * FROM information_schema.COLUMN_STATISTICSWHERE table_name = 'orders'\G9.9 行格式与大字段查看表行格式：SELECT NAME, ROW_FORMATFROM information_schema.INNODB_TABLESWHERE NAME LIKE '%/orders';常见行格式：            格式      说明                  COMPACT      传统紧凑格式              DYNAMIC      MySQL 5.7+ 常用，长变长列可溢出              COMPRESSED      压缩行格式，需要评估 CPU 和页匹配      大字段可能存储在溢出页：聚簇索引叶子保存前缀 + 指向溢出页的指针影响：  单页可容纳行数减少；  索引树变高；  查询大字段增加随机 IO；  内存命中率下降。大字段治理：  不查询不必要的 TEXT / BLOB；  高频字段和低频大字段分表；  图片和文件放对象存储；  历史详情归档；  使用覆盖索引避免读取大字段所在页。9.10 Change BufferChange Buffer 用于二级索引写优化。当修改的二级索引页不在 Buffer Pool 中时，InnoDB 可以先把变更缓存起来，避免立即读入原索引页。查看：SHOW VARIABLES LIKE 'innodb_change_buffering';SHOW ENGINE INNODB STATUS\G适合：  二级索引多；  写多读少；  页不在内存概率高；  非唯一索引。不适合：  唯一索引必须读取页判断唯一性；  写后马上读取；  索引页基本常驻内存；  高压场景需要简化不确定因素。9.11 Adaptive Hash IndexInnoDB 会监控热点页访问模式，自动为热点索引页构建自适应哈希：SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';SHOW ENGINE INNODB STATUS\G它不是用户可显式创建的哈希索引，也不是通用加速器。高并发读写下可能引入锁竞争，排障时可以谨慎关闭测试：SET GLOBAL innodb_adaptive_hash_index = OFF;生产修改全局参数必须评估影响范围并保留回滚方案。9.12 主键为什么推荐 BIGINT AUTO_INCREMENT常见理由：  趋势递增，减少随机插入和页分裂；  固定 8 字节，键比较简单；  二级索引叶子保存主键，主键越小越省空间；  便于范围划分、归档和游标分页；  便于监控自增水位。随机 UUID 的问题：  插入位置随机；  空间占用更大；  缓存局部性差；  二级索引体积膨胀；  页分裂更多。如果需要对外隐藏规律，可以保留内部递增主键，另建随机 public_id 唯一索引。9.13 索引的代价索引不是免费的：            操作      受影响                  INSERT      每个索引都可能写入              UPDATE      修改索引列时更新索引              DELETE      标记删除并维护索引              磁盘      索引占用空间              内存      索引页竞争 Buffer Pool              优化器      候选索引越多选择越复杂              DDL      索引越多变更越慢              备份      数据文件更大      经验上，单表索引数量应控制在必要范围内，并为每个索引回答：1. 支撑哪个查询；2. 预计减少多少扫描；3. 写入代价多少；4. 是否能覆盖高频查询；5. 未来是否可下线；6. 由谁负责验证；7. 由谁负责删除。本章小结InnoDB 的聚簇索引按主键组织完整行，二级索引叶子保存索引列和主键。B+Tree 的有序结构解释了最左前缀、范围扫描、回表、覆盖索引和页分裂。主键应尽量小且趋势递增，索引设计要同时考虑查询收益和写入、存储、DDL、内存成本。思考题  为什么二级索引叶子节点保存主键而不是完整行地址？  覆盖索引为什么可以减少回表？  联合索引 (a,b,c) 为什么不能直接支持只按 c 查询？  随机 UUID 主键为什么会增加页分裂？  为什么索引数量不是越多越好？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。执行计划是 MySQL 告诉你“这条 SQL 打算怎么执行”的窗口。看懂执行计划，才能判断慢查询是缺少索引、扫描量过大、临时表排序，还是 JOIN 顺序不合适。8.1 准备测试数据为了让扫描路径更明显，先构造一批数据：CREATE TABLE query_orders (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    order_no VARCHAR(32) NOT NULL,    user_id BIGINT UNSIGNED NOT NULL,    merchant_id BIGINT UNSIGNED NOT NULL,    order_status VARCHAR(16) NOT NULL,    amount DECIMAL(12, 2) NOT NULL,    created_at DATETIME(3) NOT NULL,    PRIMARY KEY (id),    UNIQUE KEY uk_order_no (order_no),    KEY idx_user_created (user_id, created_at),    KEY idx_status_created (order_status, created_at),    KEY idx_merchant_status (merchant_id, order_status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;DELIMITER $$CREATE PROCEDURE insert_query_orders(IN p_rows INT)BEGIN  DECLARE i INT DEFAULT 1;  WHILE i &lt;= p_rows DO    INSERT INTO query_orders      (order_no, user_id, merchant_id, order_status, amount, created_at)    VALUES (      CONCAT('NO', LPAD(i, 12, '0')),      FLOOR(1 + RAND() * 10000),      FLOOR(1 + RAND() * 100),      ELT(FLOOR(1 + RAND() * 4), 'CREATED', 'PAID', 'SHIPPED', 'CANCELED'),      ROUND(RAND() * 1000, 2),      DATE_ADD('2026-01-01', INTERVAL FLOOR(RAND() * 240) HOUR_MICROSECOND)    );    SET i = i + 1;  END WHILE;END$$DELIMITER ;CALL insert_query_orders(100000);ANALYZE TABLE query_orders;测试后可删除过程：DROP PROCEDURE insert_query_orders;8.2 EXPLAIN 输出总览EXPLAINSELECT id, order_no, amountFROM query_ordersWHERE user_id = 1001  AND created_at &gt;= '2026-02-01';输出主要列：            列      含义                  id      查询块编号              select_type      查询块类型              table      当前行访问的表或派生表              partitions      命中的分区              type      访问类型              possible_keys      优化器认为可用的索引              key      实际选择的索引              key_len      使用到的索引键长度              ref      与索引比较的常量或列              rows      预估扫描行数              filtered      条件过滤后的估算比例              Extra      附加执行信息      rows 是估算值，不是精确结果行数。统计信息过期、数据分布倾斜、复杂表达式都会导致偏差。8.3 id 与 select_type多表查询：EXPLAINSELECT o.order_no, u.user_idFROM query_orders AS oJOIN users AS u ON u.id = o.user_idWHERE o.order_status = 'PAID';相同 id 表示同一查询块中的 JOIN，执行顺序通常从上往下。不同 id 表示子查询、派生表或 UNION 分支。常见 select_type：            值      含义                  SIMPLE      不包含子查询或 UNION 的简单查询              PRIMARY      最外层查询              SUBQUERY      首次执行的子查询              DEPENDENT SUBQUERY      依赖外层行的相关子查询              DERIVED      派生表              MATERIALIZED      物化子查询              UNION / UNION RESULT      UNION 分支和合并结果      重点警惕 DEPENDENT SUBQUERY。如果外层行数很大，子查询可能被重复执行。8.4 type 访问类型常见类型从好到差：system &gt; const &gt; eq_ref &gt; ref &gt; range &gt; index &gt; ALL            类型      场景      评价                  system      系统表且只有一行      极少见              const      主键或唯一键等值查询      最好              eq_ref      JOIN 时按主键或唯一键匹配，最多一行      非常好              ref      普通二级索引等值匹配      常见且良好              range      索引范围扫描      需关注范围大小              index      扫描整个二级索引      好于全表但通常不理想              ALL      全表扫描      小表可接受，大表需优化      constEXPLAINSELECT *FROM query_ordersWHERE order_no = 'NO000000000001';eq_refEXPLAINSELECT o.order_no, u.usernameFROM query_orders AS oJOIN users AS u ON u.id = o.user_idWHERE o.id = 1;refEXPLAINSELECT *FROM query_ordersWHERE user_id = 1001;rangeEXPLAINSELECT *FROM query_ordersWHERE created_at &gt;= '2026-02-01';当前索引 idx_user_created 的前导列是 user_id，只有 created_at 条件无法使用它，因此该语句大概率全表扫描。8.5 possible_keys、key、key_len查看索引选择：SHOW INDEX FROM query_orders;possible_keys 只是候选，不代表使用。key 为 NULL 表示没有使用二级索引。key_len 说明实际使用了联合索引中的多少字节。示例：EXPLAINSELECT *FROM query_ordersWHERE user_id = 1001  AND created_at &gt;= '2026-02-01';如果使用 idx_user_created(user_id, created_at)：  key 显示 idx_user_created；  key_len 包含 user_id 和 created_at 两列；  访问类型通常是 range。key_len 计算与类型、是否可为 NULL、字符集有关。实际排障时不需要死记公式，重点是比较同一个联合索引在不同条件下使用了多少列。8.6 rows 与 filteredrows 是预估访问行数，filtered 是经过其他条件后预计保留的百分比。EXPLAINSELECT *FROM query_ordersWHERE merchant_id = 10  AND order_status = 'PAID';如果 rows=10000、filtered=25%，优化器估算最终约 2500 行。常见问题：            现象      可能原因                  rows 很大但结果很小      条件选择性差或索引不匹配              rows 很小但实际很慢      回表、锁等待、网络、临时表              filtered 很低      额外条件未索引化              两个索引都可用但只选一个      优化器基于代价选择              优化器选错      统计信息不准、数据倾斜、成本模型失真      更新统计信息：ANALYZE TABLE query_orders;8.7 Extra 常见值            Extra      含义      处理                  Using index      覆盖索引，不回表      通常好              Using where      Server 层过滤      结合扫描量判断              Using index condition      ICP 下推过滤      通常好              Using temporary      使用临时表      重点排查 GROUP BY / DISTINCT / UNION              Using filesort      需要额外排序      检查 ORDER BY 索引              Using join buffer      JOIN 无可用索引      补 JOIN 键索引或考虑 Hash Join              No tables      没有访问表      正常              Impossible WHERE      条件恒假      检查逻辑      覆盖索引示例：EXPLAINSELECT id, user_id, created_atFROM query_ordersWHERE user_id = 1001;查询列包含主键 id、user_id、created_at，都在二级索引中，因此可能显示 Using index。8.8 EXPLAIN ANALYZEMySQL 8.0.18 引入 EXPLAIN ANALYZE，会真实执行语句并输出实际时间、行数和循环次数：EXPLAIN ANALYZESELECT id, order_no, amountFROM query_ordersWHERE user_id = 1001  AND created_at &gt;= '2026-02-01';示例输出结构：-&gt; Filter: ...    -&gt; Index range scan on query_orders using idx_user_created       (actual time=... rows=... loops=1)注意：  DML 语句不能直接 EXPLAIN ANALYZE；  慢 SELECT 会真实执行，生产需控制超时；  建议在只读实例或低峰执行；  可配合 LIMIT 缩小验证范围，但 LIMIT 会改变执行行为；  对比估算 rows 和 actual rows，判断统计信息偏差。8.9 JSON 与 Tree 格式JSON 格式包含更多代价信息：EXPLAIN FORMAT=JSONSELECT *FROM query_ordersWHERE user_id = 1001;MySQL 8.0.16+ 支持树形格式：EXPLAIN FORMAT=TREESELECT *FROM query_ordersWHERE user_id = 1001;JSON 中重点看：            字段      含义                  cost_info      估算成本              used_columns      使用列              attached_condition      附加过滤条件              materialized_from_subquery      子查询物化              ordering_operation      排序操作              grouping_operation      分组操作      8.10 优化器跟踪当执行计划明显不合理时，可以查看优化器为什么选择某个索引：SET optimizer_trace='enabled=on,one_line=off';SET optimizer_trace_max_mem_size=1048576;SELECT *FROM query_ordersWHERE user_id = 1001  AND order_status = 'PAID';SELECT * FROM information_schema.OPTIMIZER_TRACE\GSET optimizer_trace='enabled=off';重点搜索：range_analysisindex divesrows_estimationconsidered_execution_plansbest_plan优化器跟踪输出很大，只适合单条 SQL 深度排查，不适合全量开启。8.11 执行计划误判案例案例一：隐式类型转换EXPLAINSELECT *FROM query_ordersWHERE order_no = 1;order_no 是 VARCHAR，传入数字会导致隐式转换，可能无法正常使用索引。应改为：SELECT *FROM query_ordersWHERE order_no = '1';案例二：函数包裹索引列EXPLAINSELECT *FROM query_ordersWHERE DATE(created_at) = '2026-02-01';应改写为半开区间：SELECT *FROM query_ordersWHERE created_at &gt;= '2026-02-01 00:00:00'  AND created_at &lt; '2026-02-02 00:00:00';案例三：前导列缺失EXPLAINSELECT *FROM query_ordersWHERE created_at &gt;= '2026-02-01';idx_user_created 不能从 created_at 开始使用。需要新增 idx_created_at 或调整查询条件。案例四：扫描行少但响应慢执行计划 rows 很小，仍然可能慢：  回表次数多；  排序或临时表；  锁等待；  Buffer Pool 未命中；  网络传输大字段；  应用处理慢。执行计划只描述数据库内部执行，不覆盖端到端耗时。8.12 看执行计划的标准流程拿到慢 SQL 后按顺序检查：1. 确认 SQL 业务语义和参数值；2. 看是否全表扫描或扫描量过大；3. 看是否缺少可用索引；4. 看联合索引前缀是否匹配；5. 看是否回表过多，能否覆盖索引；6. 看是否临时表或 filesort；7. 看 JOIN 顺序和关联索引；8. 对比 EXPLAIN 与 EXPLAIN ANALYZE；9. 更新统计信息后复测；10. 结合慢日志、锁等待和会话状态定位。8.13 生产注意事项  不要看到 ALL 就立刻加索引，小表全扫可能更快；  不要看到 Using filesort 就认定故障，小结果排序成本可能很低；  索引提示只能作为临时手段；  优化前保存原始 SQL 和执行计划；  优化后验证结果集一致；  关注扫描行数、返回行数和耗时三者关系；  高危查询先在只读实例验证；  SQL 超时应由应用和数据库同时治理。本章小结执行计划的核心是回答三件事：访问哪些表、用什么方式取数、额外做了多少工作。type 说明访问路径，key 与 key_len 说明索引使用程度，rows 与 filtered 说明扫描估算，Extra 说明是否回表、排序和临时表。生产排查要结合参数值、真实数据分布、EXPLAIN ANALYZE 和锁等待，而不是机械地根据一个字段下结论。思考题  ref、range、index、ALL 分别代表什么访问方式？  possible_keys 有索引但 key 为 NULL，可能是什么原因？  Using index 和 Using index condition 有什么区别？  为什么 rows 只是估算值？如何让它更可靠？  找一条生产慢 SQL，写出优化前后的执行计划差异和验证步骤。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。聚合回答“一组数据的总量是多少”，窗口函数回答“每行在分组内的排名、累计和前后关系是什么”。它们是报表、运营分析和数据治理的基础工具。7.1 聚合函数常用聚合函数：            函数      作用      是否忽略 NULL                  COUNT(*)      统计行数      不适用              COUNT(expr)      统计 expr 非 NULL 行数      是              SUM      求和      是              AVG      平均值      是              MAX      最大值      是              MIN      最小值      是              GROUP_CONCAT      拼接字符串      是      示例：SELECT  COUNT(*) AS total_orders,  COUNT(paid_at) AS paid_orders,  SUM(total_amount) AS total_amount,  AVG(total_amount) AS avg_amount,  MIN(created_at) AS first_created,  MAX(created_at) AS last_createdFROM orders;COUNT(*) 统计行；COUNT(paid_at) 统计支付时间不为空的行。如果 paid_at 为 NULL 表示未支付，两者差值就是未支付订单数。7.2 GROUP BY按商户统计已支付订单：SELECT  merchant_id,  COUNT(*) AS order_count,  SUM(total_amount) AS gmvFROM ordersWHERE status = 'PAID'GROUP BY merchant_idORDER BY gmv DESC;执行逻辑可以理解为：1. FROM / WHERE 得到基础行集；2. 按 GROUP BY 键分组；3. 对每组计算聚合函数；4. HAVING 过滤分组；5. ORDER BY 排序输出。7.2.1 ONLY_FULL_GROUP_BYMySQL 8.0 默认开启 ONLY_FULL_GROUP_BY。以下 SQL 可能报错：SELECT merchant_id, order_no, COUNT(*)FROM ordersGROUP BY merchant_id;原因是 order_no 不在分组键中，也不在聚合函数中。一个商户有多笔订单时，MySQL 不知道输出哪一笔的 order_no。正确写法：SELECT merchant_id, COUNT(*) AS order_countFROM ordersGROUP BY merchant_id;如需明细，先按订单粒度查询，再聚合。7.3 HAVING查询订单数大于等于 2 的用户：SELECT  user_id,  COUNT(*) AS order_countFROM ordersGROUP BY user_idHAVING COUNT(*) &gt;= 2ORDER BY order_count DESC;普通条件尽量放 WHERE，因为可以先过滤再分组，减少参与聚合的数据量。HAVING 用于过滤聚合结果。            条件      放哪里                  原始行条件      WHERE              聚合结果条件      HAVING              窗口函数结果      外层查询 WHERE      7.4 WITH ROLLUP按类目和小计汇总：SELECT  COALESCE(category, 'ALL') AS category,  COUNT(*) AS product_count,  SUM(price) AS price_sumFROM productsGROUP BY category WITH ROLLUP;ROLLUP 会在每个分组后追加汇总行，最后一行是总计。它适合简单报表，复杂层级报表建议使用专门的分析任务或 BI 模型。7.5 GROUP_CONCAT拼接用户订单号：SELECT  user_id,  GROUP_CONCAT(    order_no    ORDER BY created_at DESC    SEPARATOR ','  ) AS order_nosFROM ordersGROUP BY user_id;结果长度受 group_concat_max_len 限制：SHOW VARIABLES LIKE 'group_concat_max_len';SET SESSION group_concat_max_len = 10240;如果结果可能很长，应查询明细，由应用聚合，或使用 JSON 数组：SELECT  user_id,  JSON_ARRAYAGG(order_no) AS order_nosFROM ordersGROUP BY user_id;7.6 窗口函数基础窗口函数在保留每一行的同时计算一组相关行的结果。基本结构：function() OVER (  PARTITION BY partition_columns  ORDER BY sort_columns  frame_definition)            子句      作用                  PARTITION BY      分区，类似分组但不折叠行              ORDER BY      区内排序，决定排名和累计方向              frame      定义移动窗口范围      按用户给订单编号：SELECT  user_id,  order_no,  created_at,  ROW_NUMBER() OVER (    PARTITION BY user_id    ORDER BY created_at DESC, id DESC  ) AS rnFROM orders;MySQL 8.0 开始支持窗口函数，5.7 不支持。7.7 排名函数            函数      行为                  ROW_NUMBER      连续唯一编号：1、2、3              RANK      并列同名，跳号：1、1、3              DENSE_RANK      并列同名，不跳号：1、1、2      SELECT  product_name,  category,  price,  ROW_NUMBER() OVER (    PARTITION BY category ORDER BY price DESC  ) AS row_no,  RANK() OVER (    PARTITION BY category ORDER BY price DESC  ) AS rank_no,  DENSE_RANK() OVER (    PARTITION BY category ORDER BY price DESC  ) AS dense_noFROM products;7.7.1 每组前 N 条取每个类目价格前二商品：WITH ranked_products AS (  SELECT    product_name,    category,    price,    ROW_NUMBER() OVER (      PARTITION BY category      ORDER BY price DESC, id    ) AS rn  FROM products)SELECT product_name, category, priceFROM ranked_productsWHERE rn &lt;= 2;窗口函数不能直接写在 WHERE 中，因此需要派生表或 CTE。7.7.2 用户最近一单WITH ranked_orders AS (  SELECT    u.username,    o.order_no,    o.created_at,    ROW_NUMBER() OVER (      PARTITION BY o.user_id      ORDER BY o.created_at DESC, o.id DESC    ) AS rn  FROM orders AS o  INNER JOIN users AS u ON u.id = o.user_id)SELECT username, order_no, created_atFROM ranked_ordersWHERE rn = 1;7.8 聚合窗口函数累计支付金额：SELECT  user_id,  order_no,  created_at,  total_amount,  SUM(total_amount) OVER (    PARTITION BY user_id    ORDER BY created_at    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW  ) AS running_amountFROM ordersWHERE status = 'PAID';常用 frame：            Frame      含义                  UNBOUNDED PRECEDING      分区第一行              CURRENT ROW      当前行              N PRECEDING      前 N 行              N FOLLOWING      后 N 行              UNBOUNDED FOLLOWING      分区最后一行      三日移动销售额：WITH daily_gmv AS (  SELECT DATE(paid_at) AS pay_date, SUM(total_amount) AS daily_amount  FROM orders  WHERE status = 'PAID'    AND paid_at IS NOT NULL  GROUP BY DATE(paid_at))SELECT  pay_date,  daily_amount,  AVG(daily_amount) OVER (    ORDER BY pay_date    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW  ) AS moving_avg_3dFROM daily_gmv;7.9 偏移函数            函数      作用                  LAG      取排序后前一行              LEAD      取排序后后一行              FIRST_VALUE      分区第一行              LAST_VALUE      分区最后一行              NTH_VALUE      分区第 N 行      环比示例：WITH daily_gmv AS (  SELECT DATE(paid_at) AS pay_date, SUM(total_amount) AS amount  FROM orders  WHERE status = 'PAID'    AND paid_at IS NOT NULL  GROUP BY DATE(paid_at))SELECT  pay_date,  amount,  LAG(amount, 1) OVER (ORDER BY pay_date) AS prev_amount,  amount - LAG(amount, 1) OVER (ORDER BY pay_date) AS diff_amount,  ROUND(    (amount - LAG(amount, 1) OVER (ORDER BY pay_date))      / LAG(amount, 1) OVER (ORDER BY pay_date) * 100,    2  ) AS diff_percentFROM daily_gmvORDER BY pay_date;如果首日没有前值，LAG 返回 NULL，需要用 IFNULL 处理展示。7.10 去重与差集窗口去重：WITH ranked_rows AS (  SELECT    t.*,    ROW_NUMBER() OVER (      PARTITION BY order_no, product_id      ORDER BY id DESC    ) AS rn  FROM order_items_dedup AS t)SELECT *FROM ranked_rowsWHERE rn = 1;窗口去重适合查询，不适合替代唯一约束。数据质量仍必须靠主键和唯一键保证。查询最近 7 天没有下单的用户：SELECT u.id, u.usernameFROM users AS uWHERE NOT EXISTS (  SELECT 1  FROM orders AS o  WHERE o.user_id = u.id    AND o.created_at &gt;= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY));7.11 报表实践商户日报：SELECT  DATE(o.paid_at) AS stat_date,  o.merchant_id,  COUNT(*) AS paid_orders,  SUM(o.paid_amount) AS paid_amount,  COUNT(DISTINCT o.user_id) AS buyers,  ROUND(AVG(o.paid_amount), 2) AS avg_paid_amountFROM orders AS oWHERE o.status = 'PAID'  AND o.paid_at &gt;= '2026-08-01'  AND o.paid_at &lt; '2026-09-01'GROUP BY DATE(o.paid_at), o.merchant_idORDER BY stat_date, paid_amount DESC;注意：  日期范围使用半开区间；  指标口径写清楚；  状态过滤放在 WHERE；  报表优先走只读实例；  高频报表考虑预聚合表；  巨量历史扫描交给分析库。预聚合表示例：CREATE TABLE merchant_daily_stats (    stat_date DATE NOT NULL,    merchant_id BIGINT UNSIGNED NOT NULL,    paid_orders BIGINT UNSIGNED NOT NULL DEFAULT 0,    paid_amount DECIMAL(14, 2) NOT NULL DEFAULT 0.00,    buyers BIGINT UNSIGNED NOT NULL DEFAULT 0,    updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)      ON UPDATE CURRENT_TIMESTAMP(3),    PRIMARY KEY (stat_date, merchant_id),    KEY idx_merchant_date (merchant_id, stat_date)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;7.12 性能建议  聚合前先缩小数据范围；  分组列尽量与索引前缀匹配；  大表 DISTINCT 和 GROUP BY 可能排序或临时表；  窗口函数的 PARTITION BY 和 ORDER BY 也要考虑索引；  避免在超宽窗口上无限制扫描；  报表和交易查询隔离；  数据量增长后引入预聚合或 OLAP；  使用 EXPLAIN ANALYZE 验证真实耗时。本章小结GROUP BY 会把多行折叠成组，窗口函数保留每行并计算组内关系。COUNT(*) 与 COUNT(col) 的 NULL 差异、WHERE 与 HAVING 的阶段差异、窗口函数不能直接用于 WHERE，是实际写 SQL 时最容易出错的地方。排名、Top N、累计、同比环比和移动平均都可以用窗口函数清晰表达，但必须结合索引和数据量控制成本。思考题  COUNT(*)、COUNT(1)、COUNT(col) 有什么区别？  为什么窗口函数不能直接写在 WHERE 中？  ROW_NUMBER、RANK、DENSE_RANK 在并列值上有什么差异？  如何用 LAG 计算每日销售额环比？  设计一个订单日报预聚合任务，说明幂等和延迟数据处理方式。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。真实业务数据分散在多张表中。JOIN 负责把关系重新组合，子查询负责表达“基于另一批结果的查询条件”。本章重点是理解不同 JOIN 的语义、子查询执行方式和生产 SQL 的复杂度控制。6.1 准备示例数据沿用第 02 章的表，先补充几条订单：INSERT INTO orders  (order_no, user_id, merchant_id, status, total_amount, paid_at)VALUES  ('20260825000011', 1, 1, 'PAID', 25.50, NOW(3)),  ('20260825000012', 1, 2, 'CREATED', 79.00, NULL),  ('20260825000013', 2, 1, 'PAID', 17.00, NOW(3)),  ('20260825000014', 3, 3, 'CANCELED', 528.00, NULL);INSERT INTO order_items  (order_id, product_id, product_name, unit_price, quantity, subtotal)VALUES  (1, 1, 'Apple', 8.50, 3, 25.50),  (2, 3, 'MySQL Book', 79.00, 1, 79.00),  (3, 2, 'Milk', 12.00, 1, 12.00),  (3, 1, 'Apple', 8.50, 1, 8.50),  (4, 4, 'Keyboard', 399.00, 1, 399.00),  (4, 5, 'Mouse', 129.00, 1, 129.00);6.2 INNER JOIN查询订单和用户信息：SELECT  o.order_no,  o.status,  o.total_amount,  u.username,  u.mobileFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idWHERE o.status = 'PAID';INNER JOIN 只返回两边都能匹配的行。如果某个 user_id 在 users 表不存在，这条订单不会出现。多表关联：SELECT  o.order_no,  u.username,  m.merchant_name,  oi.product_name,  oi.quantity,  oi.subtotalFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idINNER JOIN merchants AS m ON m.id = o.merchant_idINNER JOIN order_items AS oi ON oi.order_id = o.idWHERE o.status = 'PAID'ORDER BY o.id, oi.id;订单和订单明细是一对多关系，JOIN 后订单金额会重复出现，不能直接 SUM(o.total_amount)。按订单粒度汇总：SELECT  u.username,  COUNT(DISTINCT o.id) AS order_count,  SUM(o.total_amount) AS paid_amountFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idWHERE o.status = 'PAID'GROUP BY u.username;6.3 LEFT JOIN查询用户及其订单，没有订单的用户也要显示：SELECT  u.id,  u.username,  o.order_no,  o.status,  o.total_amountFROM users AS uLEFT JOIN orders AS o ON o.user_id = u.idWHERE u.status = 1ORDER BY u.id, o.id;没有订单的用户，o.order_no 等右侧字段为 NULL。统计每个用户的订单数：SELECT  u.username,  COUNT(o.id) AS order_countFROM users AS uLEFT JOIN orders AS o ON o.user_id = u.idGROUP BY u.username;这里必须使用 COUNT(o.id)，因为 COUNT(*) 会把没有订单的用户也统计为 1。6.4 WHERE 对 LEFT JOIN 的影响以下查询语义会退化：SELECT u.username, o.order_noFROM users AS uLEFT JOIN orders AS o ON o.user_id = u.idWHERE o.status = 'PAID';没有订单的用户右侧字段是 NULL，不满足 o.status = 'PAID'，会被过滤掉，效果接近 INNER JOIN。如果要求“所有用户，以及他们的已支付订单，没有则显示 NULL”，应把右侧条件放到 ON：SELECT u.username, o.order_no, o.statusFROM users AS uLEFT JOIN orders AS o  ON o.user_id = u.id AND o.status = 'PAID'ORDER BY u.id;简单记法：  左表过滤条件可以放 WHERE；  右表必须保留 NULL 的过滤条件应放 ON；  放置位置代表不同业务语义，不要机械套用。6.5 RIGHT JOIN 与 FULL JOIN右连接：SELECT o.order_no, u.usernameFROM orders AS oRIGHT JOIN users AS u ON u.id = o.user_id;等价于：SELECT o.order_no, u.usernameFROM users AS uLEFT JOIN orders AS o ON o.user_id = u.id;MySQL 8.0 不支持 FULL OUTER JOIN。可以用 LEFT JOIN UNION RIGHT JOIN 模拟：SELECT u.username, o.order_noFROM users AS uLEFT JOIN orders AS o ON o.user_id = u.idUNIONSELECT u.username, o.order_noFROM orders AS oLEFT JOIN users AS u ON u.id = o.user_idWHERE u.id IS NULL;团队规范通常统一使用 LEFT JOIN，减少阅读成本。6.6 CROSS JOINCROSS JOIN 生成笛卡尔积：SELECT  u.username,  m.merchant_nameFROM users AS uCROSS JOIN merchants AS m;如果左表 10 万行，右表 5 万行，结果就是 50 亿行。生产中出现无条件笛卡尔积通常是严重错误。6.7 JOIN 条件与数据质量JOIN 依赖关联键质量。常见问题：            问题      现象      处理                  类型不一致      隐式转换，索引失效      关联字段类型一致              字符集不一致      索引无法有效使用      表间字符集和排序规则统一              脏数据      匹配不到或重复匹配      约束加对账              一对多      汇总翻倍      先聚合或分粒度统计              重复维度表      数据膨胀      维度表保证唯一      检查孤儿订单：SELECT o.id, o.order_no, o.user_idFROM orders AS oLEFT JOIN users AS u ON u.id = o.user_idWHERE u.id IS NULL;6.8 子查询分类子查询可以出现在不同位置：            位置      示例      说明                  标量子查询      SELECT (SELECT MAX(price) FROM products)      返回单行单列              列子查询      WHERE id IN (...)      返回一列多行              行子查询      WHERE (a, b) = (...)      返回一行多列              表子查询      FROM (...) AS t      派生表              EXISTS 子查询      WHERE EXISTS (...)      判断存在性      6.8.1 标量子查询查询价格高于平均价的商品：SELECT id, product_name, priceFROM productsWHERE price &gt; (SELECT AVG(price) FROM products);标量子查询如果放在 SELECT 列表中且对外层每一行执行，可能导致大量重复计算。多数简单场景优化器可以处理，但复杂 SQL 需要看执行计划。6.8.2 IN 子查询查询有订单的用户：SELECT id, usernameFROM usersWHERE id IN (SELECT user_id FROM orders);查询没有订单的用户：SELECT id, usernameFROM usersWHERE id NOT IN (SELECT user_id FROM orders);NOT IN 遇到 NULL 时风险很高。以下查询结果为空：SELECT 1WHERE 1 NOT IN (2, NULL);因为 1 &lt;&gt; NULL 是 UNKNOWN。如果子查询可能返回 NULL，应显式过滤：SELECT id, usernameFROM usersWHERE id NOT IN (  SELECT user_id FROM orders WHERE user_id IS NOT NULL);或使用 NOT EXISTS。6.8.3 EXISTS 与 NOT EXISTS有订单的用户：SELECT u.id, u.usernameFROM users AS uWHERE EXISTS (  SELECT 1  FROM orders AS o  WHERE o.user_id = u.id);没有订单的用户：SELECT u.id, u.usernameFROM users AS uWHERE NOT EXISTS (  SELECT 1  FROM orders AS o  WHERE o.user_id = u.id);EXISTS 关注是否存在匹配，通常建议：  关联列有索引；  子查询 SELECT 1 即可；  反向关系优先考虑 NOT EXISTS；  不要在外层大表上执行无法下推的相关子查询。6.9 派生表与 CTE派生表：SELECT  t.user_id,  t.order_countFROM (  SELECT user_id, COUNT(*) AS order_count  FROM orders  GROUP BY user_id) AS tWHERE t.order_count &gt;= 2;CTE 让复杂查询更清晰：WITH user_order_stats AS (  SELECT    user_id,    COUNT(*) AS order_count,    SUM(total_amount) AS total_paid  FROM orders  WHERE status = 'PAID'  GROUP BY user_id)SELECT  u.username,  s.order_count,  s.total_paidFROM user_order_stats AS sINNER JOIN users AS u ON u.id = s.user_idWHERE s.order_count &gt;= 1ORDER BY s.total_paid DESC;MySQL 8.0 开始支持 CTE，5.7 不支持。递归 CTE 查询层级：CREATE TABLE regions (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    parent_id BIGINT UNSIGNED NULL,    region_name VARCHAR(64) NOT NULL,    PRIMARY KEY (id),    KEY idx_parent (parent_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;WITH RECURSIVE region_tree AS (  SELECT id, parent_id, region_name, 1 AS level  FROM regions  WHERE parent_id IS NULL  UNION ALL  SELECT r.id, r.parent_id, r.region_name, rt.level + 1  FROM regions AS r  INNER JOIN region_tree AS rt ON r.parent_id = rt.id)SELECT * FROM region_tree;递归查询必须防止无限循环。MySQL 有默认递归深度限制，也可以设置：SET SESSION cte_max_recursion_depth = 1000;生产中应先验证数据不存在环，再执行递归查询。6.10 UNION 与 UNION ALLUNION 去重：SELECT user_id FROM ordersUNIONSELECT id FROM users;UNION ALL 不去重：SELECT user_id, 'ORDER' AS source FROM ordersUNION ALLSELECT id, 'USER' AS source FROM users;如果没有去重需求，使用 UNION ALL，避免额外去重成本。列数和类型必须匹配：SELECT order_no AS biz_no, total_amount AS amountFROM ordersUNION ALLSELECT payment_no, amountFROM payment_records;6.11 查询复杂度治理复杂 SQL 常见问题：  十几张表 JOIN；  子查询嵌套五六层；  SELECT 中包含重逻辑；  一个接口返回所有字段；  报表、交易、详情共用一条 SQL；  没有人能解释执行计划。治理建议：            问题      处理方式                  JOIN 太多      拆接口、拆查询、使用冗余快照              报表复杂      走只读库、ClickHouse 或预聚合              子查询重复      使用 CTE 命名并确认只物化一次              结果太大      分页、游标、限制字段              跨域聚合      数仓或分析引擎处理              代码难维护      中间结果落临时表或任务表      6.12 执行计划初看查看 JOIN 计划：EXPLAINSELECT u.username, o.order_noFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idWHERE o.user_id = 1;重点关注：            字段      意义                  type      访问类型，ref 通常好于 range，ALL 是全表扫描              key      实际使用的索引              rows      预估扫描行数              filtered      条件过滤后估算比例              Extra      是否 Using temporary、Using filesort      MySQL 8.0.18+ 可以查看真实执行：EXPLAIN ANALYZESELECT u.username, o.order_noFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idWHERE o.user_id = 1;第 08 章会系统展开。本章小结JOIN 的语义由连接类型和条件位置共同决定。INNER JOIN 取交集，LEFT JOIN 保留左表，右侧条件放 ON 或 WHERE 会改变结果。子查询分为标量、IN、EXISTS、派生表和 CTE，写法要结合数据量、索引和空值风险。复杂查询应优先保证语义清晰、可索引、可验证，再考虑语法精简。思考题  LEFT JOIN 下右表条件放在 ON 和 WHERE 有什么区别？  为什么统计用户订单数要使用 COUNT(o.id) 而不是 COUNT(*)？  NOT IN 包含 NULL 时有什么风险？  一对多 JOIN 后为什么直接 SUM 主表金额会翻倍？  将一个五层嵌套子查询改写为 CTE，并解释每层业务含义。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。基础 CRUD 只解决“能操作数据”。生产系统还要求操作幂等、并发安全、范围可控、失败可恢复。本章深入 INSERT、UPDATE、DELETE 的高级用法，并给出可落地的并发控制模式。5.1 插入的完整性普通插入：INSERT INTO products  (merchant_id, product_name, category, price, stock)VALUES  (3, 'Monitor', 'DIGITAL', 1299.00, 20);插入前必须知道唯一约束。查看建表语句：SHOW CREATE TABLE products\G唯一约束决定重试语义：  无唯一键：重试可能插入重复数据；  有唯一键：冲突可以被数据库发现；  幂等表：用业务 ID 唯一控制重复请求。5.2 批量插入批量插入能减少网络往返，但批次不是越大越好。INSERT INTO products  (merchant_id, product_name, category, price, stock)VALUES  (1, 'Pear', 'FRUIT', 7.20, 60),  (1, 'Grape', 'FRUIT', 15.80, 45),  (1, 'Cheese', 'FOOD', 35.00, 25),  (2, 'Kafka Book', 'BOOK', 99.00, 40),  (3, 'USB Cable', 'DIGITAL', 29.90, 200);建议：  单批几百到几千行，根据行大小压测；  不要超过 max_allowed_packet；  避免一个事务无限追加；  批量导入可临时关闭二级索引再重建；  程序中使用参数绑定和分批提交。5.3 INSERT IGNORE忽略唯一键冲突：INSERT IGNORE INTO tags (tag_name)VALUES ('HOT'), ('NEW');如果 HOT 已存在，这条数据会被跳过。风险：  不只忽略唯一键冲突，还可能把某些数据错误降级为警告；  客户端以为成功，实际没有插入；  屏蔽业务规则冲突；  不利于审计。更推荐先明确冲突处理策略，再选择 ON DUPLICATE KEY UPDATE 或查询后插入。5.4 ON DUPLICATE KEY UPDATE存在则更新，不存在则插入：CREATE TABLE IF NOT EXISTS product_visit_stats (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    stat_date DATE NOT NULL,    product_id BIGINT UNSIGNED NOT NULL,    visit_count BIGINT UNSIGNED NOT NULL DEFAULT 0,    updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)      ON UPDATE CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_date_product (stat_date, product_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;INSERT INTO product_visit_stats  (stat_date, product_id, visit_count)VALUES  ('2026-08-25', 1, 1)ON DUPLICATE KEY UPDATE  visit_count = visit_count + 1;多值插入时，每行冲突都会更新一次。如果同一个键在同一个语句里出现多次，最终结果依赖执行次数，容易被误解。MySQL 8.0.19 起，VALUES() 在这类语句中逐渐弃用，新语法可以使用别名：INSERT INTO product_visit_stats  (stat_date, product_id, visit_count)VALUES  ('2026-08-25', 2, 1) AS new_rowON DUPLICATE KEY UPDATE  visit_count = product_visit_stats.visit_count + new_row.visit_count;5.5 REPLACE INTOREPLACE INTO 的行为是：遇到唯一键冲突时先删除旧行，再插入新行。REPLACE INTO tags (id, tag_name)VALUES (1, 'VERY_HOT');它看似方便，风险却很大：  会删除再插入，不是原地更新；  可能触发级联删除或从库副作用；  未指定的字段会变成默认值；  自增 ID 可能变化；  二级索引维护成本更高。生产中通常优先使用 INSERT ... ON DUPLICATE KEY UPDATE 或显式状态更新。5.6 安全 UPDATE简单更新：UPDATE productsSET price = price * 0.9WHERE category = 'DIGITAL'  AND status = 1;执行前确认影响范围：SELECT COUNT(*) AS affected_rowsFROM productsWHERE category = 'DIGITAL'  AND status = 1;事务内确认：START TRANSACTION;UPDATE productsSET price = price * 0.9WHERE category = 'DIGITAL'  AND status = 1;SELECT ROW_COUNT() AS affected_rows;-- 人工确认后提交COMMIT;注意：ROW_COUNT() 应紧跟 DML 执行，中间执行其他语句会导致结果变化。5.6.1 基于旧值的条件更新防并发覆盖：UPDATE productsSET stock = 10WHERE id = 1  AND stock = 8;如果另一个请求已经把库存从 8 改成 7，这条 SQL 影响行数为 0，不会盲目覆盖。5.6.2 版本号乐观锁增加版本字段：ALTER TABLE products  ADD COLUMN version INT UNSIGNED NOT NULL DEFAULT 0;更新：UPDATE productsSET stock = 10,    version = version + 1WHERE id = 1  AND version = 3;应用判断：影响行数 = 1：更新成功影响行数 = 0：数据被其他事务修改，需要重试或提示冲突乐观锁适合冲突概率不高的后台管理、配置更新和工作流状态变更。5.7 扣库存模式错误写法：UPDATE productsSET stock = stock - 3WHERE id = 1;如果库存不足，可能变成负数，或因 UNSIGNED 报错。正确写法：UPDATE productsSET stock = stock - 3WHERE id = 1  AND stock &gt;= 3;应用根据影响行数判断：1：扣减成功0：库存不足或商品不存在结合订单事务：START TRANSACTION;UPDATE productsSET stock = stock - 3WHERE id = 1  AND stock &gt;= 3;-- 如果上一条影响 0 行，应用立即 ROLLBACKINSERT INTO orders  (order_no, user_id, merchant_id, status, total_amount)VALUES  ('20260825000002', 1, 1, 'CREATED', 25.50);COMMIT;热点商品扣减可能形成行锁排队。常见方案：  Redis 预扣库存，异步或事务内对账；  库存分片，把一行拆成多行；  队列串行化请求；  限流和削峰；  活动预热与库存分段释放。无论使用什么方案，最终事实来源和资金相关结果必须清晰。5.8 多表更新MySQL 支持用 JOIN 更新：UPDATE orders AS oINNER JOIN users AS u ON u.id = o.user_idSET o.status = 'RISK_HOLD'WHERE u.status = 2  AND o.status = 'CREATED';多表更新可读性较好，但影响范围更难判断。执行前应先改成 SELECT 验证：SELECT o.id, o.order_no, o.status, u.status AS user_statusFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idWHERE u.status = 2  AND o.status = 'CREATED';5.9 DELETE 与 TRUNCATE条件删除：DELETE FROM order_itemsWHERE order_id = 100;删除全表：DELETE FROM tags;清空表：TRUNCATE TABLE tags;区别：            项目      DELETE      TRUNCATE                  条件      支持 WHERE      不支持              事务回滚      InnoDB 下可回滚，但大事务成本高      通常按 DDL 处理，不应依赖回滚              触发器      触发 DELETE 触发器      不触发行级 DELETE              自增      通常保留计数      通常重置              速度      逐行删除      更快              生产建议      分批删除      仅确认可清空时使用      5.10 大批量删除删除大量历史数据时，不要一条 SQL 删除百万行。风险：  长事务；  大量行锁；  Undo 膨胀；  主从延迟；  磁盘空间不会立刻释放；  回滚时间极长。分批删除模板：DELETE FROM operation_logsWHERE created_at &lt; '2025-01-01'ORDER BY idLIMIT 1000;应用循环执行，直到影响行数为 0。每批之间可以短暂睡眠，减少复制和磁盘压力。更稳妥的方案是归档后删除：1. 创建归档表或归档库；2. 按主键或时间分片读取；3. 归档端幂等插入；4. 校验数量和抽样数据；5. 分批删除在线表；6. 记录归档任务水位。5.11 软删除标记删除：UPDATE usersSET deleted = 1WHERE id = 3;查询时排除：SELECT id, usernameFROM usersWHERE deleted = 0;软删除优点：  可恢复；  保留审计历史；  避免物理删除带来的关联问题。缺点：  所有查询都要带条件；  唯一约束复杂；  表持续膨胀；  索引选择性下降；  数据保护生命周期更复杂。更好的长期方案通常是：在线表保留活跃数据，历史数据进入归档表或数仓。5.12 Upsert 与幂等支付回调是典型幂等场景。创建支付流水表：CREATE TABLE payment_records (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    payment_no VARCHAR(64) NOT NULL COMMENT '支付流水号',    order_no VARCHAR(32) NOT NULL COMMENT '业务订单号',    pay_status VARCHAR(16) NOT NULL,    amount DECIMAL(12, 2) NOT NULL,    finished_at DATETIME(3) NOT NULL,    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_payment_no (payment_no),    KEY idx_order_no (order_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;第一次回调：INSERT INTO payment_records  (payment_no, order_no, pay_status, amount, finished_at)VALUES  ('PAY20260825001', '20260825000001', 'SUCCESS', 25.50, NOW(3));重复回调：INSERT INTO payment_records  (payment_no, order_no, pay_status, amount, finished_at)VALUES  ('PAY20260825001', '20260825000001', 'SUCCESS', 25.50, NOW(3))ON DUPLICATE KEY UPDATE  payment_no = payment_records.payment_no;然后根据影响行数或重新读取判断是否继续更新订单。不要仅凭 SQL 未报错就认为这是首次支付成功。5.13 返回元数据MySQL 协议会返回影响行数。不同驱动字段名略有差异。            场景      常见返回                  插入普通成功      1              批量插入      插入行数              条件不匹配      0              更新后值没变化      取决于客户端配置              Upsert 插入      1              Upsert 更新      2              Upsert 冲突但未修改      0 或 1，取决于客户端配置      Java JDBC 默认通常返回匹配行数而不是实际改变行数。关键业务判断必须结合唯一键和重新读取结果，不应把影响行数当作所有场景的唯一依据。5.14 DML 生产规范  所有更新和删除都写 WHERE；  先确认影响范围；  高并发写操作控制事务大小；  大任务分批执行；  幂等靠唯一键和状态机，不靠概率；  线上脚本必须可重复执行；  变更前备份数据；  保留操作人和操作原因；  跨表一致性优先本地事务；  跨库一致性使用明确的事务消息或对账机制。本章小结进阶 CRUD 的重点不是记住更多语法，而是控制结果。插入要依赖唯一约束实现幂等；更新要用旧值条件或版本号避免并发覆盖；扣库存必须检查前置条件；大批量修改要分批执行；删除要考虑归档、审计和唯一约束。影响行数是重要信号，但必须理解驱动和 Upsert 场景下的差异。思考题  INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE INTO 分别适合什么场景？  为什么扣库存必须在 WHERE 中判断 stock &gt;= quantity？  乐观锁和悲观锁分别适合什么并发场景？  百万行历史数据为什么要分批删除？  设计一个支付回调幂等方案，画出状态流转和异常处理流程。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。表结构是长期资产。业务代码可以重构，错误的数据却很难修复。数据类型、约束和索引共同决定了存储成本、查询能力、写入吞吐和数据质量。4.1 设计目标好的表设计通常同时满足：  准确性：字段类型和约束能阻止非法数据；  可扩展：能承受可预见的业务变化；  简洁：没有重复和含义混乱的字段；  可查询：高频条件能建立有效索引；  可运维：字段注释清楚，变更风险可控；  低成本：存储、内存和网络开销合理。表设计不是为了展示完美理论，而是为了让未来三到五年的业务和排障更轻松。4.2 数值类型            类型      存储范围      适合场景                  TINYINT      很小整数      状态、类型、布尔语义              SMALLINT      小整数      地区码、分类编号              INT      常规整数      普通数量、枚举值              BIGINT      大整数      ID、时间戳、金额分              DECIMAL(M,D)      精确小数      金额、费率              FLOAT / DOUBLE      近似浮点      科学计算，不适合金额      整数可选 UNSIGNED，表示无符号，范围从 0 开始：stock INT UNSIGNED NOT NULL DEFAULT 0它可以在语义上禁止负库存，但业务仍必须检查扣减数量，避免 stock - quantity 变成负数后报错或中断流程。4.2.1 金额设计方案一：DECIMAL：total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00方案二：整数分：total_amount_cent BIGINT NOT NULL DEFAULT 0两种方案都可以，关键是全链路统一。不要在数据库用元、缓存用分、日志用元、下游再用浮点数。错误示例：total_amount FLOAT NOT NULL浮点数存在精度误差，不适合订单、支付、结算等强一致性场景。4.3 字符串类型            类型      特点      适合场景                  CHAR(N)      定长，会填充空格      固定长度编码，如国家码、币种              VARCHAR(N)      变长，按实际长度存储      名称、描述、号码              TEXT      大文本      长内容，但应避免高频查询              BLOB      二进制      通常不建议直接存 MySQL              ENUM      枚举字符串      修改值需要 DDL，谨慎使用      VARCHAR(N) 的 N 是字符数，不是字节数。utf8mb4 下一个中文或 Emoji 都可能占多个字节。推荐：order_no VARCHAR(32) NOT NULLcurrency CHAR(3) NOT NULL DEFAULT 'CNY'remark VARCHAR(512) NULL不推荐把所有字段都设计成 VARCHAR(255) 或 TEXT。长度约束也是业务约束，能提前暴露错误。4.3.1 手机号和身份证手机号可能携带国家码，也可能被未来加密，建议：mobile VARCHAR(20) NOT NULL身份证应使用字符串而不是整数，因为它可能包含 X，且有前导零风险。4.4 时间类型            类型      特点      建议                  DATETIME      不带时区，保存字面时间      常规业务时间              TIMESTAMP      存 UTC，显示受会话时区影响      需要时区转换的场景              DATE      日期      生日、账期              TIME      时间      时长或时刻              YEAR      年份      年度维度      带毫秒：created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)推荐实践：  每张业务表都有 created_at；  需要追踪修改的表加 updated_at；  应用和数据库统一时区；  跨时区系统明确存储口径；  时间条件使用半开区间。查询 8 月订单：SELECT COUNT(*)FROM ordersWHERE created_at &gt;= '2026-08-01 00:00:00.000'  AND created_at &lt; '2026-09-01 00:00:00.000';半开区间不会遗漏最后一毫秒，也更容易组合时间分片。4.5 JSON 类型MySQL 8.0 对 JSON 支持较好，适合保存结构可变的扩展信息：ALTER TABLE orders  ADD COLUMN extra JSON NULL;写入：UPDATE ordersSET extra = JSON_OBJECT(  'channel', 'APP',  'activity_id', 1001,  'tags', JSON_ARRAY('new-user', 'free-shipping'))WHERE id = 1;查询：SELECT order_no, JSON_UNQUOTE(JSON_EXTRACT(extra, '$.channel')) AS channelFROM ordersWHERE JSON_EXTRACT(extra, '$.activity_id') = 1001;生成列加索引：ALTER TABLE orders  ADD COLUMN channel VARCHAR(32)    GENERATED ALWAYS AS (JSON_UNQUOTE(extra-&gt;&gt;'$.channel')) STORED,  ADD INDEX idx_extra_channel (channel);JSON 使用原则：  不把需要强约束的核心字段塞进 JSON；  不用 JSON 替代所有表设计；  高频查询字段提取为生成列；  JSON 字段可能变大，影响内存和备份成本；  敏感字段加密后再入 JSON。4.6 主键设计InnoDB 是聚簇索引组织表，主键选择直接影响页分裂、二级索引大小和复制风险。推荐：id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,PRIMARY KEY (id)优点：  趋势递增，写入友好；  占用空间比随机 UUID 小；  二级索引叶子节点保存主键，主键越小二级索引越轻；  便于分页和范围定位。不推荐随机字符串主键：id CHAR(36) NOT NULL,PRIMARY KEY (id)如果业务必须暴露随机 ID，可以分开设计：id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,public_id CHAR(36) NOT NULL,PRIMARY KEY (id),UNIQUE KEY uk_public_id (public_id)分布式系统可以用号段服务、雪花 ID 或数据库序列生成趋势递增 ID。无论选择哪种，都要处理时钟回拨、机房位、中心依赖和 ID 泄露问题。4.7 约束设计常见约束：            约束      作用                  PRIMARY KEY      唯一标识行              NOT NULL      禁止空值              UNIQUE KEY      业务唯一性              FOREIGN KEY      引用完整性              CHECK      条件检查              DEFAULT      默认值      示例：CREATE TABLE accounts (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    user_id BIGINT UNSIGNED NOT NULL,    balance DECIMAL(12, 2) NOT NULL DEFAULT 0.00,    status TINYINT UNSIGNED NOT NULL DEFAULT 1,    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_user (user_id),    CONSTRAINT chk_balance CHECK (balance &gt;= 0),    CONSTRAINT chk_status CHECK (status IN (1, 2))) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;MySQL 8.0.16 之前 CHECK 会被解析但忽略，8.0.16 起生效。4.7.1 是否使用外键互联网高并发系统通常不使用物理外键，原因：  每次写入都要检查父表；  可能引入额外锁和级联风险；  分库分表后难以使用；  大规模数据维护成本高。替代方案：  应用层校验；  定时对账；  唯一约束保留；  删除使用状态标记；  引用关系通过任务修复。小规模内部系统可以使用外键，它能减少脏数据。4.8 范式与反范式第三范式可以简单理解为：字段尽量只依赖主键，不要保存可以由其他表推导出的冗余信息。范式化示例：orders 只保存 user_idusers 保存 username查询时 JOIN：SELECT o.order_no, u.usernameFROM orders oJOIN users u ON u.id = o.user_id;反范式化示例：order_items 保存 product_name 快照这不是冗余错误。商品后来改名、改价，不应改变历史订单成交内容。何时可以冗余：  查询压力明显高于写入压力；  冗余字段变化频率低；  不一致带来的业务风险可控；  能显著减少 JOIN；  有明确的数据修复机制。何时不该冗余：  账户余额等强一致字段；  变化频繁且来源复杂的字段；  没有对账机制的统计值；  只是为了少写一个 JOIN。4.9 状态字段设计常见错误是 status 使用随意字符串，程序里同时出现 paid、PAID、1、已支付。推荐：status TINYINT UNSIGNED NOT NULL DEFAULT 10 COMMENT '10待支付，20已支付，30已取消'或使用小整数加字典表：CREATE TABLE order_status_dict (    status_code TINYINT UNSIGNED NOT NULL,    status_name VARCHAR(32) NOT NULL,    PRIMARY KEY (status_code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;状态机应收敛：CREATED -&gt; PAID -&gt; SHIPPED -&gt; FINISHEDCREATED -&gt; CANCELEDPAID -&gt; REFUNDING -&gt; REFUNDED非法流转必须拒绝，而不是靠后台脚本修数据。4.10 通用字段常见通用字段：            字段      类型建议      说明                  id      BIGINT UNSIGNED      主键              created_at      DATETIME(3)      创建时间              updated_at      DATETIME(3)      更新时间              created_by      BIGINT UNSIGNED      创建人              updated_by      BIGINT UNSIGNED      更新人              deleted      TINYINT UNSIGNED      软删标记              version      INT UNSIGNED      乐观锁版本              tenant_id      BIGINT UNSIGNED      多租户隔离      软删除示例：deleted TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0未删，1已删'软删除不是万能方案。它会让唯一键和查询条件复杂化，例如：UNIQUE KEY uk_username (username, deleted)如果同一个用户名可以多次删除，这个唯一键仍不够。可以考虑：  删除时把用户名迁入历史表；  唯一键包含 delete_version；  使用不可复用的唯一随机值；  核心表禁止物理删除，只做状态归档。4.11 索引初步设计表设计时先识别查询路径：按用户查最近订单：user_id + created_at按商户查状态订单：merchant_id + status + created_at按订单号查详情：order_no 唯一对应索引：PRIMARY KEY (id),UNIQUE KEY uk_order_no (order_no),KEY idx_user_created (user_id, created_at),KEY idx_merchant_status_created (merchant_id, status, created_at)索引不是越多越好。每个二级索引都会：  占用磁盘空间；  拖慢写入；  增加 Buffer Pool 竞争；  增加优化器选择成本；  加大大事务和 DDL 风险。4.12 表设计评审清单上线前逐项检查：            检查项      问题                  命名      库、表、字段、索引是否统一且有注释              主键      是否显式、趋势递增、类型合理              类型      金额、时间、状态、字符串是否正确              约束      非空、默认值、唯一键是否完整              查询      高频条件是否有索引              容量      一年后行数、字段大小、总容量              生命周期      是否有归档和清理策略              权限      是否需要新账号和敏感字段权限              变更      是否支持安全 DDL 和回滚              对账      派生数据如何校验      本章小结数据类型和约束是第一道数据质量防线。金额用 DECIMAL 或整数分，时间统一口径，主键保持趋势递增，状态字段收敛为小整数。JSON 适合扩展信息，不适合替代核心关系字段。范式保证清晰，反范式用于有明确收益和修复机制的场景。设计表时应从查询路径和容量出发，而不是只看当前页面需要什么字段。思考题  为什么 FLOAT 不适合保存订单金额？  随机 UUID 作为 InnoDB 主键有什么代价？如何折中？  DATETIME 和 TIMESTAMP 有什么区别？  高并发系统为什么常不用物理外键？如何补偿一致性风险？  给订单表增加一个渠道字段，你会如何评估类型、约束、索引和兼容性？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。SQL 是和 MySQL 对话的语言。写 SQL 容易，写清楚、稳定、可维护的 SQL 不容易。本章从数据库、表、列、行的基本概念开始，覆盖查询、过滤、排序、分页、事务和常见错误。3.1 数据库、表、行、列MySQL 的逻辑层级如下：Server  +-- Database / Schema       +-- Table            +-- Column：字段，定义数据类型和约束            +-- Row：记录，代表一个具体实体或关系在 MySQL 中，DATABASE 和 SCHEMA 基本同义。日常操作通常说“库”和“表”。创建数据库：CREATE DATABASE IF NOT EXISTS shop  DEFAULT CHARACTER SET utf8mb4  DEFAULT COLLATE utf8mb4_0900_ai_ci;删除数据库：DROP DATABASE IF EXISTS shop;DROP DATABASE 会删除库下所有对象，只应在明确允许丢数据的学习或重建场景使用。3.2 SQL 分类            分类      全称      常见语句                  DDL      Data Definition Language      CREATE、ALTER、DROP、TRUNCATE              DML      Data Manipulation Language      SELECT、INSERT、UPDATE、DELETE              DCL      Data Control Language      GRANT、REVOKE、CREATE USER              TCL      Transaction Control Language      BEGIN、COMMIT、ROLLBACK、SAVEPOINT      不同资料对 SELECT 和 TRUNCATE 的归类略有差异，理解行为比记分类更重要。3.3 建表与修改表创建一张简单表：CREATE TABLE tags (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,    tag_name VARCHAR(32) NOT NULL,    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_tag_name (tag_name)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;增加字段：ALTER TABLE tags  ADD COLUMN status TINYINT UNSIGNED NOT NULL DEFAULT 1 AFTER tag_name;修改字段：ALTER TABLE tags  MODIFY COLUMN tag_name VARCHAR(64) NOT NULL COMMENT '标签名';增加索引：ALTER TABLE tags  ADD INDEX idx_status_created (status, created_at);删除字段：ALTER TABLE tags DROP COLUMN status;生产修改表结构前必须确认：  表数据量和当前读写峰值；  MySQL 版本与 DDL 算法；  是否有锁表风险；  是否可回滚；  是否已备份。3.4 查询语句结构一条 SELECT 的逻辑书写顺序如下：SELECT product_name, category, priceFROM productsWHERE status = 1  AND price &gt;= 50GROUP BY categoryHAVING COUNT(*) &gt;= 1ORDER BY price DESCLIMIT 10;常见子句：            子句      作用                  SELECT      选择输出列              FROM      指定数据来源              JOIN      关联其他表              WHERE      行级过滤              GROUP BY      分组              HAVING      分组后过滤              ORDER BY      排序              LIMIT      限制返回行数      注意：逻辑书写顺序不等于执行顺序。优化器会根据统计信息和代价选择实际执行方式。3.5 查询列与表达式查询所有列：SELECT * FROM products;生产中应避免在程序里无条件 SELECT *，原因：  传输不必要的列；  破坏覆盖索引机会；  新增大字段后应用可能误加载；  代码对结果字段的依赖不清晰；  网络和 ORM 映射成本更高。明确列名：SELECT id, product_name, category, priceFROM products;使用别名和表达式：SELECT  id AS product_id,  product_name AS name,  price * 0.9 AS discount_priceFROM products;条件表达式：SELECT  product_name,  price,  CASE    WHEN price &lt; 50 THEN 'LOW'    WHEN price &lt; 200 THEN 'MIDDLE'    ELSE 'HIGH'  END AS price_levelFROM products;常用函数：            函数      示例      说明                  IFNULL      IFNULL(paid_at, '未支付')      空值兜底              CONCAT      CONCAT(order_no, '-', status)      字符串拼接              UPPER      UPPER(status)      转大写              DATE_FORMAT      DATE_FORMAT(paid_at, '%Y-%m-%d')      格式化日期              ROUND      ROUND(price, 2)      四舍五入              ABS      ABS(-1)      绝对值      3.6 WHERE 条件查询上架且有库存的商品：SELECT id, product_name, priceFROM productsWHERE status = 1  AND stock &gt; 0;查询两个类目：SELECT id, product_name, categoryFROM productsWHERE category IN ('FOOD', 'BOOK');范围查询：SELECT id, order_no, total_amount, created_atFROM ordersWHERE created_at &gt;= '2026-08-01 00:00:00'  AND created_at &lt; '2026-09-01 00:00:00';模糊查询：SELECT id, product_nameFROM productsWHERE product_name LIKE 'My%';LIKE 'My%' 可以利用索引前缀；LIKE '%Book%' 通常无法使用普通 B+Tree 索引。全文搜索应考虑 FULLTEXT 或 Elasticsearch。NULL 判断NULL 表示未知或缺失，不等于空字符串或 0。SELECT order_no, paid_atFROM ordersWHERE paid_at IS NULL;SELECT order_no, paid_atFROM ordersWHERE paid_at IS NOT NULL;以下写法不会返回 paid_at 为 NULL 的行：SELECT order_noFROM ordersWHERE paid_at != NULL;空值比较会得到 UNKNOWN，这是初学者最常见的错误之一。3.7 排序与分页排序：SELECT id, product_name, priceFROM productsWHERE category = 'DIGITAL'ORDER BY price DESC, id DESC;分页：SELECT id, product_name, priceFROM productsORDER BY idLIMIT 10 OFFSET 0;第 2 页：SELECT id, product_name, priceFROM productsORDER BY idLIMIT 10 OFFSET 10;也可以写成：SELECT id, product_name, priceFROM productsORDER BY idLIMIT 10, 10;深分页示例：SELECT id, product_nameFROM productsORDER BY idLIMIT 10 OFFSET 1000000;MySQL 仍要扫描并丢弃前 100 万行。更高效的方式是游标分页：SELECT id, product_nameFROM productsWHERE id &gt; 1000000ORDER BY idLIMIT 10;3.8 INSERT插入单行：INSERT INTO products  (merchant_id, product_name, category, price, stock)VALUES  (1, 'Banana', 'FRUIT', 5.50, 120);批量插入：INSERT INTO products  (merchant_id, product_name, category, price, stock)VALUES  (1, 'Orange', 'FRUIT', 6.80, 90),  (1, 'Bread', 'FOOD', 9.90, 40),  (2, 'Redis Book', 'BOOK', 89.00, 30);指定列插入比省略列名更安全，避免表结构变化后语义漂移。插入时处理唯一键冲突：INSERT INTO products  (merchant_id, product_name, category, price, stock)VALUES  (1, 'Banana', 'FRUIT', 5.20, 150)ON DUPLICATE KEY UPDATE  price = VALUES(price),  stock = VALUES(stock);MySQL 8.0.20 开始推荐使用别名语法：INSERT INTO products  (merchant_id, product_name, category, price, stock)VALUES  (1, 'Banana', 'FRUIT', 5.20, 150) AS new_rowON DUPLICATE KEY UPDATE  price = new_row.price,  stock = new_row.stock;3.9 UPDATE 与 DELETE更新：UPDATE productsSET stock = stock - 1,    status = 1WHERE id = 1  AND stock &gt; 0;删除：DELETE FROM tagsWHERE id = 10;生产红线：  更新和删除必须带 WHERE；  先 SELECT 或 COUNT 确认影响范围；  大量修改分批执行；  开启事务并检查后再提交；  线上手工操作要有双人复核。安全执行流程：START TRANSACTION;SELECT id, stockFROM productsWHERE id = 1FOR UPDATE;UPDATE productsSET stock = stock - 1WHERE id = 1  AND stock &gt; 0;-- 确认影响行数正确COMMIT;3.10 事务初体验事务把一组 SQL 变成一个原子执行单元：START TRANSACTION;INSERT INTO orders  (order_no, user_id, merchant_id, status, total_amount)VALUES  ('20260825000001', 1, 1, 'CREATED', 25.50);INSERT INTO order_items  (order_id, product_id, product_name, unit_price, quantity, subtotal)VALUES  (LAST_INSERT_ID(), 1, 'Apple', 8.50, 3, 25.50);COMMIT;回滚：START TRANSACTION;UPDATE productsSET stock = stock - 100WHERE id = 1;ROLLBACK;事务不是越大越安全。长事务会持有锁和 Undo 版本，影响并发，也会拖慢主从复制。业务边界应尽量短，耗时操作不应包在数据库事务中。3.11 SQL 编写规范推荐规则：  关键字统一大小写；  明确字段列表；  复杂查询缩进分层；  表使用别名，别名有业务含义；  每条 DML 都能说清影响范围；  查询条件避免隐式类型转换；  禁止拼 SQL，使用参数绑定；  对慢查询加注释说明用途；  生产 SQL 必须有索引评估；  报表 SQL 与交易 SQL 分离。示例：SELECT  o.order_no,  o.status,  o.total_amount,  u.usernameFROM orders AS oINNER JOIN users AS u ON u.id = o.user_idWHERE o.user_id = 1  AND o.created_at &gt;= '2026-08-01'ORDER BY o.created_at DESCLIMIT 20;3.12 常见错误            错误      原因      处理                  Unknown database      库不存在或连接错实例      检查库名和环境              Unknown column      字段名或表别名错误      检查 SELECT 和别名              Access denied      权限不足      检查用户、密码、来源主机              Duplicate entry      唯一键冲突      改用幂等插入或处理冲突              Data too long      字段长度不足      扩字段或修正输入              Truncated incorrect DOUBLE      字符串和数字隐式转换      修正类型              Lock wait timeout      锁等待超时      分析锁冲突和事务边界      本章小结SQL 的核心是描述“要什么数据”，而不是“怎么取数据”。掌握 SELECT、WHERE、ORDER BY、LIMIT、INSERT、UPDATE、DELETE 和事务后，就能完成日常开发。生产 SQL 要明确列名、控制影响范围、避免深分页和隐式类型转换，并用事务保护必须原子成功的业务动作。思考题  WHERE 和 HAVING 的区别是什么？  为什么程序中不建议使用 SELECT *？  NULL = NULL、NULL IS NULL、IFNULL(NULL, 0) 分别返回什么？  深分页为什么慢？如何用游标分页改写？  编写一条安全更新 SQL 时，你会做哪些检查？</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。学习 MySQL 最忌讳“只看不做”。本章会搭建一个可反复破坏、可随时重建的实验环境，并初始化一个贯穿全书的小型电商订单库。推荐使用 Docker 版本，因为它能在几分钟内得到干净的 MySQL 8.0 实例。2.1 安装方式选择            方式      适合场景      优点      缺点                  Docker      本地学习、自动化测试      干净、可重复、可多实例      需要理解容器和挂载              MySQL Installer      Windows 桌面学习      图形化安装      环境残留较多              apt / yum      Linux 服务器      贴近生产      版本取决于发行源              MySQL Shell + 远程库      只有学习环境      本地免安装      无法测试本地运维操作              云数据库      快速体验生产能力      备份和高可用完善      成本和权限受限      本书默认环境：  MySQL 8.0 或 8.4；  操作系统：macOS / Windows / Linux 均可；  客户端：mysql CLI、MySQL Shell、DBeaver、DataGrip 任选；  网络模式：本机学习，端口默认 3306。MySQL 8.0 和 8.4 的主要差异会在涉及身份认证、默认排序规则、并行能力时单独说明。没有特别说明时，示例默认面向 MySQL 8.0。2.2 使用 Docker 启动 MySQL创建工作目录：mkdir C:\mysql-labcd C:\mysql-lab启动 MySQL 8.0：docker run -d `  --name mysql-lab `  -p 3306:3306 `  -e MYSQL_ROOT_PASSWORD=Root123456! `  -e MYSQL_DATABASE=shop `  -e TZ=Asia/Shanghai `  -v C:\mysql-lab\data:/var/lib/mysql `  -v C:\mysql-lab\conf:/etc/mysql/conf.d `  mysql:8.0Linux / macOS 使用普通续行符：docker run -d \  --name mysql-lab \  -p 3306:3306 \  -e MYSQL_ROOT_PASSWORD='Root123456!' \  -e MYSQL_DATABASE=shop \  -e TZ=Asia/Shanghai \  -v "$PWD/data:/var/lib/mysql" \  -v "$PWD/conf:/etc/mysql/conf.d" \  mysql:8.0参数说明：            参数      说明                  --name mysql-lab      容器名，方便管理              -p 3306:3306      宿主机端口映射到容器端口              MYSQL_ROOT_PASSWORD      root 初始密码，仅用于本地实验              MYSQL_DATABASE=shop      首次启动时创建 shop 库              TZ=Asia/Shanghai      容器时区              数据挂载      数据保存在宿主机，容器删除后仍可恢复              配置挂载      自定义配置文件会自动加载      生产中不要把 root 密码写在命令行或脚本里，应使用密钥管理系统、云托管 Secret 或受控的配置中心。2.3 初始化配置文件创建 conf/mysql.cnf：[mysqld]port = 3306character-set-server = utf8mb4collation-server = utf8mb4_0900_ai_cidefault-time-zone = '+08:00'max_connections = 500wait_timeout = 28800interactive_timeout = 28800max_allowed_packet = 64Minnodb_buffer_pool_size = 512Minnodb_log_file_size = 256Minnodb_flush_log_at_trx_commit = 1innodb_file_per_table = 1innodb_print_all_deadlocks = 1slow_query_log = 1slow_query_log_file = /var/lib/mysql/slow.loglong_query_time = 0.5log_queries_not_using_indexes = 1sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION重启容器使配置生效：docker restart mysql-lab查看配置是否生效：SHOW VARIABLES WHERE Variable_name IN (  'version',  'character_set_server',  'collation_server',  'innodb_buffer_pool_size',  'innodb_flush_log_at_trx_commit',  'slow_query_log',  'long_query_time');几个关键配置的取舍：            配置      学习环境建议      生产建议                  innodb_buffer_pool_size      512M 到 2G      专用机器可用内存的 50% 到 70%              innodb_flush_log_at_trx_commit      1      金融和订单主库保持 1              sync_binlog      1      主库通常保持 1              slow_query_log      开启      开启并接入采集              log_queries_not_using_indexes      开启      谨慎开启，避免日志量过大      2.4 连接 MySQL进入容器：docker exec -it mysql-lab mysql -uroot -p输入密码后看到 mysql&gt; 提示即连接成功。查看版本和当前时间：SELECT VERSION(), CURRENT_TIMESTAMP(), @@server_timezone;SHOW DATABASES;使用 shop 库：USE shop;SELECT DATABASE();常用客户端命令：            命令      说明                  SHOW DATABASES;      查看数据库              USE shop;      切换数据库              SHOW TABLES;      查看当前库的表              SHOW CREATE TABLE orders\G      查看建表语句              STATUS;      查看连接和服务状态              EXIT;      退出客户端      2.5 创建学习账号学习阶段就应该避免长期使用 root。创建一个只有 shop 库权限的账号：CREATE USER 'shop_app'@'%' IDENTIFIED BY 'App123456!';GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES  ON shop.* TO 'shop_app'@'%';FLUSH PRIVILEGES;创建只读账号：CREATE USER 'shop_ro'@'%' IDENTIFIED BY 'Read123456!';GRANT SELECT ON shop.* TO 'shop_ro'@'%';查看权限：SHOW GRANTS FOR 'shop_app'@'%';SHOW GRANTS FOR 'shop_ro'@'%';本地实验可以用 % 作为客户端来源。生产账号应尽量绑定网段或主机，例如：CREATE USER 'shop_app'@'10.0.%.%' IDENTIFIED BY 'strong-password';2.6 初始化实验表在 shop 库执行以下脚本。这套表会在后续章节反复使用。CREATE TABLE users (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',    username VARCHAR(32) NOT NULL COMMENT '登录名',    nickname VARCHAR(64) NOT NULL COMMENT '昵称',    mobile VARCHAR(20) NOT NULL COMMENT '手机号',    status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态：1正常，2禁用',    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)      ON UPDATE CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_username (username),    UNIQUE KEY uk_mobile (mobile),    KEY idx_status_created (status, created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ciCOMMENT='用户表';CREATE TABLE merchants (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商户ID',    merchant_name VARCHAR(64) NOT NULL COMMENT '商户名称',    city_code VARCHAR(16) NOT NULL COMMENT '城市编码',    status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态：1营业，2停业',    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_merchant_name (merchant_name),    KEY idx_city_status (city_code, status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ciCOMMENT='商户表';CREATE TABLE products (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商品ID',    merchant_id BIGINT UNSIGNED NOT NULL COMMENT '商户ID',    product_name VARCHAR(128) NOT NULL COMMENT '商品名',    category VARCHAR(32) NOT NULL COMMENT '类目',    price DECIMAL(12, 2) NOT NULL COMMENT '售价',    stock INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '库存',    status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态：1上架，2下架',    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    KEY idx_merchant_status (merchant_id, status),    KEY idx_category_price (category, price)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ciCOMMENT='商品表';CREATE TABLE orders (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',    order_no VARCHAR(32) NOT NULL COMMENT '业务订单号',    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',    merchant_id BIGINT UNSIGNED NOT NULL COMMENT '商户ID',    status VARCHAR(16) NOT NULL DEFAULT 'CREATED' COMMENT '订单状态',    total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额',    paid_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '实付金额',    currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',    paid_at DATETIME(3) NULL COMMENT '支付时间',    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)      ON UPDATE CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_order_no (order_no),    KEY idx_user_created (user_id, created_at),    KEY idx_merchant_status_created (merchant_id, status, created_at),    KEY idx_status_created (status, created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ciCOMMENT='订单主表';CREATE TABLE order_items (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '明细ID',    order_id BIGINT UNSIGNED NOT NULL COMMENT '订单ID',    product_id BIGINT UNSIGNED NOT NULL COMMENT '商品ID',    product_name VARCHAR(128) NOT NULL COMMENT '商品快照',    unit_price DECIMAL(12, 2) NOT NULL COMMENT '成交单价',    quantity INT UNSIGNED NOT NULL COMMENT '数量',    subtotal DECIMAL(12, 2) NOT NULL COMMENT '小计',    PRIMARY KEY (id),    UNIQUE KEY uk_order_product (order_id, product_id),    KEY idx_product (product_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ciCOMMENT='订单明细表';确认表创建成功：SHOW TABLES;SHOW CREATE TABLE orders\G2.7 插入基础数据INSERT INTO users (username, nickname, mobile) VALUES('alice', 'Alice', '13800000001'),('bob', 'Bob', '13800000002'),('carol', 'Carol', '13800000003');INSERT INTO merchants (merchant_name, city_code) VALUES('Fresh Store', '010'),('Book Store', '021'),('Digital Store', '0755');INSERT INTO products  (merchant_id, product_name, category, price, stock) VALUES(1, 'Apple', 'FRUIT', 8.50, 100),(1, 'Milk', 'FOOD', 12.00, 80),(2, 'MySQL Book', 'BOOK', 79.00, 50),(3, 'Keyboard', 'DIGITAL', 399.00, 30),(3, 'Mouse', 'DIGITAL', 129.00, 60);验证：SELECT * FROM users;SELECT * FROM products;2.8 常用管理命令查看进程列表：SHOW PROCESSLIST;查看 InnoDB 状态：SHOW ENGINE INNODB STATUS\G查看表状态：SHOW TABLE STATUS FROM shop LIKE 'orders'\G查看错误日志路径：SHOW VARIABLES LIKE 'log_error';在容器内查看日志：docker exec -it mysql-lab bashtail -n 100 /var/lib/mysql/*.err2.9 常见安装问题2.9.1 客户端无法连接常见原因：  容器未启动；  宿主机端口被占用；  MySQL 账号来源限制不匹配；  云安全组未放行；  密码错误。排查命令：docker ps --filter name=mysql-labdocker logs --tail 100 mysql-lab2.9.2 认证插件错误老客户端连接 MySQL 8.0 时可能提示认证插件不支持。可以升级客户端，或为学习账号指定 mysql_native_password：ALTER USER 'shop_app'@'%' IDENTIFIED WITH mysql_native_password BY 'App123456!';MySQL 8.4 默认禁用了 mysql_native_password，需要先启用插件后才能使用。生产环境更推荐直接升级客户端。2.9.3 时区不一致查看时区：SELECT @@global.time_zone, @@session.time_zone, NOW();如果 JDBC 连接出现时间差，在连接串中显式声明：jdbc:mysql://127.0.0.1:3306/shop?serverTimezone=Asia/Shanghai2.9.4 字符集乱码确认字符集：SHOW VARIABLES LIKE 'character_set%';SHOW VARIABLES LIKE 'collation%';统一原则：  服务端使用 utf8mb4；  表和列显式指定字符集；  客户端连接使用 utf8mb4；  应用源码文件使用 UTF-8。2.10 数据导入导出导出表结构和数据：mysqldump -h127.0.0.1 -uroot -p \  --single-transaction --routines --triggers \  shop &gt; shop_backup.sql恢复：mysql -h127.0.0.1 -uroot -p shop &lt; shop_backup.sql只导出结构：mysqldump -h127.0.0.1 -uroot -p --no-data shop &gt; shop_schema.sqlmysqldump 会在备份恢复章节详细展开。这里只需要掌握基本用法，并记住 --single-transaction 可以避免 InnoDB 备份期间长时间锁表。2.11 环境管理停止和启动：docker stop mysql-labdocker start mysql-lab删除容器：docker rm -f mysql-lab如果想彻底重置实验环境，可以同时删除宿主机挂载的 data 目录。生产环境绝对不能这样做，删除数据目录前必须确认备份可恢复。本章小结一个可控的实验环境是学习 MySQL 的基础。Docker 提供了干净、可重复的启动方式；合理配置字符集、时区、慢日志和 InnoDB 参数能让后续实验更贴近生产。学习账号应与 root 分离，实验表应包含主键、唯一约束和常用复合索引。遇到连接问题时应依次排查容器状态、端口、账号来源、密码和网络策略。思考题  为什么学习环境也应该创建普通账号，而不是长期使用 root？  innodb_buffer_pool_size 为什么通常是专用数据库服务器最重要的参数之一？  utf8mb4 和历史 utf8 有什么区别？  mysqldump --single-transaction 对 InnoDB 备份有什么意义？  为你的学习环境设计一套“初始化、备份、重置”脚本。</li>
  <li>这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。MySQL 是全球使用最广泛的开源关系型数据库之一。它负责保存系统中最关键的事实数据：用户、账户、订单、支付、库存、配置和审计记录。对大多数互联网业务来说，MySQL 不是“可以替换的存储组件”，而是数据一致性的最后防线。本章帮助你建立 MySQL 的整体认知：它是什么，能解决什么问题，内部有哪些模块，和 Redis、Kafka、Elasticsearch 如何分工，以及学习 MySQL 时应该关注的重点。1.1 为什么每个后端工程师都要懂数据库一个典型的电商下单流程会涉及多个系统：用户下单  -&gt; 应用服务  -&gt; MySQL：保存订单、扣减库存、记录支付状态  -&gt; Redis：缓存商品详情、用户会话、热点计数  -&gt; Kafka：发布订单事件  -&gt; Elasticsearch：同步搜索索引和分析查询如果 Redis 挂了，业务可能变慢；如果 Kafka 挂了，下游可能延迟；如果 MySQL 挂了，订单通常无法创建，已有数据的一致性也可能面临风险。因此，MySQL 的核心价值是：  持久化：数据写入后可以长期保存；  事务：一组操作要么全部成功，要么全部失败；  一致性约束：主键、外键、唯一键和 CHECK 约束保证数据符合规则；  关系模型：用表和关系表达业务实体；  SQL 接口：提供声明式查询和统一操作语言；  生态成熟：备份、复制、监控、迁移、云服务都有完整工具链。1.2 MySQL 能做什么常见场景：            场景      示例      关键能力                  交易系统      订单、支付、账户      ACID 事务              用户系统      账号、权限、资料      唯一约束、索引              商品系统      SPU、SKU、库存      关系建模、行锁              审计日志      操作记录、资金流水      持久化、不可覆盖              配置管理      应用配置、开关      小规模高可用读写              报表辅助      低频统计查询      SQL 聚合、窗口函数      MySQL 也常被用于不适合的场景：            不适合场景      原因      更合适的选择                  秒级高频计数      每次都更新热点行      Redis              大规模全文搜索      倒排索引和分词能力有限      Elasticsearch              高吞吐事件流      不擅长 append-only 事件流      Kafka              海量 OLAP 扫描      行存和事务模型不是为分析设计      ClickHouse              超大文档存储      JSON 不是无限扩展的文档库      MongoDB / 对象存储              图查询      递归和关系遍历能力有限      图数据库      判断技术选型时，不要只问“能不能实现”，还要问“在这个场景下是否稳定、便宜、容易维护”。1.3 MySQL 的整体架构MySQL 可以分为 Server 层和存储引擎层。Client  |  vConnector          连接器、认证、权限  |  vServer Layer  |-- Parser              词法/语法解析，生成 AST  |-- Preprocessor        语义检查、权限检查  |-- Optimizer           逻辑优化、物理优化、代价估算  |-- Executor            执行计划、调用存储引擎接口  |-- Query Cache         已移除，不应再作为学习重点  |-- Binlog              Server 层归档/复制日志  +-- Utilities          管理工具、内置函数  |  vStorage Engine Layer  |-- InnoDB             事务、MVCC、锁、Buffer Pool、Redo/Undo  |-- MyISAM             老引擎，表锁，不支持事务  |-- Memory             内存表，重启丢失  |-- CSV / Archive      特殊格式和归档场景  +-- Custom Engine      自定义引擎1.3.1 Server 层Server 层负责连接、解析、优化和执行 SQL。它不直接管理数据文件，而是通过存储引擎接口读写数据。重要组件：            组件      职责                  Connector      维护连接、认证身份、检查全局权限              Parser      把 SQL 文本解析为语法树              Optimizer      选择索引、JOIN 顺序、访问路径              Executor      按执行计划调用引擎接口              Binlog      记录逻辑变更，用于复制和恢复      1.3.2 存储引擎层MySQL 的存储引擎是插件式的。现代互联网业务几乎默认使用 InnoDB。InnoDB 的关键能力：  ACID 事务；  MVCC 多版本并发控制；  行级锁；  B+Tree 聚簇索引；  Buffer Pool 缓存；  Redo Log 崩溃恢复；  Undo Log 回滚和版本链；  Change Buffer、Doublewrite Buffer、AHI 等优化机制。SELECT ENGINE, SUPPORT, COMMENTFROM information_schema.ENGINES;1.4 InnoDB 表逻辑结构一张 InnoDB 表逻辑上由表空间、段、区、页和行组成。Tablespace  +-- Segment       +-- Extent            +-- Page / Block，默认 16KB                 +-- Row最重要的事实是：InnoDB 表本身就是按主键组织的 B+Tree，这种结构叫聚簇索引。            结构      说明                  主键索引      叶子节点保存完整行数据              二级索引      叶子节点保存索引列和主键值              回表      通过二级索引找到主键，再查主键索引拿完整行              覆盖索引      查询字段都在索引中，不需要回表      这解释了为什么以下设计很重要：  尽量使用显式自增或趋势递增主键；  避免随机 UUID 作为主键导致页分裂；  二级索引不宜过多；  高频查询可以考虑覆盖索引；  大字段会影响聚簇索引行存储和内存效率。1.5 一条 SQL 的执行流程以查询为例：SELECT order_id, user_id, amountFROM ordersWHERE user_id = 10001  AND status = 'PAID';执行流程：1. 客户端发送 SQL2. 连接器检查身份和权限3. Parser 解析 SQL，生成语法树4. 优化器评估可能索引和访问方式5. Executor 按执行计划调用 InnoDB6. InnoDB 从 Buffer Pool 或磁盘读取数据页7. 根据索引结构定位记录8. Server 层过滤、投影、返回结果查看执行计划：EXPLAINSELECT order_id, user_id, amountFROM ordersWHERE user_id = 10001  AND status = 'PAID';执行计划会在后续章节详细分析，本章只需建立整体印象：SQL 是声明式语言，MySQL 优化器会决定怎么执行，而工程师要通过表结构和索引影响它做正确选择。1.6 MySQL 与其他系统的分工现代后端通常是多个存储系统的组合：            系统      职责      数据一致性角色                  MySQL      事实来源、事务、强一致      最终事实              Redis      缓存、热点计数、分布式锁      可重建的派生数据              Kafka      事件流、解耦、削峰      变更传播              Elasticsearch      搜索、日志、分析      可重建的查询投影              ClickHouse      OLAP 分析      离线或实时分析副本      常见数据流：MySQL  -&gt; Binlog / CDC     -&gt; Kafka        -&gt; Redis 缓存更新        -&gt; Elasticsearch 搜索索引        -&gt; ClickHouse 分析表这个架构的关键原则：  MySQL 是事实来源；  派生存储可以丢失后重建；  缓存和搜索同步必须考虑乱序、重复和延迟；  业务不要依赖派生数据做资金判断；  对账任务应比较 MySQL 与下游数据。1.7 MySQL 8.0 的重要变化本书以 MySQL 8.0 为主线。相比 5.7，8.0 有很多实用改进：            特性      说明                  默认 utf8mb4      默认字符集支持完整 Unicode              默认事务隔离级别 RR      5.7 也是 RR，8.0 继续保留              CTE      WITH 语法              窗口函数      ROW_NUMBER、RANK 等              JSON 增强      JSON 函数和索引能力增强              原子 DDL      DDL 变成原子操作              隐藏索引      测试索引删除影响              降序索引      真正支持降序存储              Instant DDL      部分变更秒级完成              数据字典改造      使用 InnoDB 存储元数据              角色管理      数据库权限更清晰              EXPLAIN ANALYZE      查看真实执行统计      如果生产仍使用 5.7，需要重点关注：  升级前的兼容性检查；  字符集和排序规则变化；  时区支持；  JSON / CTE / 窗口函数不可用；  官方生命周期结束后的安全风险。1.8 学习 MySQL 的五个层次第一层：会使用  连接数据库；  编写 CRUD；  设计简单表；  使用基本索引；  看懂常见错误。第二层：会优化  看懂执行计划；  设计联合索引；  处理慢查询；  分析锁等待；  控制大事务。第三层：懂原理  B+Tree；  MVCC；  Redo / Undo / Binlog；  Buffer Pool；  两阶段提交；  行锁、间隙锁和死锁。第四层：会治理  主从复制；  高可用；  备份恢复；  容量规划；  监控告警；  在线 DDL 和数据迁移。第五层：懂架构和内核  大规模分库分表；  数据迁移和双写切流；  读写隔离和弹性扩展；  优化器行为；  InnoDB 内部实现；  源码阅读与问题定位。本书会按这个路径逐层展开。1.9 生产视角的基本红线以下是生产 MySQL 的常见红线：  不允许无 WHERE 条件的 UPDATE / DELETE；  不允许直接删除未知表；  不允许在业务高峰执行大 DDL；  不允许无备份上线重大变更；  不允许长期使用超级用户连接业务；  不允许主库随意跑分析大查询；  不允许所有查询都靠超时兜底；  不允许只靠人工备份而不演练恢复；  不允许忽略慢查询和锁等待；  不允许没有主从延迟和高可用监控。这些规则看起来朴素，但大多数严重事故都来自其中一条被忽略。1.10 一个最小业务表设计订单表示例：CREATE TABLE orders (    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',    order_no VARCHAR(32) NOT NULL COMMENT '业务订单号',    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',    merchant_id BIGINT UNSIGNED NOT NULL COMMENT '商户ID',    status VARCHAR(16) NOT NULL DEFAULT 'CREATED' COMMENT '订单状态',    total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额',    paid_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '实付金额',    currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',    paid_at DATETIME(3) NULL COMMENT '支付时间',    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),    updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),    PRIMARY KEY (id),    UNIQUE KEY uk_order_no (order_no),    KEY idx_user_created (user_id, created_at),    KEY idx_status_created (status, created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;设计要点：  id 是趋势递增主键；  order_no 使用唯一键表达业务约束；  金额使用 DECIMAL，不用浮点；  时间使用 DATETIME(3) 保存毫秒；  常用查询路径建立复合索引；  表和字段都有 COMMENT；  字符集统一 utf8mb4。本章小结MySQL 是后端系统的事实来源和事务防线。它由 Server 层负责解析、优化和执行，由 InnoDB 负责事务、锁、MVCC、缓存和持久化。InnoDB 表按主键组织的 B+Tree 结构决定了索引设计和主键选择的重要性。学习 MySQL 应从 SQL 和表设计开始，逐步进入执行计划、事务锁、复制高可用和生产治理。思考题  MySQL Server 层和 InnoDB 存储引擎层分别负责什么？  为什么 Redis、Elasticsearch、ClickHouse 通常不能替代 MySQL 的事务职责？  什么是聚簇索引？为什么二级索引可能需要回表？  MySQL 8.0 有哪些对你当前业务有价值的新特性？  你们生产数据库有哪些必须固化的操作红线？</li>
</ul>
