SQL优化简单思路

1. 背景

在实际生产中,因为SQL较慢、SQL关联不合理、不了解索引的性质、不熟悉mysql执行计划分析,可能会出现一些生产事故,本文会简单说明SQL通常的优化分析思路。

基本的优化原则:

  1. 先优化SQL
  2. 再优化mysql server
  3. 最后优化硬件

2. 优化sql从执行计划开始

执行计划是mysql语句优化中最长使用的方式。相比于开发人员依赖经验、感觉进行优化,explain将会提供精确的分析结论。

其使用非常简单:

sql 复制代码
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 万倍。

显然,小表作为驱动表能大幅减少外层循环次数,降低总计算开销。

伪代码辅助理解:

py 复制代码
#伪代码:嵌套循环连接
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 强制左表为驱动表:
sql 复制代码
-- 强制 A 作为驱动表,B 作为被驱动表
SELECT * FROM A STRAIGHT_JOIN B ON A.id = B.a_id;

2.2 type字段(重要)

type 字段 描述了 表的访问方式(即 MySQL 如何查找表中的行),是判断查询效率的核心指标之一。type 的值从 "性能最优" 到 "性能最差" 有明确的优先级,直接反映了查询对索引的利用效率

type 可能出现的值及含义(按性能从优到劣排序)

  1. system:表中只有一行数据(系统表),是 const 类型的特例,几乎不会在生产中出现
  2. const:通过主键或唯一索引的 等值查询 匹配到一行数据(最多一行),唯一主键且不join时会有
  3. eq_ref:在 多表 JOIN 中,被驱动表的连接列是 主键或唯一非空索引,且每个驱动表的行只能匹配到被驱动表的一行
  4. unique_subquery:子查询中使用了唯一索引,替代了 eq_ref 的 JOIN 方式(适用于 IN 子查询),一般认为和eq_ref效率基本一致
  5. ref:非唯一索引的等值查询,或 JOIN 中被驱动表的连接列是 非唯一索引,可能匹配到多行
  6. ref_or_null:类似 ref,但额外包含对 NULL 值的查询(即条件中包含 IS NULL)
  7. index_merge:MySQL 使用 索引合并优化,即同时使用多个单列索引,将结果合并(交集 / 并集)后返回
  8. range:使用索引进行 范围查询,只检索索引中某一范围内的行,范围越小(匹配行数越少)效率越高。若范围过大(接近全表),可能退化为全表扫描
  9. index:扫描 整个索引树(而非全表),但未利用索引筛选,仅通过索引覆盖数据(即 "索引全扫描")
  10. 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 如何利用索引来定位符合条件的行,是理解索引匹配逻辑的重要依据

  1. 取值为 常量(如 const、具体值),当前表的索引列与常量进行等值匹配 时,ref 会显示为 const 或具体的常量值,说明用 "常量" 与索引列比对
  2. 取值为 其他表的列名:在 多表 JOIN 场景中,当被驱动表的索引列与驱动表的某列进行匹配时,ref 会显示驱动表的 "表名。列名",说明用驱动表的列值与被驱动表的索引列比对。
  3. 取值为 函数或表达式结果:当查询条件中,索引列与函数 / 表达式的结果比对时,ref 会显示函数或表达式的相关信息(不同 MySQL 版本可能显示方式不同)
  4. 取值为 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 值直接反映了查询的性能瓶颈(如是否需要临时表、是否需要额外排序等)。

  1. Using index:使用了覆盖索引(Covering Index),即查询所需的所有列(SELECT 后的列 + WHERE/JOIN 条件的列)都包含在索引中,无需回表访问主键索引获取数据,性能极佳,是索引优化的理想状态,避免了回表的 IO 开销
  2. Using where:MySQL 使用了 WHERE 条件过滤数据,但未使用索引(或虽使用索引但索引未覆盖所有过滤条件,需在服务器层进一步过滤),性能:若 type 为 ALL(全表扫描),则性能较差,需添加索引;若 type 为 range 等,需结合具体场景优化
  3. Using index condition:启用了索引条件下推(Index Condition Pushdown, ICP) 优化,将部分 WHERE 条件下推到存储引擎层,在索引扫描时直接过滤不符合条件的行,减少回表次数
  4. Using temporary:MySQL 需要创建临时表来存储中间结果(如用于 GROUP BY、DISTINCT 或 JOIN 时的排序 / 去重),性能较差,临时表(尤其是磁盘临时表)会增加 IO 和内存开销,数据量大时可能导致严重性能问题。必须优化(如为 GROUP BY 列添加索引)
  5. Using filesort:MySQL 需要对结果进行额外排序(无法利用索引的自然顺序完成排序),排序操作可能在内存或磁盘中进行(数据量大时会写入临时文件),性能较差,排序是 CPU 密集型操作,数据量大时会严重影响性能。需优化(如为 ORDER BY 列添加索引,或调整排序顺序与索引一致)
  6. Using filesort with small result:类似 Using filesort,但排序的结果集很小,排序开销较低(MySQL 认为无需优化)
  7. Using join buffer (Block Nested Loop) 或 Using join buffer (Batched Key Access):JOIN 操作中,被驱动表无可用索引,MySQL 使用连接缓冲区(Join Buffer)存储驱动表的数据,再与被驱动表的行逐行比对(嵌套循环连接),性能极差,无索引的 JOIN 是性能杀手,数据量大时需为连接列添加索引
  8. Range checked for each record (index map: N):MySQL 无法确定哪个索引更高效,因此对驱动表的每一行,都会检查被驱动表的多个索引(index map: N 表示候选索引的位图),选择当前行最匹配的索引。可以尝试手动执行此语句解决:ANALYZE TABLE
  9. Impossible WHERE:WHERE 条件永远为 FALSE,MySQL 无需扫描表(如 WHERE 1=0),性能极高但毫无意义
  10. 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 中,主键索引是聚簇索引(叶子节点直接存储整行数据),性能最优。

