MySQL全方位优化方案

MySQL全方位优化方案

一.表设计

数据库表设计是整个MySQL调优的根基!它直接决定了后续索引、查询、存储优化的上限。数据库表设计需要注意这样几个方面:数据类型的选择、表结构、主键/约束三个方面。

数据类型的选择,应尽量精准紧凑的选择,目的是一行数据占用更小的空间,让一个数据页存储更多的数据,减少IO操作。
  1. 数值类型,应该尽量选择满足条件的情况体积小、无符号的数据类型
类型 字节大小 有符号取值范围 无符号取值范围 案例
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 金额、税率(需精准计算)
  1. 字符串类型需要区分固定长度还是变长

固长用CHAR、严控VARCHAR,慎用TEXT/BLOB

类型 存储方式 占用空间 适用场景 性能特点
CHAR(N) 固定长度,不足补空格 始终占 N 字节 固定长度字符串(手机号、身份证) 查询快(无需计算长度)
VARCHAR(N) 变长,存储长度 + 实际内容 1-2 字节 (长度)+ 实际内容 变长字符串(用户名、标题) 节省空间,查询略慢
TEXT 溢出存储(数据页外) 1-4 字节 (指针)+ 实际内容 超长文本(商品描述、日志) 查询慢(额外 IO)
BLOB 二进制溢出存储 同 TEXT 二进制数据(图片、文件) 性能最差,尽量不用
  1. 日期类型优先原生类型,避免用字符串
类型 占用字节 取值范围 时区特性 适用场景
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 无时区 仅需年份(如统计年份)
  1. 特殊类型:枚举、集合、JSON,这些类型由于性能、业务灵活性、开发运维成本、兼容性等方面存在硬伤,所以实际情况根本不用。
  1. 枚举类型适合互斥的固定值,如性别、订单状态等,占1-2个字节,实际情况是TINYINT+枚举类代替
  2. 集合类型存储互斥多值,如用户标签(阅读、点赞、评论),占1-8字节,一般用INT+位运算代替,少数用关联表
  3. JSON类型,代替TEXT存储,比如用户偏好、商品规则等,一般是拆成结构化的数据或者用非关系型数据库存储(MySQL只存储非关系型数据库的ID),

数据类型选得对,存储少一半,查询快一倍。

表的设计方面,应避免大表,单表字段最好不多余20个,避免NULL值,再加上一点点反范式的设计。
主键约束方面,要尽量"小"且自增,保证高效唯一。

二.索引(重中之重)

索引的作用与内存(buffer pool)与CPU之间,其目的就是为了用尽可能少的逻辑读,快速定位数据,其本质就是空间换时间

要想彻底搞懂索引,必须从MySQL的底层数据结构和存储的方式入手,下面有这样几个概念,便于更深刻的理解索引,在遇到慢SQL的时候就能找到解决思路。

  1. MySQL是以B+树索引+数据页的方式存储数据,且每次创建一个索引就会形成一颗B+索引树,索引的构建是消耗性能和存储空间的;
  2. 物理读指的是将数据从磁盘读取到内存(buffer pool)中,逻辑读是指CPU从内存中读取数据页、解析筛选数据的全过程;
  3. 索引不是"想当然",用不用的到索引跟数据分布和是否回表关系很大,MySQL优化器根据成本计算做出决策。
  4. 最左匹配原则:针对联合索引,MySQL优先匹配最左侧列,只有当最左侧列索引被利用后右侧索引才会依次被利用。
  5. 前缀有序性:MySQL的字符串索引,底层B+树是字符串的字符前缀有序排列的,可以理解为 order by 第一个字符,第二个字符,第三个字符,......

MySQL索引失效的底层根源有两大核心索引有序性被破坏优化器成本计算划不划算 ,说白了就是能不能用上索引和用索引划不划算。所谓有序性被破坏就是无法通过B+树的二分查找定位连续的索引区间。所谓划不划算就是走索引的成本是否低于全表扫描;

