ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL学习路径:从会用到能扛事的三阶段实战指南

MySQL学习路径:从会用到能扛事的三阶段实战指南

MySQL 学习路径这个话题,我在不同场合被问过太多次了。问的人里有刚转行的后端新人,有写了两三年业务代码但一遇到慢查询就发怵的开发者,也有需要自己维护一套数据服务的小团队负责人。大家的起点不一样,但困惑高度相似:网上资料一大堆,官方文档厚得像砖头,视频教程看完了还是不知道怎么下手。我自己的判断是,问题不在于资料少,而在于绝大多数人把 MySQL 当成一门“要背的知识”在学,而不是当成一套“要动手调的系统”在练。这套路径我想讲的是后者:从写出一条正确的 SQL 开始,到能读懂执行计划、能定位锁等待、能判断一张表该不该拆,一步一步把能力垒起来。它不需要你事先懂多少底层原理,但要求你肯在本地把库建起来、把数据灌进去、把每条语句跑一遍。适合谁?适合已经会写基本增删改查、想在半年内把 MySQL 从“会用”推到“能扛事”的开发者,也适合需要独立负责数据层设计的技术负责人。

1. 先把学习路径的骨架立起来

1.1 三个阶段划分与时间投入的现实估算

我不太喜欢那种把学习路径切成七八个阶段的方案,切得太细,人容易在阶段之间反复横跳,最后每个阶段都只摸了个皮毛。我的划分是三大块:会用、会调、会管。会用指的是能把业务需求翻译成表结构和 SQL,并且知道自己的语句大概会走什么路径;会调指的是遇到性能问题能定位、能改、能验证;会管指的是备份、恢复、复制、容量规划这些运维侧的事。

时间上给个参考值,按每天能投入一到两小时算:会用这一块大概四到六周,会调八到十周,会管四到六周。这个节奏不是拍脑袋来的,是我观察过若干次带人的过程后总结的。会用阶段短,是因为它的反馈最快,写完一条语句马上能看到结果;会调阶段长,是因为它依赖真实的数据量和真实的慢查询,你得先攒够“案件”才有得练;会管阶段反而能压缩,因为它的知识点相对固定,操作流程一旦跑通,重复几次就熟了。

有一个容易被忽略的点:这三个阶段不是严格串行的。我的建议是在会用阶段进行到一半的时候,就把慢查询日志打开,让它在后台默默记录。等你进入会调阶段,手里已经攒了一批真实的慢语句,比任何教程里的案例都贴近你自己的操作习惯。这个小小的重叠,能让第二阶段的效率提升一大截,因为你不是在学“别人遇到的问题”,而是在解决“自己制造的问题”。

1.2 每个阶段必须交付的“作品”

光有计划没有产出,学完就忘。我给每个阶段都设了一个必须交付的东西,可以理解成通关凭证。

会用阶段的交付物是一份完整的小型业务库设计,包含不少于八张表,要有主外键关系、要有枚举型的字典表、要有带时间范围的流水表,还要写至少三十条覆盖多表关联、分组聚合、子查询的查询语句。这份东西的价值在于它会逼你面对真实设计里的取舍:这个字段该不该冗余、这个关联该不该拆成两次查询、这个状态字段用整数还是用字符串。

会调阶段的交付物是一份慢查询治理报告。做法是先往库里灌入百万级以上的模拟数据,然后人为构造十条以上的慢语句,逐条用执行计划分析,给出改写方案,并记录改写前后的耗时对比。这份报告不需要给别人看,但它会强迫你把“感觉慢”变成“有数据支撑的慢”。

会管阶段的交付物是一套可复现的备份恢复流程,从全量备份、增量恢复到误删数据后的定点恢复,全部写成脚本,并且真的演练过至少一次完整的灾难恢复。我见过太多人学了备份命令但从来没实际恢复过,真出事的时候手忙脚乱。

提示:这三个交付物都不要追求完美,第一版能跑通就行。真正产生价值的是第二遍、第三遍的修改过程,而不是那个初始版本。

1.3 几种常见的路径偏差

第一种偏差是只学不练。看视频的时候觉得每个知识点都懂了,合上电脑什么都写不出来。MySQL 这门东西的抽象程度不高,几乎所有概念都能用一句 SQL 或者一个执行计划验证,不练等于没学。

