
如果你的慢查询日志里出现了一条很“冤”的SQL两张大表关联查询关联列刚好没有索引执行计划里赫然写着Using join buffer或者一条ORDER BY本来数据量不大却要跑好几秒Extra里带着Using filesort——这两个关键词背后就是今天要聊的两个参数join_buffer_size和sort_buffer_size。这两个参数是MySQL性能调优里的经典话题也是面试官特别喜欢问的点。因为它们足够基础、足够常见又带有很多容易被忽略的隐藏语义。网上讲这两个参数的文章不少但大多停留在“把值调大一点”的层面很少说清楚它们到底在什么阶段分配分配了之后怎么被使用调大之后内存会怎么膨胀以及为什么有时候调大了反而更慢。这篇文章我会从一个实际运维者的角度把这两个参数的工作机制、常见误区和一套可落地的调优流程完整讲一遍。无论你是DBA、后端开发还是正在排查线上性能问题的同学看完应该都能直接上手操作并且知道每一步在干什么。1. 这两个参数各自管什么1.1 join_buffer_size是JOIN的临时操作台先看名字。join_buffer_size翻译成“连接缓冲区大小”但这里的“连接”指的并不是连接池或者网络连接而是JOIN操作。更准确的说法是执行JOIN时临时存放驱动表数据的一块内存区域。MySQL默认值是262144字节也就是256KB。在MySQL 8.0.18之前这个参数最小可以设到1KB8.0.18之后官方把最小值提高到了128KB。这点改动背后其实对应了执行引擎的变化后面详细说。关键的一点是它并不是连接建立时就分配的。很多同学以为这个参数像是max_connections一样每个连接固定占一块内存这是最常见的第一层误解。真实情况是MySQL只在执行特定类型的JOIN时才会按需分配这块缓冲区。语句执行完缓冲区就释放了。一句话总结它是给JOIN语句临时用的“操作台”不是给连接常驻的“工位”。另外一个反直觉的细节在于官方文档里的定义是这个参数指定的值是实际分配的最小尺寸。也就是说你设置成8MBMySQL在执行某个查询时实际分配的内存可能比8MB更大。很多人调完参数后一看内存占用“超了”以为监控出了问题其实不是。1.2 sort_buffer_size是排序的内存流水线再说sort_buffer_size默认值同样是256KB。从名字就能看出来它是给排序操作用的。当MySQL执行ORDER BY、GROUP BY、DISTINCT、UNION或者JOIN内部的排序操作时如果无法直接利用索引有序性就需要把数据先取出来放到一块内存里排序这个过程叫filesort。sort_buffer_size和join_buffer_size很像也是按需分配语句执行完即释放。同样它也是“最小分配值”而不是硬性上限。它的最小允许值是32KB在64位系统上实际分配时还会做一个向上取整对齐所以你在内存监控里看到的数值往往会比配置值略高一点。我见过很多人在优化排序慢查询时第一步就是把sort_buffer_size从256KB改成64MB以为“缓冲区越大排序越快”。这个想法有一半是对的但另一半会踩坑。因为排序缓冲区的物理含义不仅仅是“能放多少数据”它直接决定了MySQL走内存排序还是磁盘归并排序。这里面的机制是理解这个参数的关键后面专门展开。1.3 按连接数估算内存峰值防止调优变事故这两个参数有个共性都是会话级session level参数而且每个会话在需要使用它们时都可能独立分配一份。这意味着一件很重要的事评估这两个参数对内存的影响不能只看单条SQL而要按“并发连接数×参数值×单条语句可能分配的次数”来估算峰值。举个例子。假设你的连接数是500你把sort_buffer_size设成16MB理论上如果500个连接同时触发排序光是排序缓冲区这一项就可能吃掉500×16MB8GB内存。如果join_buffer_size也设成16MB那总计就是16GB。这还没算innodb_buffer_pool_size、max_connections默认包、binlog缓存等其他内存开销。所以你会发现一个规律几乎所有因为这两个参数导致的内存事故都不是“某一单条SQL太大”而是“并发一起来内存线性叠加”。这也是为什么很多人看网上配置模板直接抄一个大值结果第二天凌晨业务高峰时实例直接OOM的原因。2. 调参前先看懂执行机制2.1 join_buffer_size在三种JOIN算法里的作用要搞清楚这个参数怎么调先要知道MySQL执行JOIN时到底发生了什么。咱们从最朴素的模型讲起。第一种叫Simple Nested-Loop Join简单嵌套循环连接。它的行为可以理解为双重循环外层循环取驱动表的每一行内层循环去被驱动表里逐行匹配。如果驱动表有1000行被驱动表有10万行那么理论上要做1000×1000001亿次匹配。这种算法在没有任何索引的关联列上基本是灾难。第二种叫Block Nested-Loop Join块嵌套循环连接。它引入了缓冲区。MySQL会先把驱动表的一批行读出来放到join_buffer里然后一次性把这一整块数据拿到被驱动表里去匹配。这样被驱动表被扫描的次数就变成了“驱动表数据被切成了多少块”而不是“驱动表有多少行”。假设驱动表1000行join_buffer能放下500行那被驱动表只需要被扫描1000/5002次。这个缓冲区越大被扫描的次数越少。第三种是MySQL 8.0.18引入的Hash Join哈希连接。它同样依赖内存来构建哈希表join_buffer_size也会影响其内存可用量。当数据量超过内存能承受的范围时Hash Join会使用磁盘临时文件性能断崖式下跌。从EXPLAIN结果里很容易判断走的是哪一种。当你在Extra列看到Using join buffer (Block Nested Loop)时说明本次JOIN确实用到了join_buffer_size。此时如果你发现被驱动表的扫描次数很高、执行时间很长调大这个参数就能起到立竿见影的效果。2.2 sort_buffer_size与filesort的两段式故事排序这个场景很多人的理解也有偏差。ORDER BY不一定慢但如果执行计划里出现了Using filesort那说明MySQL已经没办法直接按索引顺序取数需要额外排序了。整个排序过程大致分两步。第一步“读”阶段MySQL按WHERE条件扫描行把需要排序的列和主键id一起放进sort_buffer里。如果这些数据能一口气放在内存里直接内存排序结束。第二步“归并”阶段如果sort_buffer放不下MySQL会把排序结果分成多个块分别写到磁盘临时文件里然后再把这些临时文件做归并排序最终生成完整的有序结果。判断是否发生过磁盘归并有一个非常直接的状态变量Sort_merge_passes。这个值表示排序过程中“归并遍数”。如果这个值一直为0说明排序都在内存里完成不需要调整sort_buffer_size如果这个值很大说明磁盘IO成了瓶颈此时增大sort_buffer_size才有可能见效。磁盘和内存的速度差距通常在三到四个数量级。一次归并看起来只是多读几个文件但放大到高频慢SQL上影响会被显著放大。所以sort_buffer_size的真正价值是尽量让排序留在内存里而不是单纯“排序更快”。2.3 参数是补位不是主力这是全文我最想强调的一点这两个参数解决的问题本质上都是“在缺乏索引或无法使用索引的情况下用内存去换时间”。如果你有一个高频JOIN查询关联列没有索引此时调大join_buffer_size确实能优化但它是在为一个“糟糕的SQL结构”兜底。同样的查询如果给关联列建一个索引可能从10秒直接降到0.01秒两个数量级的差距远不是缓冲区能拉回来的。排序同理如果ORDER BY字段可以用覆盖索引来满足压根不会触发filesort。所以调优的正确顺序永远是先看执行计划优化索引索引确实加不了再考虑SQL改写最后才轮到调整这两个内存参数。很多人上来就改参数效果不理想不是参数没用而是瓶颈根本不在内存这块。3. 实操确认触发、调参、验证一次走通3.1 查看与修改参数的正确姿势先看当前值直接执行SHOW VARIABLES LIKE join_buffer_size; SHOW VARIABLES LIKE sort_buffer_size;返回的单位是字节默认一般是| Variable_name | Value | | join_buffer_size | 262144 | | sort_buffer_size | 262144 |临时修改分会话级和全局级。如果只是想针对某一条慢SQL做验证用会话级只影响当前连接-- 8MB SET SESSION join_buffer_size 8388608; SET SESSION sort_buffer_size 8388608;这样不会影响其他连接适合临场测试。如果确认有效想全局生效再执行全局修改SET GLOBAL sort_buffer_size 4194304;注意SET GLOBAL只对之后新建的连接生效当前已经存在的连接不会感知到这个改动。很多人在测试环境执行完SET GLOBAL后立刻用同一个连接继续测试以为改了没效果其实只是作用域没搞明白。MySQL 8.0还有一个更省心的方式SET PERSIST。它会把配置写入到mysqld-auto.cnf文件里重启实例也不会丢SET PERSIST join_buffer_size 4194304; SET PERSIST sort_buffer_size 4194304;如果只想写入配置文件而让当前实例暂不生效可以用SET PERSIST_ONLY。3.2 用EXPLAIN判断参数是否真的在工作先写一个典型的“有问题”的SQL场景。两个表关联列都没有索引CREATE TABLE t_a ( id INT PRIMARY KEY, category_id INT ); CREATE TABLE t_b ( id INT PRIMARY KEY, a_id INT ); EXPLAIN SELECT * FROM t_a JOIN t_b ON t_a.id t_b.a_id WHERE t_a.category_id 100;如果两个表关联列上都没有可用索引大概率执行计划里会有一行Extra显示Extra: Using join buffer (Block Nested Loop)看到这个标记就可以确定join_buffer_size真正参与了本次查询。如果EXPLAIN里压根没有这行你调大join_buffer_size自然一点波澜都不会有。排序同理EXPLAIN SELECT * FROM t_a ORDER BY category_id;如果category_id没有索引你就会在Extra列里看到Using filesort。这时候sort_buffer_size才和这个慢查询有关系。3.3 用状态变量和慢查询日志量化优化效果改完参数怎么确认真的有效不能靠感觉要用数据。对于排序有一个很直观的会话级状态变量Sort_merge_passes。它的含义是排序过程中发生过多少次磁盘归并。我们先看基线SHOW SESSION STATUS LIKE Sort_merge_passes;刚建立连接、还没执行大排序时这个值通常是0。然后按原参数跑一次慢SQL记录执行耗时和这个状态值。接着SET SESSION sort_buffer_size再跑一次同样的SQL看Sort_merge_passes是否下降、执行耗时是否缩短。如果增大后Sort_merge_passes依然很大说明查询的排序数据集实在太大或者需要排序的列字段太长。这时候与其继续加内存不如考虑减少排序返回的字段宽度、优化索引、拆分SQL。对于join_buffer_size官方没有类似Sort_merge_passes这样直观的状态变量。一个比较间接的观察维度是看执行时间、被驱动表扫描次数以及磁盘临时表有没有减少。你可以在优化前用EXPLAIN ANALYZEMySQL 8.0.18看看实际执行耗时和循环次数优化后再跑一次对比。也可以开慢查询日志以纵向时间段观察同类SQL的平均耗时变化。4. 生产环境里最容易踩的坑4.1 “最小分配值”不等于“内存硬上限”前面提过这两个参数在官方文档中的定义是“分配的最小尺寸”。实际分配时MySQL有可能会分配比设定值更大的内存块。这一点在排序上尤其明显某些场景下MySQL需要额外的临时数据结构、要按记录长度对齐内存页最终真实分配量会是配置值的若干倍。我见过一个案例某同学监控到sort_buffer_size设置成4MB但performance_schema里显示单线程排序内存占用超过了100MB当场以为监控有bug。排查了很久才发现SQL里有子查询和UNION嵌套一条语句内部发起了多次排序每个排序阶段的缓冲区叠加起来就把内存撑上去了。所以你不要把参数值当成一个严格的上限而应当把它理解成一个“内存起步价”。4.2 全局改值容易引发雪崩式OOM很多线上内存事故的源头都出在这一条。假设你看到某一条报表SQL排序很慢一咬牙把sort_buffer_size从256KB调成256MB。单条SQL确实快了但到了业务高峰100个并发连接同时触发排序内存占用直接就是100×256MB25GB。如果实例规格只有32GB留给innodb_buffer_pool_size和操作系统的基本没有余量OOM只是时间问题。这里的教训是验证时用会话级参数上线时优先用“最小满足需求”的值。不要总想着“既然要改就一步到位”。我个人习惯是先按2MB、4MB、8MB这样阶梯式往上试找到性能收益的拐点就停住。性能提升不再明显时再加内存只会增加风险不会带来收益。4.3 配置作用域与主从不一致这个坑比较隐蔽。SET GLOBAL在MySQL里是动态的但如果你只在一台主库上执行了SET GLOBAL从库并不会自动同步这个参数。一旦发生主从切换新主库还沿用默认的256KB性能瞬间倒退回优化前。更麻烦的是当时你可能并没有意识到参数差异排查半天都找不到原因。正确的做法是所有涉及这两个参数的调整最终都必须落到配置文件里。MySQL 8.0可以用SET PERSIST它会把参数写入到持久化配置文件5.7及更早版本则需要手工同步到各节点的my.cnf并纳入配置变更管理流程。4.4 只调参数不修SQL治标不治本我再举一个很典型的例子。有次排查一条JOIN慢查询驱动表20万行被驱动表500万行关联列没索引。EXPLAIN显示Using join buffer (Block Nested Loop)。我把join_buffer_size从默认256KB调到16MB执行时间从8.5秒降到了1.2秒当时觉得效果很明显。但后来同事给关联列建了一个普通索引这条SQL的执行时间直接降到0.02秒。那一刻我才真正意识到用大内存去兜底一个没有索引的JOIN本质上是在用昂贵的机器资源掩盖SQL设计的问题。所以每当你准备调大join_buffer_size时先反问一句这个JOIN能不能加索引如果可以优先加索引。4.5 用performance_schema定位真实内存去向有时候你调完参数想知道内存到底涨在哪最直接的办法是查performance_schema的内存统计。MySQL 5.7和8.0都支持不过事件名在不同版本略有差异。一个通用且好用的入口是sys库SELECT * FROM sys.memory_global_by_current_bytes ORDER BY current_alloc DESC LIMIT 10;这个视图可以按当前内存占用量排序帮你快速看到哪些内存事件消耗最大。如果你观察到memory/join_buffer或者memory/sort相关的行占比很高那就说明当前负载里确实有大量JOIN或排序在消耗内存。此时再去调整对应参数才算有的放矢。5. 配置策略与内存预算5.1 不同场景的起步参考值这两个参数没有放之四海皆准的“最佳值”因为每个实例的内存规格、连接数、查询特征都不同。但根据经验不同负载风格可以给出不同的起步参考业务负载风格join_buffer_sizesort_buffer_size备注纯OLTP在线交易256KB - 1MB256KB - 1MB优先保证高并发下的内存安全混合负载OLTP报表2MB - 4MB2MB - 4MB观察慢查询日志后逐步微调OLAP/批量分析8MB - 16MB8MB - 32MB适合低并发、高吞吐场景需物理内存充足注意这些只是起步值不是目标值。我建议每次只调整一个参数观察至少24小时。尤其要关注业务高峰期的内存增长曲线和OOM风险。5.2 一套稳健的调优流程把前面讲的串起来可以总结出一套可复用的操作流程用慢查询日志或EXPLAIN定位具体的慢SQL确认Extra列里是否存在Using join buffer或Using filesort。先优化索引和SQL结构。关联列无索引就先加索引排序字段能进索引就尝试覆盖索引。确认索引手段穷尽后再考虑调内存参数。使用会话级设置从较小值开始阶梯式测试。对比每次调整前后的执行耗时、Sort_merge_passes、Rows_examined等指标找到收益拐点。确认有效后再决定是全局设置还是只对特定账号、特定会话生效。最终把参数固化到配置文件8.0用SET PERSIST并同步到所有实例节点。整个流程里面最关键的步骤是第4步的“量化验证”。没有量化你根本不知道当前改动是赚了还是亏了。5.3 预留内存怎么算很多人在调参时担心的核心问题是到底会不会把内存弄爆这里给一个粗略但实用的估算公式实例总内存 innodb_buffer_pool_size 系统预留内存 最大并发连接数 × (各会话缓冲之和) × 系数其中“各会话缓冲之和”通常包括sort_buffer_size、join_buffer_size、binlog_cache_size、read_buffer_size、read_rnd_buffer_size等。系数建议按2到3估算因为同一条SQL语句里可能触发多次排序或多个JOIN缓冲高并发下这类叠加很常见。举例来说实例32GBinnodb_buffer_pool_size20GB系统预留4GB剩余可给会话缓冲的空间大概8GB。假设最大并发连接100如果sort_buffer_size和join_buffer_size都设4MB那单项缓冲就是100×4MB400MB两三百个连接以内问题不大。但如果你把它们设成64MB那么100个连接就可能吃掉12.8GB立刻压垮剩余空间。记住一个原则你调的不是“单条查询的速度”而是“整个实例在并发压力下的内存水位”。参数带来的性能收益是非线性的越到后面收益越小内存风险却在直线上升。最后再分享一点实际体会这两个参数我在生产环境里调过很多次最深的一个印象是它们不是万能药但用对了地方效果非常直接。有一次线上报表库的排序查询集中变慢我通过Sort_merge_passes确认瓶颈在磁盘归并把sort_buffer_size从默认值提到8MB后核心查询耗时下降了将近70%。但后来有一次我只是顺手把join_buffer_size也调大结果业务高峰期内存飙升最后又回滚了。那次经历让我养成一个习惯每改一个参数都要知道它对应了哪一类具体问题并且用状态变量或慢日志验证收益找不到问题源头之前参数一个都不动。如果你也能按这个思路来那么join_buffer_size和sort_buffer_size就不再是两个冷冰冰的配置项而是你手里两把真正的“手术刀”。