如果再细分一下,我将失效原因分为五大类:

  1. 索引被操作过,破坏了索引的有序性(用不到索引),这类问题本质原因是B+树中存储是索引的原值,相当于把有序的原值经过一系列操作后变成了无序的操作后的值;
  1. 进行函数计算,如 where age+1 = 20
  2. 算数计算 如 where YEAR(create_time) = 2024
  3. 隐式转换 如 where phone = 1888888888
  4. 列对比列计算 如 where col1+col2=col3等。
  1. 匹配规则不兼容,无法定位连续的索引区间(用不到索引或不能充分用到索引)
  1. 常见的有不符合前缀有序性 如 like '%XXX'、
  2. 不符合最左匹配原则 如 有索引 idx_a_b,条件中只使用 where b = xxx,记住如果最左侧列索引没用到,后面的也不会起作用;
  3. 符合最左匹配原则但被中断::这种情况不会导致复合索引完全失效,如有 idx_a_b,条件中使用 where a > 20 and b = 10;
  1. 索引设计的缺陷,索引本身无法被利用
  1. 索引列的离散型太低,如性别列使用索引
  2. 索引列操作频繁,不会影响索引的使用,但修改的时候会频繁的构建索引,非常消耗性能
  1. 特殊的语法或规则
  1. 使用 OR 连接低效条件:如where name='张三' OR age is not null,这样age的低效条件导致无法用到索引,可以改成 union,保证name索引被用到;
  2. 使用 NOT IN(极端场景):NOT IN (1,2,3)(等价于<1 OR >3,离散区间,成本高于全表);
  3. 子查询嵌套过深:IN (SELECT ... FROM ... WHERE ...)(优化器无法准确预估子查询结果集,默认放弃索引);
  4. 使用 LIMIT 但筛选性差:SELECT * FROM user WHERE age>0 LIMIT 10(age>0 筛选性差,优化器直接全表取前 10 行);
  1. 成本计算后,走索引查询不如全表扫描。不是索引不能用而是用了更慢,是优化器主动放弃索引的表现;
  1. 连续性低,B+树的特点是定位连续区间或单点位置高效,如果你查询的内容在多个区间内需要多次索引扫描+合并数据,成本高,可能不用索引;
  2. 筛选性能极差的条件,比如 age>0,几乎是查全表的数据,有索引也不会用;
  3. 数据倾斜分配,比如 is null 条件,null值占比90%,还是会全表扫描;
  4. 小表全扫描,当表中数据量极少的时候,走索引的IO开销比直接读取慢;
  5. 回表成本高:这种情况就是没有覆盖索引,且查出的数据量巨大,导致成本太高;

索引优化绝对不是一蹴而就的,要结合数据分布、业务场景、查询条件等等等等。上面的情况只是冰山一角,实际情况的索引优化复杂很多;

最后送大家一个口诀:
「前缀要有序,最左要匹配;
列不做加工,区间少合并;
子查尽量避,NOT IN 少用;
成本算清楚,索引不失效」

三.代码

代码层面的优化两个核心原则:尽量让查询走索引,避免全表扫描;索引的核心作用是快速定位数据,而不是直接提供查询结果;

查询优化

  1. 禁止使用SELECT * ,原因是会查询出不需要的数据,浪费磁盘的IO、内存资源,另外无法覆盖索引,会有回表操作;增加网络传输的开销;
  2. 避免使用索引失效的高频写法,如:
    1. where条件中对索引字段进行函数处理、运算处理或者隐式转换;
    2. 模糊查询以%开头;
    3. where条件中使用 or 链接索引和非索引字段,而用union代替;
    4. where条件中使用不等号条件、使用is null或is not null条件;
    5. 联合索引不遵循最左匹配原则;
    6. in或者not in条件中使用大量值;
    7. 使用子查询嵌套,而不用join替代;
  3. 偏移量过大的分页优化:
    1. 用有序的注解作为分页锚点,如:select id,name from table where id >10000 order by id limit 10000,10;
    2. 子查询定位主键,然后关联查询,如:select u.id,u.name from user u left join (select id from user order by id limit 10000,10) t on t.id = u.id
  4. 排序优化,核心原则是避免文件排序(filesort),优化的原则是让排序字段和索引字段一致,利用索引进行排序;

插入优化

  1. 批量查询优先使用多行insert代替多条insert;
  2. 批量插入是,按需关闭自动提交和唯一性校验
  3. 避免逐条更新,优先使用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测试延迟过高或丢包、主从复制延迟持续增大、客户端连接超时频繁。

总结:进行硬件优化的前提是,在上面的其他方面的优化都已经做到极致(慢查询数量无明显下降、业务响应时间仍高于预期,且数据库的资源使用率已接近硬件上限)。比如:

  1. 索引优化:已经为高频查询、排序、join操作建立合理索引,避免索引冗余、无效索引且无索引失效情况等;
  2. SQL语句优化:优化慢查询(避免全表扫描、大表join、子查询转join),减少不必要的排序、聚合操作等;
  3. 数据库参数优化:合理配置了innodb_buffer_pool_size、innodb_log_file_size、max_connections等核心参数;
  4. 架构优化:读写分离、分库分表、缓存(如Redis)等;

五.架构设计

索引、表设计都是针对单例、单表方面的优化,而架构设计是从整体层面,突破单例、单表的性能的上限,解决高并发、大数据量、高可用的核心问题!

首先明确两个名词TPS(Transactions Per Second)指每秒可支持的事务数量;QPS(Query Per Second)指每秒处理请求的数量。架构设计的初心是:高可用,高性能、高拓展性

  1. 高可用是指避免单点故障。
  2. 高性能是指通过提高读写能力减少存储压力,让数据库可以支持更高的TPS和QPS。
  3. 高拓展性是指可以支持数据量和并发量线下增长时候,无需重构核心架构。

数据库存储架构经历了单实例、主从复制、分库分表、分布式的演化过程,接下来从架构的演化过程,分析每个阶段的优化方案,以深入了解架构化的思维。

单实例,使用场景:数据量 < 1000W & QPS < 10W

这部分的优化基本就是前面说的从表设计、索引、代码、硬件这几个方面进行优化,不必过多赘述。

