SQL优化简单思路
1. 背景
在实际生产中,因为SQL较慢、SQL关联不合理、不了解索引的性质、不熟悉mysql执行计划分析,可能会出现一些生产事故,本文会简单说明SQL通常的优化分析思路。
基本的优化原则:
- 先优化SQL
- 再优化mysql server
- 最后优化硬件
2. 优化sql从执行计划开始
执行计划是mysql语句优化中最长使用的方式。相比于开发人员依赖经验、感觉进行优化,explain将会提供精确的分析结论。
其使用非常简单:
explain select data from A where id = "asdaqsdqw"
explain后面加上你想要分析的语句,执行即可。
重点在于执行计划结果的分析。
来看一个典型的explain结果:
2.1 table字段
2.1.1 分析
涉及到的表。一般而言,只涉及一张表就只有一行,但涉及多张表如n张表时,则至少有n行,因为多表join的过程可能出现中间临时表,这也是表,虽然不会出现在sql中,但执行时是会计算在执行计划内的。
对于inner join情况,先出现的表是驱动表,后出现的表是被驱动表。
2.1.1.1 驱动表
驱动表(Driving Table) 是作为外层循环的表,被驱动表(Driven Table)是作为内层循环的表。选择合适的驱动表能显著减少连接的总计算量,核心原则是:“小表驱动大表”(用数据量更小的表作为驱动表)
理由:
总操作次数 ≈ 驱动表行数 × 被驱动表中每行的匹配次数。
若驱动表是小表(如 100 行),被驱动表是大表(如 100 万行):总操作次数约为 100 × 平均匹配次数。
若反过来(大表驱动小表):总操作次数约为 100万 × 平均匹配次数,是前者的 1 万倍。
显然,小表作为驱动表能大幅减少外层循环次数,降低总计算开销。
伪代码辅助理解:
#伪代码:嵌套循环连接
for 驱动表中的每一行 a in A:for 被驱动表中匹配 a 的行 b in B:输出 (a, b)
所以,explain的结果,行数越少越好,用于驱动的表规模越小越好。
注意,这里的 “小” 不是指表的原始大小,而是 “经过 WHERE 过滤后参与连接的行数”,比如A表原本有100万数据,但谓词下推过滤后剩30条,那么驱动规模按30计算。
此外,被驱动表中,即使一个字段被加了索引,也很有可能在查询中不生效。
比如:
A表内连接B表,在where子句中使用B表的create_time作为时间范围限制,B.create_time已经加了索引,但为什么这个索引没生效?
因为MySQL 会先扫描 A表 的行,再逐行到 B表 中匹配连接条件(如 A.id = B.a_id)。此时,WHERE B.create_time 的过滤条件可能在 连接之后 执行(即先匹配所有满足连接条件的行,再过滤时间范围),导致索引失效,退化为普通全集扫描。
这个要点在后面线上事故分析时会看到实际案例。
2.1.2 使用
多数数据库(如 MySQL、PostgreSQL、SQL Server)的优化器会根据表的统计信息(行数、索引分布等)自动选择 “成本最低” 的驱动表,无需人工干预。
但在以下场景可能需要手动指定:
- 统计信息过时(如刚批量插入数据未更新统计信息),导致优化器误判表大小(可以执行ANALYZE TABLE Table_name来解决);
- 复杂多表连接(3 张以上),优化器可能因计算复杂度选择次优顺序。
手动指定方式(以 MySQL 为例):
用 STRAIGHT_JOIN 强制左表为驱动表:
-- 强制 A 作为驱动表,B 作为被驱动表
SELECT * FROM A STRAIGHT_JOIN B ON A.id = B.a_id;
2.2 type字段(重要)
type 字段 描述了 表的访问方式(即 MySQL 如何查找表中的行),是判断查询效率的核心指标之一。type 的值从 “性能最优” 到 “性能最差” 有明确的优先级,直接反映了查询对索引的利用效率
type 可能出现的值及含义(按性能从优到劣排序)
- system:表中只有一行数据(系统表),是 const 类型的特例,几乎不会在生产中出现
- const:通过主键或唯一索引的 等值查询 匹配到一行数据(最多一行),唯一主键且不join时会有
- eq_ref:在 多表 JOIN 中,被驱动表的连接列是 主键或唯一非空索引,且每个驱动表的行只能匹配到被驱动表的一行
- unique_subquery:子查询中使用了唯一索引,替代了 eq_ref 的 JOIN 方式(适用于 IN 子查询),一般认为和eq_ref效率基本一致
- ref:非唯一索引的等值查询,或 JOIN 中被驱动表的连接列是 非唯一索引,可能匹配到多行
- ref_or_null:类似 ref,但额外包含对 NULL 值的查询(即条件中包含 IS NULL)
- index_merge:MySQL 使用 索引合并优化,即同时使用多个单列索引,将结果合并(交集 / 并集)后返回
- range:使用索引进行 范围查询,只检索索引中某一范围内的行,范围越小(匹配行数越少)效率越高。若范围过大(接近全表),可能退化为全表扫描
- index:扫描 整个索引树(而非全表),但未利用索引筛选,仅通过索引覆盖数据(即 “索引全扫描”)
- ALL:全表扫描(Full Table Scan),MySQL 会遍历整个表的所有行来查找匹配的记录,最差的情况
目标是至少确保 type 达到 range 级别,最好能达到 ref 或更高。若出现 ALL 或 index,需检查是否缺少索引、索引失效或查询条件不合理,及时优化。
2.3 possible_keys字段
mysql执行计划预先判断某一次查询可能涉及的索引,如果你期望命中的索引没有命中,可以先在这里看你的索引是否被排除在预期之外,并据此优化。
有时mysql的执行计划如果只看keys字段,看起来好像有索引使用,但其实如果把possible_keys和keys连起来看,那么就会发现possible_keys为null时,即使keys有值也不一定走索引。
2.4 key字段(重要)
关键字段,代表实际命中的索引。如果为null则代表没有命中索引。要结合possible_keys字段一起分析。
2.5 ref字段
ref 字段用于描述:在使用索引查找匹配行时,哪些 “值”(可能是列名、常量或函数结果)被用来与索引列进行比较。它直观地展示了 MySQL 如何利用索引来定位符合条件的行,是理解索引匹配逻辑的重要依据
- 取值为 常量(如 const、具体值),当前表的索引列与常量进行等值匹配 时,ref 会显示为 const 或具体的常量值,说明用 “常量” 与索引列比对
- 取值为 其他表的列名:在 多表 JOIN 场景中,当被驱动表的索引列与驱动表的某列进行匹配时,ref 会显示驱动表的 “表名。列名”,说明用驱动表的列值与被驱动表的索引列比对。
- 取值为 函数或表达式结果:当查询条件中,索引列与函数 / 表达式的结果比对时,ref 会显示函数或表达式的相关信息(不同 MySQL 版本可能显示方式不同)
- 取值为 NULL:特殊情况下(如 type 为 index 或 ALL 时),ref 可能为 NULL,表示没有使用 “具体值” 与索引列比对(可能是全索引扫描或全表扫描,无需比对值)
总而言之,这个字段是key字段的延展,用来告诉你你的索引和什么进行比对
2.6 rows字段
rows 表示 MySQL 优化器估计需要扫描的行数(即 “预计要检查多少行才能找到符合条件的记录”),rows 值越小,说明查询需要检查的行数越少,效率通常越高
2.7 filtered字段
filtered 表示经过表级条件过滤后,剩余行所占的百分比(取值范围 0~100),filtered = (满足条件的行数 / 扫描的行数) * 100
2.8 Extra字段(重要)
Extra 字段用于提供查询执行计划的额外细节信息,补充 type、ref 等字段未涵盖的执行逻辑(如索引使用细节、排序方式、临时表使用等)。它是判断查询是否需要优化的关键依据,许多 Extra 值直接反映了查询的性能瓶颈(如是否需要临时表、是否需要额外排序等)。
- Using index:使用了覆盖索引(Covering Index),即查询所需的所有列(SELECT 后的列 + WHERE/JOIN 条件的列)都包含在索引中,无需回表访问主键索引获取数据,性能极佳,是索引优化的理想状态,避免了回表的 IO 开销
- Using where:MySQL 使用了 WHERE 条件过滤数据,但未使用索引(或虽使用索引但索引未覆盖所有过滤条件,需在服务器层进一步过滤),性能:若 type 为 ALL(全表扫描),则性能较差,需添加索引;若 type 为 range 等,需结合具体场景优化
- Using index condition:启用了索引条件下推(Index Condition Pushdown, ICP) 优化,将部分 WHERE 条件下推到存储引擎层,在索引扫描时直接过滤不符合条件的行,减少回表次数
- Using temporary:MySQL 需要创建临时表来存储中间结果(如用于 GROUP BY、DISTINCT 或 JOIN 时的排序 / 去重),性能较差,临时表(尤其是磁盘临时表)会增加 IO 和内存开销,数据量大时可能导致严重性能问题。必须优化(如为 GROUP BY 列添加索引)
- Using filesort:MySQL 需要对结果进行额外排序(无法利用索引的自然顺序完成排序),排序操作可能在内存或磁盘中进行(数据量大时会写入临时文件),性能较差,排序是 CPU 密集型操作,数据量大时会严重影响性能。需优化(如为 ORDER BY 列添加索引,或调整排序顺序与索引一致)
- Using filesort with small result:类似 Using filesort,但排序的结果集很小,排序开销较低(MySQL 认为无需优化)
- Using join buffer (Block Nested Loop) 或 Using join buffer (Batched Key Access):JOIN 操作中,被驱动表无可用索引,MySQL 使用连接缓冲区(Join Buffer)存储驱动表的数据,再与被驱动表的行逐行比对(嵌套循环连接),性能极差,无索引的 JOIN 是性能杀手,数据量大时需为连接列添加索引
- Range checked for each record (index map: N):MySQL 无法确定哪个索引更高效,因此对驱动表的每一行,都会检查被驱动表的多个索引(index map: N 表示候选索引的位图),选择当前行最匹配的索引。可以尝试手动执行此语句解决:ANALYZE TABLE
- Impossible WHERE:WHERE 条件永远为 FALSE,MySQL 无需扫描表(如 WHERE 1=0),性能极高但毫无意义
- No tables used:不涉及表查询,比如select 1+1
总结而言:
- Using temporary:避免临时表,通过索引优化 GROUP BY/DISTINCT;
- Using filesort:添加排序索引,或调整 ORDER BY 顺序与索引一致;
- Using join buffer:为 JOIN 的 ON 条件列添加索引;
- Using where 且 type=ALL:为过滤列添加索引,避免全表扫描。
3. Mysql索引
特别地,任何索引在经过函数计算后都会失效。
3.1 主键索引
唯一标识表中的记录,不允许重复,且不允许为 NULL,每个表只能有一个主键索引。
InnoDB 中,主键索引是聚簇索引(叶子节点直接存储整行数据),性能最优。
执行计划:
{
"explain\r\nselect * from A where id = 1944811872387059713": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "const","possible_keys" : "PRIMARY","key" : "PRIMARY","key_len" : "8","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
3.2 唯一索引
确保索引列的值唯一(允许 NULL,但 NULL 只能出现一次)。Mysql非主键索引中最高效的键,它在建表时表达为UNIQUE,可以唯一定位一条数据。
对于唯一键,建议的使用方式只有一种,就是等于。
举例:
A表的唯一键是id,那么查询时应当:
select data from A where id = xxx
其执行计划表达为:
{
"explain\r\nselect * from service_entity where service_address=\"127.0.0.1\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "service_entity","partitions" : null,"type" : "const","possible_keys" : "UKjety3jc210qsbnuij9yl61nf2","key" : "UKjety3jc210qsbnuij9yl61nf2","key_len" : "1022","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
特别地,如果此唯一索引为int或bigint,且符合严格递增,那么我们遇到深分页问题时,可以尝试用唯一索引的范围来优化查询,这同样也适用在主键索引为数值的情况:
select id from A where id >= xxx and id < yyy and ...
其执行计划表达为:
{
"explain\r\nselect * from test_a where connections > 1 and connections <= 3\r\n\r\n\r\n": [{"id" : 1,"select_type" : "SIMPLE","table" : "test_a","partitions" : null,"type" : "range","possible_keys" : "idx_conn","key" : "idx_conn","key_len" : "5","ref" : null,"rows" : 2,"filtered" : 100.0,"Extra" : "Using where; Using index"}]
}
此处我们可以看到Extra中仍然存在using where,这是因为除了范围没有使用别的索引,如果查询还有其他索引,则还有优化空间
3.3 时间索引
时间索引一般而言使用模式比较固定,例如create_time,大多数场合都是create_time > xxx and create_time < yyy的使用模式。
不过也有一些细节可以注意。
看下面的SQL
select data from A where create_time >= '2025-09-01 00:00:00' and create_time < '2025-09-02 23:59:59'
这是我们在实际生产中见到的一类写法,这一类写法没有什么大问题,在必须精确到毫秒数的场合一定是这种写法。
其执行计划表现为:
{
"explain\r\n select * from A where create_time >= '2025-09-01 00:00:00' and create_time < '2025-09-02 23:59:59'": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "range","possible_keys" : "timer_cre","key" : "timer_cre","key_len" : "4","ref" : null,"rows" : 45534,"filtered" : 100.0,"Extra" : "Using index condition; Using MRR"}
]}
特别地,Extra 列出现 “Using MRR” ,表示优化器启用了 MRR(Multi-Range Read,多范围读取)优化策略。这是 MySQL 针对 “二级索引查询 + 回表” 场景设计的性能优化手段,核心目标是将 “随机 IO” 转化为 “顺序 IO” ,减少磁盘 IO 开销,提升查询效率。总之,是一种比using where更优的表现。
如果是限定时间范围查询,比如某些场合已经预先确定一次查询必然查且只查一个自然日,那么更好的写法是:
select data from A where create_time = '2025-09-02'
其执行计划表现为:
{
"explain\r\nselect * from async_main_task_0 where create_time = \"2025-09-09\"\r\n\r\n\r\n": [{"id" : 1,"select_type" : "SIMPLE","table" : "async_main_task_0","partitions" : null,"type" : "ref","possible_keys" : "timer_cre","key" : "timer_cre","key_len" : "4","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
3.4 字符串索引
这是实际生产中最常见到的一类索引,这一类索引不像数字索引那样可以简单的比较大小,其效率极大地取决于用户的使用方式。
3.4.1 最高效的方式
当然是直接等于:
select data from A where trace_id = "asdqwdzscqwfcqf"
其执行计划表现为:
{
"explain\r\n\r\nselect * from A where task_id = \"47f8fc36febb44f8afb5d08240bf4868\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "ref","possible_keys" : "idx_task_id","key" : "idx_task_id","key_len" : "1022","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
3.4.2 次优的方式
左like:
select data from A where trace_id like "asdwd%"
其执行计划表现为:
{
"explain\r\n\r\nselect * from A where task_id like \"47f8fc36febb44f8afb5d08240bf4%\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "range","possible_keys" : "idx_task_id","key" : "idx_task_id","key_len" : "1022","ref" : null,"rows" : 1,"filtered" : 100.0,"Extra" : "Using index condition"}
]}
3.4.3 不生效的方式
- 两侧like: select data from A where trace_id like “%asdqwd%”
- 对列做函数计算:select data from A where len(trace_id) > 100
以上两种,绝对不要在大数据量、大QPS场合出现必死。
3.5 联合索引
3.5.1 联合索引的建立
联合索引涉及到多个字段,由于mysql基础数据结构的限定,必须要在建立索引时就决定好从左到右的顺序。
以A表为例,如果其包含字段x,y,z,j,k,l,当我们想使用联合索引时,就必须预先分析下面两个问题:
- 要用哪些字段加入到联合索引
- 这些索引的排序
这两个问题极其重要,会极大影响索引的过滤效果。
下面给出一些分析标准来确定哪些字段应当加入联合索引:
- 区分度极高(如各种ID),且固定出现在where条件里
- 可以大规模过滤掉非目标数据(如时间段,如各种数值区间)
- 在条件查询的where中高频出现
- 绝不参与任何函数计算
假如根据上面的标准,我们筛选出x,z,j三个作为联合索引,那么排序应当按照如下原则从左到右排列:
- 固定出现的查询条件
- 高频出现的查询条件
- 可以过滤更多数据的查询条件
- 未必会出现的查询条件
比如,z条件必定出现在每次查询中,则z排第一位;x大概率出现,且为id字段,则排第2位;j不长出现,则排第三位,那么创建索引的语句为:
create index `udx_z_x_j` on A (z,x,j)
一定要注意,越靠左的索引越重要,使用联合索引查询时,左边的索引一旦缺失,整个查询效率会有巨幅下降。
创建该索引后,只要z,x出现,那么j不出现,也至少可以走两个索引。
3.5.2 联合索引的查询
查询过程务必遵守创建联合索引时的左右顺序,尽最大可能保证建立索引时左边的索引在where中出现:
select data from A where z="asdqdqw" and x like "niqdwnu%"
如果没有遵循这个原则,那么从左侧哪一个查询条件开始缺失,就会从哪一个索引开始失效。例如:
select data from A where x = 'aedqw'
看起来好像使用了联合索引中的x,但由于最左的z缺失,所以整个联合索引都失效了。
实际SQL:
-- test_jpa.test_union_index definitionCREATE TABLE `test_union_index` (`id` bigint(20) NOT NULL,`task_type` varchar(128) NOT NULL COMMENT '任务分类:智批改任务,划题任务,直接干预任务等',`task_status` varchar(32) NOT NULL COMMENT '任务状态,INIT:初始化,DOING:进行中,FAILED:失败,SUCCEED:成功',`task_stage` varchar(32) NOT NULL COMMENT '任务阶段,LAYOUT_ANALYSIS:版面分析,GET_TOPIC:取题,RECOGNIZATION:识别,CORRECTION:批改,INTERENTION:干预',PRIMARY KEY (`id`),KEY `udx_type_status_stage` (`task_type`,`task_status`,`task_stage`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;explain
select * from test_union_index
where task_status="STATUS_092" andtask_stage = "STAGE_092"
实际的执行计划为:
{
"explain\r\nselect\r\n\t*\r\nfrom\r\n\ttest_union_index\r\nwhere\r\n\t\r\n\ttask_status=\"STATUS_092\" and\r\n\ttask_stage = \"STAGE_092\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "test_union_index","partitions" : null,"type" : "index","possible_keys" : null,"key" : "udx_type_status_stage","key_len" : "774","ref" : null,"rows" : 10119,"filtered" : 1.0,"Extra" : "Using where; Using index"}
]}
看似type为index好像走了索引,但实际上possible_keys为null,实际扫描的行数10119,就是全表的行数,根本未走索引。
而真正命中索引的效果为:
{
"explain\r\nselect\r\n\t*\r\nfrom\r\n\ttest_union_index\r\nwhere\r\n\t\r\n\ttask_type=\"TYPE_092\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "test_union_index","partitions" : null,"type" : "ref","possible_keys" : "udx_type_status_stage","key" : "udx_type_status_stage","key_len" : "514","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : "Using index"}
]}
3.5.3 合理使用联合索引避免回表
mysql中同一个索引涉及到的内容维护在同一棵索引树,如果要查询的字段包含在索引里且索引生效,那么就可以避免查询到索引后还需要回查真实数据。
3.6 多种索引冲突
有时SQL太过于复杂,join的表太多,索引之间会有冲突,未必真就会命中预期的索引。这种时候需要从未加索引的状态开始,一点点加索引做测试,千万不要偷懒。
4. 跨表JOIN问题
4.1 使用join的基本原则
这是整个Mysql生产中最容易出问题的部分。这里提出如下一些基本思想:
- 首先判断是否只能使用join解决问题,而不能使用任何别的手段。只要还有别的更优方案,尽量避开join
- 如果一定要使用join,那么inner join优先其他join(内连接取交集,数据量相对小)
- 尽最大可能避免join超过2张表(多表情况,mysql执行器会产生优化障碍和驱动表选择障碍)
- Join on的字段一定要对两张表都是索引,必须要用等于,比如task_id对于A和B都是索引,那么select data from A inner B where A.task_id=B.taskId
- join后的表绝对不要写select *,一是未必需要全字段,二是不同表也许有同名字段,一旦和实体字段映射错了那么查询直接崩溃报错,三是有时候精确查询是可以省略回表的,全查则没有这种可能性
- 一定要注意A和B表连接的字段是否是同样的数据类型,比如A中taskId为varchar,B中为BIGINT,那么就会发生隐式转换,这是一种函数计算,会导致索引失效
- 一定要使用explain分析执行计划,多方比较,直到explain的所有行都走索引、Extra字段中几乎没有using temporary、using where情况才算优化完成
4.2 谓词下推
在数据库查询优化中,谓词下推(Predicate Pushdown) 是一种通过将过滤条件(谓词)尽可能提前应用来减少数据处理量的优化技术。其核心思想是:让数据在进入后续处理阶段(如连接、排序、聚合等)之前,就被过滤掉不需要的部分,从而降低整体计算和 IO 开销。
4.2.1 谓词
指查询中用于筛选数据的条件,通常出现在 WHERE、ON 或 HAVING 子句中,例如 age > 30、status = ‘active’ 等
4.2.2 下推
指将这些过滤条件从查询的 “上层”(如外层查询、聚合阶段)“推” 到 “下层”(如子查询、表扫描阶段、数据源读取阶段)执行,让过滤操作在数据处理的早期阶段完成
4.2.3 为什么需要谓词下推
没有谓词下推时,查询可能会先读取大量无关数据,再在后续阶段过滤,导致资源浪费。例如:
假设查询是 A和B join并查询A.a大于10,B.b小于100 的行:
-- 无优化时的执行逻辑(低效)
select * from A Join B on A.id = B.id where A.a > 10 and B.b < 100;
此时子查询会先返回表中所有行,再在外部过滤。
应该改为:
select * from (select * from A where a>10)A1 Join (select * from B where b<100)B1 on A1.id = B1.id;
4.3 不使用join的一些解决思路
4.3.1 从复杂join更替为多个简单查询,在代码中筛选
有时候一条语句实在太过复杂,没有优化的思路,那么也许可以试试把复杂sql拆成多个简单查询。
比如A和B和C表关联查询,A表的join字段需要做函数计算导致索引失效。
也许可以尝试先查一下A表,如果规模并不大,比如结果集只有几十条上百条,那不妨先查A,而后在代码中对A的结果做你想要做的函数计算,再把计算结果作为过滤条件参与到B和C的关联查询中。
4.3.2 将join改为where中的exists
比如需要查询A表中符合B表某些条件的数据,如果用join则涉及到关联,但如果替换为exists,mysql会自动优化为单表查询,直接优化掉一整张表的扫描,这比任何索引优化都强。
4.3.3 合理冗余字段
如果存储数据时预先考虑到join可能出现的情况,那么设计表时可以考虑冗余一些字段,比如原本要AjoinB来查询B中的一两个字段,那么有没有可能直接在A中添加这两个字段来避免JOIN呢?
现在随着固态硬盘的发展和降价,少量空间上的冗余并不耗费多少成本,但慢SQL一旦出现线上问题,损失可能比存储贵得多。
5. 分治策略
对于特别复杂的查询情况,涉及多表多条件,不妨试试看用分治的思想来应对。
核心思路是把一个统一大查询拆分成几种不同的分支情况,在代码里决定不同情况走不同查询,而后针对不同的查询专项优化。
比如某种查询,涉及到AB表关联查询,B表查询条件视情况发生变化,未必出现,最终的结果要按照A表的时间字段进行排序分页。
那么就可以分出两种情况:
- 不存在B表的查询条件
- 存在
针对1情况,结合上面的知识,就可以比较容易的得出优化方案,首先把A表做一次查询和分页,记为小表再inner joinB表,只要A表查询中索引设计合理,该查询的整体效率可以堆到近乎于单表索引查询的程度。
针对2情况,再针对B不同查询条件出现的可能性,针对性调整驱动表、联合索引等部分。
这样,通过部分的优化,逐渐逼近整体的最优效果。
6. 涉及分库分表
6.1 选择优良的分库分表键
优良的分库分表键的标准:
- 分布均匀:分片键需能将数据尽可能均匀地分散到所有分库分表中,避免某几个库 / 表数据量过大(“数据倾斜”)或过小(资源浪费)。例:若用 “用户性别” 作为分片键,仅能分为 2 组,必然导致数据倾斜;而用 “用户 ID 哈希” 则可分散到 N 个分片。
- 匹配高频查询场景:分片键需与业务中最频繁的查询条件强关联,确保绝大多数查询能通过分片键直接定位到目标库 / 表,避免 “跨库全表扫描”。例:订单查询 90% 场景是 “按用户 ID 查询我的订单”,则优先用 “用户 ID” 作为分片键,而非 “订单创建时间”。
- 贴合用户需要:统计业务中 80% 以上的查询 SQL,提取WHERE条件中最常出现的字段(如用户 ID、订单 ID、租户 ID)。典型案例:电商订单表中,“用户 ID” 是高频查询字段(用户查自己的订单),适合作为分片键
- 实体唯一标识优先:这类字段天然具备 “唯一性” 和 “稳定性”,且与业务逻辑强绑定,便于理解和维护,如用户表的user_id、商品表的product_id
- 如若必须用多字段组成分库分表键,则建议数据双写,双表查询。例如:通过 “数据冗余”(如订单表按用户 ID 分片,同时冗余一份按时间分片的表)实现。理由:组合分片键会增加查询复杂度,且难以保证所有查询都能匹配组合条件,容易引发跨库表查询
6.2 确保查询携带分库分表键
如果热点查询没有携带分库分表键,会触发跨库跨表全扫描,这个操作的性能开销是线上高并发业务不可承受的
7. mysql服务端的配置(DBA操作部分)
有时查询语句本身囿于业务强需求已经没有太多优化空间,那么考虑调优服务端配置也是一种思路。
需要注意,这种调整相对复杂,影响比较广,一定要充分和DBA协商清楚;如果数据库存在多业务复用,那么所有相关业务方都需要纳入讨论。
7.1 InnoDB专项优化
7.1.1 InnoDB 缓冲池(innodb_buffer_pool_size)
缓存 InnoDB 表的数据、索引、插入缓冲等,是 MySQL 中最重要的内存配置。命中缓冲池的查询可直接从内存获取数据,无需读磁盘。
对于专用 MySQL 服务器,设置为系统总内存的 50%~70%(预留部分内存给操作系统和其他进程)
7.1.2 日志相关配置
- innodb_log_buffer_size:事务日志(redo log)的内存缓冲区,满了会刷盘。
- innodb_flush_log_at_trx_commit:控制事务日志的刷盘策略(平衡性能与安全性)。
- 1(默认):每次事务提交立即刷盘,最安全但性能最低(适合金融等核心业务)。
- 2:事务提交后写入操作系统缓存,每秒刷盘一次,安全性与性能平衡(推荐)。
- 0:每秒刷盘一次,性能最高但可能丢失 1 秒内的数据(适合非核心业务)。
- innodb_log_file_size:单个 redo log 文件的大小。可酌情提高至 512M~2G(需同时调整innodb_log_files_in_group,通常 2~3 个文件),减少日志切换频率(频繁切换会导致 IO 波动)。
7.1.3 IO 并发配置
- innodb_read_io_threads 和 innodb_write_io_threads:控制 InnoDB 的读写 IO 线程数,提升 IO 并发处理能力。
- innodb_flush_method:控制 InnoDB 与磁盘 IO 的交互方式(减少双重缓存)。建议:Linux 系统:O_DIRECT(绕过操作系统缓存,避免 InnoDB 缓冲池与 OS 缓存的重复缓存)。Windows 系统:async_unbuffered。
7.2 连接与排序缓存
- join_buffer_size:用于表连接(JOIN)的缓存,非索引连接(ALL或index类型)会使用此缓存。
- sort_buffer_size:用于ORDER BY或GROUP BY的排序缓存,不足时会使用磁盘临时文件排序。
- read_buffer_size:用于顺序扫描(filesort)的缓存,提升全表扫描或大范围查询的效率。
7.3 临时表缓存
tmp_table_size 和 max_heap_table_size:控制内存临时表的最大大小(两者取较小值),超过则转为磁盘临时表(存储在tmpdir目录)。
默认值 16M,可酌情提高至 64M~256M(根据业务中临时表的使用频率调整),减少磁盘临时表的 IO 开销。
8. 硬件上的优化
有时软件上的优化已经没有更多空间,这时可以考虑从硬件层面下手把查询效率带上来。
需要注意,并非所有云厂商都可以指定mysql服务器的硬件,这一点需要预先确认是否可用以及预算是否支持。
8.1 选择更合适的CPU
8.1.1 Intel 至强 6 性能核处理器(OLTP 首选)
代表型号:Platinum 6787P(56 核 112 线程,全核睿频 3.5GHz,L3 缓存 140MB)
核心优势:
单核性能突出:全核睿频 3.5GHz,配合 Intel AVX-512 指令集,单线程执行效率比上一代提升 36%,尤其适合高并发事务中的单行查询、锁竞争场景。
硬件加速技术:内置数据保护与压缩加速技术(QAT),可将 SSL 加密和数据压缩卸载至硬件,减少 CPU 占用率 20%-30%。例如,MySQL 备份速度可提升 2.56 倍。
内存带宽优化:支持 8 通道 DDR5-6400 内存,带宽高达 204.8GB/s,比上一代提升 2.3 倍,显著缓解内存瓶颈。
服务器整合能力:单台服务器可替代 17 台旧代服务器,TCO 降低 87%,适合企业级集中式数据库部署。
典型场景:电商秒杀、金融交易、实时订单系统等需毫秒级响应的 OLTP 场景。
8.1.2 AMD EPYC 9004 系列(OLAP 首选)
代表型号:Genoa-X 9384X(48 核 96 线程,主频 2.4GHz,L3 缓存 1.152TB)
核心优势:
3D V-Cache 技术:堆叠式 L3 缓存容量达 1.152TB,是传统 CPU 的 3 倍,可将复杂分析查询的内存访问延迟降低 50% 以上,尤其适合需频繁扫描大表的 OLAP 场景。
多核并行处理:64 核 / 128 线程设计,配合 Zen4 架构的高 IPC(每周期指令数),在 sysbench 测试中 OLAP 性能比 Intel 同核数 CPU 提升 27%。
高扩展性:支持 12 通道 DDR5-4800 内存和 128 条 PCIe 5.0 通道,可直连高速 NVMe 存储或 GPU 加速卡,构建分析型数据湖。
能效比领先:5nm 工艺使功耗比上一代降低 30%,在高密度数据中心中每瓦性能比 Intel 高 60%。
典型场景:供应链分析、用户行为画像、金融风控等需多表关联和复杂聚合的 OLAP 场景。
8.2 选择更合适的硬盘
基本原则:
- 固态硬盘 > 机械硬盘
- 新一代固态硬盘 > 前代
- OLTP 用 PCIE更新的版本
- OLAP -> U.2 及以上
- 需求均衡 -> SAS 4.0 及以上