第二种偏差是过早钻底层。刚学会写 JOIN 就去啃 InnoDB 的页结构、行格式、undo 日志组织方式。这些知识当然有价值,但它解决的是“为什么这样设计”的问题,而你当下要解决的是“我的语句为什么慢”。顺序反了,学习曲线会陡到让人放弃。我的建议是把底层原理放到会调阶段的中后期,那时候你已经有具体的性能现象作为锚点,读原理才有代入感。

第三种偏差是迷信参数调优。很多人的第一反应是去改配置文件,把缓冲池调大、把刷盘策略调激进。实测下来,八成的性能问题出在语句和索引上,参数能带来的提升通常是个位数百分比,而且改错了还有风险。参数该懂,但它不该是你的第一把刀。

2. SQL 基本功:从会写到写对

2.1 建表这件事,决定了后面八成的麻烦

我个人的经验是,一个项目的数据库设计质量,在建表那一刻就基本定型了。后面无论怎么优化索引、怎么改写语句,都很难弥补表结构本身的缺陷。所以路径的第一站必须是建表,而且要认真对待每一个字段。

先说几个几乎不会错的默认约定。字符集统一用utf8mb4,排序规则用对应的通用规则,不要在一个库里混用多种字符集,否则跨表关联时会发生隐式转换,索引直接失效,而且这种失效在执行计划里不一定显眼。主键统一用自增的BIGINT UNSIGNED,不要用业务字段做主键,也不要用随机字符串做主键。原因很直接:InnoDB 的表是按主键顺序组织数据的,自增主键意味着新数据总是追加在末尾,写入是顺序的;随机主键会让插入位置在整棵树里跳来跳去,页分裂频繁,写入性能下降得很明显。

时间字段的选择有个小坑。TIMESTAMP占四个字节,范围只到 2038 年,而且它的值会随时区设置变化;DATETIME占八个字节,范围大,存什么就是什么。业务里的创建时间、更新时间,我一般用DATETIME,避免时区带来的困惑;但如果表非常大、时间字段又多,用TIMESTAMP省下来的空间也值得考虑。这里没有标准答案,关键是你得知道自己在选什么。

金额字段一律用DECIMAL,绝对不要用FLOAT或DOUBLE。浮点数存不了精确的十进制小数,做累加的时候会出现尾差,财务对账时这种误差是致命的。长度按业务上限给,比如金额用DECIMAL(15,2),能覆盖到万亿级。状态字段用TINYINT加注释,不要用字符串,一是省空间,二是比较效率高。

CREATE TABLE order_flow ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '业务单号', user_id BIGINT UNSIGNED NOT NULL COMMENT '用户标识', amount DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '金额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_created (user_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单流水';

这段建表语句里有几个刻意的设计。order_no建了唯一索引,因为业务上它必须唯一,靠唯一索引来保证比在代码里查一遍再插入可靠得多,后者在并发下必然出问题。idx_user_created是一个联合索引,顺序是用户在前时间在后,这是为了支撑“查某个用户最近 N 条订单”这个最常见的查询,具体原因在第三章会展开讲。

2.2 查询语句的渐进式训练法

我把查询能力分成四层,每层都有明确的训练方法。

第一层是单表条件筛选与排序。这一层要练的是条件顺序和索引的配合。训练方法很简单:建一张十万行以上的表,写十条不同的 WHERE 组合,每条都用执行计划看一遍走的是哪个索引、扫了多少行。这一层练熟了,你会对“什么样的条件能用上索引”形成肌肉记忆。

第二层是多表关联。这一层的难点不在语法,在于关联顺序和驱动表的选择。我建议的练法是:同一份需求,分别用 JOIN 写一遍、用子查询写一遍、拆成两次查询在应用层拼装一遍,然后比较三者的执行计划和耗时。你会发现有些场景下 JOIN 明显更优,有些场景下拆开反而更快,这中间的判断依据是驱动表的行数和关联字段的索引情况,不是某种固定的写法。

第三层是分组聚合与窗口函数。分组聚合要注意的是分组字段和聚合字段的索引配合,以及 HAVING 和 WHERE 的区别——前者在聚合后过滤,后者在聚合前过滤,能用 WHERE 就别用 HAVING,因为 WHERE 阶段就能用索引减少扫描量。窗口函数在较新的版本里已经支持得很好,处理“取每组前 N 条”这类需求时,比传统的自关联写法清晰太多,性能也往往更好。