执行计划:

json 复制代码
{
"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

其执行计划表达为:

json 复制代码
{
"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,且符合严格递增,那么我们遇到深分页问题时,可以尝试用唯一索引的范围来优化查询,这同样也适用在主键索引为数值的情况:

sql 复制代码
select id from A where id >= xxx and id < yyy and ...

其执行计划表达为:

json 复制代码
{
"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

sql 复制代码
select data from A where create_time >= '2025-09-01 00:00:00' and create_time < '2025-09-02 23:59:59'

这是我们在实际生产中见到的一类写法,这一类写法没有什么大问题,在必须精确到毫秒数的场合一定是这种写法。

其执行计划表现为:

json 复制代码
{
"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更优的表现。

如果是限定时间范围查询,比如某些场合已经预先确定一次查询必然查且只查一个自然日,那么更好的写法是:

sql 复制代码
select data from A where create_time = '2025-09-02'

其执行计划表现为:

json 复制代码
{
"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 最高效的方式

当然是直接等于:

sql 复制代码
select data from A where trace_id = "asdqwdzscqwfcqf"

其执行计划表现为:

json 复制代码
{
"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:

sql 复制代码
select data from A where trace_id like "asdwd%"

其执行计划表现为:

json 复制代码
{
"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 不生效的方式

  1. 两侧like: select data from A where trace_id like "%asdqwd%"
  2. 对列做函数计算:select data from A where len(trace_id) > 100
    以上两种,绝对不要在大数据量、大QPS场合出现必死。

3.5 联合索引

3.5.1 联合索引的建立

联合索引涉及到多个字段,由于mysql基础数据结构的限定,必须要在建立索引时就决定好从左到右的顺序。

以A表为例,如果其包含字段x,y,z,j,k,l,当我们想使用联合索引时,就必须预先分析下面两个问题:

  1. 要用哪些字段加入到联合索引
  2. 这些索引的排序
    这两个问题极其重要,会极大影响索引的过滤效果。
    下面给出一些分析标准来确定哪些字段应当加入联合索引:
  • 区分度极高(如各种ID),且固定出现在where条件里
  • 可以大规模过滤掉非目标数据(如时间段,如各种数值区间)
  • 在条件查询的where中高频出现
  • 绝不参与任何函数计算

假如根据上面的标准,我们筛选出x,z,j三个作为联合索引,那么排序应当按照如下原则从左到右排列:

  • 固定出现的查询条件
  • 高频出现的查询条件
  • 可以过滤更多数据的查询条件
  • 未必会出现的查询条件

比如,z条件必定出现在每次查询中,则z排第一位;x大概率出现,且为id字段,则排第2位;j不长出现,则排第三位,那么创建索引的语句为:

sql 复制代码
create index `udx_z_x_j` on A (z,x,j)

一定要注意,越靠左的索引越重要,使用联合索引查询时,左边的索引一旦缺失,整个查询效率会有巨幅下降。

创建该索引后,只要z,x出现,那么j不出现,也至少可以走两个索引。

3.5.2 联合索引的查询

查询过程务必遵守创建联合索引时的左右顺序,尽最大可能保证建立索引时左边的索引在where中出现:

sql 复制代码
select data from A where z="asdqdqw" and x like "niqdwnu%"

如果没有遵循这个原则,那么从左侧哪一个查询条件开始缺失,就会从哪一个索引开始失效。例如:

sql 复制代码
select data from A where x = 'aedqw'

看起来好像使用了联合索引中的x,但由于最左的z缺失,所以整个联合索引都失效了。

实际SQL:

sql 复制代码
-- test_jpa.test_union_index definition

CREATE 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" and
        task_stage = "STAGE_092"

实际的执行计划为:

json 复制代码
{
"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,就是全表的行数,根本未走索引。

而真正命中索引的效果为:

json 复制代码
{
"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生产中最容易出问题的部分。这里提出如下一些基本思想:

  1. 首先判断是否只能使用join解决问题,而不能使用任何别的手段。只要还有别的更优方案,尽量避开join
  2. 如果一定要使用join,那么inner join优先其他join(内连接取交集,数据量相对小)
  3. 尽最大可能避免join超过2张表(多表情况,mysql执行器会产生优化障碍和驱动表选择障碍)
  4. Join on的字段一定要对两张表都是索引,必须要用等于,比如task_id对于A和B都是索引,那么select data from A inner B where A.task_id=B.taskId
  5. join后的表绝对不要写select *,一是未必需要全字段,二是不同表也许有同名字段,一旦和实体字段映射错了那么查询直接崩溃报错,三是有时候精确查询是可以省略回表的,全查则没有这种可能性
  6. 一定要注意A和B表连接的字段是否是同样的数据类型,比如A中taskId为varchar,B中为BIGINT,那么就会发生隐式转换,这是一种函数计算,会导致索引失效
  7. 一定要使用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 的行:

sql 复制代码
-- 无优化时的执行逻辑(低效)
select * from A Join B on A.id = B.id where A.a > 10 and B.b < 100;

此时子查询会先返回表中所有行,再在外部过滤。

应该改为:

sql 复制代码
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表的时间字段进行排序分页。

那么就可以分出两种情况:

  1. 不存在B表的查询条件
  2. 存在
    针对1情况,结合上面的知识,就可以比较容易的得出优化方案,首先把A表做一次查询和分页,记为小表再inner joinB表,只要A表查询中索引设计合理,该查询的整体效率可以堆到近乎于单表索引查询的程度。
    针对2情况,再针对B不同查询条件出现的可能性,针对性调整驱动表、联合索引等部分。
    这样,通过部分的优化,逐渐逼近整体的最优效果。

6. 涉及分库分表

6.1 选择优良的分库分表键

优良的分库分表键的标准:

  1. 分布均匀:分片键需能将数据尽可能均匀地分散到所有分库分表中,避免某几个库 / 表数据量过大("数据倾斜")或过小(资源浪费)。例:若用 "用户性别" 作为分片键,仅能分为 2 组,必然导致数据倾斜;而用 "用户 ID 哈希" 则可分散到 N 个分片。
  2. 匹配高频查询场景:分片键需与业务中最频繁的查询条件强关联,确保绝大多数查询能通过分片键直接定位到目标库 / 表,避免 "跨库全表扫描"。例:订单查询 90% 场景是 "按用户 ID 查询我的订单",则优先用 "用户 ID" 作为分片键,而非 "订单创建时间"。
  3. 贴合用户需要:统计业务中 80% 以上的查询 SQL,提取WHERE条件中最常出现的字段(如用户 ID、订单 ID、租户 ID)。典型案例:电商订单表中,"用户 ID" 是高频查询字段(用户查自己的订单),适合作为分片键
  4. 实体唯一标识优先:这类字段天然具备 "唯一性" 和 "稳定性",且与业务逻辑强绑定,便于理解和维护,如用户表的user_id、商品表的product_id
  5. 如若必须用多字段组成分库分表键,则建议数据双写,双表查询。例如:通过 "数据冗余"(如订单表按用户 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 选择更合适的硬盘

基本原则:

  1. 固态硬盘 > 机械硬盘
  2. 新一代固态硬盘 > 前代
  3. OLTP 用 PCIE更新的版本
  4. OLAP -> U.2 及以上
  5. 需求均衡 -> SAS 4.0 及以上
相关推荐
启明真纳42 分钟前
PostgreSQL 单库备份
数据库·postgresql
Amd7941 小时前
PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前?
数据库·postgresql
wzg20161 小时前
pyqt5 简易入门教程
开发语言·数据库·qt
你是狒狒吗4 小时前
为什么mysql要有主从复制,主库,从库这种东西
数据库·mysql
倔强的石头1067 小时前
【金仓数据库】ksql 指南(一) 连接本地 KingbaseES 数据库与基础交互
数据库·oracle·kingbasees·金仓数据库·ksql
卷Java11 小时前
违规通知功能修改说明
java·数据库·微信小程序·uni-app
养生技术人12 小时前
Oracle OCP认证考试题目详解082系列第54题
数据库·sql·oracle·运维开发·database·开闭原则·ocp
数据知道13 小时前
Go基础:用Go语言操作MongoDB详解
服务器·开发语言·数据库·后端·mongodb·golang·go语言
爱喝白开水a13 小时前
2025时序数据库选型,从架构基因到AI赋能来解析
开发语言·数据库·人工智能·架构·langchain·transformer·时序数据库