MySQL全方位优化方案
一.表设计
数据库表设计是整个MySQL调优的根基!它直接决定了后续索引、查询、存储优化的上限。数据库表设计需要注意这样几个方面:数据类型的选择、表结构、主键/约束三个方面。
数据类型的选择,应尽量精准紧凑的选择,目的是一行数据占用更小的空间,让一个数据页存储更多的数据,减少IO操作。
- 数值类型,应该尽量选择满足条件的情况体积小、无符号的数据类型
| 类型 | 字节大小 | 有符号取值范围 | 无符号取值范围 | 案例 |
|---|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 0 ~ 255 | 年龄、状态(0-9)、性别 |
| SMALLINT | 2 | -32768 ~ 32767 | 0 ~ 65535 | 数量(如商品库存) |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 0 ~ 16777215 | 中等规模 ID(如万级订单) |
| INT(INTEGER) | 4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | 常规 ID(如百万级用户) |
| BIGINT | 8 | 9e18 ~ 9e18 | 0 ~ 1.8e19 | 超大 ID(如亿级订单) |
| FLOAT | 4 | 内容6 | 非精准小数(如温度) | |
| DOUBLE | 8 | 内容6 | 非精准大数(如距离) | |
| DECIMAL(M,D) | 可变 | 内容6 | 金额、税率(需精准计算) |
- 字符串类型需要区分固定长度还是变长
固长用CHAR、严控VARCHAR,慎用TEXT/BLOB
| 类型 | 存储方式 | 占用空间 | 适用场景 | 性能特点 |
|---|---|---|---|---|
| CHAR(N) | 固定长度,不足补空格 | 始终占 N 字节 | 固定长度字符串(手机号、身份证) | 查询快(无需计算长度) |
| VARCHAR(N) | 变长,存储长度 + 实际内容 | 1-2 字节 | (长度)+ 实际内容 变长字符串(用户名、标题) | 节省空间,查询略慢 |
| TEXT | 溢出存储(数据页外) | 1-4 字节 | (指针)+ 实际内容 超长文本(商品描述、日志) | 查询慢(额外 IO) |
| BLOB | 二进制溢出存储 | 同 TEXT | 二进制数据(图片、文件) | 性能最差,尽量不用 |
- 日期类型优先原生类型,避免用字符串
| 类型 | 占用字节 | 取值范围 | 时区特性 | 适用场景 |
|---|---|---|---|---|
| DATE | 3 | 1000-01-01 ~ 9999-12-31 | 无时区 | 仅需日期(如生日) |
| TIME | 3 | -838:59:59 ~ 838:59:59 | 无时区 | 仅需时间(如打卡时间) |
| DATETIME | 8 | 1000-01-01 ~ 9999-12-31 | 无时区 | 业务时间(如订单创建时间) |
| TIMESTAMP | 4 | 1970-01-01 ~ 2038-01-19 | 随时区自动转换 | 更新时间(如update_time) |
| YEAR | 1 | 1901 ~ 2155 | 无时区 | 仅需年份(如统计年份) |
- 特殊类型:枚举、集合、JSON,这些类型由于性能、业务灵活性、开发运维成本、兼容性等方面存在硬伤,所以实际情况根本不用。
- 枚举类型适合互斥的固定值,如性别、订单状态等,占1-2个字节,实际情况是TINYINT+枚举类代替
- 集合类型存储互斥多值,如用户标签(阅读、点赞、评论),占1-8字节,一般用INT+位运算代替,少数用关联表
- JSON类型,代替TEXT存储,比如用户偏好、商品规则等,一般是拆成结构化的数据或者用非关系型数据库存储(MySQL只存储非关系型数据库的ID),
数据类型选得对,存储少一半,查询快一倍。
表的设计方面,应避免大表,单表字段最好不多余20个,避免NULL值,再加上一点点反范式的设计。
主键约束方面,要尽量"小"且自增,保证高效唯一。
二.索引(重中之重)
索引的作用与内存(buffer pool)与CPU之间,其目的就是为了用尽可能少的逻辑读,快速定位数据,其本质就是空间换时间。
要想彻底搞懂索引,必须从MySQL的底层数据结构和存储的方式入手,下面有这样几个概念,便于更深刻的理解索引,在遇到慢SQL的时候就能找到解决思路。
- MySQL是以B+树索引+数据页的方式存储数据,且每次创建一个索引就会形成一颗B+索引树,索引的构建是消耗性能和存储空间的;
- 物理读指的是将数据从磁盘读取到内存(buffer pool)中,逻辑读是指CPU从内存中读取数据页、解析筛选数据的全过程;
- 索引不是"想当然",用不用的到索引跟数据分布和是否回表关系很大,MySQL优化器根据成本计算做出决策。
- 最左匹配原则:针对联合索引,MySQL优先匹配最左侧列,只有当最左侧列索引被利用后右侧索引才会依次被利用。
- 前缀有序性:MySQL的字符串索引,底层B+树是字符串的字符前缀有序排列的,可以理解为 order by 第一个字符,第二个字符,第三个字符,......
MySQL索引失效的底层根源有两大核心索引有序性被破坏 和优化器成本计算划不划算 ,说白了就是能不能用上索引和用索引划不划算。所谓有序性被破坏就是无法通过B+树的二分查找定位连续的索引区间。所谓划不划算就是走索引的成本是否低于全表扫描;
如果再细分一下,我将失效原因分为五大类:
- 索引被操作过,破坏了索引的有序性(用不到索引),这类问题本质原因是B+树中存储是索引的原值,相当于把有序的原值经过一系列操作后变成了无序的操作后的值;
- 进行函数计算,如 where age+1 = 20
- 算数计算 如 where YEAR(create_time) = 2024
- 隐式转换 如 where phone = 1888888888
- 列对比列计算 如 where col1+col2=col3等。
- 匹配规则不兼容,无法定位连续的索引区间(用不到索引或不能充分用到索引)
- 常见的有不符合前缀有序性 如 like '%XXX'、
- 不符合最左匹配原则 如 有索引 idx_a_b,条件中只使用 where b = xxx,记住如果最左侧列索引没用到,后面的也不会起作用;
- 符合最左匹配原则但被中断::这种情况不会导致复合索引完全失效,如有 idx_a_b,条件中使用 where a > 20 and b = 10;
- 索引设计的缺陷,索引本身无法被利用
- 索引列的离散型太低,如性别列使用索引
- 索引列操作频繁,不会影响索引的使用,但修改的时候会频繁的构建索引,非常消耗性能
- 特殊的语法或规则
- 使用 OR 连接低效条件:如where name='张三' OR age is not null,这样age的低效条件导致无法用到索引,可以改成 union,保证name索引被用到;
- 使用 NOT IN(极端场景):NOT IN (1,2,3)(等价于<1 OR >3,离散区间,成本高于全表);
- 子查询嵌套过深:IN (SELECT ... FROM ... WHERE ...)(优化器无法准确预估子查询结果集,默认放弃索引);
- 使用 LIMIT 但筛选性差:SELECT * FROM user WHERE age>0 LIMIT 10(age>0 筛选性差,优化器直接全表取前 10 行);
- 成本计算后,走索引查询不如全表扫描。不是索引不能用而是用了更慢,是优化器主动放弃索引的表现;
- 连续性低,B+树的特点是定位连续区间或单点位置高效,如果你查询的内容在多个区间内需要多次索引扫描+合并数据,成本高,可能不用索引;
- 筛选性能极差的条件,比如 age>0,几乎是查全表的数据,有索引也不会用;
- 数据倾斜分配,比如 is null 条件,null值占比90%,还是会全表扫描;
- 小表全扫描,当表中数据量极少的时候,走索引的IO开销比直接读取慢;
- 回表成本高:这种情况就是没有覆盖索引,且查出的数据量巨大,导致成本太高;
索引优化绝对不是一蹴而就的,要结合数据分布、业务场景、查询条件等等等等。上面的情况只是冰山一角,实际情况的索引优化复杂很多;
最后送大家一个口诀:
「前缀要有序,最左要匹配;
列不做加工,区间少合并;
子查尽量避,NOT IN 少用;
成本算清楚,索引不失效」
三.代码
代码层面的优化两个核心原则:尽量让查询走索引,避免全表扫描;索引的核心作用是快速定位数据,而不是直接提供查询结果;
查询优化
- 禁止使用SELECT * ,原因是会查询出不需要的数据,浪费磁盘的IO、内存资源,另外无法覆盖索引,会有回表操作;增加网络传输的开销;
- 避免使用索引失效的高频写法,如:
- where条件中对索引字段进行函数处理、运算处理或者隐式转换;
- 模糊查询以%开头;
- where条件中使用 or 链接索引和非索引字段,而用union代替;
- where条件中使用不等号条件、使用is null或is not null条件;
- 联合索引不遵循最左匹配原则;
- in或者not in条件中使用大量值;
- 使用子查询嵌套,而不用join替代;
- 偏移量过大的分页优化:
- 排序优化,核心原则是避免文件排序(filesort),优化的原则是让排序字段和索引字段一致,利用索引进行排序;
插入优化
- 批量查询优先使用多行insert代替多条insert;
- 批量插入是,按需关闭自动提交和唯一性校验
- 避免逐条更新,优先使用insert...on duplicate key update,如 insert into user (id,age,name) values (1,18,"小明") on duplicate key update name = values(name),age=values(age);
更新/删除优化
核心原则是where条件命中索引,批量更新/删除必须分批操作;避免索引字段更新,因为维护索引需要消耗大量性能;
最后补充一下,SQL不是凭感觉写,必须通过SQL性能分析工具的SQL才能上生产环境!
四.硬件
硬件层面的优化是MySQL性能优化的基石,所有上层的优化的本质都是为了减少资源的消耗,让硬件能力被MySQL的底层机制最大化的利用。
影响MySQL速度的硬件主要有四个:硬盘、内存、CPU、网络。下面从这四个方面分析它对MySQL的影响;
内存
MySQL是磁盘中存储,内存中计算,内存优先,磁盘兜底的数据库。内存是MySQL的核心载体,磁盘只做数据持久化。内存的大小/配置/命中率,直接影响MySQL的性能。MySQL的InnoDB存储引擎中有个核心组件BufferPool ,
它是用来缓存磁盘上的数据页和索引页,MySQL永远是先操作BufferPool中的数据,当BufferPool中没有要操作的数据时候,才会进行IO操作从磁盘重新读取数据,内存的大小直接决定了BufferPool的大小。理论上来说,只要
内存足够大,BufferPool就可以足够大,能够一下子将磁盘中所有的数据加载到BufferPool中,这样就避免了IO操作,效率得到显著提高。从硬件层面来讲,当内存不能将磁盘中的数据一下子加载到内存中的时候,提高内存大小是
提高MySQL性能最有效的方法。 另外内存的速度(频率/带宽)跟CPU不是一个量级的,因此,提高内存的速度也更能适配CPU的速度,进而提高MySQL效率
内存瓶颈的表现是:BufferPool命中率低于95%、vmstat 命令中si/so(内存交换)数值持续不为0、磁盘I/O使用率过高。
CPU
MySQL是IO密集型数据库,而非CPU密集型数据库 ,在大多数场景下,只要你SQL优化的到位 那么CPU大多是都是等待的一方,而非被榨干的一方。所以一般情况下,CPU都不是木桶效应中的短板。因此,在内存没有达到瓶颈的时候提高CPU的性能效果是不明显的。
另外需要了解的是在高并发短事务的场景下,CPU的核心数目影响大,在复杂查询/大事务的场景下,CPU的主频影响大.
CPU瓶颈的表现是:CPU使用率持续90%以上、top命令中%sys(内核态 CPU 占用)过高、SQL 执行时间长但磁盘/内存无压力。
磁盘
磁盘只在MySQL的BufferPool加载磁盘数据的时候影响速度,一般是第一次查询或者BufferPool不能一次性加载磁盘中的所有数据导致之前加载进入的数据被清除。因此,只要内存足够大,磁盘只会影响第一次查询的速度,可以通过数据预热解决这个问题。
磁盘瓶颈表现:iostat命令中%util(磁盘利用率)持续100%、rMB/s/wMB/s 达到磁盘带宽上限、SQL执行中Data file read等待事件频繁。
网络
网络是MySQL客户端与服务端、主从复制、集群之间数据传输的通道,其带宽、延迟、稳定性会影响远程访问效率和分布式部署性能。
网络瓶颈表现:iftop命令显示网络带宽占满、ping测试延迟过高或丢包、主从复制延迟持续增大、客户端连接超时频繁。
总结:进行硬件优化的前提是,在上面的其他方面的优化都已经做到极致(慢查询数量无明显下降、业务响应时间仍高于预期,且数据库的资源使用率已接近硬件上限)。比如:
- 索引优化:已经为高频查询、排序、join操作建立合理索引,避免索引冗余、无效索引且无索引失效情况等;
- SQL语句优化:优化慢查询(避免全表扫描、大表join、子查询转join),减少不必要的排序、聚合操作等;
- 数据库参数优化:合理配置了innodb_buffer_pool_size、innodb_log_file_size、max_connections等核心参数;
- 架构优化:读写分离、分库分表、缓存(如Redis)等;
五.架构设计
索引、表设计都是针对单例、单表方面的优化,而架构设计是从整体层面,突破单例、单表的性能的上限,解决高并发、大数据量、高可用的核心问题!
首先明确两个名词TPS(Transactions Per Second)指每秒可支持的事务数量;QPS(Query Per Second)指每秒处理请求的数量。架构设计的初心是:高可用,高性能、高拓展性。
- 高可用是指避免单点故障。
- 高性能是指通过提高读写能力减少存储压力,让数据库可以支持更高的TPS和QPS。
- 高拓展性是指可以支持数据量和并发量线下增长时候,无需重构核心架构。
数据库存储架构经历了单实例、主从复制、分库分表、分布式的演化过程,接下来从架构的演化过程,分析每个阶段的优化方案,以深入了解架构化的思维。
单实例,使用场景:数据量 < 1000W & QPS < 10W
这部分的优化基本就是前面说的从表设计、索引、代码、硬件这几个方面进行优化,不必过多赘述。
主从复制,使用场景:读多写少(读/写 > 80%)& QPS < 50W
- 什么是主从复制
所谓主从复制就是主数据库只负责写操作即新增、更新、删除操作,从数据库复制读操作即查询操作,一般的架构是一主多从,通过读写分离分流数据库读压力。可通过增加从数据库的数量线性提升读的性能,扩展性强。
- 需要解决的问题
- 写操作的压力仍然在一个数据库,无法突破单数据库的写性能上限;
- 另外本质还是每个库都是存放全量数据,存储的数据量有限;
- 存在数据一致性问题;
- 存在高可用问题;
- 需要读写分离路由
- 还有主从库之间同步数据有延迟
- 如何解决问题
- 第1.2个问题是主从架构的局限性,是由架构决定的,无法解决;
- 读写分离路由有两个方法,第一就是在代码层面解决:写操作走主库 ,读操作随机走从库,这样代码耦合高,一般不用,而是用第二个方法中间件,如MyCat、Sharding-JDBC、ProxySQL等,解耦业务与数据库,这些插件不但能解决路由问题还能解决高可用问题,有类似于Redis哨兵的解决方案;
- 解决数据一致性问题有这么几个方案,核心逻辑的读请求走主库,这样不会有延迟;从库同步延迟监控,超过阈值自动切主库查询;使用半同步复制的复制模式减少延迟;
分库分表,可支撑百万级别的QPS
一般情况下当单库数据量超过100G、单表超过1亿行或主库写的QPS超过10W,主从架构也无法满足,需要通过分库分表拆分数据,突破单库和单表的存储性能。分库分表可以从拆分维度分为垂直拆分 和水平拆分 ;而从拆分的对象来说,库 和表都能拆分。
- 垂直拆分
- 垂直分库:拆分的对象是数据库表,按照业务模块、字段属性将表拆分到不同的数据库中,不改变数据行数,如:电商系统中将用户信息、订单信息、商品信息等拆分到不同的数据库中。
- 垂直分表:拆分对象是列字段,将一个表中的列按照业务或字段属性拆分到两个或多个表中,如订单表中,将订单号、商品ID等常用字段与大字段如订单详情JSON拆分为主表和拓展表。
- 垂直拆分的适用场景是:单库压力过大且业务个模块边界清晰;单表字段过多且有大字段列拖慢查询性能;核心业务与非核心业务隔离;
- 水平拆分
- 水平拆分库:本质是将表中的数据行按照规则分配的不同的库中,表结构一样,仅数据数据分片不同;
- 水平分表:将一个表拆分为多个表,比如Table_0、Table-1、......,每个表结构一样,仅数据分片不同;
- 水平拆分时,当单个实例的性能足够用时,优先拆分表。当单个实例的CPU、IO、连接数不足时,单库性能达到瓶颈,再做分库操作。总结一下就是先解决表容量问题,再解决单例瓶颈问题
总结一下:
- 垂直分表解决的是单表查询的压力
- 垂直分库解决的是单实例的压力
- 水平分表解决是单表的压力
- 水平分即解决单表压力也解决单实例压力
这部分是比较复杂的,后面会整理一份各种拆分情况的难点和解决方案以及适用场景。
云原生或者分布式数据库,可支撑千万级别的QPS
对于大多数开发人来说,很难接触到这两种数据库,即使你是大厂开发,用到了这两种数据库,你也只是停留在应用的层面,仅通过JDBC、ORM框架调用数据库;底层分库分表、主从切换、弹性扩容完全透明;几乎无需关注数据库类型。仅做了解即可。
简单了解一下市场上主流的分布式关系型数据库TiDB、OceanBase(阿里自研)、CockroachDB 、TDSQL-C(腾讯云)