第四层是递归与复杂分析。递归查询适合处理树形结构,比如组织架构、类目层级。这一层的使用频率不高,但遇到的时候如果没有概念会很痛苦。练法就是找一张带父子关系的表,写一条递归查询把整棵树展开,再写一条查询某个节点的所有祖先。

2.3 数据类型选择的成本账

很多人觉得数据类型是小事,随便给个VARCHAR(255)就行。我把这笔账算给你看,你就知道为什么值得认真对待。

假设一张表有一千万行,某个字段从VARCHAR(255)改成VARCHAR(32)。字符集是utf8mb4,变长字段在行内只存实际长度加长度前缀,看起来改动不大。但问题在于,InnoDB 的索引页大小是固定的 16KB,字段的最大长度会影响索引能存多少条记录。如果一个联合索引里包含这个字段,字段的最大长度从 255 降到 32,单个索引页能容纳的记录数可能翻几倍,索引树的高度可能从四层降到三层。三层到四层听起来只差一层,但每次查询的磁盘读取次数可能从三次变成四次,在缓存命中率不高的情况下,这个差别相当可观。

再看一个更常见的例子:用INT还是BIGINT做外键。INT UNSIGNED的上限是四十多亿,绝大多数业务表永远到不了;BIGINT占八个字节,是前者的两倍。如果一张表有五个外键字段,一千万行的话,光这几个字段就相差 200MB 的存储,索引体积同样翻倍。当然,主键用BIGINT是对的,因为主键的值一旦用尽再改是灾难性的;但普通外键字段,如果你能确认业务量级,用INT UNSIGNED完全够。

场景常见误选推荐选择差异说明
主键INTBIGINT UNSIGNED预留增长空间,避免后期改主键
金额FLOAT/DOUBLEDECIMAL(15,2)精确小数,避免累加误差
状态VARCHAR(20)TINYINT省空间,比较快,配合注释即可读
短文本VARCHAR(1000)VARCHAR(实际上限)影响索引页容量与内存占用
布尔标记VARCHAR(1)TINYINT(1) 或 BIT语义清晰,存储紧凑
长文本VARCHAR(5000)TEXT 单独存表避免大字段拖累主表扫描

还有一个容易被忽略的点:允许 NULL 的字段在索引和比较中有额外开销。InnoDB 对 NULL 值有专门的处理,索引里会额外存储标记信息,统计信息里也要单独考虑。业务上如果某个字段确实必须有值,就加上NOT NULL DEFAULT,不要图省事全部允许 NULL。这个习惯在表小的时候看不出差别,表大了之后统计信息的准确性会受影响,优化器选错执行计划的概率会上升。

3. 索引与执行计划:性能优化的主战场

3.1 从数据结构到索引设计的映射

索引的本质是用额外的存储和写入开销,换取查询时的定位效率。InnoDB 用的是 B+ 树,这个结构有两个特点值得记住:一是所有数据都存在叶子节点,非叶子节点只存键值和指针;二是叶子节点之间用链表相连,所以范围查询很高效。

这两个特点直接推导出两条设计原则。第一,索引列越短越好,因为非叶子节点要存键值,键值短了,一个页能放下更多的键,树就更矮,从根到叶的路径就更短。第二,范围查询适合放在联合索引的最后一列,因为一旦用了范围条件,后面的列在索引里就无法继续用于精确定位了。

我常用来解释联合索引的类比是电话簿。电话簿按“姓、名”排序,你要找“张”姓的所有人,能快速定位;你要找“张”姓且名字是“三”的人,也能快速定位;但你要找名字是“三”而不管姓的人,就没法用这个排序了。这就是最左前缀原则的直观含义。

联合索引的列顺序怎么定?我的判断顺序是:等值条件优先,区分度高的优先,范围条件放最后,排序字段尽量跟上。举例来说,查询是“查某用户在某时间段内的订单,按时间倒序”,那么(user_id, created_at)就是很自然的顺序:user_id等值匹配,created_at既是范围条件又能顺带满足排序,一条索引解决三个需求,还避免了额外排序。

3.2 执行计划的正确读法

执行计划是排查性能问题的第一手材料,但很多人只盯着type那一列看,这是不够的。我按重要性给几个关键列排个序。