主从复制,使用场景:读多写少(读/写 > 80%)& QPS < 50W

  1. 什么是主从复制

所谓主从复制就是主数据库只负责写操作即新增、更新、删除操作,从数据库复制读操作即查询操作,一般的架构是一主多从,通过读写分离分流数据库读压力。可通过增加从数据库的数量线性提升读的性能,扩展性强。

  1. 需要解决的问题
  1. 写操作的压力仍然在一个数据库,无法突破单数据库的写性能上限;
  2. 另外本质还是每个库都是存放全量数据,存储的数据量有限;
  3. 存在数据一致性问题;
  4. 存在高可用问题;
  5. 需要读写分离路由
  6. 还有主从库之间同步数据有延迟
  1. 如何解决问题
  1. 第1.2个问题是主从架构的局限性,是由架构决定的,无法解决;
  2. 读写分离路由有两个方法,第一就是在代码层面解决:写操作走主库 ,读操作随机走从库,这样代码耦合高,一般不用,而是用第二个方法中间件,如MyCat、Sharding-JDBC、ProxySQL等,解耦业务与数据库,这些插件不但能解决路由问题还能解决高可用问题,有类似于Redis哨兵的解决方案;
  3. 解决数据一致性问题有这么几个方案,核心逻辑的读请求走主库,这样不会有延迟;从库同步延迟监控,超过阈值自动切主库查询;使用半同步复制的复制模式减少延迟;

分库分表,可支撑百万级别的QPS

一般情况下当单库数据量超过100G、单表超过1亿行或主库写的QPS超过10W,主从架构也无法满足,需要通过分库分表拆分数据,突破单库和单表的存储性能。分库分表可以从拆分维度分为垂直拆分水平拆分 ;而从拆分的对象来说,都能拆分。

  1. 垂直拆分
  1. 垂直分库:拆分的对象是数据库表,按照业务模块、字段属性将表拆分到不同的数据库中,不改变数据行数,如:电商系统中将用户信息、订单信息、商品信息等拆分到不同的数据库中。
  2. 垂直分表:拆分对象是列字段,将一个表中的列按照业务或字段属性拆分到两个或多个表中,如订单表中,将订单号、商品ID等常用字段与大字段如订单详情JSON拆分为主表和拓展表。
  3. 垂直拆分的适用场景是:单库压力过大且业务个模块边界清晰;单表字段过多且有大字段列拖慢查询性能;核心业务与非核心业务隔离;
  1. 水平拆分
  1. 水平拆分库:本质是将表中的数据行按照规则分配的不同的库中,表结构一样,仅数据数据分片不同;
  2. 水平分表:将一个表拆分为多个表,比如Table_0、Table-1、......,每个表结构一样,仅数据分片不同;
  3. 水平拆分时,当单个实例的性能足够用时,优先拆分表。当单个实例的CPU、IO、连接数不足时,单库性能达到瓶颈,再做分库操作。总结一下就是先解决表容量问题,再解决单例瓶颈问题

总结一下:

  1. 垂直分表解决的是单表查询的压力
  2. 垂直分库解决的是单实例的压力
  3. 水平分表解决是单表的压力
  4. 水平分即解决单表压力也解决单实例压力

这部分是比较复杂的,后面会整理一份各种拆分情况的难点和解决方案以及适用场景。

云原生或者分布式数据库,可支撑千万级别的QPS

对于大多数开发人来说,很难接触到这两种数据库,即使你是大厂开发,用到了这两种数据库,你也只是停留在应用的层面,仅通过JDBC、ORM框架调用数据库;底层分库分表、主从切换、弹性扩容完全透明;几乎无需关注数据库类型。仅做了解即可。

简单了解一下市场上主流的分布式关系型数据库TiDB、OceanBase(阿里自研)、CockroachDB 、TDSQL-C(腾讯云)

作为一个合格的码农,必须应该了解分层的概念分而治之的思想,正所谓有道无术,术尚可求。问题的解决方案不同,但是底层的思想相同。

相关推荐
大爱编程♡17 小时前
Spring IoC&DI
数据库·mysql·spring
周末吃鱼18 小时前
MySQL CTE:SQL查询新模式
数据库·sql·mysql
HL计算机菜鸟19 小时前
一对多的实现关系 在数据库表中多的一方添加字段,来关联一的一方主键
mysql
Psycho_MrZhang19 小时前
MySQL/PgSQL设计思想总结
数据库·mysql
风吹落叶花飘荡19 小时前
将mysql数据库的内容备份至阿里云 oss归档存储
数据库·mysql·阿里云
数据大魔方20 小时前
【期货量化入门】期权交易入门:从零开始学期权量化(TqSdk完整教程)
数据库·python·mysql·算法·区块链·程序员创富
w***954921 小时前
mysql之如何获知版本
数据库·mysql
q_19132846951 天前
基于Springboo和vue开发的企业批量排班系统人脸识别考勤打卡系统
前端·javascript·vue.js·spring boot·mysql·毕业设计·人脸识别
风吹落叶花飘荡1 天前
mysql数据库创建新用户,并只给其必要的权限
数据库·mysql