type列反映访问类型,从好到差大致是:system、const、eq_ref、ref、range、index、ALL。看到ALL就是全表扫描,看到index是全索引扫描,这两个都要警惕。但要注意,ALL不一定是坏事——如果表只有几百行,或者查询要返回大部分数据,全表扫描反而比走索引更划算,因为走索引还要回表,回表的随机读可能比顺序全扫更慢。

rows列是优化器估算的扫描行数。这个数字是估算值,基于统计信息,不一定准,但它的量级很有参考价值。如果rows是几十万而你预期只返回几十行,说明索引没用上或者用错了。

filtered列表示扫描到的行中,有多少比例能通过后续条件过滤。这个列常被忽略,但它能解释“为什么扫了十万行只返回一百行还这么慢”——因为过滤是在扫描之后做的,扫描量本身就构成了成本。

Extra列信息量最大。看到Using filesort说明有额外排序,看到Using temporary说明用了临时表,这两个都是性能信号。看到Using index是好消息,说明覆盖索引生效了,查询只读索引不回表。看到Using index condition说明用上了索引条件下推,也是好事。

EXPLAIN SELECT order_no, amount FROM order_flow WHERE user_id = 10086 AND created_at >= '2024-01-01' ORDER BY created_at DESC LIMIT 20;

对这条语句,理想情况是type=ref,key=idx_user_created,rows在几十到几百之间,Extra里出现Using index condition而没有Using filesort。如果Extra里出现了Using filesort,说明排序没被索引覆盖,需要检查索引顺序是否和排序方向一致。

注意:EXPLAIN给出的是估算,不是实测。真正判断优化效果,要用实际执行并记录耗时,或者看EXPLAIN ANALYZE(较新版本支持)给出的真实执行数据。我踩过的坑就是照着EXPLAIN的rows优化了半天,结果实际耗时没变,因为瓶颈根本不在扫描行数上。

3.3 索引失效的典型场景清单

这一节我列的是实战中反复遇到的失效场景,几乎每条我都实际遇到过。

第一种是在索引列上做函数运算或类型转换。比如WHERE DATE(created_at) = '2024-01-01',对列用了函数,索引直接失效。正确写法是范围查询:WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02'。类型转换的坑更隐蔽,如果字段是字符串类型而传入的是数字,或者反过来,会发生隐式转换导致索引失效。这种情况在执行计划里未必显眼,需要你对自己的表结构足够熟悉。

第二种是联合索引跳过最左列。索引是(a, b, c),查询条件只有b和c,用不上。这个大家都知道,但变体很多:WHERE a = 1 AND c = 3能用上a这一列,c用不上;WHERE a > 1 AND b = 2只能用上a,因为a是范围条件,后面的列无法精确定位。

第三种是以通配符开头的模糊匹配。LIKE '%关键字'用不上索引,LIKE '关键字%'可以。如果业务上确实需要前后模糊,考虑全文索引或者外部的检索方案,不要指望普通索引。

第四种是否定条件。!=、NOT IN、NOT LIKE这类条件,优化器往往判断走索引的收益不如全扫,尤其是在数据分布均匀的情况下。

第五种是OR 连接的条件中有一侧没有索引。只要 OR 的某一侧无法使用索引,整个条件可能退化为全表扫描。改写方式是用UNION ALL把两部分拆开。

第六种是排序方向与索引不一致。联合索引按升序存储,如果查询要求一列升序一列降序,索引无法直接满足排序,会触发文件排序。较新的版本支持降序索引,可以在建索引时显式指定方向,但要注意版本兼容性。

失效场景典型写法改写方案
列上做函数WHERE DATE(col) = '2024-01-01'改为范围条件
隐式类型转换字符串列传数字保证类型一致
跳过最左列索引(a,b),只查b调整索引顺序或补索引
前置通配符LIKE '%kw'改后缀匹配或换检索方案
否定条件!=、NOT IN改为正向条件或拆分查询
OR 混合一侧无索引拆成UNION ALL
排序方向冲突一升一降建降序索引或调整排序

3.4 索引不是越多越好:写入侧的代价

新手容易走另一个极端,给每个查询字段都建一个索引。这在读多写少的场景里问题不大,但在写入频繁的表上会带来明显负担。

每多一个索引,每次插入就要多维护一棵 B+ 树,更新索引列也要同步修改。一张表如果有八个二级索引,一次插入实际上要写九次(聚簇索引加八个二级索引),磁盘写入量和日志量都成倍增加。更麻烦的是,索引多了之后优化器选择执行计划的搜索空间变大,选错计划的概率也会上升。

我的经验是,一张表的索引数量控制在五个以内比较舒服,超过这个数就要审视一下是不是查询模式太发散。审视的方法是:把这张表上所有高频查询列出来,看每个索引被哪些查询用到,有没有可以合并的、有没有从来没被用到的。未被使用的索引是纯粹的负担,该删就删。较新的版本提供了索引使用情况的统计视图,可以定期查一下,把使用次数为零的索引清理掉。

4. 事务、锁与并发控制

4.1 隔离级别与多版本并发的配合关系

事务这块,很多人背得出四个隔离级别,但说不清每个级别到底解决了什么问题、代价是什么。我用一个具体场景把这条链路串起来。

假设两个事务同时对同一行做操作。读未提交级别下,一个事务能读到另一个事务还没提交的修改,这叫脏读,几乎所有业务都不能接受。读已提交解决了脏读,但同一个事务里两次读同一行可能得到不同结果,这叫不可重复读。可重复读保证同一事务内多次读结果一致,但可能出现幻读——第二次读的时候多出了几行。串行化则通过加锁让事务排队,代价是并发度大幅下降。

重点在于:MySQL 的默认可重复读级别,实现方式不是靠给每一行加锁,而是靠多版本并发控制。每次修改都会生成一个旧版本,读操作根据事务开始时的快照去读对应的版本,读写互不阻塞。这就是为什么在高并发下,读多写少的场景性能依然很好——读根本不需要等写。

但这个机制有个容易误解的地方:快照读和当前读是两回事。普通的SELECT是快照读,读的是历史版本;而UPDATE、DELETE、SELECT ... FOR UPDATE是当前读,读的是最新版本,并且要加锁。很多“为什么我读到了旧数据”的困惑,根源就在于没分清这两种读。

4.2 锁等待与死锁的排查实录

锁相关的问题,我遇到最多的两类是锁等待超时和死锁。

锁等待超时的典型现象是:某个操作卡住不动,等了一段时间报超时错误。默认的等待阈值是五十秒,很多业务等不了这么久,会把这个值调小。排查顺序是这样:先看当前有哪些事务在跑、各自持有什么锁、在等什么锁。较新版本里查询事务和锁等待的视图已经很完善,一条查询就能看到阻塞关系。

我印象比较深的一次是,一个批量更新任务把一批行锁住了,同时另一个单行更新在等同一批里的某一行,形成了长链阻塞。表面上看是批量任务太慢,根本原因是没有控制批量大小,一次更新了太多行,持锁时间太长。改成按主键分批、每批几百行、批间留一点间隔,问题就消失了。这个经验我后来一直沿用:任何批量写操作都必须分批,且每批的行数要受控。

死锁的现象是两个事务互相等对方持有的锁,谁也走不下去。数据库会主动检测并回滚其中一个,所以死锁本身不是致命问题,但频繁发生说明业务逻辑有问题。获取死锁信息的直接方式是查看最近一次死锁的详细记录,里面会写明两个事务各自执行了什么语句、持有什么锁、等什么锁。

最常见的死锁成因是加锁顺序不一致。两个事务分别先锁 A 再锁 B、先锁 B 再锁 A,必然撞车。解决办法是统一加锁顺序,比如永远按主键升序处理。另一个常见成因是间隙锁:可重复读级别下,范围条件不仅锁住存在的行,还会锁住区间间隙,防止其他事务插入。两个事务各自持有相邻区间的间隙锁,再互相申请对方的区间,就会死锁。这类问题可以通过缩小范围条件的区间、或者调整索引让范围更精确来缓解。

提示:死锁被回滚的那个事务,业务代码里必须能正确处理重试。我见过不少实现只管正常路径,一遇到死锁就抛异常给用户,体验很差。重试逻辑要注意加次数上限和退避,不要无脑循环。

4.3 长事务的治理思路

长事务是所有并发问题的放大器。事务开得越久,持有的锁越久,生成的旧版本数据越多,清理压力越大,回滚段膨胀,整体性能都会受影响。

长事务的来源通常有三类。第一类是应用层开启事务后做了耗时的非数据库操作,比如调用外部接口、处理文件。这类问题的修法是把非数据库操作挪到事务外面,事务里只留必要的读写。第二类是批量任务,一次处理太多数据。前面说的分批策略同样适用。第三类是交互式操作,比如在客户端里手动执行了BEGIN之后忘了提交。这个只能靠规范约束,加上定期巡检发现。

巡检的方法很直接:定期查询当前运行时长超过阈值的事务,把它们的开始时间、执行的语句、所属连接都列出来。我一般把阈值设在三十秒,超过就告警。这个习惯帮我提前发现过好几次隐藏的逻辑问题。

5. 性能优化与容量规划

5.1 用慢查询日志构建优化闭环

性能优化最怕的是凭感觉。慢查询日志的价值在于把“感觉慢”变成“可度量、可排序、可验证”。

开启方式很简单,把慢查询阈值设成一个较小的值(比如一秒),打开日志开关。这里有个细节值得说:日志记录的是执行时间超过阈值的语句,但阈值设得太低会产生大量日志,影响性能;设得太高又会漏掉问题语句。我的做法是初期设一秒,跑一段时间后看日志分布,如果日志量很大,说明系统里有大量亚秒级但高频的语句,这时候要结合聚合统计来看,而不是逐条看原始日志。

日志里最有价值的信息是执行次数和总耗时。一条语句单次只花零点一秒,但一天执行一百万次,总耗时就是二十多个小时,这种语句的优化收益远大于一条偶尔跑十秒的报表查询。所以我的排序原则是按总耗时排序,而不是按单次耗时排序。

优化的闭环是这样:从日志里挑出总耗时最高的几条,用执行计划分析,提出改写或加索引的方案,改完后用同样的数据量和同样的语句做对比测试,记录前后耗时。这个过程至少走三轮,因为很多优化是连锁的,改了第一条可能暴露出第二条的问题。

5.2 几个真正值得调的参数

虽然我说过参数不是第一把刀,但有那么几个参数确实值得了解,因为它们影响的是全局行为。

缓冲池大小是最重要的一个。它的作用是缓存数据和索引页,减少磁盘读取。经验值是设成物理内存的百分之五十到七十,具体要看这台机器上还跑不跑别的服务。设得太小,缓存命中率低,读操作频繁落盘;设得太大,可能触发系统层面的内存交换,性能反而断崖式下跌。

连接数上限需要结合应用的连接池配置来定。常见的问题是应用侧连接池开得很大,数据库侧上限却很小,导致连接被拒。反过来,上限设得过大也有风险,每个连接都要占内存,连接数暴涨时可能把内存吃光。我的做法是让上限略高于所有应用连接池之和,留一点余量给运维排查用。

还有一个容易被忽略的是空闲连接的回收时间。连接建立后长期空闲,占着资源不干活。设置一个合理的空闲超时,让闲置连接自动断开,能缓解连接数压力。但要注意这个值必须大于应用侧连接池的最小空闲检测周期,否则会出现应用以为连接还在、实际已被服务端断开的情况,表现出来就是间歇性的连接错误。

参数方向调大倾向调小倾向判断依据
缓冲池读多写少、内存充足机器共用、内存紧张缓存命中率与系统内存水位
连接数上限应用连接池较大内存有限、连接暴涨应用侧池配置之和加余量
空闲超时连接复用频繁连接数紧张与连接池检测周期配合
慢查询阈值日志量过大想抓更多问题语句先宽后严,结合聚合统计

5.3 什么时候该考虑分库分表

这个话题我被问得很多,答案可能和很多人的预期不一样:大多数情况下你不该分。

先看判断标准。单表数据量的经典参考值是两千万行,但这不是硬线,实际要看行宽和访问模式。一张只有五六个窄字段的表,几千万行也未必有问题;一张有十几个宽字段、还带大文本的表,几百万行就可能很吃力。更靠谱的判断方式是看单表的数据文件大小和索引大小,如果索引已经超过缓冲池能缓存的范围,缓存命中率会持续走低,那才是真正的瓶颈信号。

在考虑拆分之前,有一系列成本低得多的手段值得先试:优化索引、改写语句、归档历史数据、把大字段拆到独立表、加读写分离分担读压力。这些手段能把系统的承载能力往上推一大截,而且改动风险小。

如果确实到了必须拆的地步,先做垂直拆分——按业务把不同的表分到不同的库,这个改动相对可控。水平拆分(把一张表按某个维度切到多个库)是最后手段,因为它会引入跨分片查询、分布式事务、全局唯一标识、数据迁移这一系列复杂问题,运维成本陡增。

我做过的水平拆分里,踩得最深的坑是分片键的选择。选得不好,会出现数据倾斜——某个分片的数据量是其他分片的几倍,等于没拆。分片键要满足两个条件:分布均匀,且大多数查询都带这个条件。如果业务查询模式很发散,没有哪个字段能同时满足,那就说明还不具备拆分条件,应该先改查询模式。

6. 备份恢复与复制:容易被跳过的必修课

6.1 备份策略的层次设计

备份这件事,只有真正丢过数据的人才会重视。我的建议是不要等到出事才学,把它放进学习路径的固定环节。

备份分两个层次。逻辑备份导出的是 SQL 语句,好处是可读、可跨版本、可 selective 恢复单表;缺点是慢,数据量大时导出和导入都耗时,而且对业务有压力。物理备份直接复制数据文件,快得多,适合大数据量;缺点是版本和平台绑定较紧,不能随便跨版本用。

我的常规策略是两者结合:每周做一次全量物理备份,每天做一次增量,逻辑备份按需用于单表恢复和小范围数据订正。保留周期按业务合规要求定,至少要能覆盖“发现问题的时间差”——很多数据问题是几天后才被发现的,如果备份只留一天,等于没有。

备份做完了必须验证。验证方式不是看文件大小对不对,而是真的在另一套环境里恢复一遍,然后比对关键表的行数和校验值。这个演练我建议每季度做一次,因为恢复流程里涉及的命令、路径、权限,隔几个月就可能忘光。

6.2 复制的基本原理与常见延迟

复制是读写分离和数据冗余的基础。基本原理是主库把变更写进二进制日志,从库拉取日志并在本地重放。这里的关键概念是日志格式:基于语句的格式记录原始 SQL,日志小但某些函数会导致主从数据不一致;基于行的格式记录每一行的变更,日志大但结果确定。现在的默认选择一般是基于行的格式,我建议保持这个选择,一致性比日志体积重要得多。

复制延迟是最常见的问题。现象是从库的数据比主库慢,读从库可能读到旧数据。延迟的原因通常是主库写入量大而从库重放能力跟不上,或者从库上跑了重的查询抢占了资源。应对方式有几层:从库上禁止跑大报表查询,把分析类负载隔离到专门的实例;控制大事务,一次改太多行的操作会形成一个巨大的日志事件,从库重放时是原子的,会卡很久;监控延迟指标并设置告警,延迟超过阈值就把读流量切回主库。

还有一个细节:从库重放是单线程还是多线程,直接决定了追赶能力。较新的版本支持并行重放,配置得当的话延迟能显著降低。这块的配置需要根据实际负载调,没有放之四海皆准的值。

7. 练习环境与实操数据准备

7.1 本地环境的搭建要点

学 MySQL 一定要有能随便折腾的本地环境。最省事的方式是用容器跑一个实例,一条命令起来,端口映射到本机,数据目录挂载到本地卷上,这样容器删了数据还在。配置文件可以通过挂载的方式覆盖,改参数不用进容器。

资源分配上给个参考:练习用的实例,内存给两到四G,其中缓冲池设一到两G,足够支撑百万级数据的各种实验。磁盘留二十G以上,因为你要灌数据、要做备份、要留恢复演练的空间。

版本选择上,建议用当前主流的稳定大版本,不要用太老的版本,因为新版本在索引、执行计划、窗口函数、降序索引这些方面都有改进,学老版本的很多“经验”在新版本里已经不适用了。同时也不要追最新的尝鲜版本,练习环境求的是行为可预期。

7.2 练手数据怎么造

没有数据练不了手,但造数据也有讲究。太小的数据集看不出性能差异,太大的数据集跑一次查询要等很久,影响练习节奏。我的建议是分三档准备。

第一档是千行级,用于验证语句的正确性。这个量级下任何语句都是瞬间返回,可以快速试错。

第二档是百万行级,用于观察索引效果和执行计划差异。这个量级足以让全表扫描和索引查找产生肉眼可见的耗时差,又不至于慢到无法忍受。

第三档是千万行级,只在做特定实验时用,比如验证分页深翻页的优化、验证大表加索引的耗时、验证分片前后的对比。这个量级的数据准备需要点技巧,用批量插入比逐条插入快几个数量级,一次插入几千行、循环若干次,比写一个循环插一千万次快上百倍。

造数据时要注意数据分布的仿真。如果所有行的某个字段值都是均匀分布,优化器的估算会非常准,你就看不到“统计信息不准导致选错计划”这类真实问题。刻意造一些倾斜数据——比如某个用户有一百万条记录,其他用户各有一百条——才能复现真实场景里的各种怪现象。这一点我觉得是练习数据准备里最容易被忽略、但对能力提升帮助最大的部分。

8. 常见问题速查与避坑经验

8.1 高频问题速查表

下面这张表是我这些年被问得最多的问题,按现象、原因、处理三列整理,可以直接拿去对照。

现象常见原因处理方向
查询突然变慢统计信息过期导致计划变化更新统计信息,复核执行计划
写入越来越慢索引过多或页分裂清理无用索引,检查主键是否自增
连接数打满连接泄漏或池配置不当查活跃连接与空闲连接分布
主从数据不一致日志格式或非确定函数改行格式,避免非确定写法
磁盘占用暴涨旧版本数据未清理、日志未归档检查回滚段与日志保留策略
分页越翻越慢大偏移量的深度分页改用游标式分页
批量更新卡住一次锁太多行分批处理,控制批大小
死锁频繁加锁顺序不一致统一按主键升序处理

深度分页这一条值得单独说两句。LIMIT 1000000, 20这种写法,数据库要先扫描并丢弃前一百万行,再取二十行,越往后越慢。改成游标式分页,用上一页最后一条记录的排序键作为下一页的起点条件,扫描量就恒定在每页大小,跟页码无关。这个改动的收益在数据量大时非常明显。

8.2 几条踩过才知道的经验

第一条:不要在生产环境直接跑EXPLAIN之后就改索引。加索引本身是有成本的,大表加索引可能锁表或者消耗大量IO,影响在线业务。稳妥的做法是先在有同等数据量的影子环境验证,确认收益后再排期在低峰期操作。

第二条:改语句之前先留证据。我见过有人优化完发现更慢了,想回滚却记不清原来的写法。养成习惯,把原始语句、执行计划、耗时都记下来,改完再对比。这个习惯看起来笨,但它是唯一能让你确认“优化真的有效”的方法。

第三条:不要迷信网上抄来的参数模板。每台机器的内存、磁盘、负载特征都不一样,别人机器上的最优值到你这里可能是灾难。参数要基于自己的监控数据来调,调完要有对比观察期。

第四条:边界数据要比正常数据多测。空值、超长字符串、零值、负数、跨年跨月的时间,这些边界在测试环境里经常被忽略,上线后集中爆发。我在练习阶段就养成了专门造边界数据的习惯,这个习惯后来帮我挡掉了不少线上问题。

第五条:版本升级前先读变更说明。优化器行为、默认参数、弃用特性,每个大版本都可能有变化。有些在老版本上跑得好好的语句,升级后执行计划变了,性能可能下降。升级前在影子环境跑一遍核心查询做对比,能提前发现问题。

8.3 关于学习节奏的一点个人体会

最后说说节奏。我个人的体会是,MySQL 这门东西不适合集中突击,更适合小步高频。每天花一小时,其中四十分钟动手、二十分钟看原理,持续三个月,效果远好于连续两周每天八小时然后放下。原因是它的知识点之间有大量隐含关联,比如索引和锁的关系、事务和日志的关系、复制和日志格式的关系,这些关联需要时间在脑子里慢慢连起来,突击只能记住孤立的知识点。

另外一个体会是,找一个真实的小项目来承载学习。哪怕只是给自己做一个小工具,只要有真实的表结构、真实的查询需求、真实的数据增长,学习就有了抓手。纯看教程最大的问题是所有问题都是别人替你定义好的,而真实项目里你会自己遇到“这个字段到底该建索引吗”“这个查询为什么慢”这类问题,解决问题的过程才是能力增长的来源。我在带人的时候,第一件事总是让对方先把自己的小项目库建起来,后面的所有知识点都往这个库上套,效果比按章节学要好得多。

返回列表