MySQL高级(SQL优化)

MySQL高级

  • 一、字符集
    • 1.1、4个级别的字符集
    • 1.2、字符集小结
    • 1.3、字符集与比较规则
    • 1.4、请求到响应过程中字符集的变化
  • 二、SQL大小写规范
    • 2.1、Windows和Linux平台区别
    • 2.2、Linux下大小写规则设置
    • 2.3、SQL编写建议
  • 三、宽松模式、严格模式(sql_model)
    • 3.1、宽松模式
    • 3.2、严格模式
    • 3.3、查看当前模式
  • 四、数据库和文件系统的关系
    • 4.1、MySQL自带的四个数据库
    • 4.2、表在文件系统中的表示
      • 4.2.1 InnoDB存储引擎模式
      • 4.2.2 MyISAM存储引擎模式
      • 4.2.3 小结
  • 五、用户与权限管理
    • 5.1、登录MySQL服务器参数介绍
    • 5.2、用户管理
      • 5.2.1、创建用户
      • 5.2.2、修改用户名
      • 5.2.3、删除用户
      • 5.2.4、修改用户密码
      • 5.2.5、密码过期策略
    • 5.3、权限管理
      • 5.3.1、权限列表
      • 5.3.2、授予权限的原则
      • 5.3.3、授予权限
      • 5.3.4、查看权限
      • 5.3.5、回收权限
    • 5.4、权限表
      • 5.4.1、user表
      • 5.4.2、db表
      • 5.4.3、 tables_priv表和columns_priv表
      • 5.4.4、 procs_priv表
    • 5.5、访问控制
      • 5.5.1、连接核实阶段
      • 5.5.2、请求核实阶段
  • 六、角色管理
    • 6.1、什么是角色
    • 6.2、创建角色
    • 6.3、给角色赋予权限
    • 6.4、查看角色权限
    • 6.5、回收角色权限
    • 6.6、删除角色权限
    • 6.7、给用户赋予角色
    • 6.8、激活角色
    • 6.9、撤销用户角色
    • 6.10、设置强制角色
    • 6.11、小结
  • 七、MySQL逻辑架构
    • 7.1、逻辑架构剖析
      • 7.1.1、服务器处理客户端请求
      • 7.1.2、第一层:连接层
      • 7.1.3、第二层:服务层(SQL层)
      • 7.1.4、第三层:引擎层
      • 7.1.5、存储层
      • 7.1.6、小结
    • 7.2、SQL执行流程
    • 7.3、数据库缓冲池(buffer pool)
      • 7.3.1、什么是缓冲池?
      • 7.3.2、缓冲池如何读取数据?
  • 八、存储引擎
    • 8.1、什么是存储引擎?
    • 8.2、查看、修改系统默认存储引擎
    • 8.3、设置表的存储引擎
    • 8.4、引擎介绍
      • 8.4.1、InnoDB引擎
      • 8.4.2、MyISAM引擎
      • 8.4.3、InnoDB引擎、MyISAM引擎 对比
      • InnoDB表的优势
      • InnoDB和ACID模型
      • InnoDB架构
      • 8.4.4、 Archive 引擎
      • 8.4.5、 Blackhole 引擎
      • 8.4.6、CSV 引擎
      • 8.4.7、Memory 引擎:置于内存的表
      • 8.4.8、Federated 引擎:访问远程表
      • 8.4.9、Merge引擎
      • 8.4.10、NDB引擎:MySQL集群专用存储引擎
      • 8.4.10、所有引擎对比
  • 九、数据结构—索引
    • 9.1、什么是索引?
    • 9.2、索引优缺点
    • 9.3、常见的索引
      • 9.3.1、聚簇索引
      • 9.3.2、二级索引(辅助索引、非聚簇索引)
      • 9.3.3、回表
      • 9.3.4、联合索引
      • 9.3.5、InnoDB的B+树索引的注意事项
    • 9.4、MyISAM中的索引
    • 9.5、MyISAM 与 InnoDB 索引的对比
    • 9.6、索引的缺点
    • MySQL为什么选择B+树?
  • 十、InnoDB数据存储结构
    • 10.1、磁盘与内存交互基本单位:页
    • 10.2、页结构概述
    • 10.3、页的上层结构
    • 10.4、页的内部结构
      • 10.4.1、文件头、文件尾
      • 10.4.2、用户记录、最大/小记录
      • 10.4.3、页目录、页头
    • 10.5、InnoDb行格式(记录格式)
      • 10.5.1、指定、修改行格式
      • 10.5.2、COMPACT行格式
        • 10.5.2.1、变长字段长度列表
        • 10.5.2.2、NULL值列表
        • 10.5.2.3、记录头信息
        • 10.5.2.4、记录的真实数据
      • 10.5.3、Dynamic和Compressed行格式
        • 10.5.3.1、行溢出
        • 10.5.3.2、Dynamic和Compressed行格式
        • 10.5.3.3、Redundant行格式
    • 10.6、区、段、碎片区、表空间
      • 10.6.1、数据页加载的三种方式(附加)
        • 10.6.1.1、内存读取
        • 10.6.1.2、随机读取
        • 10.6.1.3、顺序读取
      • 10.6.2、为什么要有区?
      • 10.6.3、为什么要有段?
      • 10.6.4、为什么要用碎片区?
      • 10.6.5、区的分类
      • 10.6.6、表空间
        • 10.6.6.1、独立表空间:单表
        • 10.6.6.1、系统表空间
  • 十一、索引的创建与设计原则
    • 11.1、索引的分类
      • 11.1.1、普通索引
      • 11.1.2、唯一性索引
      • 11.1.3、主键索引
      • 11.1.4、单列索引
      • 11.1.5、联合索引
      • 11.1.6、全文索引
      • 11.1.7、空间索引
      • 11.1.8、小结
    • 11.2、索引的查看、创建、删除、隐藏、降序
      • 11.2.1、查看索引
      • 11.2.2、创建索引
      • 11.2.3、删除索引
      • 11.2.4、降序索引(MySQL 8 新特性)
      • 11.2.5、隐藏索引(MySQL 8 新特性)
    • 11.3、索引的设计原则
      • 11.3.1、适合创建索引的情况
      • 11.3.2、不适合创建索引的情况
  • 十二、性能分析工具的使用
    • 12.1、数据库服务器的优化步骤
    • 12.2、查看系统性能参数
    • 12.3、统计SQL的查询成本:last_query_cost
    • 12.4、定位执行慢的SQL:慢查询日志
      • 12.4.1、开启慢查询日志
      • 12.4.2、查看慢查询日志阈值
      • 12.4.3、修改慢查询日志阈值
      • 12.4.4、查看慢查询数目
      • 12.4.5、关闭慢查询日志
      • 12.4.6、删除慢查询日志
    • 12.5、慢查询日志分析工具:mysqldumpslow
    • 12.6、查看 SQL 执行成本:SHOW PROFILE
    • 12.7、分析查询语句:EXPLAIN
      • 12.7.1、概述
      • 12.7.2、语法
      • 12.7.3、explain4种输出格式
        • 传统格式
        • JSON格式
        • TREE格式
        • 可视化输出
    • 12.7、SHOW WARNINGS的使用
    • 12.8、分析优化器执行计划:trace
    • 12.8、MySQL监控分析视图-sys schema
      • 12.8.1、Sys schema视图摘要
      • 12.8.2、Sys schema视图使用场景
  • 十三、索引优化与查询优化
    • 13.1、索引优化建议
      • 13.1.1、全值索引
      • 13.1.2、最佳左前缀法则
      • 13.1.3、主键连续递增插入
      • 13.1.4、运算、函数、类型转换(自动或手动)会导致索引失效
      • 13.1.5、范围查询的索引会失效
      • 13.1.6、不等于(!=/<>)会导致索引失效
      • 13.1.7、is not null无法使用索引
      • 13.1.8、like以通配符%开头索引失效
      • 13.1.9、OR 前后存在非索引的列,索引失效
      • 13.1.10、数据库和表的字符集统一使用utf8mb4
      • 13.1.11、总结
    • 13.2、多表(关联)查询优化
      • 13.2.1、外连接优化
      • 13.2.2、内连接
    • 13.3、JOIN语句原理
      • 13.3.1、驱动表和被驱动表
      • 13.3.2、Simple Nested-Loop Join(简单嵌套循环连接)
      • 13.3.3、Index Nested-Loop Join(索引嵌套循环连接)
      • 13.3.4、Block Nested-Loop Join(块嵌套循环连接)
      • 13.3.5、小结
    • 13.4、Hash Join
    • 13.5、子查询优化与排序优化
      • 13.5.1、使用多表查询代替子查询
      • 13.5.2、给排序的字段创建索引
      • 13.5.3、小结
    • 13.6、filesort算法:双路排序和单路排序
    • 13.7、GROUP BY优化
    • 13.8、分页查询优化
    • 13.9、虑覆盖索引
      • 13.9.1、什么是覆盖索引?
      • 13.9.2、覆盖索引的利与弊
    • 13.10、给字符串添加索引
      • 13.10.1、前缀索引
      • 13.10.2、前缀索引对覆盖索引的影响
    • 13.11、索引下推ICP(索引条件下推)
      • 13.11.1、什么是ICP
      • 13.11.2、ICP的使用条件
      • 13.11.3、IPC的开启和关闭
    • 13.12、其它查询优化策略
      • 13.12.1、EXISTS 和 IN 的区分
      • 13.12.2、CONUT(*)与COUNT(字段)效率
      • 13.12.3、关于SELECT(*)
      • 13.12.4、LIMIT 1 对优化的影响
      • 13.12.5、多使用COMMIT
    • 13.13、淘宝数据库,主键如何设计的?
      • 13.13.1、自增ID的问题
      • 13.13.2、推荐的主键设计(初识UUID)
      • 13.13.3、UUID的组成
      • 13.13.4、改造UUID
      • 13.13.5、有序UUID性能测试
      • 13.14.6、如果不是MySQL8.0 肿么办?
  • 十四、数据库的设计规范(范式)
    • 14.1、范式简介
      • 14.1.2、范式包括哪些
      • 14.1.3、键和相关属性的概念
    • 14.2、第一范式
    • 14.3、第二范式
    • 14.4、第三范式
      • 14.4.5、小结
      • 14.4.6、范式的优缺
    • 14.5、反范式化
    • 14.6、反范式带来的问题
    • 14.7、反范式化的适用建议
      • 14.7.1、增加冗余字段的建议
      • 14.7.2、历史快照、历史数据的需要
    • 14.8、巴斯范式
    • 14.9、第四范式
    • 14.10、第五范式(完美范式)、域键范式(DKNF)
    • 14.11、ER模型
      • 14.11.1、ER模型三要素
      • 14.11.2、ER建模案例分析
      • 14.11.3、 ER 模型图转换成数据表
    • 14.12、数据表的设计原则
    • 14.13、数据库对象编写规范
      • 14.13.1、数据库规范
      • 14.13.2、表、字段规范
      • 14.13.3、索引规范
      • 14.12.4、SQL编写规范
  • 十五、数据库其它调优策略
    • 15.1、数据库调优的策略
      • 15.1.1、数据库调优目标
      • 15.1.2、调优的维度和步骤
        • 15.1.2.1、第一步:选择合适的DBMS
        • 15.1.2.2、第二步:优化表设计
        • 15.1.2.3、第三步:优化逻辑查询
        • 15.1.2.4、第四步:物理查询优化
        • 15.1.2.5、第五步:使用Redis或Memcached作为缓存
        • 15.1.2.6、库级优化
    • 15.2、MySQL服务器优化
      • 15.2.1、优化服务器硬件
      • 15.2.2、MySQL的参数优化
    • 15.3、优化数据库结构
      • 15.3.1、拆分表:冷热数据分离
      • 15.3.2、增加中间表
      • 15.3.3、增加冗余字段
      • 15.3.4、数据类型优化
      • 15.3.5、优化INSERT插入记录的速度
      • 15.3.6、尽量使用非空约束
      • 15.3.7、分析表、检查表、优化表
      • 15.3.8、小结
    • 15.4、大表优化
      • 15.4.1、限定查询范围
      • 15.4.2、读/写分离
      • 15.4.3、垂直分表
      • 15.4.4、水平拆分
    • 15.5、其他调优策略
      • 15.5.1、服务器语句超时处理
      • 15.5.2、创建全局通用表空间
      • 15.5.3、隐藏索引对调优的帮助(MySQL 8.0 新特性)
  • 十六、事务
    • 16.1、数据库事务概述
      • 16.1.1、什么是事务?
      • 16.1.2、事务的ACID特性
      • 16.1.3、小结
      • 16.1.4、事务的状态
    • 16.2、事务的使用
      • 16.2.1、显式事务
      • 16.2.2、隐式事务
      • 16.2.3、会隐式提交事务的情况
      • 16.2.4、completion_type的作用
    • 16.3、事务隔离级别
      • 16.3.1、数据并发问题
      • 16.3.2、SQL中的四种隔离级别
      • 16.3.3、如何设置事务的隔离级别
      • 16.3.4、事务分类
  • 十七、MySQL事务日志
    • 17.1、redo日志
      • 17.1.1、为什么需要redo日志
      • 17.1.2、redo日志的优点、特点
      • 17.1.3、redo日志的组成
      • 17.1.4、redo日志执行过程
      • 17.1.5、redo log刷盘策略
      • 17.1.6、不同策略刷盘流程图
      • 17.1.7、redo log file
        • 1. 相关参数设置
        • 2. 日志文件组
        • 3. checkpoint
        • redo log小结
    • 17.2、undo日志
      • 17.2.1、什么是undo日志
      • 17.2.2、undo日志的作用
      • 17.2.3、undo的存储结构
        • 1. 回滚段与undo页
        • 2. 回滚段与事务
        • 3. 回滚段中的数据分类
        • 4. undo的类型
        • purge线程
    • 17.3、总结
  • 十八、锁
    • 18.1、什么是锁?
    • 18.2、MySQL并发事务访问相同记录
      • 18.2.1、读-读情况
      • 18.2.2、写-写情况
      • 18.2.3、读-写情况
    • 18.3、并发问题的解决方案
    • 18.4、锁的分类
      • 从数据操作的类型划分:读锁、写锁
        • 1. 锁定读
        • 2. 锁定写
      • 从数据操作的粒度划分:表级锁、页级锁、行锁
        • 1. 表锁(Table Lock)
          • ① 表级共享锁、排他锁
          • ② 意向锁 (intention lock)
          • ③ 自增锁(AUTO-INC锁)
          • ④ 元数据锁(MDL锁)
        • 2. 行锁(Row Lock)
          • ① 记录锁(Record Locks)
          • ② 间隙锁(Gap Locks)
          • ③ 临键锁(Next-Key Locks)
          • ④ 插入意向锁(Insert Intention Locks)
        • 3. 页锁
      • 从对待锁的态度划分:乐观锁、悲观锁
        • 1. 悲观锁
        • 2. 乐观锁
          • ① 乐观锁的版本号机制
          • ② 乐观锁的时间戳机制
          • 两种锁的使用场景
      • 按加锁的方式划分:显式锁、隐式锁
        • 1. 隐式锁
        • 2. 显式锁
      • 全局锁
        • 死锁
          • 1. **概念:**
          • 2. 死锁举例:
          • 3. **如何解决死锁问题:**
          • 4. 如何避免死锁
    • 18.5 、锁的内存结构
    • 18.6、锁监控
  • 十九、MVCC多版本并发控制
    • 19.1、什么是MVCC
    • 19.2、快照读与当前读
      • 19.2.1、快照读
      • 19.2.2、当前读
        • 即加锁的是当前读,不加锁的是快照读
    • 19.3、回顾 隔离级别、 隐藏字段、Undo Log版本链
    • 19.4、MVCC实现原理之ReadView
      • 19.4.1、什么是ReadView
      • 19.4.2、设计思路
      • 19.4.3、ReadView的规则
      • 19.4.4、MVCC整体执行流程
      • 19.4.5、MVCC如何解决幻读
    • 19.5、总结
  • 二十、其他数据库日志
    • 20.1、日志类型
    • 20.2、日志的缺点
    • 20.3、通用查询日志(general query log)
      • 20.3.1、查看当前状态(是否开启)
      • 20.3.2、启动日志
      • 20.3.3、查看日志
      • 20.3.4、关闭日志
      • 20.3.5、 删除\刷新\备份日志
    • 20.4、错误日志(error log)
      • 20.4.1、启动日志
      • 20.4.2、查看日志
      • 20.4.3、删除\刷新日志
      • 20.4.4、MySQL 8 新特性
    • 20.5、二进制日志(bin log)
      • 20.5.1、查看状态(是否开启)
      • 20.5.2、日志参数设置
      • 20.5.3、查看日志
      • 20.5.4、使用日志恢复数据
      • 20.5.5、删除二进制日志
      • 20.5.6、其他场景
    • 20.6、二进制日志底层原理
      • 20.6.1、写入机制
      • 20.6.2、binlog与redolog对比
      • 20.6.3、两阶段提交
    • 20.7、中继日志
      • 20.7.1、介绍
      • 20.7.2、查看中继日志
      • 20.7.3、恢复的典型错误
  • 二十一、主从复制
    • 21.1、概述
      • 21.1.1、 如何提升数据库并发能力
      • 21.1.2 主从复制的作用
    • 21.2、主从复制的原理
      • 21.2.1、三个线程
      • 21.2.2、复制三步骤
      • 21.2.3、复制的问题
      • 21.2.4、复制的基本原则
    • 21.3、一主一从架构搭建
      • 21.3.1、主机配置文件
      • 21.3.2、从机配置文件
      • 21.3.3、主机:建立账户并授权
      • 21.3.4、从机:配置需要复制的主机
      • 21.3.5、停止主从同步
      • 21.3.6、binlog格式
        • 格式1:STATEMENT模式(基于SQL语句的复制(statement-based replication, SBR))
        • 格式2:ROW模式(基于行的复制(row-based replication, RBR))
        • 格式3:MIXED模式(混合模式复制(mixed-based replication, MBR))
    • 21.4、主从复制:双主双从
    • 21.5、同步数据一致性问题
      • 21.5.1、理解主从延迟问题
      • 21.5.2、主从延迟问题原因
      • 21.5.3、如何减少主从延迟
      • 21.5.4、如何解决一致性问题
        • 方法 1:异步复制
        • 方法 2:半同步复制
        • 方法 3:组复制
    • 21.6、读写分离
    • 21.7、主备切换
  • 二十二、数据库备份与恢复
    • 22.1、物理备份与逻辑备份
    • 22.2、mysqldump实现逻辑备份
      • 22.2.1、备份一个数据库
      • 22.2.2、备份全部数据库
      • 22.2.3、备份部分数据库
      • 22.2.4、备份部分表
      • 22.2.5、备份单表的部分数据
      • 22.2.6、排除某些表的备份
      • 22.2.7、只备份结构或只备份数据
      • 22.2.8、备份 存储过程、函数、事件
      • 22.2.9、mysqldump常用选项
    • 22.3、从mysqldump备份恢复数据
      • 22.3.1、单库备份中恢复单库
      • 22.3.2、全量备份恢复
      • 22.3.3、从全量备份中恢复单库
      • 22.3.4、从单库备份中恢复单表
    • 22.4、物理备份:直接复制整个数据库
    • 22.5、物理恢复:直接复制到数据库目录
    • 22.6、表的导出与导入
      • 22.6.1、表的导出
        • 1. 使用SELECT…INTO OUTFILE导出文本文件
        • 2. 使用mysqldump命令导出文本文件
        • 3. 使用mysql命令导出文本文件
      • 22.6.2、表的导入
        • 1. 使用LOAD DATA INFILE方式导入文本文件
        • 2. 使用mysqlimport方式导入文本文件
    • 22.7、数据库迁移
      • 22.7.1、概述
      • 22.7.2、迁移方案
      • 22.7.3、迁移注意点
        • 1. 相同版本的数据库之间迁移注意点
        • 2. 不同版本的数据库之间迁移注意点
        • 3. 不同数据库之间迁移注意点
    • 22.8、小结
  • 二十三、误删了库怎么办
    • 23.1、delete:误删行
    • 23.2、truncate/drop:误删库/表
    • 23.3、预防truncate/drop:误删库/表
    • 23.4、rm:误删MySQL实例

一、字符集

在MySQL 8.0版本之前,默认字符集为latin1,utf8字符集指向的是utf8mb3。网站开发人员在数据库设计的时候往往会将编码修改为utf8字符集。如果遗忘修改默认的编码,就会出现乱码的问题。从MySQL8.0开始,数据库的默认编码将改为utf8mb4 ,从而避免上述乱码的问题。

1.1、4个级别的字符集

MySQL有4个级别的字符集和比较规则,分别是:

  • 服务器级别:character_set_server‘
    • 我们可以在启动服务器程序时通过启动选项或者在服务器程序运行过程中使用 SET 语句修改这两个变量的值。比如我们可以在配置文件中这样写:
      [server]
      character_set_server=gbk # 默认字符集
      collation_server=gbk_chinese_ci #对应的默认的比较规则
      
    • 当服务器启动的时候读取这个配置文件后这两个系统变量的值便修改了。
  • 数据库级别:character_set_database
    • 我们在创建和修改数据库的时候可以指定该数据库的字符集和比较规则,具体语法如下:
      CREATE DATABASE 数据库名
      	[[DEFAULT] CHARACTER SET 字符集名称]
      	[[DEFAULT] COLLATE 比较规则名称];
      
      ALTER DATABASE 数据库名
      	[[DEFAULT] CHARACTER SET 字符集名称]
      	[[DEFAULT] COLLATE 比较规则名称];
      
  • 表级别
    • 我们也可以在创建和修改表的时候指定表的字符集和比较规则,语法如下:
      CREATE TABLE 表名 (列的信息)
      	[[DEFAULT] CHARACTER SET 字符集名称]
      	[COLLATE 比较规则名称]]
      
      ALTER TABLE 表名
      	[[DEFAULT] CHARACTER SET 字符集名称]
      	[COLLATE 比较规则名称]
      	```
      
    • 如果创建和修改表的语句中没有指明字符集和比较规则,将使用该表所在数据库的字符集和比较规则作为该表的字符集和比较规则。
  • 列级别
    • 对于存储字符串的列,同一个表中的不同的列也可以有不同的字符集和比较规则。我们在创建和修改列定义的时候可以指定该列的字符集和比较规则,语法如下:
      	CREATE TABLE 表名(
      		列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
      		其他列...
      	);
      	
      	ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称][COLLATE 比较规则名称];
      
    • 对于某个列来说,如果在创建和修改的语句中没有指明字符集和比较规则,将使用该列所在表的字符集和比较规则作为该列的字符集和比较规则。

提示:
在转换列的字符集时需要注意,如果转换前列中存储的数据不能用转换后的字符集进行表示会发生错误。比方说原先列使用的字符集是utf8,列中存储了一些汉字,现在把列的字符集转换为ascii的话就会出错,因为ascii字符集并不能表示汉字字符。

1.2、字符集小结

我们介绍的这4个级别字符集和比较规则的联系如下:

  • 如果 创建或修改列 时没有显式的指定字符集和比较规则,则该列 默认用表的 字符集和比较规则
  • 如果 创建表时 没有显式的指定字符集和比较规则,则该表 默认用数据库的 字符集和比较规则
  • 如果 创建数据库时 没有显式的指定字符集和比较规则,则该数据库 默认用服务器的 字符集和比较规则

知道了这些规则之后,对于给定的表,我们应该知道它的各个列的字符集和比较规则是什么,从而根据这个列的类型来确定存储数据时每个列的实际数据占用的存储空间大小了。

1.3、字符集与比较规则

  1. utf8 与 utf8mb4
    utf8 字符集表示一个字符需要使用1~4个字节,但是我们常用的一些字符使用1~3个字节就可以表示了。而字符集表示一个字符所用的最大字节长度,在某些方面会影响系统的存储和性能,所以设计MySQL的设计者偷偷的定义了两个概念:
  • utf8mb3 :阉割过的 utf8 字符集,只使用1~3个字节表示字符。
  • utf8mb4 :正宗的 utf8 字符集,使用1~4个字节表示字符。
  1. 比较规则
    MySQL版本一共支持41种字符集,其中的 Default collation 列表示这种字符集中一种默认的比较规则,里面包含着该比较规则主要作用于哪种语言,比如 utf8_polish_ci 表示以波兰语的规则比较, utf8_spanish_ci 是以西班牙语的规则比较, utf8_general_ci 是一种通用的比较规则。

    后缀表示该比较规则是否区分语言中的重音、大小写。具体如下:

    后缀 英文释义 描述
    _ai accent insensitive
    _as accent sensitive
    _ci case insensitive
    _cs case sensitive
    _bin binary 以二进制方式比较

    最后一列 Maxlen ,它代表该种字符集表示一个字符最多需要几个字节。

    常用操作1:

    • 查看GBK字符集的比较规则
      SHOW COLLATION LIKE 'gbk%';
    • 查看UTF-8字符集的比较规则
      SHOW COLLATION LIKE 'utf8%';

    常用操作2:

    • 查看服务器的字符集和比较规则
      SHOW VARIABLES LIKE '%_server';
    • 查看数据库的字符集和比较规则
      SHOW VARIABLES LIKE '%_database';
    • 查看具体数据库的字符集
      SHOW CREATE DATABASE dbtest1;
    • 修改具体数据库的字符集
      ALTER DATABASE dbtest1 DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

    常用操作3:

    • 查看表的字符集
      show create table employees;
    • 查看表的比较规则
      show table status from atguigudb like 'employees';
    • 修改表的字符集和比较规则
      ALTER TABLE emp1 DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

1.4、请求到响应过程中字符集的变化

系统变量 描述
character_set_client 服务器解码请求时使用的字符集
character_set_connection 服务器处理请求时会把请求字符串从character_set_client 转为character_set_connection
character_set_results 服务器向客户端返回数据时使用的字符集

二、SQL大小写规范

2.1、Windows和Linux平台区别

windows系统默认大小写不敏感 ,但是 linux系统是大小写敏感的

MySQL在Linux下数据库名、表名、列名、别名大小写规则是这样的:

  1. 数据库名、表名、表的别名、变量名是严格区分大小写的;
  2. 关键字、函数名称在 SQL 中不区分大小写;
  3. 列名(或字段名)与列的别名(或字段别名)在所有的情况下均是忽略大小写的;

你可以手动更改让数据库区分或不区分大小写。通过如下命令查看是否区分大小写:
SHOW VARIABLES LIKE '%lower_case_table_names%'
MySQL高级(SQL优化)_第1张图片

lower_case_table_names参数值的设置:

  • 默认为0,大小写敏感 。
  • 设置1,大小写不敏感。创建的表,数据库都是以小写形式存放在磁盘上,对于sql语句都是转换为小写对表和数据库进行查找。
  • 设置2,创建的表和数据库依据语句上格式存放,凡是查找都是转换为小写进行。

2.2、Linux下大小写规则设置

当想设置为大小写不敏感时,要在 my.cnf 这个配置文件 [mysqld] 中加入lower_case_table_names=1 ,然后重启服务器。

  • 但是要在重启数据库实例之前就需要将原来的数据库和表转换为小写,否则将找不到数据库名。
  • SHOW VARIABLES LIKE '%lower_case_table_names%'
    • 此参数适用于MySQL5.7。在MySQL 8下禁止在重新启动 MySQL 服务时将
      lower_case_table_names 设置成不同于初始化 MySQL 服务时设置的
      lower_case_table_names 值。如果非要将MySQL8设置为大小写不敏感,具体步骤为:

1、停止MySQL服务
2、删除数据目录,即删除 /var/lib/mysql 目录
3、在MySQL配置文件( /etc/my.cnf)中添加 lower_case_table_names=1
4、启动MySQL服务

2.3、SQL编写建议

如果你的变量名命名规范没有统一,就可能产生错误。这里有一个有关命名规范的建议:

1、 关键字和函数名称全部大写;
2、 数据库名、表名、表别名、字段名、字段别名等全部小写;
3、 SQL 语句必须以分号结尾。

数据库名、表名和字段名在 Linux MySQL 环境下是区分大小写的,因此建议你统一这些字段的命名规则,比如全部采用小写的方式。

虽然关键字和函数名称在 SQL 中不区分大小写,也就是如果小写的话同样可以执行。但是同时将关键词和函数名称全部大写,以便于区分数据库名、表名、字段名。

三、宽松模式、严格模式(sql_model)

3.1、宽松模式

如果设置的是宽松模式,那么我们在插入数据的时候,即便是给了一个错误的数据,也可能会被接受,并且不报错。

举例 :我在创建一个表时,该表中有一个字段为name,给name设置的字段类型时 char(10) ,如果我在插入数据的时候,其中name这个字段对应的有一条数据的 长度超过了10 ,例如’1234567890abc’,超过了设定的字段长度10,那么不会报错,并且取前10个字符存上,也就是说你这个数据被存为了’1234567890’,而’abc’就没有了。但是,我们给的这条数据是错误的,因为超过了字段长度,但是并没有报错,并且mysql自行处理并接受了,这就是宽松模式的效果。

应用场景 :通过设置sql mode为宽松模式,来保证大多数sql符合标准的sql语法,这样应用在不同数据库之间进行迁移时,则不需要对业务sql 进行较大的修改。

3.2、严格模式

出现上面宽松模式的错误,应该报错才对,所以MySQL5.7版本就将sql_mode默认值改为了严格模式。所以在生产等环境中,我们必须采用的是严格模式,进而 开发、测试环境 的数据库也必须要设置,这样在开发测试阶段就可以发现问题。并且我们即便是用的MySQL5.6,也应该自行将其改为严格模式。

开发经验 :MySQL等数据库总想把关于数据的所有操作都自己包揽下来,包括数据的校验,其实开发中,我们应该在自己开发的项目程序级别将这些校验给做了,虽然写项目的时候麻烦了一些步骤,但是这样做之后,我们在进行数据库迁移或者在项目的迁移时,就会方便很多。

改为严格模式后可能会存在的问题:

若设置模式中包含了 NO_ZERO_DATE ,那么MySQL数据库不允许插入零日期,插入零日期会抛出错误而不是警告。例如,表中含字段TIMESTAMP列(如果未声明为NULL或显示DEFAULT子句)将自动分配DEFAULT ‘0000-00-00 00:00:00’(零时间戳),这显然是不满足sql_mode中的NO_ZERO_DATE而报错。

3.3、查看当前模式

  • 查看当前的sql_mode

    • select @@session.sql_mode;

    • select @@global.sql_mode;

      或者

    • show variables like 'sql_mode';

  • 临时设置方式:设置当前窗口中设置sql_mode

    • 全局
      SET GLOBAL sql_mode = 'modes...';
    • 当前会话
      SET SESSION sql_mode = 'modes...';
  • 永久设置方式:在/etc/my.cnf中配置sql_mode

    在my.cnf文件(windows系统是my.ini文件),新增:
    [mysqld]
    sql_mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
    然后 重启MySQL 。

    当然生产环境上是禁止重启MySQL服务的,所以采用 临时设置方式 + 永久设置方式 来解决线上的问题,那么即便是有一天真的重启了MySQL服务,也会永久生效了。

四、数据库和文件系统的关系

4.1、MySQL自带的四个数据库

  • mysql
    MySQL 系统自带的核心数据库,它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。
  • information_schema
    MySQL 系统自带的数据库,这个数据库保存着MySQL服务器 维护的所有其他数据库的信息 ,比如有哪些表、哪些视图、哪些触发器、哪些列、哪些索引。这些信息并不是真实的用户数据,而是一些描述性信息,有时候也称之为 元数据 。在系统数据库 information_schema 中提供了一些以innodb_sys 开头的表,用于表示内部系统表。
  • performance_schema
    MySQL 系统自带的数据库,这个数据库里主要保存MySQL服务器运行过程中的一些状态信息,可以用来 监控 MySQL 服务的各类性能指标 。包括统计最近执行了哪些语句,在执行过程的每个阶段都花费了多长时间,内存的使用情况等信息。
  • sys
    MySQL 系统自带的数据库,这个数据库主要是通过 视图 的形式把 information_schema 和performance_schema 结合起来,帮助系统管理员和开发人员监控 MySQL 的技术性能。

4.2、表在文件系统中的表示

4.2.1 InnoDB存储引擎模式

  1. 表结构
    为了保存表结构, InnoDB 在 数据目录 下对应的数据库子目录下创建了一个专门用于 描述表结构的文件 ,文件名是这样:

    表名.frm

  2. 表中数据和索引
    系统表空间(system tablespace)
    默认情况下,InnoDB会在数据目录下创建一个名为 ibdata1 、大小为 12M 的文件,这个文件就是对应的 系统表空间 在文件系统上的表示。怎么才12M?注意这个文件是 自扩展文件 ,当不够用的时候它会自己增加文件大小。

    当然,如果你想让系统表空间对应文件系统上多个实际文件,或者仅仅觉得原来的 ibdata1 这个文件名难听,那可以在MySQL启动时配置对应的文件路径以及它们的大小,比如我们这样修改一下my.cnf 配置文件:

    [server]
    innodb_data_file_path=data1:512M;data2:512M:autoextend
    

    独立表空间(file-per-table tablespace)
    在MySQL5.6.6以及之后的版本中,InnoDB并不会默认的把各个表的数据存储到系统表空间中,而是为 每一个表建立一个独立表空间 ,也就是说我们创建了多少个表,就有多少个独立表空间。使用 独立表空间 来存储表数据的话,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名和表名相同,只不过添加了一个 .ibd 的扩展名而已,所以完整的文件名称长这样:

    表名.ibd

    系统表空间与独立表空间的设置
    我们可以自己指定使用 系统表空间 还是 独立表空间 来存储数据,这个功能由启动参数innodb_file_per_table 控制,比如说我们想刻意将表数据都存储到 系统表空间 时,可以在启动MySQL服务器的时候这样配置:

    [server]
    innodb_file_per_table=0 # 0:代表使用系统表空间; 1:代表使用独立表空间
    

    其他类型的表空间
    随着MySQL的发展,除了上述两种老牌表空间之外,现在还新提出了一些不同类型的表空间,比如通用表空间(general tablespace)、临时表空间(temporary tablespace)等。

4.2.2 MyISAM存储引擎模式

  1. 表结构
    在存储表结构方面, MyISAM 和 InnoDB 一样,也是在 数据目录 下对应的数据库子目录下创建了一个专门用于描述表结构的文件:

    表名.frm

  2. 表中数据和索引
    在MyISAM中的索引全部都是 二级索引 ,该存储引擎的 数据和索引是分开存放 的。所以在文件系统中也是使用不同的文件来存储数据文件和索引文件,同时表数据都存放在对应的数据库子目录下。假如 test表使用MyISAM存储引擎的话,那么在它所在数据库对应的 atguigu 目录下会为 test 表创建这三个文件:

    test.frm 存储表结构
    test.MYD 存储数据 (MYData)
    test.MYI 存储索引 (MYIndex)

4.2.3 小结

举例: 数据库a表b

  1. 如果表b采用 InnoDB ,data\a中会产生1个或者2个文件:
  • b.frm :描述表结构文件,字段长度等
  • 如果采用 系统表空间 模式的,数据信息和索引信息都存储在 ibdata1 中
  • 如果采用 独立表空间 存储模式,data\a中还会产生 b.ibd 文件(存储数据信息和索引信息)

此外:
① MySQL5.7 中会在data/a的目录下生成db.opt 文件用于保存数据库的相关配置。比如:字符集、比较规则。而MySQL8.0不再提供db.opt文件。
② MySQL8.0中不再单独提供b.frm,而是合并在b.ibd文件中。

  1. 如果表b采用 MyISAM ,data\a中会产生3个文件:
  • MySQL5.7 中: b.frm :描述表结构文件,字段长度等。
  • MySQL8.0 中 b.xxx.sdi :描述表结构文件,字段长度等
  • b.MYD (MYData):数据信息文件,存储数据信息(如果采用独立表存储模式)
  • b.MYI (MYIndex):存放索引信息文件

五、用户与权限管理

5.1、登录MySQL服务器参数介绍

启动MySQL服务后,可以通过mysql命令来登录MySQL服务器,命令如下:
mysql –h hostname|hostIP –P port –u username –p DatabaseName –e "SQL语句"
其中:

  • h参数 后面接主机名或者主机IP,hostname为主机,hostIP为主机IP。
  • P参数 后面接MySQL服务的端口,通过该参数连接到指定的端口。MySQL服务的默认端口是3306,不使用该参数时自动连接到3306端口,port为连接的端口号。
  • u参数 后面接用户名,username为用户名。
  • p参数 会提示输入密码。
  • DatabaseName参数 指明登录到哪一个数据库中。如果没有该参数,就会直接登录到MySQL数据库中,然后可以使用USE命令来选择数据库。
  • e参数 后面可以直接加SQL语句。登录MySQL服务器以后即可执行这个SQL语句,然后退出MySQL
    服务器。

5.2、用户管理

5.2.1、创建用户

CREATE USER 语句的基本语法形式如下:
CREATE USER 用户名1 [IDENTIFIED BY '密码1'], 用户名2 [IDENTIFIED BY '密码2']

  • 用户名表示新建用户的账户,由用户(User)和主机名(host)组成。
  • [ ]里边的内容是可选的,也就是说可以指定用户直接通过账户登录,不需要密码,但这样不安全,不推荐使用。这里需要使用IDENTIFIED BY指定明文密码值。
  • CREATE USER语句可以同时创建多个用户。

5.2.2、修改用户名

修改用户名本质是修改表中某一条记录的值:

UPDATE mysql.user SET USER='新值' WHERE USER='旧值';

FLUSH PRIVILEGES; # 刷新权限

最后一定要记得刷新权限,否则旧的用户还是可以登录。

5.2.3、删除用户

方式1:使用DROP方式删除(推荐)
使用DROP USER语句来删除用户时,必须用于DROP USER权限。DROP USER语句的基本语法形式如下:
DROP USER 用户名1,用户名2;

方式2:使用DELETE方式删除(不推荐)
DELETE FROM mysql.user WHERE Host='用户对应的host' AND User='用户名;'
执行完DELETE语句后一定要执行FLUSH语句,刷新权限。
FLUSH PRIVILEGES;

不推荐使用方式2来删除用户,因为系统会有残留信息保留。而drop user命令会删除用户以及对应的权限,执行命令后你会发现mysql.user表和mysql.db表的相应记录都消失了。

5.2.4、修改用户密码

1、修改当前用户密码:
方式1:使用ALTER USER命令来修改当前用户密码 用户可以使用ALTER命令来修改自身密码,如下语句代表修改当前登录用户的密码。基本语法如下:
ALTER USER USER() IDENTIFIED BY '新密码';

方式2:使用SET语句来修改当前用户密码 使用root用户登录MySQL后,可以使用SET语句来修改密码,具体SQL语句如下:
SET PASSWORD='新密码';
该语句会自动将密码加密后再赋给当前用户。

2、修改其他用户密码:
方式1:使用ALTER语句修改普通用户的密码 可以使用ALTER USER语句来修改普通用户的密码。基本语法形式如下:
ALERT USER 用户 [IDENTIFIED BY '新密码'];

方式2:使用SET命令来修改普通用户的密码 使用root用户登录到MySQL服务器后,可以使用SET语句来修改普通用户的密码。SET语句的代码如下:
SET PASSWORD FOR '用户名'@'用户host主机'='新密码';

方式3:使用UPDATE语句修改普通用户的密码(不推荐)
UPDATE MySQL.user SET authentication_string=PASSWORD("新密码") WHERE User = "用户名" AND Host = "用户主机";

5.2.5、密码过期策略

  • 在MySQL中,可以给密码设置有效期,这个过期策略可以是全局的,也可以专门给某个用户指定。
  • 在MySQL中,可以指定用户密码不能与近期的密码重复,或者前面几个密码重复。

5.3、权限管理

5.3.1、权限列表

你可以通过以下命令查看权限列表:
show privileges;

1. CREATE、DROP:可以创建或删除数据库或表。
2. SELECT、INSERT、UPDATE、DELETE:允许在数据库现有表上实施操作。
3. SELECT:允许查询。
4. INDEX:允许创建 或删除索引,INDEX适用于已有的表。
5. ALTER:允许更改表结构或重命名表。
6. CREATE ROUTINE:允许创建保存的程序(或函数)。
7. ALERT ROUTINE:允许修改和删除保存的程序。
8. EXECUTE:允许执行保存的程序。
9. GRANT:允许授权给其他用户,可用于数据库、表和保存的程序。
10.FITE: 使用户可以使用LOAD DATA INFILE和SELECT … INTO OUTFILE语句读或写服务器上的文件,任何被授予FILE权限的用户都能读或写MySQL服务器上的任何文件(说明用户可以读任何数据库目录下的文件,因为服务器可以访问这些文件)。

5.3.2、授予权限的原则

权限控制主要是出于安全因素,因此需要遵循以下几个 经验原则

  1. 只授予能 满足需要的最小权限 ,防止用户干坏事。比如用户只是需要查询,那就只给select权限就可以了,不要给用户赋予update、insert或者delete权限。
  2. 创建用户的时候 限制用户的登录主机 ,一般是限制成指定IP或者内网IP段。
  3. 为每个用户 设置满足密码复杂度的密码
  4. 定期清理不需要的用户 ,回收权限或者删除用户。

5.3.3、授予权限

给用户授权的方式有 2 种,分别是通过把 角色赋予用户给用户授权 和 直接给用户授权 。用户是数据库的使用者,我们可以通过给用户授予访问数据库中资源的权限,来控制使用者对数据库的访问,消除安全隐患。

授权命令:
GRANT 权限1,权限2 ON 数据库名.表名 TO 用户名@主机地址 [IDENTIFIED BY ‘密码’];

  • 该权限如果发现没有该用户,则会新建一个用户
  • 若主机地址为’ %',则表示通配符,代表所有主机。

我们在开发应用的时候,经常会遇到一种需求,就是要根据用户的不同,对数据进行横向和纵向的分组。

  • 所谓横向的分组,就是指用户可以接触到的数据的范围,比如可以看到哪些表的数据;
  • 所谓纵向的分组,就是指用户对接触到的数据能访问到什么程度,比如能看、能改,甚至是删除。

5.3.4、查看权限

  • 查看当前用户权限:
    SHOW GRANTS;

    SHOW GRANTS FOR CURRENT_USER;

    SHOW GRANTS FOR CURRENT_USER();
  • 查看某用户的全局权限:
    SHOW GRANTS FOR '用户'@'主机地址' ;

5.3.5、回收权限

收回权限就是取消已经赋予用户的某些权限。收回用户不必要的权限可以在一定程度上保证系统的安全性。MySQL中使用 REVOKE语句 取消用户的某些权限。使用REVOKE收回权限之后,用户账户的记录将从db、host、tables_priv和columns_priv表中删除,但是用户账户记录仍然在user表中保存(删除user表中的账户记录使用DROP USER语句)。
注意:在将用户账户从user表删除之前,应该收回相应用户的所有权限。

收回权限命令
REVOKE 权限1,权限2,…权限n ON 数据库名称.表名称 FROM 用户名@主机地址;
注意用户必须重新登录才生效。

5.4、权限表

5.4.1、user表

user表是MySQL中最重要的一个权限表, 记录用户账号和权限信息 ,有49个字段。这些字段可以分成4类,分别是范围列(或用户列)、权限列、安全列和资源控制列。

1、范围列(或用户列)

  • host : 表示连接类型
    • % 表示所有远程通过 TCP方式的连接
    • IP 地址 如 (192.168.1.2、127.0.0.1) 通过制定ip地址进行的TCP方式的连接
    • 机器名 通过制定网络中的机器名进行的TCP方式的连接
    • ::1 IPv6的本地ip地址,等同于IPv4的 127.0.0.1
      -localhost本地方式通过命令行方式的连接 ,比如mysql -u xxx -p xxx 方式的连接。
  • user : 表示用户名,同一用户通过不同方式链接的权限是不一样的。
  • password : 密码
    • 所有密码串通过 password(明文字符串) 生成的密文字符串。MySQL 8.0 在用户管理方面增加了角色管理,默认的密码加密方式也做了调整,由之前的 SHA1 改为了 SHA2 ,不可逆 。同时加上 MySQL 5.7 的禁用用户和用户过期的功能,MySQL 在用户管理方面的功能和安全性都较之前版本大大的增强了。
    • mysql 5.7 及之后版本的密码保存到 authentication_string 字段中不再使用password 字段。

2、权限列

  • Grant_priv字段
    • 表示是否拥有GRANT权限
  • Shutdown_priv字段
    • 表示是否拥有停止MySQL服务的权限
  • Super_priv字段
    • 表示是否拥有超级权限
  • Execute_priv字段
    • 表示是否拥有EXECUTE权限。拥有EXECUTE权限,可以执行存储过程和函数。
  • Select_priv , Insert_priv等
    • 为该用户所拥有的权限。

3、安全列 安全列只有6个字段,其中两个是ssl相关的(ssl_type、ssl_cipher),用于 加密 ;两个是x509相关的(x509_issuer、x509_subject),用于 标识用户 ;另外两个Plugin字段用于 验证用户身份 的插件,该字段不能为空。如果该字段为空,服务器就使用内建授权验证机制验证用户身份。

4、资源控制列 资源控制列的字段用来 `限制用户使用的资源 ,包含4个字段,分别为:
①max_questions,用户每小时允许执行的查询操作次数;
②max_updates,用户每小时允许执行的更新操作次数;
③max_connections,用户每小时允许执行的连接操作次数; ④max_user_connections,用户允许同时建立的连接次数。

查看字段:
DESC mysql.user;

查看用户,以列的方式显示数据:
SELECT * FROM mysql.user \G;

查询特定字段:
SELECT host,user,authentication_string,select_priv,insert_priv,drop_priv FROM mysql.user;

5.4.2、db表

使用DESCRIBE查看db表的基本结构:
DESCRIBE mysql.db;

  1. 用户列
    db表用户列有3个字段,分别是Host、User、Db。这3个字段分别表示主机名、用户名和数据库名。表示从某个主机连接某个用户对某个数据库的操作权限,这3个字段的组合构成了db表的主键。
  2. 权限列
    Create_routine_priv和Alter_routine_priv这两个字段决定用户是否具有创建和修改存储过程的权限。

5.4.3、 tables_priv表和columns_priv表

tables_priv表用来 对表设置操作权限 ,columns_priv表用来对表的 某一列设置权限 。tables_priv表和columns_priv表的结构分别如图:
desc mysql.tables_priv;

tables_priv表有8个字段,分别是Host、Db、User、Table_name、Grantor、Timestamp、Table_priv和Column_priv,各个字段说明如下:

  • Host 、 Db 、 User 和 Table_name 四个字段分别表示主机名、数据库名、用户名和表名。
  • Grantor表示修改该记录的用户。
  • Timestamp表示修改该记录的时间。
  • Table_priv 表示对象的操作权限。包括Select、Insert、Update、Delete、Create、Drop、Grant、References、Index和Alter。
  • Column_priv字段表示对表中的列的操作权限,包括Select、Insert、Update和References。
    desc mysql.columns_priv;

5.4.4、 procs_priv表

procs_priv表可以对 存储过程和存储函数设置操作权限 ,表结构如图:
desc mysql.procs_priv;

5.5、访问控制

5.5.1、连接核实阶段

当用户试图连接MySQL服务器时,服务器基于用户的身份以及用户是否能提供正确的密码验证身份来确定接受或者拒绝连接。即客户端用户会在连接请求中提供用户名、主机地址、用户密码,MySQL服务器接收到用户请求后,会使用user表中的host、user和authentication_string这3个字段匹配客户端提供信息

服务器只有在user表记录的Host和User字段匹配客户端主机名和用户名,并且提供正确的密码时才接受连接。如果连接核实没有通过,服务器就完全拒绝访问;否则,服务器接受连接,然后进入阶段2等待用户请求

5.5.2、请求核实阶段

一旦建立了连接,服务器就进入了访问控制的阶段2,也就是请求核实阶段。对此连接上进来的每个请求,服务器检查该请求要执行什么操作、是否有足够的权限来执行它,这正是需要授权表中的权限列发挥作用的地方。这些权限可以来自user、db、table_priv和column_priv表。

确认权限时,MySQL首先 检查user表 ,如果指定的权限没有在user表中被授予,那么MySQL就会继续 检查db表 ,db表是下一安全层级,其中的权限限定于数据库层级,在该层级的SELECT权限允许用户查看指定数据库的所有表中的数据;如果在该层级没有找到限定的权限,则MySQL继续 检查tables_priv表 以及 columns_priv表 ,如果所有权限表都检查完毕,但还是没有找到允许的权限操作,MySQL将 返回错误信息 ,用户请求的操作不能执行,操作失败。

提示: MySQL通过向下层级的顺序(从user表到columns_priv表)检查权限表,但并不是所有的权限都要执行该过程。例如,一个用户登录到MySQL服务器之后只执行对MySQL的管理操作,此时只涉及管理权限,因此MySQL只检查user表。另外,如果请求的权限操作不被允许,MySQL也不会继续检查下一层级的表。

六、角色管理

6.1、什么是角色

我们给角色赋予相应的权限,再将角色赋给用户,用户也就有了相应的权限。引入角色的目的是 方便管理拥有相同权限的用户 。恰当的权限设定,可以确保数据的安全性,这是至关重要的。

6.2、创建角色

语法:
CREATE ROLE '角色名称1'[@'主机地址1'],'角色名称2'[@'主机地址2'];
主机地址可省,默认为%,角色名称不可省。

6.3、给角色赋予权限

创建角色后,这个角色默认是没有任何权限的,我们需要给角色赋予权限,语法:
GRANT 权限1,权限2 ON 表名 TO '角色名'[@'主机'];
可以适用SHOW语句查看权限名称:
SHOW PRIVILEGES\G;

6.4、查看角色权限

赋予角色权限后我们可以查看权限是否赋予成功,语法:
SHOW GRANTS FOR '角色';
角色创建后有一个默认权限,USAGE意为连接登录数据库的权限

6.5、回收角色权限

角色授权后,可以对角色的权限进行维护,对权限进行添加或撤销。
添加权限:GRANT 权限1,权限2 ON 表名 TO '角色名'[@'主机'];
撤销权限:REVOKE 权限1,权限2 ON 表 FROM '角色名';

6.6、删除角色权限

DROP ROLE 角色1,角色2;
注意:如果你删除了角色,用户也就失去了这个角色对应的所有权限。

6.7、给用户赋予角色

角色再赋予用户后需要激活才能生效。
语法格式:GRANT 角色1,角色2 TO 用户1,用户2;
添加完后,可以适用SHOW语句,查看是否添加成功:
SHOW GRANTS FOR '用户'@'主机';
再使用赋予了角色的用户登录后,执行SELECT CURRENT_ROLE();,以查看当前用户角色是否激活,若显示NONE,则未激活

6.8、激活角色

方式1:SET DEFAULT ROLE ALL TO '用户'@'主机';
方式2:SET GLOBAL activate_all_roles_on_login=ON;,默认是OFF状态。该语句会对用户所有角色永久激活

6.9、撤销用户角色

撤销用户角色语法:
REVOKE 角色 FROM 用户;
插销后查看用户角色信息:
SHOW GRANTS FOR '用户'@‘主机’;

6.10、设置强制角色

方式1:服务启动前设置

[mysqld]
mandatory_roles='role1,role2@localhost,r3@%.atguigu.com'

方式2:运行时设置

SET PERSIST mandatory_roles = '角色1,角色2@主机,角色3@主机'; #系统重启后仍然有效
SET GLOBAL mandatory_roles = '角色1,角色2@localhost,角色3@主机'; #系统重启后失效

6.11、小结

MySQL管理角色主要语句如下:

语句 作用
CREATE ROLE、DROP ROLE 创建、删除角色
GRANT、REVOKE 分配、撤销权限
SHOW GRANTS 显示角色或权限信息
SET DEFAULT ROLE 设置账户默认使用的角色
SET ROLE 修改当前会话的角色
CURRENT_ROLE() 函数 显示当前会话的角色
mandatory_roles、activate_all_roles_on_login 系统变量 允许定义用户登录时强制的或者激活授权的角色

七、MySQL逻辑架构

7.1、逻辑架构剖析

7.1.1、服务器处理客户端请求

服务器进程对客户端进程发送的请求做了什么处理,才能产生最后的处理结果呢?这里以查询请求为例展示:
MySQL高级(SQL优化)_第2张图片

7.1.2、第一层:连接层

系统(客户端)访问MySQL服务器前,做的第一件事情就是建立TCP连接。

经过三次握手成功建立连接后,MySQL服务器对TCP传输过来的账号密码进行身份认证、权限获取。

  • 若用户名或密码不对,会收到一个Access denied for user错误,客户端程序结束执行
  • 若用户名密码认证通过,会从权限表查出账号拥有的权限与连接关联,之后的权限判断逻辑,都将依赖于此时读到的权限

TCP 连接收到请求后,必须要分配给一个线程专门与这个客户端的交互。所以还会有个线程池,去走后面的流程。每一个连接从线程池中获取线程,省去了创建和销毁线程的开销。

7.1.3、第二层:服务层(SQL层)

  • SQL Interface:SQL接口

    • 接收用户的SQL命令,并且返回用户需要查询的结果。比如SELECT…FROM就是调用SQL Interface。
    • MySQL支持DML(数据操作语言)、DDL(数据定义语言)、存储过程、视图、触发器、自定义函数等多种SQL语言接口。
  • Parser:解析器

    • 在解析器中对SQL语句进行语法分析、语义分析。将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错误,那么就说明这个SQL语句是不合理的。
    • 在SQL命令传递到解析器的时候会被解析器验证和解析,并为其创建 语法树 ,并根据数据字典丰富查询语法树,会 验证该客户端是否具有执行该查询的权限 。创建好语法树后,MySQL还会对SQl查询进行语法上的优化,进行查询重写。
  • Optimizer:查询优化器

    • SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个执行计划
    • 这个执行计划表明应该 使用哪些索引 进行查询(全表检索还是使用索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。
    • 它使用“ 选取-投影-连接 ”策略进行查询。例如:
      SELECT id,name FROM student WHERE gender = '女';
      这个SELECT查询先根据WHERE语句进行 选取 ,而不是将表全部查询出来以后再进行gender过滤。 这个SELECT查询先根据id和name进行属性 投影 ,而不是将属性全部取出以后再进行过滤,将这两个查询条件 连接 起来生成最终查询结果。
  • Caches & Buffers: 查询缓存组件(已弃用)

    • MySQL内部维持着一些Cache和Buffer,比如Query Cache用来缓存一条SELECT语句的执行结果,如果能够在其中找到对应的查询结果,那么就不必再进行查询解析、优化和执行的整个过程了,直接将结果反馈给客户端。
    • 这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等 。
    • 这个查询缓存可以在 不同客户端之间共享
    • 从MySQL 5.7.20开始,不推荐使用查询缓存,并在 MySQL 8.0中删除

7.1.4、第三层:引擎层

插件式存储引擎层( Storage Engines),真正的负责了MySQL中数据的存储和提取,对物理服务器级别维护的底层数据执行操作,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。

7.1.5、存储层

所有的数据,数据库、表的定义,表的每一行的内容,索引,都是存在 文件系统 上,以 文件 的方式存在的,并完成与存储引擎的交互。当然有些存储引擎比如InnoDB,也支持不使用文件系统直接管理裸设备,但现代文件系统的实现使得这样做没有必要了。在文件系统之下,可以使用本地磁盘,可以使用DAS、NAS、SAN等各种存储系统。

7.1.6、小结

MySQL架构图本节开篇所示。下面为了熟悉SQL执行流程方便,我们可以简化如下:
MySQL高级(SQL优化)_第3张图片
简化为三层结构:

  1. 连接层:客户端和服务器端建立连接,客户端发送 SQL 至服务器端;
  2. SQL 层(服务层):对 SQL 语句进行查询处理;与数据库文件的存储方式无关;
  3. 存储引擎层:与数据库文件打交道,负责数据的存储和读取。

7.2、SQL执行流程

在这里插入图片描述
MySQL执行过程:

  1. 查询缓存:Server如果在查询缓存中发现了这条SQL语句,就会直接将结果返回给客户端。
    • 查询缓存在MySQL 8.0 中已经弃用了,原因:
      ① 只有完全相同的语句才会匹配查询缓存语句,一个空格、分号都不能有差错。
      ② 比如一些有NOW()获取当前时间的函数,每次执行时间都不一样,SQL也不会执行查询缓存的操作。
      ③ 缓存的时效性,没每当一张表的数据被进行了操作或修改,即执行了INSERT、DELETE等语句,那么这张表对应的缓存都会被删除。
      一般静态表才推荐使用查询缓存,因为静态表不会被修改、操作。
  2. 解析器:分析器会先做词法分析,再做语法分析
    • 词法分析:分析SQL语句中的关键字,如SELECT、INSERT等,通过关键字分析要做的操作。
    • 语法分析:通过词法分析之后,解析器会根据语法规则进行语法分析,判断你输入的这个SQL语句是否满足MySQL语法规则。若满足,会生成一个语法树。
  3. 优化器:在优化器中会确定SQL语句的执行路径,比如时根据全表检索,还是根据索引检索。
    • 在查询优化器中,可以分为 逻辑查询 优化阶段和 物理查询 优化阶段。
  4. 执行器:
    • 在执行器之前,MySQL都没有进入真正的语句执行阶段,只是产生了一个执行计划。
    • 在执行之前需要判断该用户是否具备权限,若有,就执行SQL语句,并返回结果;若没有,则返回权限错误。

总结:SQL语句 ——> 查询缓存 ——> 解析器 ——> 执行器——> 查询结果
在这里插入图片描述

语法树:
SQL语句:select department_id,job_id,avg(salary) from employees group by department_id;
MySQL高级(SQL优化)_第4张图片

7.3、数据库缓冲池(buffer pool)

InnoDB 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。而磁盘 I/O 需要消耗的时间很多,而在内存中进行操作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,DBMS 会申请 占用内存来作为数据缓冲池 ,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。

这样做的好处是可以让磁盘活动最小化,从而 减少与磁盘直接进行 I/O 的时间 。要知道,这种策略对提升 SQL 语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。

7.3.1、什么是缓冲池?

首先我们需要了解在 InnoDB 存储引擎中,缓冲池都包括了哪些。

在 InnoDB 存储引擎中有一部分数据会放到内存中,缓冲池则占了这部分内存的大部分,它用来存储各种数据的缓存,如下图所示:
MySQL高级(SQL优化)_第5张图片
从图中,你能看到 InnoDB 缓冲池包括了数据页、索引页、插入缓冲、锁信息、自适应 Hash 和数据字典信息等。

缓存原则:

位置 * 频次 ”这个原则,可以帮我们对 I/O 访问效率进行优化。

首先,位置决定效率,提供缓冲池就是为了在内存中可以直接访问数据。

其次,频次决定优先级顺序。因为缓冲池的大小是有限的,比如磁盘有 200G,但是内存只有 16G,缓冲池大小只有 1G,就无法将所有数据都加载到缓冲池里,这时就涉及到优先级顺序,会优先对使用频次高的热数据进行加载

7.3.2、缓冲池如何读取数据?

缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行读取。

缓存在数据库中的结构和作用如下图所示:
MySQL高级(SQL优化)_第6张图片

八、存储引擎

8.1、什么是存储引擎?

存储引擎就是表的类型,其实存储引擎以前叫做表处理器,后来改名为存储引擎,他的功能就是接收上层传下来的指令,然后对表中的数据进行提取或写入操作

8.2、查看、修改系统默认存储引擎

默认存储引擎是可以修改的,MySQL 8 中默认的存储引擎为InnoDB。

  • 查看MySQL有哪些存储引擎:show engines;show engines \G;
  • 设置系统默认存储引擎:show variables like '%storage_engine%';
    SELECT @@default_storage_engine;
  • 修改系统默认的存储引擎:
    执行SET DEFAULT_STORAGE_ENGINE=存储引擎;
    或者修改 my.cnf 文件:default-storage-engine=存储引擎;
    然后重启服务:systemctl restart mysqld.service;

8.3、设置表的存储引擎

存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为 不同的表设置不同的存储引擎 ,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。

  • 创建表时,指定存储引擎:CREATE TABLE 表名( 建表语句; ) ENGINE = 存储引擎名称;
  • 修改表的存储引擎:ALTER TABLE 表名 ENGINE = 存储引擎名称;

8.4、引擎介绍

8.4.1、InnoDB引擎

InnoDB是支持外键功能的事务存储引擎。

  • 在MySQl 5.5 之后默认采用InnoDB。
  • 支持事务:保证事务的完整提交和回滚。
  • 支持外键
  • 除了新增和查询操作,若还需要更新、删除操作,那么建议使用InnoDB存储引擎,更新、删除效率更高。
  • 支持行锁,对高并发更友好。
  • InnoDB是为处理巨大数据量的最大性能设计,即数据多的时候采用InnoDB,
  • 对比MyISAM的存储引擎, InnoDB写的处理效率差一些 ,并且会占用更多的磁盘空间以保存数据和索引。
  • MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据, 对内存要求较高 ,而且内存大小对性能有决定性的影响。
  • 数据文件结构:
    • 表.frm:存储表的结构信息。
    • 表.ibd:存储数据和索引。
  • 总结:数据少,且只有查询和新增操作的选择MyISAM,数据多且关联事务的,选择InnoDB。

8.4.2、MyISAM引擎

MyISAM是主要的非事务处理引擎。

  • MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM不支持事务、行级 锁、外键,有一个毫无疑问的缺陷就是 崩溃后无法安全恢复
  • 5.5之前默认的存储引擎
  • 优势是访问的速度快 ,对事务完整性没有要求或者以SELECT、INSERT为主的应用
  • 针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高
  • 数据文件结构:(在《第02章_MySQL数据目录》章节已讲)
    • 表名.frm 存储表结构
    • 表名.MYD 存储数据 (MyData)
    • 表名.MYI 存储索引 (MyIndex)
  • 应用场景:只读应用或者以读为主的业务

8.4.3、InnoDB引擎、MyISAM引擎 对比

对比项 MyISAM InnoDB
外键 不支持 支持
事务 不支持 支持
行表锁 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作
缓存 只缓存索引,不缓存真实数据 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响
自带系统表使用 Y N
关注点 性能:节省资源、消耗少、简单业务 事务:并发写、事务、需要更大的内存空间
默认安装 Y Y
默认使用 N Y

InnoDB表的优势

InnoDB存储引擎在实际应用中拥有诸多优势,比如操作便利、提高了数据库的性能、维护成本低等。如果由于硬件或软件的原因导致服务器崩溃,那么在重启服务器之后不需要进行额外的操作。InnoDB崩溃恢复功能自动将之前提交的内容定型,然后撤销没有提交的进程,重启之后继续从崩溃点开始执行。

InnoDB存储引擎在主内存中维护缓冲池,高频率使用的数据将在内存中直接被处理。这种缓存方式应用于多种信息,加速了处理进程。

在专用服务器上,物理内存中高达80%的部分被应用于缓冲池。如果需要将数据插入不同的表中,可以设置外键加强数据的完整性。更新或者删除数据,关联数据将会被自动更新或删除。如果试图将数据插入从表,但在主表中没有对应的数据,插入的数据将被自动移除。如果磁盘或内存中的数据出现崩溃,在使用脏数据之前,校验和机制会发出警告。当每个表的主键都设置合理时,与这些列有关的操作会被自动优化。插入、更新和删除操作通过做改变缓冲自动机制进行优化。 InnoDB不仅支持当前读写,也会缓冲改变的数据到数据流磁盘

InnoDB的性能优势不只存在于长时运行查询的大型表。在同一列多次被查询时,自适应哈希索引会提高查询的速度。使用InnoDB可以压缩表和相关的索引,可以 在不影响性能和可用性的情况下创建或删除索引 。对于大型文本和BLOB数据,使用动态行形式,这种存储布局更高效。通过查询INFORMATION_SCHEMA库中的表可以监控存储引擎的内部工作。在同一个语句中,InnoDB表可以与其他存储引擎表混用。即使有些操作系统限制文件大小为2GB,InnoDB仍然可以处理。 当处理大数据量时,InnoDB兼顾CPU,以达到最大性能

InnoDB和ACID模型

ACID模型是一系列数据库设计规则,这些规则着重强调可靠性,而可靠性对于商业数据和任务关键型应用非常重要。MySQL包含类似InnoDB存储引擎的组件,与ACID模型紧密相连,这样出现意外时,数据不会崩溃,结果不会失真。如果依赖ACID模型,可以不使用一致性检查和崩溃恢复机制。如果拥有额外的软件保护,极可靠的硬件或者应用可以容忍一小部分的数据丢失和不一致,可以将MySQL设置调整为只依赖部分ACID特性,以达到更高的性能。下面讲解InnoDB存储引擎与ACID模型相同作用的四个方面。

  1. 原子方面 ACID的原子方面主要涉及InnoDB事务,与MySQL相关的特性主要包括:
    自动提交设置。
  • COMMIT语句。
  • ROLLBACK语句。
  • 操作INFORMATION_SCHEMA库中的表数据。
  1. 一致性方面 ACID模型的一致性主要涉及保护数据不崩溃的内部InnoDB处理过程,与MySQL相关的特性
    主要包括:
  • InnoDB双写缓存。
  • InnoDB崩溃恢复。
  1. 隔离方面 隔离是应用于事务的级别,与MySQL相关的特性主要包括:
  • 自动提交设置。
  • SET ISOLATION LEVEL语句。
  • InnoDB锁的低级别信息。
  1. 耐久性方面 ACID模型的耐久性主要涉及与硬件配置相互影响的MySQL软件特性。由于硬件复杂多样化,耐久性方面没有具体的规则可循。与MySQL相关的特性有:
  • InnoDB双写缓存,通过innodb_doublewrite配置项配置。
  • 配置项innodb_flush_log_at_trx_commit。
  • 配置项sync_binlog。
  • 配置项innodb_file_per_table。
  • 存储设备的写入缓存。
  • 存储设备的备用电池缓存。
  • 运行MySQL的操作系统。
  • 持续的电力供应。
  • 备份策略。
  • 对分布式或托管的应用,最主要的在于硬件设备的地点以及网络情况

InnoDB架构

  1. 缓冲池 缓冲池是主内存中的一部分空间,用来缓存已使用的表和索引数据。缓冲池使得经常被使用的数据能够直接在内存中获得,从而提高速度。
  2. 更改缓存 更改缓存是一个特殊的数据结构,当受影响的索引页不在缓存中时,更改缓存会缓存辅助索引页的更改。索引页被其他读取操作时会加载到缓存池,缓存的更改内容就会被合并。不同于集群索引,辅助索引并非独一无二的。当系统大部分闲置时,清除操作会定期运行,将更新的索引页刷入磁盘。更新缓存合并期间,可能会大大降低查询的性能。在内存中,更新缓存占用一部分InnoDB缓冲池。在磁盘中,更新缓存是系统表空间的一部分。更新缓存的数据类型由innodb_change_buffering配置项管理。
  3. 自适应哈希索引 自适应哈希索引将负载和足够的内存结合起来,使得InnoDB像内存数据库一样运行,不需要降低事务上的性能或可靠性。这个特性通过innodb_adaptive_hash_index选项配置,或者通过–skip-innodb_adaptive_hash_index命令行在服务启动时关闭。
  4. 重做日志缓存 重做日志缓存存放要放入重做日志的数据。重做日志缓存大小通过innodb_log_buffer_size配置项配置。重做日志缓存会定期地将日志文件刷入磁盘。大型的重做日志缓存使得大型事务能够正常运行而不需要写入磁盘。
  5. 系统表空间 系统表空间包括InnoDB数据字典、双写缓存、更新缓存和撤销日志,同时也包括表和索引数据。多表共享,系统表空间被视为共享表空间。
  6. 双写缓存 双写缓存位于系统表空间中,用于写入从缓存池刷新的数据页。只有在刷新并写入双写缓存后,InnoDB才会将数据页写入合适的位置。
  7. 撤销日志 撤销日志是一系列与事务相关的撤销记录的集合,包含如何撤销事务最近的更改。如果其他事务要查询原始数据,可以从撤销日志记录中追溯未更改的数据。撤销日志存在于撤销日志片段中,这些片段包含于回滚片段中。
    8.每个表一个文件的表空间 每个表一个文件的表空间是指每个单独的表空间创建在自身的数据文件中,而不是系统表空间中。这个功能通过innodb_file_per_table配置项开启。每个表空间由一个单独的.ibd数据文件代表,该文件默认被创建在数据库目录中。
  8. 通用表空间 使用CREATE TABLESPACE语法创建共享的InnoDB表空间。通用表空间可以创建在MySQL数据目录之外能够管理多个表并支持所有行格式的表。
    10. 撤销表空间 撤销表空间由一个或多个包含撤销日志的文件组成。撤销表空间的数量由innodb_undo_tablespaces配置项配置。
  9. 临时表空间 用户创建的临时表空间和基于磁盘的内部临时表都创建于临时表空间。innodb_temp_data_file_path配置项定义了相关的路径、名称、大小和属性。如果该值为空,默认会在
    innodb_data_home_dir变量指定的目录下创建一个自动扩展的数据文件。
  10. 重做日志 重做日志是基于磁盘的数据结构,在崩溃恢复期间使用,用来纠正数据。正常操作期间,重做日志会将请求数据进行编码,这些请求会改变InnoDB表数据。遇到意外崩溃后,未完成的更改会自动在初始化期间重新进行

8.4.4、 Archive 引擎

Archive 引擎主要用于数据存档。仅仅支持插入和查询两种功能(行被插入后不能修改)。

  • 在MySQL 5.0 以后支持索引功能。
  • 拥有很好的压缩机制,使用zlib压缩库,在记录请求的时候实时的进行压缩,经常被用来作为仓库使用。
  • 创建ARCHIVE表时,存储引擎会创建名称以表名开头的文件。文件拓展名为.ARZ
  • 同样数量下,Archive表比MyISAM表要小约75%,比InnoDB表小约83%。
  • ACHIVE存储引擎采用了行级锁。该引擎支持AUVTO_INCREMENT列属性,AUVTO_INCREMENT列可以具有唯一索引或非唯一索引,尝试在任何其他列上创建索引会导致错误。
  • Archive表适合日志和数据采集(归档)类应用;适合储存大量的独立的作为历史记录的数据拥有很高的插入速度,但对查询支持较差。

Archive存储引擎功能:

特征 支持
B树索引 不支持
备份/时间点恢复 (在服务器中实现,而不是在存储引擎中) 支持
集群数据库支持 不支持
聚集索引 不支持
压缩数据 支持
数据缓存 不支持
加密数据(加密功能在服务器中实现) 支持
外键支持 不支持
全文检索索引 不支持
地理空间数据类型支持 支持
地理空间索引支持 不支持
哈希索引 不支持
索引缓存 不支持
锁粒度 行锁
MVCC 不支持
存储限制 没有任何限制
交易 不支持
更新数据字典的统计信息 支持

8.4.5、 Blackhole 引擎

Blackhole 引擎丢弃写操作,读操作会返回空内容。

  • Blackhole引擎没有实现任何存储机制,它会丢弃所有插入的数据,不做任何保存。
  • 但服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者简单的记录到日志。但这种应用方式会碰到很多问题,因此不推荐。

8.4.6、CSV 引擎

CSV 引擎在存储数据时,以逗号分隔各个数据项

  • CSV引擎可以将普通的CSV文件作为MySQL表来处理,但不支持索引。
  • CSV引擎可以作为一种数据交互的机制,非常有用。
  • CSV存储的数据直接可以在操作系统里,用文本编辑器,或者excel读取。
  • 对于数据的快速导入、导出有明显的优势。

创建CSV表时,服务器会创建一个纯文本数据文件,名称以表名开头,拓展名为.CSV。当你将数据存储到表中时,存储引擎会将其以逗号分隔值格式保存到数据文件中。

8.4.7、Memory 引擎:置于内存的表

  • 概述:
    Memory采用的逻辑介质是 内存响应速度很快 ,但是当mysqld守护进程崩溃的时候 数据会丢失 。另外,要求存储的数据是数据长度不变的格式,比如,Blob和Text类型的数据不可用(长度不固定的)。

  • 主要特征:

    • Memory同时 支持哈希(HASH)索引B+树索引
    • Memory表至少比MyISAM表要 快一个数量级
    • MEMORY 表的大小是受到限制 的。表的大小主要取决于两个参数,分别是 max_rows 和max_heap_table_size 。其中,max_rows可以在创建表时指定;max_heap_table_size的大小默认为16MB,可以按需要进行扩大。
    • 数据文件与索引文件分开存储。
    • 缺点:其数据易丢失生命周期短。基于这个缺陷,选择MEMORY存储引擎时需要特别小心。
      因为数据保存在内存中,一旦进程中断数据就会丢失。
  • 使用Memory存储引擎的场景:

    1. 目标数据比较小 ,而且非常 频繁的进行访问 ,在内存中存放数据,如果太大的数据会造成内存溢出 。可以通过参数 max_heap_table_size 控制Memory表的大小,限制Memory表的最大的大小。
    2. 如果数据是临时的 ,而且必须立即可用得到,那么就可以放在内存中。
    3. 存储在Memory表中的数据如果突然间丢失的话也没有太大的关系 。

8.4.8、Federated 引擎:访问远程表

Federated引擎是访问其他MySQL服务器的一个代理 ,尽管该引擎看起来提供了一种很好的跨服务器的灵活性 ,但也经常带来问题,因此 默认是禁用的 。

8.4.9、Merge引擎

Merge引擎可以管理多个MyISAM表构成的表集合。

8.4.10、NDB引擎:MySQL集群专用存储引擎

也叫做 NDB Cluster 存储引擎,主要用于 MySQL Cluster 分布式集群 环境,类似于 Oracle 的 RAC 集群。

8.4.10、所有引擎对比

MySQL中同一个数据库,不同的表可以选择不同的存储引擎。如下表对常用存储引擎做出了对比。

特点 MyISAM InnoDB MEMORY MERGE NDB
存储限制 64TB 没有
事务安全 支持
锁机制 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作 表锁 表锁 行锁
B树索引 支持 支持 支持 支持 支持
哈希索引 支持 支持
全文索引 支持
集群索引 支持
数据缓存 支持 支持 支持
索引缓存 只缓存索引,不缓存真实数据 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 支持 支持 支持
数据可压缩 支持
空间使用 N/A
内存使用 中等
批量插入的速度
支持外键 支持

其实我们最常用的就是 InnoDBMyISAM ,有时会提一下 Memory 。其中 InnoDB 是 MySQL 默认的存储引擎。

九、数据结构—索引

9.1、什么是索引?

  • 索引(Index)是帮助MySQL高效获取数据的数据结构。就像书的目录,你可以根据目录的页面快速定位到相应的文章,否则你就需要全表扫描
  • 索引的本质:索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现 高级查找算法 。
  • 索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。同时存储引擎可以定义每个表的最大索引数最大索引长度。所有存储引擎每个表至少支持16个索引,总索引长度至少为256字节。有些存储引擎支持更多的索引数和更大的索引长度。

9.2、索引优缺点

优点:
(1)提高数据检索的效率(硬盘中),降低 数据库的IO成本 ,这也是创建索引最主要的原因。
(2)通过创建唯一索引,可以保证数据库表中每一行 数据的唯一性
(3)在实现数据的参考完整性方面,可以 加速表和表之间的连接 。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
(4)在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗。
缺点:
增加索引也有许多不利的方面,主要表现在如下几个方面:
(1)创建索引和维护索引要 耗费时间 ,并且随着数据量的增加,所耗费的时间也会增加。
(2)索引需要占 磁盘空间 ,除了数据表占数据空间之外,每一个索引还要占一定的物理空间, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。
(3)虽然索引大大提高了查询速度,同时却会 降低更新表的速度 。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。

因此,选择使用索引时,需要综合考虑索引的优点和缺点。

提示:
索引可以提高查询的速度,但是会影响插入记录的速度。这种情况下,最好的办法是先删除表中的索引,然后插入记录,插入完成后再创建索引。

9.3、常见的索引

索引按照物理实现方式,索引可以分为 2 种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集索引称为二级索引或者辅助索引。

9.3.1、聚簇索引

聚簇索引就是按主键作为索引构建的B+数。
特点:

  1. 使用主键值的大小进行记录和页的排序,这包括三个方面的含义:
    • 页内 的记录是按照主键的大小顺序排成一个 单向链表
    • 各个存放 用户记录的页 也是根据页中用户记录的主键大小顺序排成一个 双向链表
    • 存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个 双向链表
  2. B+树的 叶子节点 存储的是完整的用户记录。
    • 所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

优点:

  • 数据访问更快 ,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
  • 聚簇索引对于主键的 排序查找范围查找 速度非常快
  • 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以 节省了大量的io操作

缺点:

  • 插入速度严重依赖于插入顺序 ,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
  • 更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新
  • 二级索引访问需要两次索引查找 ,第一次找到主键值,第二次根据主键值找到行数据。

9.3.2、二级索引(辅助索引、非聚簇索引)

二级索引就是用非主键的其他字段作为索引构成的B+数。

9.3.3、回表

回表就是我们根据二级索引只能找到记录对应的主键,要查询具体的记录,还需要根据找到的主键去聚簇索引中查找具体的记录。

问题:为什么我们需要回表操作呢?直接把完整的用户记录放到叶子节点不就ok了吗?

  • 回到本质,什么是索引,就是这一个数据页中全是索引,便于我们快速定位数据的位置,没有具体数据的,试想一下,我们在新华字典目录里边还加上目录对应的数据,那索引岂不多余,并且,每一级索引都加上完整的记录,页存在大量冗余。

9.3.4、联合索引

  • 联合索引也是一种非聚簇索引。
  • 联合,即将多个字段联合在一起作为索引,如a、b。
    • 联合索引会将字段a进行排序,再在a排序后的基础上用b进行排序。

9.3.5、InnoDB的B+树索引的注意事项

  1. 根页面位置万年不变。

    • B+数的形成功过程:
      1. 每当为一个表创建一个B+数索引的时候(聚簇索引不是手动创建的,默认就有),都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+数索引对应的根节点中既没有记录,页没有目录项记录。
      2. 向表中插入记录时,先把记录储存到这个根节点中。
      3. 当根节点中的可用空间用完时,继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页a中,然后对这个新页进行页分裂(当一个数据页装不下数据的时候,会分裂为两个数据页来装数据)的操作,得到另一个新页b。
      4. 之后再插入的记录就会按其值的大小按二分法分配到页a或页b中,而根节点便升级为存储目录项记录的页,即索引页
    • 这个过程需要注意的是:B+数的索引一旦建立变不会移动。当我们为某一个表创建了索引,那么他的根节点的页号便会被记录,当InnoDB存储引擎用到这个索引的时候,都会从那个固定的地方取出根节点的页号,从而访问这个索引。
  2. 内节点中目录项记录的唯一性

    • 作为索引的目录项必须具有唯一性,这是针对非聚簇索引来说的,因为聚簇索引是根据主键来创建,是具有唯一性的。
    • 为了保证非聚簇索引具有唯一性,目录项通常是这样构成的索引列+主键+页号,增加了主键,这样就保证了目录项的唯一性。
    • 为什么要保证目录项具有唯一性呢?
      • 因为在插入记录的时候,如果遇到两个索引一样的目录项,MySQL也不知道该往哪里插入,就像你要找一个人,但是出现了两个人名相同,住址不同的人,你也不知道哪个才是你要找的不是吗。
  3. 一个页面最少存储2条记录
    因为如果只有一条记录,也就只有一个目录项,就无法通过二分法来索引数据,目录层级也会特别的深,索引速度变慢。

9.4、MyISAM中的索引

  • MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是数据记录的地址
  • MyISAM的索引在结构上和InnoDB是差不多的,区别就在于叶子节点上,InnoDB中叶子节点是存储的主键,而MyISAM储存的是数据的地址。

9.5、MyISAM 与 InnoDB 索引的对比

MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。小结两种引擎中索引的区别:
① 在InnoDB存储引擎中,我们只需要根据主键值对 聚簇索引 进行一次查找就能找到对应的记录,而在MyISAM 中却需要进行一次 回表 操作,意味着MyISAM中建立的索引相当于全部都是 二级索引
② InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是 分离的 ,索引文件仅保存数据记录的地址。
③ InnoDB的非聚簇索引data域存储相应记录 主键的值 ,而MyISAM索引记录的是 地址 。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
④ MyISAM的回表操作是十分 快速 的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
⑤ InnoDB要求表 必须有主键 ( MyISAM可以没有 )。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。

9.6、索引的缺点

索引是个好东西,可不能乱建,它在空间和时间上都会有消耗:

  • 空间上的代价:
    每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
  • 时间上的代价:
    每次对表中的数据进行 增、删、改 操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值 从小到大的顺序排序 而组成了 双向链表 。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些 记录移位 , 页面分裂 、 页面回收 等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。

MySQL为什么选择B+树?

存储结构的选择是基于索引时的磁盘I/O数

  • Hash结构:等值查找效率高

    • hash即 相同的输入永远可以得到相同的输出
    • hash结构是以key-value的形式存储数据,对比时将key通过hash函数运算得到一个值,然后比较这个值即可。即输入相同的key一定能得到相同的value
    • 从效率上来说,Hash比B+树更快。因为Hash基本一次就能计算出相应的值找到相应的数据,而B+树需要层层的查找,经过多个节点才能找到数据。
    • Hash结构更快为什么还有用树型机构呢?
      ①Hash索引只能用于等于、不等于、IN的查询操作。如果要进行范围查询,具有有序性的树型结构会更快。
      ②Hash结构是无序的,在进行ORDER BY排序操作时,速度很慢,而树型结构是有序的,会更快。
      ③对应联合索引的情况,Hash是将联合索引合并在一起计算出一个结果,而这样查询出来的数据会不准确。
      ④对于性别这种重复值较多的情况,hash的查询效率会很低,因为对于这种等值,hash结构是以链表的形式储存的,查找非常耗时。
  • 二叉搜索树:磁盘IO次数和树的高度相关

    • 特点:
      • 一个节点只能由两个子节点
      • 左子节点 < 本节点 <= 右节点,即比节点小的在左边,大的在右边
    • 查找规则:
      • 如果key大于根节点,则在树的右侧查找;
      • 如果key小于根节点,则在书的左侧查找;
      • 如果key等于根节点,则找到了节点,返回根节点即可。
    • 缺点:当查找的数据全在左侧或右侧的时候,就形成了链表,书的深度会很深,查询效率低,因而引出了平衡二叉树。
  • 平衡二叉树:也称AVL树。

    • 平衡二叉树就是为了解决二叉查找树退化成链表的问题,它在二叉树的基础上增加了一些约束:
      • 要么是一棵空树,要么左右两棵树的高度差不超过1。
      • 并且左右两个子树都是一棵平衡二叉树。
    • 树的越矮越宽,io次数越少,查询效率越高。因为每一层在查询的时候会进行一次io。
  • B-Tree:多路平衡查找树

    • B树英文是Balance Tree,也就是多路平衡查找树,他的高度远远小于平衡二叉树的高度。
    • B树将树的节点分为三个表示范围的节点,左中右三个节点分别对应,小于某个范围,在某个范围,大于某个范围的数据。这样就降低了树的高度。
    • B树的叶子节点和非叶子节点都会存储数据。
  • B+树:多路搜索树

    • B+树是基于B树进行的改进适合做文件索引系统

    • B+树和B树区别:

      • B+树有k个子节点就有k个关键字(列数据)。也就是子节点数量 = 关键字数,而 B 树中,子节点数量 = 关键字数+1。
      • B+树所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
      • B+树非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而B树中, 非叶子节点既保存索引,也保存数据记录 。
      • B+树比B树查询速度快。因为B+树每一层放的索引项更多,只储存了关键字和索引,而B树还保存了数据。
      • B+树查询效率更稳定。因为从B+树中查询数据的层数是一样的也就是io次数一样,索引时间相对稳定。但B数中还保存了数据,会出现io次数不一样的情况。
      • B+数在范围查找上也比B数快。因为B+树是有序排序的,数据又是自增的,我们做范围查询的时候可以通过指针连接查找,而B树需要变量所有数据,效率低。
    • 问题:

      • 为了减少IO,索引树会一次性加载吗?
        • 不会,数据库索引是储存在磁盘上的,如果数据量很大,那么索引树也会很大。
        • 当我们索引查询的时候,会逐一加载每一个磁盘页,因为磁盘页对应着索引树的节点。
      • B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO?
        • InnoDB引擎的页存储大小为16kb,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),
        • 指针类型一般为4或8个字节,页就是说一个页中大概储存16KB/(8B+8B)=1k 个键值,
        • 也就是B+树1层放100条记录,2层放10万条记录,3层放10亿条记录,4层放超过万亿条记录(10*16)。
        • 10亿条记录的容量完全满足了大多企业需求,而实际情况下,每个节点不可能都填满,因此数据库中B+树的高度一般在2~4层,而MySQL的InnoDB引擎在设计时,是将根节点作为常驻内存的,减去这常驻内存的一次io,也就是说在查找某条记录时,最多只需要1~3此磁盘io操作。
      • 为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
        • B+树查询速度更快。因为B+树每一层放的索引项更多,只储存了关键字和索引,而B树还保存了数据。
        • B+树查询效率更稳定。因为从B+树中查询数据的层数是一样的也就是io次数一样,索引时间相对稳定。但B数中还保存了数据,会出现io次数不一样的情况。
      • Hash 索引与 B+ 树索引的区别?
        • Hash索引不能进行范围查找。
        • Hash索引不支持联合索引的最左侧原则。
        • Hash索引不支持ORDER BY排序。
        • InnoDB不支持hash索引。
      • Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?
        • InnoDB和MyISAM都默认使用B+树索引。InnoDB提供的自适应Hash时不需要手动指定的,而Memory/Heap/NDB存储引擎时可以选择Hash索引的。
  • R树

    • R-Tree在MySQL很少使用,仅支持 geometry数据类型 ,支持该类型的存储引擎只有myisam、bdb、innodb、ndb、archive几种。
    • 举个R树在现实领域中能够解决的例子:查找20英里以内所有的餐厅。如果没有R树你会怎么解决?一般情况下我们会把餐厅的坐标(x,y)分为两个字段存放在数据库中,一个字段记录经度,另一个字段记录纬度。这样的话我们就需要遍历所有的餐厅获取其位置信息,然后计算是否满足要求。如果一个地区有100家餐厅的话,我们就要进行100次位置计算操作了,如果应用到谷歌、百度地图这种超大数据库中,这种方法便必定不可行了。
    • R树就很好的 解决了这种高维空间搜索问题 。它把B树的思想很好的扩展到了多维空间,采用了B树分割空间的思想,并在添加、删除操作时采用合并、分解结点的方法,保证树的平衡性。
    • 因此,R树就是一棵用来 存储高维数据的平衡树 。相对于B-Tree,R-Tree的优势在于范围查找。

十、InnoDB数据存储结构

10.1、磁盘与内存交互基本单位:页

  • InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16kb
  • 作为磁盘和内存之间交互的基本单位,也就是说每一次IO至少从硬盘读取16kb内容到内存中。无论你操作的时一条记录,还是多条记录,都会将整个页进行加载,数据库管理存储空间的基本单位是页,数据库I/O操作的最小单位也是页。

10.2、页结构概述

  • 数据页在磁盘中并不是按顺序排列的,但是每一张数据页都通过双向链表实现了逻辑上的顺序连接
  • 数据页内部的记录右按主键从小到大的顺序形成了一个单项链表
  • 数据页会根据存储的记录生成一个页目录以数组储存,
  • 在通过主键查找的记录的时候,可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽中对应的分组即可找到对应的记录。
    MySQL高级(SQL优化)_第7张图片

10.3、页的上层结构

在数据库中,除了数据页(Page),还存在区(Extent)、段(Segment)、表空间(Tablespace)。行、页、区、段、表空间的关系如下图:
MySQL高级(SQL优化)_第8张图片

  • 区(Extent):

    • 区是比页大一级的存储结构。
    • 在InnoDB中一个区会分配64个连续的页,一个页的大小为16kb,因而一个区的大小是64*16kb=1MB
  • 段(Segment):

    • 段是由一个或多个区组成的,不要求区是连续的。
    • 段是数据库中的分配单位,不同类型的数据库对象以不同段形式存在
    • 当我们创建表或索引的时候,都会创建对应的段。
  • 表空间(Tablespace):

    • 表空间是一个逻辑容器。
    • 表是存储段的,在一个表空间中可以由一个或多个段,但是一个段只能属于一个表空间。
    • 数据库由一个或多个表空间组成,表空间从管理上可分为系统表空间用户表空间撤销表空间临时表空间

10.4、页的内部结构

  • 页按类型划分可分为:数据页(保存B+树节点)系统页Undo页事务数据页等是我们最常使用的页。
  • 数据页的16kb存储空间被划分为7个部分:
    • 文件头(File Header)
    • 页头(Page Header)
    • 最大/最小记录(Infimum+supermum)
    • 用户记录(User Records)
    • 空闲空间(Free Space)
    • 页目录(Page Directory)
    • 文件尾(FileTailer)

页结构示意图如下:
MySQL高级(SQL优化)_第9张图片
这7个部分的作用如下:
MySQL高级(SQL优化)_第10张图片

10.4.1、文件头、文件尾

文件头

  • 作用:描述各种页的信息。(比如页的编号、上一页和下一页是谁等)
  • 大小:38字节
  • 构成:
    MySQL高级(SQL优化)_第11张图片

文件尾:

  • 构成:
    • FIL_PAGE_SPACE_OR_CHKSUM:保存页的校验和
    • FIL_PAGE_FILE_FLUSH_LSN:保存日志序列位置LSN

校验和:

  • 比如我们要比较两个很长的字符串的大小,我们可以将这两个很长的字符串通过某种算法计算成一个较短的值,再比较这两个值的大小,这个值就是校验和。就是Hash算法。
  • 作用:校验数据的完整性。
  • 在我们修改内存中的一个页时,在修改前后都会将文件头和文件尾的校验和计算出来,文件头和文件尾的校验和是相等的,则说明数据是完整的,若同步之后出现文件头和文件尾部校验和不相等,则说明文件没有同步完成,可以通过LSN继续同步,或者回滚。这里的校验和就是采用的Hash算法进行校验的。

LSN:

  • 保存了 页最后被修改时对应的日志序列位置
  • 作用:也是为了校验页的完整性,如果首部和尾部LSN值校验不成功的话,则说明同步过程中出现了问题。

10.4.2、用户记录、最大/小记录

空闲空间(Free Space):

  • 我们自己存储的记录会按照指定的行格式保存到用户记录(User Records)中,一开始的时候并不存在用户记录的内存空间,当我们插入记录的时候才会向空闲空间申请用户记录空间。当空闲空间被用户记录占满也就代表空闲空间用尽,再插入新的记录就需要申请新的页了。

用户记录(User Records):

  • 用户记录就是我们储存的数据。
  • 用户记录中的记录也就是数据,会按照指定的行格式一条一条的存放在User Records中,相互之间形成单链表
  • 但链表的形成信息都保存在行格式的记录头信息中。

最大/小记录:

  • 最大最小记录其实是保存的主键的最大最小值,
  • 用于在存储数据的时候根据主键的大小建立索引目录。

10.4.3、页目录、页头

页目录:

  • InnoDB会将磁盘中所有的记录进行分组,第一组有且仅有保存最小记录,最后一组包含最大记录共1 ~ 8条记录,其余组由4 ~ 8条记录。
  • 页目录就是保存每组记录中主键最大记录的地址偏移量,这些地址偏移量按照先后顺序保存起来,每组地址偏移量称为一个槽(slot)
  • 地址偏移量就是,在该记录之后多少条记录是下一条记录。
  • 在查找记录的时候,会根据二分法先对槽进行查找,找到对应的组;再对组进行二分法查找,直到找到对应的记录。

页面头部:

  • 页头中记录了数据页中存储的记录的所有信息。
  • 比如本页中存储了多少条数据、第一条数据的地址是什么、页目录中有多少个槽等等。
  • 具体结构如下图:
    MySQL高级(SQL优化)_第12张图片

10.5、InnoDb行格式(记录格式)

MySQL 5.7 和MySQL 8.0默认的行格式都是Dynamic。

10.5.1、指定、修改行格式

  • 查看InnoDB数据库默认行格式:SELECT @@innodb_defalut_row_format;
  • 查看InnoDB表默认行格式:SHOW TABLE STATUS like '表名'\G;
  • 创建表时指定行格式:CREATE TABLE 表名(列信息) ROW_FORMAT=行格式名称;
  • 修改表的行格式:ALTER TANLE 表名 ROW_FORMAT=行格式名称;

10.5.2、COMPACT行格式

在COMPACT行格式中,一条完整的记录会被分为记录的真实数据记录的额外信息两个部分。

MySQL高级(SQL优化)_第13张图片

10.5.2.1、变长字段长度列表

  • MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型、BLOB类型,这些数据类型修饰的字段称为变长字段
  • 因为这些变长字段存储的数据占用的空间是不固定的,所以在储存这些变长的字段时,会将其真实的占用空间记录并保存下来。
  • compact行格式把所有变长字段的真实数据占用空间都保存在记录的开头部位,从而形成一个变长字段长度列表

10.5.2.2、NULL值列表

  • COMPACT行格式会将可以为NULL值得字段同一管理起来,保存在一个标记为NULL值列表中。
  • 为什么要定义NULL值列表?
    • 因为数据是要对齐的,而NULL值是没有数据的,如果我们在添加数据时没有记录NULL值的位置,就会将数据添加到NULL值上,就会造成数据混乱,
    • 如果用一个符号来标记NULL值位置,有会浪费空间,
    • 因而在数据行的头部开辟一块空间专门用来记录哪些是非空数据,哪些是空数据。

10.5.2.3、记录头信息

  • 记录头信息就是一条记录的头部信息。
  • 记录的头部信息中包含:
    • 记录是否被删除;
      • 被删除的记录并不会真正的从磁盘中删除,而是通过标记为0或1表示是否被删除,
      • 被标记为删除的记录会形成一条垃圾链,新添加的记录会覆盖垃圾链上的记录。
      • 注意,记录在物理磁盘上是非连续的,而是通过链表的的指针将其在逻辑上形成连续的链表。
    • 是否为非叶子节点的最小记录;
    • 当前记录拥有的记录数量;
    • 当前记录的地址偏移量等信息;

记录头信息在COMPACT内部结构如下图:
MySQL高级(SQL优化)_第14张图片
记录头信息的内部结构如下图:
MySQL高级(SQL优化)_第15张图片

10.5.2.4、记录的真实数据

一条记录除了我们定义的真实数据以为,还会有三个隐藏列:

列名 是否必须 占用空间 描述
DB_ROW_ID 6字节 行ID,一条记录的唯一标识
DB_TRX_ID 6字节 事务ID
DB_ROLL_PTR 7字节 回滚指针
  • 一个表若没有手动定义主键,则自动会选取一个Unique的键作为主键,如果没有声明为Unique的键,则会默认添加一个名为row_id的隐藏列作为主键。所有row_id是没有自定义主键和Unique键的情况下才会存在。
  • 事务ID和回滚指针在《MySQL事务日志》这一章节会讲。

10.5.3、Dynamic和Compressed行格式

10.5.3.1、行溢出

  • 行溢出就是某一条记录的真实占用空间大于的页的实际存储空间。

  • 比如:

    • 一个页的大小是16kb,也就是16384个字节,
    • 而一个varchar类型的字段最多可以存储65535个字节,
    • 这样就会出现一个页放不下一条记录的情况,这种现象就称为行溢出
  • 行溢出解决方案:

    • 在compact和reduntant行格式中,对于占用空间非常大的字段(列),在保存真实记录时,只会保存该列的部分数据,把剩余的数据分散存储在其他几个页中进行分页存储
    • 在真实记录处用20个字节存储指向这些分页的地址,从而可以找到这些分页。

10.5.3.2、Dynamic和Compressed行格式

Dynamic和Compressed行格式对数据的存储方式和compact差不多,区别就在于对行溢出的处理方案不同:

  • Compressed和Dynamic两种行格式对于存放在BLOB中的数据采用了完全行溢出的方式,即数据页只保存20个字节的分页地址,所有数据全部保存在分页中,数据页只保存地址。
  • Compact和Redundant两种行格式,则采用部分真实数据+分页地址的方式保存溢出的数据。
  • Compressed行格式的另一个功能就是,会将行数据一zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据可以进行有效的存储。

10.5.3.3、Redundant行格式

Redundant行格式示意图如下:
MySQL高级(SQL优化)_第16张图片

  • 从图中可以看出,Redundant不同于compact行格式在于首部,compact的首部是变长字段长度列表
  • Redundant首部是字段长度偏移列表,没有了变长的特性,也就不需要NULL值列表来记录NULL值得位置了。
  • 但是列表得排列方式同样是逆序排序的。
  • 少了“变长”:
    • Redundant行格式会将该条记录所有的列(包括隐藏列)的长度信息全部逆序存储到字段长度偏移列表中。
  • 多了“偏移”:
    • 这里的偏移是指计算列值长度的方式,是通过字段长度偏移列表中前面两个相邻数值的差值来计算列值的长度。

10.6、区、段、碎片区、表空间

10.6.1、数据页加载的三种方式(附加)

  • InnoDB从磁盘读取数据的最小单位数据页。你想得到的id=xxx的数据,就是这个数据页众多行中的一行。
  • 对于MySQL存放的数据,逻辑概念上我们称之为,在物理磁盘上则是按数据页进行存放的。当其被加载到MySQL中,我们称之为缓存页

10.6.1.1、内存读取

如果是从内存中读取数据,读取时间在1ms左右,效率还是很高的

10.6.1.2、随机读取

  • 如果数据没有在内存中,就需要从物理磁盘中读取数据,
  • 一个页读取时间在10ms左右,但实际数据传输时间只有1ms,其他9ms都是浪费的。
  • 6ms是磁盘的实际繁忙时间(包括寻道和半圈旋转时间),3ms是对可能发生排队时间的估计值,1ms才是将页从磁盘服务器缓冲区传输到数据库缓冲区的时间。

10.6.1.3、顺序读取

  • 顺序读取其实是一种批量读取方式,因为我们请求的数据在磁盘上往往是按顺序储存的
  • 顺序读取可以帮我们批量读取数据页,这样一次性将页面批量读取到数据库缓冲池中,就不需要单独对页面进行读取,平均一个数据页读取时间为0.4ms,效率十分高。

10.6.2、为什么要有区?

  • 从10.6.1小节中,我们得知如果从磁盘中随机读取数据的话速度是很慢的,因而我们需要顺序读取。
  • 因而我们就引入了,区在物理磁盘上保存了连续的64个页,这样我们按区来读取数据,一次就可以加载64个数据页,效率很高。
  • 当区数据溢出的时候,我们可以考虑让区页连续起来,这样效率又得到了提高。(区是可以不连续的)

10.6.3、为什么要有段?

  • B+树结构中的叶子节点(数据页)和非叶子节点(目录页)如果都放在同一个区中,不便于查询,
  • 我们考虑将数据页和目录页分别保存在不同的区中,这些不同的区就构成了不同的
  • 一个段中可以有多个区,这些区中要么都是数据页,要么都是目录页。
  • 常见的段:数据段(数据页)索引段(目录页)回滚段
  • 段在物理空间上不存在,只是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成。

10.6.4、为什么要用碎片区?

解决空间浪费问题。

  • 默认情况下,一个索引会生成2个段,而段时以区为单位申请存储空间的,一个区默认占用1M(64*16kb=1024kb)空间,因为默认情况下哪怕只有几条记录,也会占用2M的储存空间,十分浪费。
  • InnoDB提出了碎片区的概念,碎片区中可以储存不同段的数据,碎片区直属于表空间,不属于任何段

有了碎片区后,分配存储空间的策略时这样的:

  • 刚开时向表中插入数据的时候,段是从某个碎片区以页为单位来分配存储空间的。
  • 当某个段占用32个碎片区页面之后,就会申请以区为单位来存储数据。

10.6.5、区的分类

区可以分为4中类型:

  • 空闲区(FREE):没有使用的区
  • 有剩余空间的碎片区(FREE_FRAG):碎片区中还有可用的空间
  • 没有剩余空间的碎片区(FULL_FRAG):碎片区空间已经使用完
  • 附属于某个段的区(FSEG):每一个索引都可以分为叶子节点段和非叶子节点段

其中前三者都直属于表空间,最后一个FSEG属于段。

10.6.6、表空间

  • 表空间是InnoDB存储引擎逻辑架构的最高层,所有的数据都存放在表空间中。
  • 表空间是一个逻辑容器,其存储对象是段;一个表空间可以有一个或多个段,但一个段只能属于一个表空间。
  • 表空间数据库由一个或多个表空间组成。
  • 表空间从管理上可分为:系统表空间(system tablespace)独立表空间(file-per-table tablespace)撤销表空间(undo tablespace)临时表空间(temporary tablespace)等等

10.6.6.1、独立表空间:单表

  • 独立表空间,也就是数据和索引信息都会保存在自己的表空间中,也称单表;每张表都有自己的表空间;单表可以在不同数据库之间进行迁移
  • 对于DROP TABLE删除表的操作,会自动回收表空间;其他情况表空间不能自动回收。
  • 对于统计分析或日志表,删除大量的数据后可以执行ALERT TABLE 表名 engine=innodb;回收不用的空间。
  • 独立表空间的结构:
    • 独立表空间由段、区、页组成。
  • 真实表空间对应的文件大小:
    • 一个新建的表对应的.ibd文件由于一开始是没有数据的,所以只有96k,只有6个页的大小,但.ibd文件时自拓展的,随着表中数据的增多,表空间对应的文件也会逐渐增大。
    • 每一张表都会单独的保存为一个.ibd文件。
  • 查看InnoDB的表空间类型:
    • SHOW VARIABLES LIKE 'innodb_file_per_table';

10.6.6.1、系统表空间

  • 一个MySQL进程只有一个系统表空间。系统表空间会记录一些有关整个系统信息的页面,这些页面是独立表空间中没有的。
  • 系统表空间的结构:
    • 系统表空间由段、区、页组成。

InnoDB数据字典
当我们向表中插入一条记录的时候,MySQL校验过程如下:

  1. 首先校验插入语句对应的表是否存在;
  2. 其次校验插入的列和表的列是否符合;
  3. 然后判断该表的聚簇索引和所有非聚簇索引对应的根页面是哪个表空间的哪个页面;
  4. 最后将记录插入到对应索引的B+树中。

所以MySQL除了保存我们插入的数据之外,还保存着许多额外的信息,比如:

  • 某个表属于哪个空间,表里由多少字段。
  • 表对应的每个字段的类型是什么。
  • 该表有多少索引,每个索引对应哪几个字段。
  • 该表有哪些外键,外键对应哪个表的哪些列。

上述这些额外的数据不是我们插入的数据,而是数据库为了管理数据而不得不引入的一些额外的数据,这些数据也称为元数据。InnoDB定义了一些列的内部系统表来记录这些元数据,这些内部系统表也称为数据字典。它们都以B+树的形式保存在系统表空间的某些页面中,其中前四个表尤其重要,也称之为基本系统表
MySQL高级(SQL优化)_第17张图片
注意:

  • 我们是不能直接访问这些内部系统表的,除非你直接去解析系统表空间对应文件系统上的文件。
  • 但是为了方便我们分析问题,所以在infomation_schema中提供了一些以innodb_sys开头的表供我们查看,但这些表并不是真正的内部系统表,只是从内部系统表中提取了部分字段供我们去分析问题。
  • 查看语法:SHOW TABLES LIKE 'innodb_sys%';

十一、索引的创建与设计原则

11.1、索引的分类

MySQL的索引包括普通索引、唯一索引、全文索引、单列索引、多列索引、空间索引等。

  • 功能逻辑上可分为:普通索引、唯一索引、主键索引、全文索引。
  • 物理实现方式上可分为:聚簇索引(主键)、非聚簇索引。
  • 作用的字段个数上可分为:单列索引(作用于一个字段)、联合索引(作用于多个字段)。

11.1.1、普通索引

  • 普通索引在创建时,不附加任何限制条件,只是用于提高查询效率
  • 普通索引可以在任何数据类型中使用。
  • 其值是否唯一和非空,要由字段本身的完整性约束条件决定。

11.1.2、唯一性索引

  • 使用UNIQUE关键字声明唯一性索引。
  • 在创建表时限制该索引的值必须是唯一可为空
  • 在一张数据表里可以有多个唯一索引。

11.1.3、主键索引

  • 主键索引就是一种特殊的唯一性索引,也是聚簇索引。
  • 主键索引唯一且不为空
  • 一张表只能有一个主键索引。

11.1.4、单列索引

  • 单列索引创建在表中的某个字段上。
  • 一张表可以有多个单列索引。
  • 单列索引只根据字段进行索引,可以是唯一索引、普通索引、全文索引,只要保证该索引只对应一个字段即可。

11.1.5、联合索引

  • 联合索引是在表的多个字段组合上创建的索引。
  • 该索引指向这几个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。
  • 比如在字段id、name、gender上创建联合索引idx_id_name_gender,只有在查询条件中使用了字段id时,该联合索引才会被使用。使用联合索引时,遵循最左前缀集合

11.1.6、全文索引

  • 全文索引也称全文检索,时目前搜索引擎使用的一种关键技术。
  • 它能够利用分词技术等多种算法智能分析处文本文字中关键词的频率和重要性,然后按照一定的算法规则智能的筛选出我们想要的搜索结果。
  • 全文索引适用于大型数据集,对应小型数据集,它的用处很小。
  • 使用FULLTEXT声明全文索引。定义为全文索引的字段支持全文查找,并允许全文索引字段插入重复值和空值
  • 全文索引只能创建在CHAR、VARCHAR、TEXT及其系列类型的字段上。
  • 查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。
  • 全文索引最典型的有两种类型:自然语言的全文索引、布尔全文索引。
    • 自然语言搜索引擎会计算每一个文档对象和查询的相关度(这里的相关度是指,匹配到的关键词的个数),以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语,匹配的相关度越高。相反常见的词汇不会被搜索,如果某个词语在超过50%的记录中都出现了,那么自然语言搜索引擎不会搜索这类词语。

MySQL数据库从3.23.23版本开始支持全文索引,InnoDB从5.6.4版本后开始支持全文索引,但是官方版本不支持中文分词,需要第三方分词插件。在5.7.6版本,MySQL内置了ngram全文解析器,用来支持亚洲语种的分词。
随着大数据时代的到来,关系型数据库对应的全文索引的需求以力不从心,逐渐被solr、ElasticSeach等专门的搜索引擎锁替代。

11.1.7、空间索引

  • 空间索引使用SPATIAL关键字声明。
  • 只有空间类型的数据才能创建空间索引,这样可以提高系统获取空间数据的效率。
  • MySQL的空间数据类型包括GEOMETRY、POINT、LINESTRING、POLYGON等等。
  • 目前只有MyISAM支持空间索引,而且索引的字段不能为空值

11.1.8、小结

不同的存储引擎支持的索引也不一样:

InnoDB MyISAM Memory NDB Archive
B-树 支持 支持 支持 不支持 支持
全文索引 支持 支持 不支持 不支持 不支持
Hash索引 不支持 不支持 支持 支持 不支持

11.2、索引的查看、创建、删除、隐藏、降序

11.2.1、查看索引

查看表中的索引:

  • SHOW CREATE TABLE 表名\G;
  • SHOW INDEX FROM 表名;
  • 直接在图形化界面中查看。

11.2.2、创建索引

  • 创建表时创建索引:

    • 创建表时,我们可以不用显示的将某个字段声明为索引,被声明为主键约束、唯一性约束、外键约束的字段都会被隐式的声明为索引。
    • 显示的声明索引:
      • CREATE TABLE 表名 ([字段名 数据类型] 索引类型 [INDEX|KEY][index_name](col_name[length])[ASC|DESC]);
      • 索引类型:
        • UNIQUE INDEX:唯一索引
        • FULLTEXT INDEX:全文索引
        • SPATIAL INDEX:空间索引
        • 主键索引通过主键约束的方式,隐式声明,删除主键索引也是通过删除主键约束来删除。
      • INDEX、KEY:作用一样都是用来声明索引。
      • index_name:索引的名称,为可选参数,若不指定,默认使用col_name。
      • col_name:声明为索引的字段列。
      • length:索引的长度。
      • ASC、DESC:指定升序或降序的索引值存储。
    • eg:创建唯一性索引:
      • CREATE TABLE table(col_id INT,col_name VARCHAR(15) UNIQUE INDEX uk_idx_name(col_name));
  • 给已有表创建索引:

    • 方式1:ALTER TABLE...ADD...
      ALERT TABLE 表名 ADD [索引类型] [INDEX|KEY] [index_name](col_name[length])[ASC|DESC]
    • 方式2:CREAT... INDEX ON...
      CREATE 索引类型 INDEX index_name ON 表名 (col_name[length])[ASC|DESC];

11.2.3、删除索引

  • 方式1:ALTER TABLE...DROP INDEX...
    ALTER TABLE table_name DROP INDEX index_name;
  • 方式2:DROP INDEX...ON...
    DROP INDEX index_name ON table_name;
  • 注意:
    • 添加了AUTO_INCREMENT约束字段的唯一索引不能删除。
    • 如果索引字段列表中的字段被删除,那么对应的索引字段也会被删除。

11.2.4、降序索引(MySQL 8 新特性)

  • 在MySQL 8.0 之前创建的索引默认是升序排序的,使用时进行反向扫描,大大降低了数据库的效率
  • 在某些场景下,降序索引可以大大提高查询效率。
  • 语法,在索引的字段名称后边加上DESC即可
  • eg:CREATE TABLE table_name (a int,b int,index idx_a_b(a DESC ,b DESC));
  • 官方说明:当一个查询 SQL,需要按多个字段,以不同的顺序进行排序时,8.0 之前无法使用索引已排序的特性,因为 order by 的顺序与索引的顺序不一致,而使用降序引,就能够指定联合索引中每一个字段的顺序,以适应 SQL 语句中的 order by 顺序,让 SQL 能够充分使用索引已排序的特性,提升 SQL 性能。

11.2.5、隐藏索引(MySQL 8 新特性)

  • 有时候我们需要删除索引,但是删除后发现会出现错误,又要显示的将索引创建回来,这样的操作会消耗过多的资源,操作成本很高,
  • 隐藏索引的作用就是,可以先将要删除的索引设置为隐藏索引,使查询优化器不再使用这个索引,当索引被隐藏后没有出现错误再删除这个索引。
  • 这种线将索引设置为隐藏索引,再删除的方式称为软删除
  • 如果你想验证某个索引删除之后的查询性能,就可以先将其设置为隐藏索引。
  • 索引默认是可见的,我们可以在CREATE TABLECREATE INDEXALTER TABLE等语句末尾添加 VISIBLEINVISIBLE来显示、隐藏索引。
  • eg: ALTER TABLE 表 ALTER INDEX 索引 INVISIBLE;
  • 注意:
    • 当索引被隐藏时,它的内容时和正常索引一样实时更新的,
    • 如果一个索引长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新、删除的性能。

11.3、索引的设计原则

创建索引的目的就是让数据按某种顺序进行存储或检索。

11.3.1、适合创建索引的情况

  1. 有唯一性限制的字段

    • 如果某个字段是唯一性的,就可以直接创建唯一性索引或主键索引。
    • 业务上具有唯一性的字段,即使是组合字段,页必须创建成唯一索引(来源:Alibaba)。
    • 说明:不要以为唯一索引影响了insert的速度,这个速度是可以忽略的,但会明显提高查询速度。
  2. 频繁在WHERE查询语句中使用的字段

    • 给频繁在WHERE语句中使用的字段创建普通索引就可以大大提升查询效率,尤其在数据量大的情况下。
  3. 经常GROUP BY 和 ORDER BY 的字段

    • 创建索引的目的就是让数据按某种顺序进行存储或检索。
    • 我们用ORDER BY排序,或者GROUP BY分组查询的时候,数据就已经是排好序的,就节省了排序的时间,分组查询也更快。
    • 如果待排序的字段有多个,就可以创建联合索引
  4. UPDATE、DELETE的WHERE语句中的条件字段

    • 因为我们们需要先根据WHERE条件列检索出这条记录,然后再对其进行更新或删除。
    • 如果更新的字段是非索引字段,更新效率会明显提升,因为非索引字段更新不需要对索引进行维护。
  5. DISTINCT去重的字段

    • 因为索引字段在B+树中是按顺序排序的,去重、排序、分组等速度就会更快。
  6. 多表JOIN连接操作时,创建索引注意事项

    • 首先,连接表的数量尽量不要超过3张,因为每增加一张表就相当于增加了一次嵌套循环,数量级增长很快,严重影响查询效率。
    • 其次,对WHERE的条件字段创建索引,WHERE对于已排序的数据过滤会更快。
    • 最后,对JOIN...ON连接表的字段创建索引,并且连接表的两个字段的数据类型必须一致,因为不一致的话就会存在隐式类型转换,这个转换是需要调用函数来实现的,而索引在函数中使用就会失效
  7. 使用字段类型小的创建索引

    • 这里的类型大小是指表示数据范围的大小
    • 以整型为例:TINYINT、MENIUMINT、INT、BIGINT,我们能使用TINYINT就不要使用BIGINT,因为:
      • 数据类型越小,查询的时候的比较操作更快
      • 数据类型越小,索引占用的存储空间就越小,数据页中就能放更多的记录,从而减少磁盘IO次数,加快了读写效率。
    • 尤其是表的主键更加适用,因为聚簇索引和非聚簇索引都会存储主键值,主键的数据类型小,占用的存储空间就小,数据页就可以存储更多的记录,从而减少了磁盘IO次数,也节省了存储空间。
  8. 使用字符串前缀创建索引

    • 假设我们的字符串很长,那么存储一个字符串就会占用很大的存储空间,我们用这个很长的字符串创建索引B+树中就会存在下列两个问题:
      • 字符串越长,在B+树中占用的存储空间就越大。
      • 字符串越长,在字符串比较操作时会占用更多的时间。
    • 因而我们可以截取字符串的前面一部分字符创建索引,这就是前缀索引
    • 但这也会存在一些问题,比如截取多少个字符?截取多了会浪费存储空间;截取少了,重复内容太多(可能前面几个字符刚好是一样的,后边的才不一样),字段的散列型(选择性)会降低。
      • 【Alibaba开发手册】在varchar字段上创建索引时,必须指定索引长度,每毕业对全字段创建索引,根据实际文本区分度决定索引长度。
      • 说明:索引的长度于区分度是一对矛盾体,一般字符串类型数据,长度为20的索引,区分度会达到90%以上,可以使用count(distinct left(字段,索引长度))/count(*)来计算区分度进而确定索引长度,区分度越接近1越好,因为重复的数据越少。
  9. 区分度高(散列性高)的字段适合创建索引

    • 列的基数是指某一列中不重复的数据个数,也就是去重后的数据个数。
    • 在记录行数一定的情况下,列的基数越大,该列的区分度、散列性就越高(重复的数据少),就越适合做索引。
    • count(distinct left(字段,索引长度))/count(*)来计算区分度,值越接近1越适合做为索引,因为重复的数据越少,一般超过33%就很适合做索引了。
  10. 将使用最多的字段放到联合索引的左侧

    • 因为最左前缀原则,可以提高联合索引的使用率,因为最左前缀原则就是从联合索引最左边开始依次查询
  11. 在多个字段都要创建索引的情况下,联合索引优于单值索引

  • 建议 单表索引数量不超过6个
  • 在实际工作中我们要限制每张表的索引的数量,索引并非越多越好,原因:
    • 每个索引都需要占用磁盘空间,索引越多,占用的磁盘空间就越多。
    • 索引会影响INSERT、DELETE、UPDATE等语句性能,因为每张表的数据被更改时,索引也会进行调整和更新。
    • 优化器在选择如何优化查询时,会在所有的索引中选择最优的索引来创建一个执行计划,如果索引太多,这个选择最优索引的时间就会更长,会增加MySQL优化器生成执行计划的时间,降低查询性能。

11.3.2、不适合创建索引的情况

  1. 在WHERE中用不到的字段,不要设置索引
    • 索引的价值是快速定位,如果起不到快速定位作用的字段,是不需要创建索引的。
  2. 数据量小的表,就不要创建索引
    • 比如记录小于1000个的表就不要创建索引了。
    • 数据少的表查询的速度可能比遍历索引还要快,索引可能不会产生优化效果。
  3. 有大量重复值的字段不适合创建索引
    • 当数据重复度大,比如高于10%的时候,我们就不要用于创建索引了,
    • 如果创建了,不但不会提高查询效率,还会严重降低数据更新速度。
  4. 避免对经常更新的表创建过多的索引
    • 第一层含义:频繁更新的字段尽量不要创建索引。因为更新数据的时候,也需要更新索引。
    • 第二层含义:经常更新的表不要创建过多索引,并且索引中的列尽可能的少。否则虽然会提高查询速度,但会降低更新表的速度。
  5. 不要使用无序的值作为索引
    • 因为索引是有序排列的,若数据是无序的在插入数据时,会重复的重新排序,页分裂等十分耗时。
  6. 删除不再使用或很少使用的索引
    • 删除不再使用的索引可以减少对更新操作的影响。
  7. 不要定义冗余或重复的索引

十二、性能分析工具的使用

在数据库调优中,我们的目标就是响应时间更快吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮助我们快速的找到调优的思路和方式。

12.1、数据库服务器的优化步骤

当我们遇到数据库调优问题的时候,该如何思考呢?这里把思考的流程整理成下面这张图。整个流程划分成了 观察(Show status)行动(Action) 两个部分。字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。

MySQL高级(SQL优化)_第18张图片
MySQL高级(SQL优化)_第19张图片
小结:
MySQL高级(SQL优化)_第20张图片

12.2、查看系统性能参数

在MySQL中,可以使用 SHOW STATUS 语句查询一些MySQL数据库服务器的 性能参数执行频率
语法如下:SHOW [GLOBAL|SESSION] STATUS LIKE '参数';
一些常用的性能参数如下:

  • Connections:连接MySQL服务器的次数。
  • Uptime:MySQL服务器的上线时间。
  • Slow_queries:慢查询的次数。
  • Innodb_rows_read:Select查询返回的行数 •
  • Innodb_rows_inserted:执行INSERT操作插入的行数
  • Innodb_rows_updated:执行UPDATE操作更新的行数
  • Innodb_rows_deleted:执行DELETE操作删除的行数
  • Com_select:查询操作的次数。
  • Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
  • Com_update:更新操作的次数。
  • Com_delete:删除操作的次数。

12.3、统计SQL的查询成本:last_query_cost

一条SQL查询语句在执行前需要确定查询执行计划,如果存在多个查询计划的话,MySQL会计算每个执行计划所需要的成本,从中选择成本最小的作为最终的执行计划并执行。

如果我们想要查看某条SQL语句的查询成本,可以在这条语句执行完之后,通过查看当前会话中的last_query_cost变量得到当前查询的成本。它通常也是我们评价一个查询的执行效率的一个常用标准。这个查询成本对应的是SQL语句所需要读取的页的数量

语法:SHOW STATUS LIKE 'last_query_cost';

SQL查询时一个动态过程,从页加载的角度来看,我们可以得到以下两点结论:

  1. 位置决定效率
    • 如果数据页就在数据库缓冲池中,那么效率时最高的,否则就需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
  2. 批量决定效率
    • 如果我们从磁盘中对单一页进行随机读取,那么效率时很低的,差不多10ms,而采用顺序读取的方式,对页进行批量读取,效率就会高很多。
  • 所以说我们要尽量将经常使用的数据放到缓冲池中,或者充分利用磁盘的吞吐能力,一次批量读取更多的数据,这样单个页的读取效率也就得到了提升。

12.4、定位执行慢的SQL:慢查询日志

  • MySQL的慢查询日志用来记录那些响应时间超过阈值的语句,超过阈值的语句都会被记录到慢查询日志中,默认的阈值为10s,即long_query_time=10
  • 默认情况下,MySQL数据库没有开启慢查询日志,一般在我们需要调优的时候手动开启,因为开启慢查询日志会影响性能。
  • 他的作用是帮助我们发现那些执行时间长的SQL查询语句。

12.4.1、开启慢查询日志

set global slow_query_log='ON';

12.4.2、查看慢查询日志阈值

SHOW VARIABLES LIKE '%long_query_time%';

12.4.3、修改慢查询日志阈值

SET GLOBAL long_query_time=数值;

  • 注意:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。
  • 也可以修改my.cnf文件,然后重启服务器
    [mysqld]
    slow_query_log=ON
    slow_query_log_file=/var/lib/mysql/文件名.log #若不指定存储路径,慢查询日志会默认保存在MySQL数据库的数据文件夹下,若不指定文件名,默认为hostname-slow.log
    long_query_time=阈值时间
    log_output=FILE
    

12.4.4、查看慢查询数目

SHOW GLOBAL STATUS LIKE '%slow_queries%';

12.4.5、关闭慢查询日志

  • 方式1:永久性方式,在my.cnf文件中将slow_query_log一项注释掉 删除,然后重启MySQL服务。
  • 方式2:临时性方式
    set global slow_query_log='OFF';

12.4.6、删除慢查询日志

  • 我们可以先执行SHOW VARIABLES LIKE 'slow_query_log';查看日志所在的目录,然后找到日志文件,通过命令行手动删除即可。
  • 也可以使用mysqladmin -uroot -p flush-logs slow;来重新生成日志文件。

提示:
慢查询日志都是使用mysqladmin flush-logs命令来删除重建的。使用时一定要注意,这个命令一旦执行,慢查询日志只存在于日志文件中,如果需要旧的查询日志,就必须事先备份。

12.5、慢查询日志分析工具:mysqldumpslow

在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具
mysqldumpslow
语法:mysqldumpslow 参数;,需要在数据库根目录下执行,因为这是一个脚本文件。
参数:

  • a: 不将数字抽象成N,字符串抽象成S
  • s: 是表示按照何种方式排序:
    • c: 访问次数
    • l: 锁定时间
    • r: 返回记录
    • t: 查询时间
    • al:平均锁定时间
    • ar:平均返回记录数
    • at:平均查询时间 (默认方式)
    • ac:平均查询次数
  • t: 即为返回前面多少条的数据;
  • g: 后边搭配一个正则匹配模式,大小写不敏感的;

示例:

#得到返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log
#得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log
#得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/atguigu-slow.log
#另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more

12.6、查看 SQL 执行成本:SHOW PROFILE

通过SHOW PROFILE指令可以查看执行过的SQL语句的执行时间,cpu损耗等信息。
查看是否开启:show variables like 'profiling';
开启:set profiling = 'ON';
查询执行过的语句的执行成本:show profiles;
查询上一条语句的执行成本:show profile;
查询具体某条语句的某些执行成本: show profile cpu,block io for query 编号,show profiles;可以显示所有执行过的语句,即每一条的编号。

show profile的常用查询参数:
① ALL:显示所有的开销信息。
② BLOCK IO:显示块IO开销。
③ CONTEXT SWITCHES:上下文切换开销。
④ CPU:显示CPU开销信息。
⑤ IPC:显示发送和接收开销信息。
⑥ MEMORY:显示内存开销信息。
⑦ PAGE FAULTS:显示页面错误开销信息。
⑧ SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。
⑨ SWAPS:显示交换次数开销信息。

日常开发需要注意的结论:
converting HEAP to MyISAM:查询结果太大,内存不够,数据往磁盘上搬了。
Creating tmp table:创建临时表。先拷贝数据到临时表,用完再删除临时表。
Copying to tmp table on disk:把内存中临时表复制到磁盘上,警惕!
locked
注意如果在show profile诊断结果中出现以上任意一条,则sql语句需要优化。

**注意:**show profile命令将被弃用,我们可以从information_schema中的profiling数据表进行查看。

12.7、分析查询语句:EXPLAIN

12.7.1、概述

在通过慢查询日志定位了查询慢的SQL语句之后,我们就可以使用EXPLAINDESCRIBE工具对语句进行分析。

EXPLAIN能查看什么?

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

版本情况:

  • MySQL 5.6.3以前只能 EXPLAIN SELECT ;MYSQL 5.6.3以后就可以 EXPLAIN SELECT,UPDATE,DELETE。
  • 在5.7以前的版本中,想要显示 partitions 需要使用 explain partitions 命令;想要显示filtered 需要使用 explain extended 命令。在5.7版本后,默认explain直接显示partitions和filtered中的信息。

注意: EXPLAIN或DESCRIBE分析的语句并不会真正的执行,比如你分析一条DELETE语句,但DELETE语句并不会真正的执行删除操作,只是分析这条语句的执行计划。

12.7.2、语法

EXPLAIN 或 DESCRIBE语句的语法形式如下:

  • EXPLAIN SQL语句;
    或者
  • DESCRIBE SQL语句;
  • 示例:EXPLAIN SELECT 字段 FROM 表;
  • 注意:结果集中第一行记录对应的表为驱动表

分析结果集字段:
在这里插入图片描述

字段 描述
id 每一个SELECT关键字都对应一个唯一id,若有两个SELECT关键字,则有两个唯一id,即1,2。有时候优化器可能会将我们的一条多表查询语句拆分为两条执行,导致本来有两个SELECT关键字的一条语句,变成了只有一个SELECT关键字的语句两条语句。
select_type 查询的类型
table 表名,一行记录对应一个单表
partitions 分区表的信息,非分区表的值为NULL
type 表的查询方法system、 const、 eq_ref、ref、fulltext、 ref_or_null、index_merge、 unique_subquery、 index_subquery、 range、 index、 ALL,越靠前性能越好
possible_keys 可能用到的索引
key 实际上使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息

12.7.3、explain4种输出格式

这里谈谈EXPLAIN的输出格式。EXPLAIN可以输出四种格式:传统格式JSON格式TREE格式 以及 可视化输出 。用户可以根据需要选择适用于自己的格式。

传统格式

传统格式简单明了,输出是一个表格形式,概要说明查询计划。
在这里插入图片描述

JSON格式

  • JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON
    EXPLAIN FORMAT=JSON SELECT ....
  • JSON格式会显示比传统格式更详细的信息。

TREE格式

TREE格式是8.0.16版本之后引入的新格式,主要根据查询的 各个部分之间的关系各部分的执行顺序 来描述如何查询。
eg: EXPLAIN FORMAT=tree SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field = 'a'\G

可视化输出

可视化输出,可以通过MySQL Workbench可视化查看MySQL的执行计划。通过点击Workbench的放大镜图标,即可生成可视化的查询计划。

上图按从左到右的连接顺序显示表。红色框表示 全表扫描 ,而绿色框表示使用 索引查找 。对于每个表,显示使用的索引。还要注意的是,每个表格的框上方是每个表访问所发现的行数的估计值以及访问该表的成本。

12.7、SHOW WARNINGS的使用

在我们使用EXPLAIN语句查看了某个查询计划后,还可以紧接着使用SHOW WARNINGS 语句查看与这个查询执行计划有关的一些拓展信息,和具体的执行计划执行的语句。

语法:SHOW WARNINGS\G

12.8、分析优化器执行计划:trace

OPTIMIZER_TRACE是MySQL 5.6 引入的一项跟踪功能,它可以跟踪优化器做出的各种决策(比如访问表的方法、各种开销计算、各种转换等),并将跟踪结果记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE表中。

语法:select * from information_schema.optimizer_trace\G

此功能默认是关闭的,开启方式:

# 开启trace,并设置格式为JSON
SET optimizer_trace = "enabled=on",end_markers_in_json=on;
#设置trace的最大内存,避免分析过程中因为默认内存过小而不能完整展示分析信息
SET optimizer_trace_max_size=1000000;

开启后,可分析如下语句:

  • SELECT
  • INSERT
  • REPLACE
  • UPDATE
  • DELETE
  • EXPLAIN
  • SET
  • DECLARE
  • CASE
  • IF
  • RETURN
  • CALL

结果集分析:

//第1部分:查询语句
QUERY: select * from student where id < 10
//第2部分:QUERY字段对应语句的跟踪信息
TRACE: {
"steps": [
{
"join_preparation": { //预备工作
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `student`.`id` AS
`id`,`student`.`stuno` AS `stuno`,`student`.`name` AS `name`,`student`.`age` AS
`age`,`student`.`classId` AS `classId` from `student` where (`student`.`id` < 10)"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { //进行优化
"select#": 1,
"steps": [
{
"condition_processing": { //条件处理
"condition": "WHERE",
"original_condition": "(`student`.`id` < 10)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(`student`.`id` < 10)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(`student`.`id` < 10)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "(`student`.`id` < 10)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": { //替换生成的列
} /* substitute_generated_columns */
},
{
"table_dependencies": [ //表的依赖关系
{
"table": "`student`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [ //使用键
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [ //行判断
{
"table": "`student`",
"range_analysis": {
"table_scan": {
"rows": 3973767,
"cost": 408558
} /* table_scan */, //扫描表
"potential_range_indexes": [ //潜在的范围索引
{
"index": "PRIMARY",
"usable": true,
"key_parts": [
"id"
] /* key_parts */
}
] /* potential_range_indexes */,
"setup_range_conditions": [ //设置范围条件
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
"skip_scan_range": {
"potential_skip_scan_indexes": [
{
"index": "PRIMARY",
"usable": false,
"cause": "query_references_nonkey_column"
}
] /* potential_skip_scan_indexes */
} /* skip_scan_range */,
"analyzing_range_alternatives": { //分析范围选项
"range_scan_alternatives": [
{
"index": "PRIMARY",
"ranges": [
"id < 10"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": true,
"using_mrr": false,
"index_only": false,
"rows": 9,
"cost": 1.91986,
"chosen": true
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */,
"chosen_range_access_summary": { //选择范围访问摘要
"range_access_plan": {
"type": "range_scan",
"index": "PRIMARY",
"rows": 9,
"ranges": [
"id < 10"
] /* ranges */
} /* range_access_plan */,
"rows_for_plan": 9,
"cost_for_plan": 1.91986,
"chosen": true
} /* chosen_range_access_summary */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [ //考虑执行计划
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`student`",
"best_access_path": { //最佳访问路径
"considered_access_paths": [
{
"rows_to_scan": 9,
"access_type": "range",
"range_details": {
"used_index": "PRIMARY"
} /* range_details */,
"resulting_rows": 9,
"cost": 2.81986,
"chosen": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100, //行过滤百分比
"rows_for_plan": 9,
"cost_for_plan": 2.81986,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": { //将条件附加到表上
"original_condition": "(`student`.`id` < 10)",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [ //附加条件概要
{
"table": "`student`",
"attached": "(`student`.`id` < 10)"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"finalizing_table_conditions": [
{
"table": "`student`",
"original_table_condition": "(`student`.`id` < 10)",
"final_table_condition ": "(`student`.`id` < 10)"
}
] /* finalizing_table_conditions */
},
{
"refine_plan": [ //精简计划
{
"table": "`student`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": { //执行
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
//第3部分:跟踪信息过长时,被截断的跟踪信息的字节数。
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0 //丢失的超出最大容量的字节
//第4部分:执行跟踪语句的用户是否有查看对象的权限。当不具有权限时,该列信息为1且TRACE字段为空,一般在
调用带有SQL SECURITY DEFINER的视图或者是存储过程的情况下,会出现此问题。
INSUFFICIENT_PRIVILEGES: 0 //缺失权限

12.8、MySQL监控分析视图-sys schema

关于MySQL的性能监控和问题诊断,我们一般都从performance_schema中去获取想要的数据,在MySQL 5.7.7 版本中新增sys schema,它将performance_schema和information_schema中的数据以更容易理解的方式总结归纳为“视图”,其目的就是为了降低查询performance_schema的复杂度,让DBA能够快速的定位问题。下面看看这些库中都有哪些监控表和视图,掌握了这些,在我们开发和运维的过程中起到事半功倍的效果。

12.8.1、Sys schema视图摘要

  1. 主机相关:以host_summary开头,主要汇总了IO延迟的信息。
  2. Innodb相关:以innodb开头,汇总了innodb buffer信息和事务等待innodb锁的信息。
  3. I/o相关:以io开头,汇总了等待I/O、I/O使用量情况。
  4. 内存使用情况:以memory开头,从主机、线程、事件等角度展示内存的使用情况
  5. 连接与会话信息:processlist和session相关视图,总结了会话相关信息。
  6. 表相关:以schema_table开头的视图,展示了表的统计信息。
  7. 索引信息:统计了索引的使用情况,包含冗余索引和未使用的索引情况。
  8. 语句相关:以statement开头,包含执行全表扫描、使用临时表、排序等的语句信息。
  9. 用户相关:以user开头的视图,统计了用户使用的文件I/O、执行语句统计信息。
  10. 等待事件相关信息:以wait开头,展示等待事件的延迟情况。

12.8.2、Sys schema视图使用场景

索引情况

#1. 查询冗余索引
select * from sys.schema_redundant_indexes;
#2. 查询未使用过的索引
select * from sys.schema_unused_indexes;
#3. 查询索引的使用情况
select index_name,rows_selected,rows_inserted,rows_updated,rows_deleted
from sys.schema_index_statistics where table_schema='dbname' ;

表相关

# 1. 查询表的访问量
select table_schema,table_name,sum(io_read_requests+io_write_requests) as io from
sys.schema_table_statistics group by table_schema,table_name order by io desc;
# 2. 查询占用bufferpool较多的表
select object_schema,object_name,allocated,data
from sys.innodb_buffer_stats_by_table order by allocated limit 10;
# 3. 查看表的全表扫描情况
select * from sys.statements_with_full_table_scans where db='dbname';

语句相关

#1. 监控SQL执行的频率
select db,exec_count,query from sys.statement_analysis
order by exec_count desc;
#2. 监控使用了排序的SQL
select db,exec_count,first_seen,last_seen,query
from sys.statements_with_sorting limit 1;
#3. 监控使用了临时表或者磁盘临时表的SQL
select db,exec_count,tmp_tables,tmp_disk_tables,query
from sys.statement_analysis where tmp_tables>0 or tmp_disk_tables >0
order by (tmp_tables+tmp_disk_tables) desc;

IO相关

#1. 查看消耗磁盘IO的文件
select file,avg_read,avg_write,avg_read+avg_write as avg_io
from sys.io_global_by_file_by_bytes order by avg_read limit 10;

Innodb 相关

#1. 行锁阻塞情况
select * from sys.innodb_lock_waits;

十三、索引优化与查询优化

  • 优化的本质就是你需要清楚MySQL 各个语句的执行流程数据的存储机制
  • SQL优化的大方向就是 查询速度快、吞吐量大

哪些维度可以进行数据库调优:

  • 索引失效、没有充分利用到索引——建立索引
  • 关联查询太多JOIN(设计缺陷或不得已的需求)——SQL优化,关联表一般不超过3个
  • 服务器调优及各参数设置(缓冲、线程数)——调整my.cnf
  • 数据过多——分库分表

虽然SQL查询优化的技术很多,但是大方向上可以分成物理查询优化逻辑查询优化两部分。

  • 物理查询优化:通过索引表连接方式等技术来进行优化,重点是索引的使用。
  • 逻辑查询优化:通过SQL等价变换提升查询效率,也就是换一种查询写法执行效率可能更高。

13.1、索引优化建议

合理的索引是MySQL中提高性能的有效方式之一。

  • 使用索引可以快速定位查询的记录。
  • 若查询的时候没有使用索引,MySQL就会扫描表中的所有记录。在数据量大的时候,这样的查询速度很慢。

其实,用不用索引最终都是优化器说了算。优化器是基于什么优化的?优化器是基于cost开销来生成相应的执行计划,怎样开销小就怎样来。另外,SQL语句是否使用索引,与数据库版本、查询数量、数据选择度都有关系

13.1.1、全值索引

  • 将WHERE语句中的条件字段全部创建联合索引,比部分创建索引效率更高。
  • 因为优化器会更具索引列表中的索引字段选择最佳的字段最为索引。

13.1.2、最佳左前缀法则

在MySQL建立联合索引时,会遵循最佳左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边的字段开始匹配。

  • WHERE条件字段的顺序也要保持和联合索引列表中字段的顺序一致,
  • 优化器会根据条件语句中的字段的顺序选择最佳的联合索引。

注意:

  • 联合索引可以为多个字段创建索引,1个联合索引可以有16个字段,
  • 对于联合索引,过滤条件要使用联合索引必须按照索引建立是的顺序,依次满足,一旦跳过某个字段,索引后边的字段都无法使用。
  • 如果查询条件中没有使用联合索引中的第1个字段,则联合索引不会被使用。

13.1.3、主键连续递增插入

  • 对于一个使用InnoDB存储引擎的表来说,如果我们没有显示的创建索引,表中的数据都会存储在聚簇索引的叶子节点上的数据页中,
  • 数据页和记录都是按主键值从小到大的顺序排列的,如果我们插入的记录的主键值是依次增大且连续的,那么,我们没插满一个数据页就会切换到下一个数据页。
  • 如果我们插入的主键值忽大忽小的话,数据页就需要页分裂,重新排列,十分麻烦。
  • 所以我们建议:让主键AUTO_INCREMENT,让存储引擎自己为表生成主键,而不是我们手动插入 。
  • 我们自定义的主键列id拥有AUTO_INCREMENT属性,在插入记录时存储引擎会自动为我们填入自增的主键值。这样的主键占用空间小,顺序写入,减少页分裂。

13.1.4、运算、函数、类型转换(自动或手动)会导致索引失效

运算、函数、类型转换(自动或手动)都会导致索引失效。

  • 比如索引字段在函数中使用,字段会先在函数中被调用,不会直接走B+树的索引目录页,

13.1.5、范围查询的索引会失效

  • 在联合索引中,若索引列表中的字段用在了“大于”、“小于”等范围查询中,会导致索引失效。
  • 因为表示范围的不能直接在B+树中找到对应的值,需要逐一进行排查。
  • 解决方案:
    将用于范围查询的字段定义在联合索引列表的后边,因为联合索引遵循最左前缀原则。

13.1.6、不等于(!=/<>)会导致索引失效

因为“=”的时候可以在B+树中直接查找相应的值,不等于的时候,需要从B+树中一个一个的排查。

13.1.7、is not null无法使用索引

  • is null可以使用索引,is not null无法使用索引。
  • is null可以直接查询相应的值。
  • is not null无法直接查找相应的值,需要在B+树中逐一查询。
  • 结论:
    • 在设计表时,都将字段设置为NOT NULL,INT类型设置默认值0,字符串类型设置默认值’ '。
    • 同理:not like也无法使用索引,会导致全表扫描

13.1.8、like以通配符%开头索引失效

以%开头的模糊匹配索引会失效,因为SQL会进行全表扫描,无法直接定位具体的值,需要逐一排查。

【Alibaba开发手册】
【强制】页面搜索严禁使用左模糊或者全模糊,如果需要请走搜索引擎来解决。

13.1.9、OR 前后存在非索引的列,索引失效

因为索引列会进行索引查询,非索引字段会进行全表扫描,反而降低了查询效率。

13.1.10、数据库和表的字符集统一使用utf8mb4

  • 统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的字符集进行比较前需要进行转换会造成索引失效。
  • 因为字符集转换也需要调用函数。

13.1.11、总结

  • 对于单列索引,尽量选择针对当前query过滤性更好的索引。
  • 在选择组合索引的时候,当前query中过滤性最好的字段在所有字段顺序中,位置越靠前越好。
  • 在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引。
  • 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引列表的最后面。
  • 索引并非创建了会被使用,具体用不用索引由优化器说了算,由时候使用索引的效率还不如不适用索引高。因为在非主键索引的情况下需要回表操作,在数据少的时候,直接遍历数据表会更快。优化器会根据具体成本生成最优的执行计划。

13.2、多表(关联)查询优化

13.2.1、外连接优化

外连接以左外连接为例,其他外连接类比即可。

  • 给外连接的连接条件的字段创建索引,来优化连接。
  • 连接条件的两个字段的数据类型必须一致,否则就会调用函数做隐式类型转换,索引会失效。
  • 多表查询的左表一般为驱动表,后者一般为被驱动表。先读取驱动表的数据,再读取被驱动表。也可能不是这样,因为小表驱动大表,优化器底层会将外连接转换为内连接来执行。
  • 如果只能添加一个索引,最好给被驱动表的连接字段添加索引,可以避免全表扫描

13.2.2、内连接

  • 对于内连接来说,驱动表和被驱动表的选择是由优化器来决定的,因为内连接查询的是量表中都满足条件的数据,优化器会根据表来分析谁作为驱动表。
  • 对于内连接,如果表的连接条件中只能有一个字段有索引,那么有索引的字段会被作为被驱动表
  • 对于内连接,优化器会选择小表(数据少的表)作为驱动表。小表驱动大表
  • 可以先用explain分析一下谁是驱动表,谁是被驱动表,再添加索引。结果集第一条记录对应的表是驱动表。

13.3、JOIN语句原理

  • 多表连接通过join实现,本质就是各个表之间数据的循环匹配
  • MySQL5.5之前,仅支持一种表间关联方式,就是嵌套循环算法(Nested Loop Join),如果关联表的数据量大,则join关联的执行时间会很长。
  • 在MySQL5.5之后,MySQL通过引入BNLJ算法来优化嵌套执行。

13.3.1、驱动表和被驱动表

  • 驱动表就是主表,被驱动表就是从表、非驱动表。
  • 小表驱动大表。即数据量小的表驱动数据量大的表。

13.3.2、Simple Nested-Loop Join(简单嵌套循环连接)

简单嵌套循环连接就是没有索引的多表连接

算法原理:比如a(主表),b(从表)两张没有索引的表。

  • 主表a中每一条数据都会与从表b中的所有数据匹配一遍,因而效率极低
  • MySQL肯定不会使用这种方式进行表的连接,因而引出了Index Nested-Loop Join优化算法。
    MySQL高级(SQL优化)_第21张图片

13.3.3、Index Nested-Loop Join(索引嵌套循环连接)

  • 索引嵌套循环连接即用索引作为多表连接的条件
  • INLJ优化的思路主要就是减少内层表数据匹配的次数,所以要求被驱动表上必须有索引才行。
  • 通过外层匹配条件直接与内层表索引进行匹配,避免内层表的每条记录都去进行比较,这样极大减少了对内层表的匹配次数。
  • 驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故MySQL优化器都倾向于使用记录少的表作为驱动表(外表)。(小表驱动大表原因)
  • 但如果被驱动表的索引不是主键索引,还得进行一次回表查询,索引被驱动表的索引是主键索引,效率会更高。
    MySQL高级(SQL优化)_第22张图片

13.3.4、Block Nested-Loop Join(块嵌套循环连接)

块嵌套循环连接就是解决没有索引的多表连接的效率问题

如果存在索引,那么会使用索引的方式进行多表连接,如果没有索引,被驱动表就要多次全表扫描。驱动表中有多少条记录,被驱动表就要被加载到内存中多少次,然后进行匹配,因为每一次匹配结束都会清除内存,这样的周而复始的操作,大大增加了IO次数。为了减少IO次数,就出现了Block Nested-Loop Join的方式。

BNLJ不再一条一条的从磁盘读取数据到内存,而是一块一块的读取,引入了join buffer(缓冲区),将驱动表join相关的部分数据列缓存到join buffer中(缓存多少数据由缓冲区大小决定),然后全表扫描驱动表,被驱动表的每一条记录一次性和缓冲区中所有的驱动表数据进行匹配(在内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。

默认情况下block_nested_loop是开启的,我们可以通过SHOW VARIABLES LIKE '%optimizer_switch%';查看开启状态。

注意:

  • 这里缓存的不仅是关联表的列,select查询语句中的列也会被缓存起来。所以查询的时候尽量减少不必要的字段,让缓冲区可以存放更多的字段。
  • 在有N个表join连接的时候,会分配N-1个缓冲区。
  • 默认情况下缓冲区大小join_buffer_size=256k,缓冲区最大值在32位操作系统中可以申请4G,而64位操作系统中可以大于4G(Window系统除外,最大值会被截断为4G并发出警告)
  • 在MySQL 80.20废弃了Block Nested-Loop Join,使用了Hash Join。

MySQL高级(SQL优化)_第23张图片

13.3.5、小结

  1. 总体效率:用索引作为多表连接条件效率最高。
  2. 永远用小结果集驱动大结果集(其本质就是减少外层循环的数量,外层循环:与磁盘IO)(小的度量单位是 表的行数*每行大小
    • 小表驱动大表,这里小的度量是结果集数量还是表的记录数量,要看有没有用索引。(个人理解)
      • 有索引,则结果集大小是度量单位。有索引会直接通过索引匹配,不会进行全表扫描,因而看结果集大小,就是匹配次数。
      • 无索引,则表中的记录行数是度量单位。没有索引的时候主表和从表都要进行全表扫描(考虑WHERE条件,可能只扫描部分),因而应该从扫描次数去衡量表的大小。
  3. 为被驱动表匹配的条件创建索引(减少内存表的循环匹配次数)。
  4. 增大缓冲区的大小(一次缓存的数据越多,内存表的扫描次数就越少)。
  5. 除多表连接字段以外,减少驱动表不必要的select查询字段(查询的字段也会被缓存,查询字段越少,缓冲区缓存的数据就越多)。
  6. 不建议使用子查询,建议将子查询拆开结合程序多次查询,或使用JOIN多表关联来代替子查询,能够直接多表关联的尽量直接关联。
  7. 衍生表(临时表)建不了索引。

13.4、Hash Join

MySQL从8.0.20开始废弃了BNLJ,因为从MySQL8.0.18开始默认都采用了Hash Join。

  • Nested Loop:对于连接的数据集较小的情况,是个较好的选择,但对于数据集大的表就不太理想。
  • Hash Join:做大数据集连接时的常用方式,优化器使用两个表中较小的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出于Hash表匹配的行。
    • 这种方式适用于较小的表完全可以防御内存中的情况,这样总成本就是访问两个表的成本之和。
    • 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。
    • 它能够很好的工作于没有索引的大表和并行查询的环境中,并能提供最好的性能。Hash Join只能用于等值连接,这是由Hash的特点决定的。

Nested Loop和Hash Join对比

类别 Nested Loop Hash Join
适用条件 任何条件 等值连接(=)
占用资源 CPU、磁盘I/O 内存、临时空间
特点 当由高选择性索引或进行限制性搜索时效率较高,能快速返回第一次搜索结果。 当缺乏索引或者索引条件模糊时,效率较高。在数据仓库环境下,如果表的记录多,效率高。
缺点 当索引丢失或者查询条件限制不够时,效率极低;当表的记录行数多时,效率极低。 为建立哈希表,需要大量内存。第一次的结果返回较慢。

13.5、子查询优化与排序优化

13.5.1、使用多表查询代替子查询

  • MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。

  • 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作

  • 子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子查询的执行效率不高。原因:

    ① 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。

    ② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。

    ③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

在MySQL中,可以 使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引的话,性能就会更好。

结论:
尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN...ON...WHERE... IS NULL替代

13.5.2、给排序的字段创建索引

  • 问题: 在WHERE条件字段上加索引,但是为什么在ORDER BY字段上还要加索引呢?

    • 因为索引是有序排列的,不需要在ORDER BY的时候再次排序。
    • Index排序中,索引可以保证数据的有序性(在磁盘上就是有序排序的),不需要在进行排序,效率更高。
    • FileSort排序,一般在内存中进行排序,占用CPU较多。如果排序的数据多,内存一次放不下那么多数据,就会产生临时文件I/O到磁盘进行排序,效率较低。
  • 优化建议:

    • SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中避免全表扫描,在 ORDER BY 子句避免使用 FileSort 排序。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
    • 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
    • 无法使用 Index 时,需要对 FileSort 方式进行调优。
    • 索引并非创建了会被使用,具体用不用索引由优化器说了算,由时候使用索引的效率还不如不适用索引高。因为在非主键索引的情况下需要回表操作,在数据少的时候,直接用BNLJ的方式扫描数据表会更快。优化器会根据具体成本生成最优的执行计划。
  • 注意:

    • ORDER BY 后边的字段如果需要升序或者降序排序需要保持统一,也就是要么都是升序,要么都是降序;如果一个升序,一个降序索引会失效。因为因为索引会正向和反向分别扫描,效率不佳。

13.5.3、小结

  • 多个索引同时存在,MySQL会自动选择最优的方案,但是随着数据量的变化,选择的索引也会随之改变
  • 当【范围条件】和【group by或者order by】的字段出现二选一时,优先观察字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之亦然

13.6、filesort算法:双路排序和单路排序

  • 双路排序(慢)
    • MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据.
    • 首先读取行指针和order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
    • 第一次从磁盘取排序字段,在buffer进行排序,第二次从磁盘取其他字段 。

取一批数据,要对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。

  • 单路排序 (快)

    • 一次从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。
    • 并且把随机IO变成了顺序IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。
  • 单路排序问题:

    • 在sort_buffer中,单路比多路要占用更多的空间,因为单路将所有的字段都读取出来,所有有可能读取的数据超出了sort_buffer的容量,导致每次只能读取一定数量的数据,单次能排序的数据数量就是sort_buffer的容量,会导致多次排序,多次IO,
    • 单路本来就是为了减少IO操作,反而导致了大量的IO操作,反而得不偿失。
  • 单路排序优化策略:

    • 提高sort_buffer_size的容量
      • 查看容量:show variables like '%sort_buffer_size%';
      • 要根据使用的系统去调整,因为参数时针对每个进程(connection)的1M~8M之间调整。MySQL5.7,InnoDB默认为104576字节,1MB。
    • 提高max_length_for_sort_data
      • 这个参数会增加使用单路算法的概率。
      • 查看参数:show variables like '%max_length_sort_data%';
      • 默认大小为1024字节。
    • 不要使用select *,最好只query需要的字段

当query的字段大小总和小于max_length_for_sort_data,而且排序字段不是TEXT、BLOB类型时,会使用单路算法,否则会用多路算法。

单路和多路两种算法的数据都有可能超出sort_buffer_size的容量,超出后,会创建tmp文件进行合并排序,导致多次IO,但是单路算法的这个风险要大一些,所以要提高sort_buffer_size

13.7、GROUP BY优化

  • group by使用索引的原则几乎跟order by一致,group by即使没有过滤条件用到索引,也可以直接使用索引。
  • group by先排序再分组,遵照索引建立的最佳最前缀法则。
  • 当无法使用索引时,可以通过增大max_length_for_sort_datasort_buffer_size参数来优化查询。
  • where效率高于having,能写在where中的查询条件就不要写在having中了。
  • 减少order by排序操作,能不排序就不排序,或将排序放到程序端去做。order by、group by、distinct这些语句都较为损耗cpu,数据库的cpu资源时及其宝贵的。
  • 包含了order by、group by、distinct等操作的查询语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。

13.8、分页查询优化

  • 一般分页查询时通过创建覆盖索引能够较好地提高性能
  • 另一种情况就很头疼:select * from student 2000000,10,每次扫描了2000000条记录,但是只返回10条记录,就会造成严重的性能浪费。
    • 优化思路1:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容:
      EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a WHERE t.id = a.id;
    • 优化思路2:该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 :
      SELECT * FROM student WHERE id > 2000000 LIMIT 10;

13.9、虑覆盖索引

13.9.1、什么是覆盖索引?

  • 比如对于一个联合索引来说,select查询的字段全在联合索引列表中,也就是索引包含了所有查询的字段,这样就可以避免InnoDB进行索引的二次查询 (回表) ,这就是覆盖索引
  • 简单的说就是:索引列+主键 = SELECT查询的列

13.9.2、覆盖索引的利与弊

好处:

  • 避免InnoDB进行索引的二次查询(回表)

    • InnoDB是以聚簇索引的顺序来存储的,对于InnoDB来说,二级索引在叶子节点中保存的是行的主键信息,如果是用二级索引查询数据,在查找到相应的键值后,还需要通过对主键进行二次查询才能获取我们真实所需的数据。
    • 在覆盖索引中,二次索引的键值中可以获取所有的数据,避免了对主键的二次查询,减少了IO操作,提升了查询效率。
  • 可以把随机IO变成顺序IO加快查询效率

    • 由于覆盖索引是按键值的顺序存储的,对于IO密集型的范围来说,对比随机从磁盘读取每一行的数据IO要少得多,因此利用覆盖索引在访问时也可以把磁盘的随机IO转换成索引查询的顺序IO
    • 由于覆盖索引可以减少数的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段

弊端:

  • 索引字段的维护是有代价的,因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了,这是业务DBA,或者称为业务数据架构师的工作。

13.10、给字符串添加索引

如果select查询的是一个字符串字段,这个字段上又没有索引的话就会做全表扫描

13.10.1、前缀索引

  • MySQL是支持前缀索引的。默认的,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
  • 前缀索引就是使用字符串的前面部分字符作为索引
  • 使用前缀索引查询,定义好长度就可以做到即节省空间,又不用额外增加太多成本的去查询。前面已经研究过区分度了,区分度又高越好,因为区分度越高,就意味着重复的键值越少。

13.10.2、前缀索引对覆盖索引的影响

  • 使用前缀索引,覆盖索引就会失效。
  • 这也是你在选择是否使用前缀索引时需要考虑的一个因素。

13.11、索引下推ICP(索引条件下推)

13.11.1、什么是ICP

  • Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
  • 比如:select * from people where code='1' and fname like '%唐%' and address like '%北京%';,其中code和fname都是索引,但由于fname用于模糊匹配中因而是失效的。
    • 该查询语句没有ICP的执行流程:
      • 先查询所有code=1的记录的id,然后回表查询到所有code=1的记录,再过滤 fname 和 address 得到最终得数据。
    • 有ICP的执行流程:
      • 先查询所有code=1的记录的id,紧接着过滤fname ,再回表,再过滤address ,得到最终得数据

有ICP优化器就会先将有索引的条件先过滤完,再回表,这样就可以减少回表操作,因为每一条数据都是要回表的。

注意:

  • 必须是第二个字段是索引,但是索引失效的情况才会进行ICP。
  • 比如上面的案例,若三个字段全是索引,且未失效,就不会进行ICP,因为通过索引查询本来就是一个最优的查询语句。

13.11.2、ICP的使用条件

① 只能用于二级索引(secondary index),因为有回表行为ICP才有意义。

②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。

③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。

④ ICP可以用于MyISAM和InnnoDB存储引擎。

⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。

⑥ 当SQL使用覆盖索引时,不支持ICP优化方法,因为覆盖索引没有回表操作。

13.11.3、IPC的开启和关闭

ICP默认情况下是打开的,我们也可以手动设置。

打开:SET optimizer_switch = 'index_condition_pushdown=on';
关闭:SET optimizer_switch = 'index_condition_pushdown=off';

打开时,使用explain分析输出结果的Extra显示为Using index condition

13.12、其它查询优化策略

13.12.1、EXISTS 和 IN 的区分

问题:
不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?

回答:
索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率时最高的。

13.12.2、CONUT(*)与COUNT(字段)效率

问题: 在MySQL中统计数据表的行数,可以使用三种方式:sqlect count(*)select count(1)select count(具体字段),使用者三者之间的查询效率时怎样的?

回答:

  • 前提: 如果你要统计的时某个字段的非空数据行数,则另当别论,毕竟,比较执行效率的前提是结果是一样才可以。

  • 环节1:

    • count(*)count(1)都是对所有结果进行count统计求和,count(*)count(1)本质上没有区别,只是执行时间上可能略有差距,不过你可以当他们执行效率是一样的。
    • 如果有where子句,则是对所有符合筛选条件的数据进行统计;
    • 如果没有where子句,则是对表中所有的数据进行统计。
  • 环节2:

    • 如果是MyISAM引擎,统计表的行数的时间复杂度为o(1),因为每张MyISAM的数据表都有一个meta信息存储了row_count值,而一致性则由表级锁来保证。
    • 如果是InnoDB引擎,因为InnoDB是支持事务的,采用行级锁和MVCC机制,所有无法像MyISAM一样,维护一个row_count变量,因此需要进行全表扫描,时间复杂度是o(n),进行循环+计数的方式来完成统计。
  • 环节3:

    • 在InnoDB引擎中,如果采用count(字段)来统计记录行数,要尽量使用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,对于count(*)count(1)来说,它们不需要查找具体的记录,只是统计记录数量,系统会自动采用占用空间更小的二级索引来进行统计。
    • 如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引进行统计。

13.12.3、关于SELECT(*)

  • 在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,
  • 推荐使用SELECT <字段列表> 查询。原因:
    ① MySQL 在解析的过程中,会将 * 转换成表中所有的列来查询,这会大大的耗费资源和时间。
    ② 无法使用覆盖索引。

13.12.4、LIMIT 1 对优化的影响

  • 当我们确定只查询1条记录的时候,就加上 LIMIT 1 ,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
  • 如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。

13.12.5、多使用COMMIT

  • 只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
  • COMMIT 所释放的资源:
    • 回滚段上用于恢复数据的信息
    • 被程序语句获得的锁
    • redo / undo log buffer 中的空间
    • 管理上述 3 种资源中的内部花费

13.13、淘宝数据库,主键如何设计的?

聊一个实际问题:淘宝的数据库,主键是如何设计的?

  • 某些错的离谱的答案还在网上年复一年的流传着,甚至还成为了所谓的MySQL军规。其中,一个最明显的错误就是关于MySQL的主键设计。

  • 大部分人的回答如此自信:用8字节的 BIGINT 做主键,而不要用INT。

  • 这样的回答,只站在了数据库这一层,而没有从业务的角度 思考主键。主键就是一个自增ID吗?站在2022年的新年档口,用自增做主键,架构设计上可能 连及格都拿不到

  • 大胆猜测,淘宝的订单ID设计应该是:
    订单ID = 时间 + 去重字段 + 用户ID后6位尾号

  • 这样的设计能做到全局唯一,且对分布式系统查询及其友好。

13.13.1、自增ID的问题

自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:
1. 可靠性不高
存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。
2. 安全性不高
对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。
3. 性能差
自增ID的性能较差,需要在数据库服务器端生成。
4. 交互多
业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。
5. 局部唯一性
最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。

13.13.2、推荐的主键设计(初识UUID)

  • 非核心业务 :对应表的主键自增ID,如告警、日志、监控等信息。

  • 核心业务主键设计至少应该是全局唯一且是单调递增。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。

这里推荐最简单的一种主键设计:UUID。

  • UUID的特点:
    全局唯一,占用36字节,数据无序,插入性能差。

  • 为什么UUID是全局唯一的?
    在UUID中时间部分占用60位,存储的类似TIMESTAMP的时间戳,但表示的是从1582-10-15 00:00:00.00到现在的100ns的计数。可以看到UUID存储的时间精度比TIMESTAMPE更高,时间维度发生重复的概率降低到1/100ns。

时钟序列是为了避免时钟被回拨导致产生时间重复的可能性。MAC地址用于全局唯一。

  • 为什么UUID占用36个字节?
    UUID根据字符串进行存储,设计时还带有无用"-"字符串,因此总共需要36个字节。

  • 为什么UUID是随机无序的呢
    因为UUID的设计中,将时间低位放在最前面,而这部分的数据是一直在变化的,并且是无序。

13.13.3、UUID的组成

MySQL数据库的UUID组成如下所示:
UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)

我们以UUID值e0ea12d4-6473-11eb-943c-00155dbaa39d举例:

MySQL高级(SQL优化)_第24张图片

13.13.4、改造UUID

  • 若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。

  • MySQL 8.0可以更换时间低位和时间高位的存储方式,这样UUID就是有序的UUID了。

  • MySQL 8.0还解决了UUID存在的空间占用的问题,除去了UUID字符串中无意义的"-"字符串,并且将字符串用二进制类型保存,这样存储空间降低为了16字节

  • 可以通过MySQL8.0提供的uuid_to_bin函数实现上述功能,同样的,MySQL也提供了bin_to_uuid函数进行转化:
    SET @uuid = UUID();
    SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);

  • 通过函数uuid_to_bin(@uuid,true)将UUID转化为有序UUID了全局唯一 + 单调递增,这不就是我们想要的主键!

13.13.5、有序UUID性能测试

16字节的有序UUID,相比之前8字节的自增ID,性能和存储空间对比究竟如何呢?

我们来做一个测试,插入1亿条数据,每条数据占用500字节,含有3个二级索引,最终的结果如下所示:在这里插入图片描述
MySQL高级(SQL优化)_第25张图片
从上图可以看到插入1亿条数据有序UUID是最快的,而且在实际业务使用中有序UUID在 业务端就可以生成 。还可以进一步减少SQL的交互次数。

另外,虽然有序UUID相比自增ID多了8个字节,但实际只增大了3G的存储空间,还可以接受。

在当今的互联网环境中,非常不推荐自增ID作为主键的数据库设计。更推荐类似有序UUID的全局唯一的实现。
另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样的主键设计就更为考验架构师的水平了。

13.14.6、如果不是MySQL8.0 肿么办?

  • 手动赋值字段做主键!

  • 比如,设计各个分店的会员表的主键,因为如果每台机器各自产生的数据需要合并,就可能会出现主键重复的问题。

  • 可以在总部 MySQL 数据库中,有一个管理信息表,在这个表中添加一个字段,专门用来记录当前会员编号的最大值。

  • 门店在添加会员的时候,先到总部 MySQL 数据库中获取这个最大值,在这个基础上加 1,然后用这个值作为新会员的“id”,同时,更新总部 MySQL 数据库管理信息表中的当前会员编号的最大值。

  • 这样一来,各个门店添加会员的时候,都对同一个总部 MySQL 数据库中的数据表字段进 行操作,就解决了各门店添加会员时会员编号冲突的问题

十四、数据库的设计规范(范式)

14.1、范式简介

  • 在关系型数据库中,关于数据表设计的基本原则、规则就称为范式

  • 要设计一个合理的关系型数据库,就必须满足一定的范式。

  • 范式的英文名称是Normal From,简称NF

  • 范式是关系型数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则指导方法

14.1.2、范式包括哪些

  • 目前关系型数据库有6种常见的范式,按照级别,从低到高分别是:
    第一范式(1NF)第二范式(2NF)第三范式(3NF)巴斯·科德范式第四范式(4NF)第五范式(5NF)也称完美范式

  • 这些范式后者都是满足前者规范的。

  • 数据库的范式设计阶级越高,冗余度就越低,但相应的会牺牲一些性能。因而我们在设计的时候需要在空间和性能上做平衡。

  • 一般来说,在关系型数据库中,最高也就遵循到第三范式,最多遵循的就是第三范式,巴斯·科德范式只是在第三范式上进行了部分优化,因而没有列为第四范式。

  • 有时候我们为了提高查询性能,还需要破坏范式规则,也就是反规范化

14.1.3、键和相关属性的概念

  • 超键:能够唯一标识元组(一行记录)的属性集,一个元组就是一行记录。
  • 候选键:如果超键不包括多余的属性,那么这个超键就是候选键。
  • 主键:用户可以从候选键中选择一个作为主键。
  • 外键:就是表中的某个属性集不是所在表的主键,而是其他表的主键
  • 主属性:包含在任一候选键中的属性称为主属性(主要属性)。
  • 非主属性:与主属性相对,指的是不包括在任何一个候选键中的属性。

通常,我们也将候选键称为“”,把主键也称为“主码”。因为键可能是由多个属性组成的,针对单个属性,我们还可以用主属性和非主属性来进行区分。

14.2、第一范式

  • 第一范式是最基本的范式,也是我们必须要遵守的。
  • 第一范式主要是确保数据表中每个字段的值必须具有原子性,也就是说数据表中的每个字段的值是不可再次拆分的最小数据单元。
  • 也就是同一条记录中的一个列只能有一个值。比如手机号,如果同一个人有两个手机号,那么他的两个手机号是不能保存在同一条记录中的,我们可以将其拆分为两条记录分别保存两个手机号。

一条记录有两个手机号:
MySQL高级(SQL优化)_第26张图片
将这条记录拆分为两条记录,将两个手机号分别保存。
MySQL高级(SQL优化)_第27张图片

14.3、第二范式

  • 第二范式是建立在第一范式的基础上的。
  • 第二范式要求数据表里的每一条数据记录,都是可唯一标识的(唯一标识通过超键实现)。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。
  • 如果知道主键的所有属性的值,就可以检索到任何元组(行)的任何属性的任何值。(要求中的主键,其实可以拓展为候选键,因为超键和候选键都是主键构成的)
  • 也就是一条记录中有两个字段共同构成了一个联合索引作为主键,我们要知道其他字段的值,就必须要同时知道联合索引的两个字段的值。

举例:
比赛表 player_game ,里面包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地等属性,这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键(或主键)来决定如下的关系:

(球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分)

但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:
(球员编号) → (姓名,年龄)
(比赛编号) → (比赛时间, 比赛场地)

对于非主属性来说,并非完全依赖候选键。这样会产生怎样的问题呢?

  1. 数据冗余:如果一个球员可以参加 m 场比赛,那么球员的姓名和年龄就重复了 m-1 次。一个比赛也可能会有 n 个球员参加,比赛的时间和地点就重复了 n-1 次。
  2. 插入异常 :如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。
  3. 删除异常 :如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。
  4. 更新异常 :如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况。

14.4、第三范式

  • 第三范式是在第二范式的基础上的。
  • 第三范式要求数据表中的每一个非主键字段和主键字段直接相关
  • 也就是说数据表中的非主键字段不能依赖于其他非主键字段。
  • 即非主键之间不能有依赖关系,必须相互独立
  • 这里的主键可以拓展为候选键。

举例:
部门信息表 :每个部门有部门编号(dept_id)、部门名称、部门简介等信息。
员工信息表 :每个员工有员工编号、姓名、部门编号。列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。

如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。

一句话概括2NF和3NF:
每一列非主键字段都完全依赖于主键字段,并且除了主键别无他物。
尤其在复合主键的情况下,非主键字段不能只依赖部分主键。

14.4.5、小结

关于数表的设计,有三个范式要遵循。

  • 第一范式,确保每列保持原子性。即不可再拆分。
  • 第二范式,确保每一列非主键字段都完全依赖于主键字段。尤其在复合主键的情况下,非主键字段不能只依赖部分主键。
  • 第三范式,确保每一列非主键字段和主键字段都直接相关,非主键字段之间必须相互独立,不能有依赖关系。

14.4.6、范式的优缺

优点:

  • 数据的标准化有助于降低数据库中数据的冗余度
  • 第三范式通常被认为在性能、拓展性和数据完整性方面达到了最好的平衡。

缺点:

  • 范式的使用,可能降低查询效率,因为范式的阶级越高,设计出来的数据表就越多、越精细,数据的冗余度就越低,查询数据的时候就需要进行多表关联查询,者不仅代价昂贵,也可能使一些索引策略无效

范式只是提出了设计的标准,实际上设计表时,未必一定要符合这些标准。开发中,我们会出现为了性能和读取效率违反范式化的原则,通过增加少量的冗余数据来提高数据库的读性能,减少关联查询、join表的次数,实现空间换取时间的目的。因此在实际的设计过程中要理论结合实际,灵活运用。

  • 范式本身没有优劣之分,只是适用场景不同。
  • 没有完美的设计,只有合适的设计。
  • 我们在设计表的时候,还需要根据需求将范式和反范式混合使用。

14.5、反范式化

  • 有的时候不能简单按照规范要求设计数据表,因为有的数据看似冗余,其实对业务来说十分重要。这个时候我们就需要遵循业务优先的原则,首先满足业务需求,再尽量减少冗余。
  • 如果数据库中的数据量比较大,系统分UV和PV访问频次比较高,则完全按照MySQL的三大范式设计数据表,读数据时会产生大量的关联查询,在一定程度上会影响数据库的读性能。如果我们想对查询效率进行优化,反范式优化也是一种优化思路。此时,可以通过在数据表中增加冗余字段来提高数据库的读性能。
  • 反范式化必须建立在满足第三范式的情况下才可以进行,如果连第三范式都不能满足,就不要谈什么优化性能了(反范式化就是为了优化性能)

业务优先原则:
就是指一切以业务需求为主,技术服务于业务。按照理论的设计不一定是最优的,还要根据具体情况来确定。

规范化 VS 性能

  • 为满足某种商业目标,数据库性能比规范化数据库更重要。
  • 在数据规范化的同时,要综合考虑数据库的性能。
  • 通过在给定的表中添加额外的字段,以大量减少需要从表中搜索信息的时间。
  • 通过在给定的表中插入计算列,以方便查询。

14.6、反范式带来的问题

  • 存储空间变大了。
  • 一个表中字段做了修改,另一个表中冗余的字段也需要做同步修改,否则数据不一致
  • 若采用存储过程来支持数据的更新、删除等额外操作,如果更新频繁,会非常消耗系统资源
  • 数据量小的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加复杂

14.7、反范式化的适用建议

当冗余信息有价值或者能大幅度提高查询效率的时候,我们才会采取反范式的优化。

14.7.1、增加冗余字段的建议

增加冗余字段一定要符合如下两个条件。只有满足这两个条件,才可以考虑增加冗余字段。

  • 这个冗余字段不需要经常进行修改
  • 这个冗余字段在查询的时候不可缺少的。

14.7.2、历史快照、历史数据的需要

  • 当我们需要历史快照、历史数据的时候,才增加冗余字段

在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息都属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息时非常有必要的。

反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更加方便进行数据分析。

我简单总结下数据仓库和数据库在使用上的区别:

  • 数据库设计的目的在于捕获数据,而数据仓库设计的目的在于分析数据。
  • 数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据
  • 数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计。

14.8、巴斯范式

巴斯范式就是主属性必须完全依赖于候选键。 即主属性的值必须靠所有候选键的值共同确定,不能只靠一个或部分候选键就能确定主属性的值。

  • 人们在第三范式的基础上进行了改进,提出了巴斯范式,也叫巴斯-科德范式(Boyce-Codd Normal Form)
  • BCNF被认为没有新的设计规范加入,只是对第三范式中设计规范要求更强,使得数据库冗余度更小。称为是修正的第三范式,或扩充的第三范式,BCNF不被称为第四范式。
  • 一般来说,一个数据库设计符合第三范式或巴斯范式就可以了。

14.9、第四范式

第四范式就是满足平凡多值依赖的关系。

多值依赖的概念:

  • 多值依赖:即属性之间存在一对多的关系。
  • 函数依赖:函数依赖属于单值依赖(一点对一),所有不能表达属性之间的一对多的关系。
  • 平凡多值依赖:全集U=K+A,一个K可以对应多个A。此时整个表就是一组一对多的关系
  • 非平凡多值依赖:全集U=K+A+B,一个K可以对应多个A,也可以对应多个B,但A与B之间相互独立。整个表有多组一对多的关系,但这个“一”必须是同一个字段,比如K可以对应多个A和B。

第四范式是建立在巴斯范式的基础上的,消除了非平凡且非函数的多值依赖关系(即把同一个表内的多对多的关系删除),只保留了平凡多值依赖。

14.10、第五范式(完美范式)、域键范式(DKNF)

  • 第五范式又称完美范式
  • 第五范式在第四范式的基础上,消除了不是由候选键所蕴含的连接依赖。如果关系模式R中的每一个连接依赖均由R的候选键所隐含,则称此关系模式符合第五范式。
  • 函数依赖是多值依赖的一种特殊情况,而多值依赖实际上是连接依赖的一种特殊情况。但连接依赖不像函数依赖和多值依赖可以由语义直接导出,而是在关系连接运算时才反映出来。存在连接依赖的关系模式仍可能遇到数据冗余及插入、修改、删除异常等问题。
  • 第五范式处理的是无损连接问题,这个范式基本没有实际意义,因为无损连接很少出现,而且难以察觉。
  • 域键范式试图定义一个终极范式,该范式考虑所有的依赖和约束类型,但实用价值也是最小的,只存在于理论研究中。

14.11、ER模型

数据库设计是牵一发而动全身的。那有没有什么办法提前看到数据库的全貌呢?比如需要哪些数据表、数据表中应该有哪些字段,数据表与数据表之间有什么关系、通过什么字段进行连接,等等。这样我们才能进行整体的梳理和设计。

其实,ER模型就是这样一个工具。ER模型也叫做实现关系模型,是用来描述现实生活中客观存在的事物、事物的属性,以及事物之间关系的一种数据模型。在开发基于数据库的信息系统的设计阶段,通常使用ER模型来描述信息需求特性,帮助我们理清业务逻辑,从而设计出优秀的数据库

14.11.1、ER模型三要素

ER模型有三个要素:实体、属性、关系。

  • 实体(就是对象或表):可以看做是数据对象,往往对应于现实生活中的真实存在的个体。在ER模型中,用矩形来表示。实体分为两类,分别是强实体和弱实体。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系实体(因为他要依赖别的实体,所以比较弱)。
  • 属性(就是字段):是指实体的特性。在ER模型中用椭圆形来表示。
  • 关系:实体与实体之间的联系。在ER模型中用棱形来表示。

注意:

  • 我们要从系统整体的角度触发去看实体和属性,可以独立存在的是实体,不可再分的是属性
  • 也就是说,属性不能包含其他属性。

14.11.2、ER建模案例分析

ER 模型看起来比较麻烦,但是对我们把控项目整体非常重要。如果你只是开发一个小应用,或许简单设计几个表够用了,一旦要设计有一定规模的应用,在项目的初始阶段,建立完整的 ER 模型就非常关键了。开发应用项目的实质,其实就是建模

我们设计的案例是 电商业务 ,由于电商业务太过庞大且复杂,所以我们做了业务简化,比如针对SKU(StockKeepingUnit,库存量单位)和SPU(Standard Product Unit,标准化产品单元)的含义上,我们直接使用了SKU,并没有提及SPU的概念。本次电商业务设计总共有8个实体,如下所示。

  • 地址实体
  • 用户实体
  • 购物车实体
  • 评论实体
  • 商品实体
  • 商品分类实体
  • 订单实体
  • 订单详情实体

其中,用户商品分类是强实体,因为它们不需要依赖其他任何实体。而其他属于弱实体,因为它们虽然都可以独立存在,但是它们都依赖用户这个实体,因此都是弱实体。知道了这些要素,我们就可以给电商业务创建 ER 模型了,如图:
MySQL高级(SQL优化)_第28张图片

ER模型细化:
有了这个 ER 模型,我们就可以从整体上理解电商的业务了。刚刚的 ER 模型展示了电商业务的框架,但是只包括了订单,地址,用户,购物车,评论,商品,商品分类和订单详情这八个实体,以及它们之间的关系,还不能对应到具体的表,以及表与表之间的关联。我们需要把 属性加上 ,用 椭圆 来表示,
这样我们得到的 ER 模型就更加完整了。

因此,我们需要进一步去设计一下这个 ER 模型的各个局部,也就是细化下电商的具体业务流程,然后把它们综合到一起,形成一个完整的 ER 模型。这样可以帮助我们理清数据库的设计思路。

通过分析各个实体都有哪些属性,我们就可以重新设计电商业务了,ER 模型如图:
MySQL高级(SQL优化)_第29张图片

14.11.3、 ER 模型图转换成数据表

通过绘制 ER 模型,我们已经理清了业务逻辑,现在,我们就要进行非常重要的一步了:把绘制好的 ER模型,转换成具体的数据表,下面介绍下转换的原则:

  • 一个实体通常转换成一个 数据表
  • 一个 多对多的关系 ,通常也转换成一个 数据表
    • 多对多的关系需要建立一个独立的中间表来连接。
  • 一个 1 对 1 ,或者 1 对多 的关系,往往通过表的 外键 来表达,而不是设计一个新的数据表;
    • 一对多的关系需要用外键来连接。
    • 外键约束主要是在数据库层面上保证数据的一致性,但是因为插入和更新数据需要检查外键,理论上性能会有所下降,对性能是负面影响。
    • 实际的项目,不建议使用外键,一方面是降低开发的复杂度(有外键的化主从表类的操作必须先操作主表),另外是外键在处理数据的时候非常麻烦。在电商平台,由于并发业务量比较大,所以一般不设置外键,以免影响数据库性能
    • 在应用层面做数据的一致性检查,本来就是一个正常的功能需求。比如学生选课的场景,课程肯定不是输入的,而是通过下来后查找等方式从系统进行选取,就能够保证是合法的课程ID,因此就不需要靠数据库的外键来检查了。
  • 属性 转换成表的 字段

在任何一个基于数据库的应用项目,都可以通过这种先建立ER模型,再转换成数据表的方式,完成数据库的设计工作。创建ER模型不是目的,目的是把业务逻辑梳理清楚,设计出优秀的数据库。不建议为了建模而建模,要利用创建ER模型的过程来梳理思路,这样创建ER模型才有意义。
MySQL高级(SQL优化)_第30张图片

14.12、数据表的设计原则

数据表的设计一般遵循一个原则:三少一多

  • 表越少越好
    • RDBMS的核心在于对实体和联系的定义,也就是E-R图,表越少,证明实体和联系设计得越简洁,既方便理解又方便操作。
  • 表中的字段越少越好
    • 字段越多,冗余数据得可能性就越大。设置字段个数少得前提是各个字段相互独立,而不是某个字段得取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常会在数据冗余减少效率中进行平衡。
  • 表中的联合主键的字段越少越好
    • 设置主键是为了唯一性,当一个字段无法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键中的字段个数越多,占用的索引空间越大,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合索引主键的字段个数越少越好。
  • 使用主键和外键越多越好
    • 数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率。这里的外键指的是表与表之间的一对一,一对多等关系。

“三少一多”原则的核心就是简单可复用。简单指的是用更少的表、更少的字段、更少的联合主键字段来完成数据表的设计。可复用则是通过主键、外键的使用来增强数据表之间的复用率。因为一个主键可以理解是一张表的代表。键设计得越多,证明它们之间得利用率越高。

注意:
这个原则并非绝对的,有时候我们需要牺牲数据的冗余度来换取数据处理的效率。

14.13、数据库对象编写规范

14.13.1、数据库规范

  1. 【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以英文字母开头。
  2. 【强制】库名中英文 一律小写 ,不同单词采用 下划线 分割。须见名知意。
  3. 【强制】库的名称格式:业务系统名称_子系统名。
  4. 【强制】库名禁止使用关键字(如type,order等)。
  5. 【强制】创建数据库时必须 显式指定字符集 ,并且字符集只能是utf8或者utf8mb4。创建数据库SQL举例:CREATE DATABASE crm_fund DEFAULT CHARACTER SET 'utf8' ;
  6. 【建议】对于程序连接数据库账号,遵循 权限最小原则。
    使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号 原则上不准有drop权限
  7. 【建议】临时库以 tmp_ 为前缀,并以日期为后缀。
    备份库以 bak_ 为前缀,并以日期为后缀。

14.13.2、表、字段规范

  1. 【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议以 英文字母开头
  2. 【强制】 表名、列名一律小写 ,不同单词采用下划线分割。须见名知意。
  3. 【强制】表名要求有模块名强相关,同一模块的表名尽量使用 统一前缀 。比如:crm_fund_item
  4. 【强制】创建表时必须 显式指定字符集 为utf8或utf8mb4。
  5. 【强制】表名、列名禁止使用关键字(如type,order等)。
  6. 【强制】创建表时必须 显式指定表存储引擎 类型。如无特殊需求,一律为InnoDB。
  7. 【强制】建表必须有comment。
  8. 【强制】字段命名应尽可能使用表达实际含义的英文单词或 缩写 。如:公司 ID,不要使用corporation_id, 而用corp_id 即可。
  9. 【强制】布尔值类型的字段命名为 is_描述 。如member表上表示是否为enabled的会员的字段命名为 is_enabled。
  10. 【强制】禁止在数据库中存储图片、文件等大的二进制数据。
    通常文件很大,短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时。通常存储于文件服务器,数据库只存储文件地址信息。
  11. 【建议】建表时关于主键: 表必须有主键
    (1)强制要求主键为id,类型为int或bigint,且为auto_increment 建议使用unsigned无符号型。
    (2)标识表里每一行主体的字段不要设为主键,建议设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机插入,则会导致innodb内部页分裂和大量随机I/O,性能下降。
  12. 【建议】核心表(如用户表)必须有行数据的创建时间字段(create_time)和 最后更新时间字段
    (update_time),便于查问题。
  13. 【建议】表中所有字段尽量都是 NOT NULL 属性,业务可以根据需要定义DEFAULT值。 因为使用
    NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问
    题。
  14. 【建议】所有存储相同数据的 列名和列类型必须一致 (一般作为关联列,如果查询时关联列类型
    不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。
  15. 【建议】中间表(或临时表)用于保留中间结果集,名称以 tmp_开头。
    备份表用于备份或抓取源表快照,名称以 bak_ 开头。中间表和备份表定期清理。
  16. 【示范】一个较为规范的建表语句:
CREATE TABLE user_info (
	`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',#COMMENT为备注信息 
	`user_id` bigint(11) NOT NULL COMMENT '用户id',
	`username` varchar(45) NOT NULL COMMENT '真实姓名',
	`email` varchar(30) NOT NULL COMMENT '用户邮箱',
	`nickname` varchar(45) NOT NULL COMMENT '昵称',
	`birthday` date NOT NULL COMMENT '生日',
	`sex` tinyint(4) DEFAULT '0' COMMENT '性别',
	`short_introduce` varchar(150) DEFAULT NULL COMMENT '一句话介绍自己,最多50个汉字',
	`user_resume` varchar(300) NOT NULL COMMENT '用户提交的简历存放地址',
	`user_register_ip` int NOT NULL COMMENT '用户注册时的源ip',
	`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
	CURRENT_TIMESTAMP COMMENT '修改时间',
	`user_review_status` tinyint NOT NULL COMMENT '用户资料审核状态,1为通过,2为审核中,3为未通过,4为还未提交审核',
	PRIMARY KEY (`id`),
	UNIQUE KEY `uniq_user_id` (`user_id`),
	KEY `idx_username`(`username`),
	KEY `idx_create_time_status`(`create_time`,`user_review_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='网站用户基本信息'

  1. 【建议】创建表时,可以使用可视化工具。这样可以确保表、字段相关的约定都能设置上。实际上,我们通常很少自己写 DDL 语句,可以使用一些可视化工具来创建和操作数据库和数据表。可视化工具除了方便,还能直接帮我们将数据库的结构定义转化成 SQL 语言,方便数据库和数据表结构的导出和导入。

14.13.3、索引规范

  1. 【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值 禁止被更新
  2. 【强制】InnoDB和MyISAM存储引擎表,索引类型必须为 BTREE
  3. 【建议】主键的名称以 pk_ 开头,唯一键以uni_uk_ 开头,普通索引以 idx_ 开头,一律使用小写格式,以字段的名称或缩写作为后缀。
  4. 【建议】多单词组成的columnname,取前几个单词首字母,加末单词组成column_name。如:sample 表 member_id 上的索引:idx_sample_mid。
  5. 【建议】单个表上的索引个数 不能超过6个
  6. 【建议】在建立索引时,多考虑建立 联合索引 ,并把区分度最高的字段放在最前面(区分度高就是值重复少的)。
  7. 【建议】在多表 JOIN 的SQL里,保证被驱动表的连接列上有索引,这样JOIN 执行效率最高。
  8. 【建议】建表或加索引时,保证表里互相不存在 冗余索引 。 比如:如果表里已经存在key(a,b),则key(a)为冗余索引,需要删除。

14.12.4、SQL编写规范

  1. 【强制】程序端SELECT语句必须指定具体字段名称,禁止写成 *
  2. 【建议】程序端insert语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)。
  3. 【建议】除静态表或小表(100行以内),DML语句必须有WHERE条件,且使用索引查找。
  4. 【建议】INSERT INTO…VALUES(XX),(XX),(XX)… 这里XX的值不要超过5000个。 值过多虽然上线很快,但会引起主从同步延迟。
  5. 【建议】SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。
  6. 【建议】线上环境,多表 JOIN 不要超过5个表。
  7. 【建议】减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDERBY、GROUP BY、DISTINCT 这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
  8. 【建议】包含了ORDER BY、GROUP BY、DISTINCT 这些查询的语句,WHERE 条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
  9. 【建议】对单表的多次alter操作必须合并为一次
    对于超过100W行的大表进行alter table,必须经过DBA审核,并在业务低峰期执行,多个alter需整合在一起。 因为alter table会产生 表锁 ,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。
  10. 【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep
  11. 【建议】事务里包含SQL不超过5个。
    因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等问题。
  12. 【建议】事务里更新语句尽量基于主键或UNIQUE KEY,如UPDATE… WHERE id=XX;
    否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁

十五、数据库其它调优策略

15.1、数据库调优的策略

15.1.1、数据库调优目标

  • 尽可能 节省系统资源 ,以便系统可以提供更大负荷的服务。(吞吐量更大)
  • 合理的结构设计和参数调整,以提高用户操作 响应的速度 。(响应速度更快)
  • 减少系统的瓶颈,提高MySQL数据库整体的性能。

15.1.2、调优的维度和步骤

15.1.2.1、第一步:选择合适的DBMS

如果对事务性处理以及安全性要求高的话,可以选择SQL Server、Oracle这类数据库,单表存储上亿条数据是没有问题。

你也可以采用开源的MySQL进行存储,如果进行事务处理的话可以选择InnoDB,非事务处理可以选择MyISAM。

NoSQL阵营包括键值型数据库文档型数据库搜索引擎列式存储图形数据库

DBMS的选择关系到后面整个设计过程,所以第一步就是要选择合适的DBMS。如果以及确定好了DBMS,那么这步就可以跳过。

15.1.2.2、第二步:优化表设计

  • 表结构要尽量遵循第三范式的原则。这样可以让数据结构更加清晰规范,减少冗余字段,同时也减少了在更新,插入和删除数据时等异常情况的发生。
  • 如果查询应用比较多,尤其是需要进行多表联查的时候,可以采用反范式进行优化。反范式采用空间换时间的方式,通过增加冗余字段提高查询的效率。
  • 表字段的数据类型选择,关系到了查询效率的高低以及存储空间的大小。一般来说,如果字段可以采用数值类型就不要采用字符类型;字符长度要尽可能设计得短一些。针对字符类型来说,当确定字符长度固定时,就可以采用CHAR类型;当长度步固定时,通常采用VARCHAR类型。

数据表得设计很基础,也很关键。好的表结构可以在业务和用户量增加情况下依然发挥作用,不好的表结构设计会让数据变得非常雍总,查询效率也会降低

15.1.2.3、第三步:优化逻辑查询

  • SQL查询优化,可以分为逻辑查询优化物理查询优化。逻辑查询优化是用过改变SQL语句,让SQL执行效率更高,采用的方式是对SQL语句进行等价变换,对查询进行重写。
  • SQL的查询重写包括子查询优化、等价谓词重写、视图重写、条件简化、连接消除和嵌套连接消除等。

15.1.2.4、第四步:物理查询优化

  • 物理查询优化最重要的就是索引的创建和使用
  • SQL查询时需要对不同的数据表进行查询,因此在物理查询优化阶段也需要确定这些查询采用的路径,具体的情况包括:
    • 单表扫描:对于单表扫描来说,我们可以全表扫描所有的数据,也可以局部扫描。
    • 两张单表的连接:常用的连接方式包括了嵌套循环连接、Hash连接和合并连接。
    • 多张表的连接:多张数据表进行连接的时候,顺序很重要,因为不同的连接路径查询的效率不同,搜索空间也会不同。我们在进行多表连接的时候,搜索空间可能会达到很高的数量级,巨大的搜索空间显然会占用更多的资源,因此我们需要通过调整连接顺序,将搜索空间调整在一个可接受的范围。

15.1.2.5、第五步:使用Redis或Memcached作为缓存

因为数据都是存放到数据库中,我们需要从数据库层中取出数据放到内存中进行业务逻辑的操作,当用户量增大的时候,如果频繁地进行数据查询,会消耗数据库地更多资源。如果我们将常用地数据之间放到内存中,就会大幅提升查询地效率。

键值存储数据库可以帮我们解决这个问题。

常用的键值数据库由Redis和Memcached,它们都可以将数据存放到内存中。

从可靠性来说,Redis支持持久化,可以让我们的数据保存在硬盘上,不过这样一来性能消耗也会比较大。而Memcached仅仅是内存存储,不支持持久化。

从支持的数据类型来说,Redis比Memcache要多,它不仅支持key-value类型的数据,还支持List,Set,Hash等数据结构。当我们有持久化需求或者是更高级的数据处理的需求的时候,就可以使用Redis。如果是简单的key-value存储,则可以使用Memcached。

通常我们对于查询响应要求高的场景(响应时间短,吞吐量大),可以考虑内存数据库,毕竟术业有专攻。传统的RDBMS都是讲数据存储在硬盘上,而内存数据库则存放在内存中,查询起来要快得多。不过使用不同得工具,也增加了开发人员的而是用成本。

15.1.2.6、库级优化

库级优化是站在数据库的维度上进行的优化策略,比如控制一个库中的数据表数量。另外,单一的数据库总会遇到各种限制,不如取长补短,利用外援的方式。通过主从架构优化我们的读写策略,通过对数据库进行垂直或水平切分,突破单一数据库或数据表的访问限制,提升查询的性能。

  1. 读写分离
    如果读和写的业务量都很大,并且它们都在同一个数据库服务器中进行操作,那么数据库的性能就会出现瓶颈,这时为了提升系统的性能,优化用户体验,我们可以采用读写分离的方式降低主数据库的负载,比如用主数据库(master)完成写操作,用从数据库(slave)完成读操作。
    MySQL高级(SQL优化)_第31张图片
    MySQL高级(SQL优化)_第32张图片
  2. 数据库分片
    数据库分库分表。当数据量级达到千万级以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少单一数据库服务器的访问压力。如果你使用的是MySQL,就可以使用MySQL自带的分区表功能,当然你也可以考虑做垂直拆分(分库)水平拆分(分表)垂直+水平拆分(分库分表)

但是需要注意的是,分拆在提升数据库性能的同时,也会增加维护和使用成本。

15.2、MySQL服务器优化

优化MySQL服务器主要从两个方面来优化,一方面是对硬件进行优化;另一方面是对MySQL服务的参数进行优化。这部分的内容需要较全面的知识,一般只有专业的数据库管理员才能进行这一类的优化。对于可以定制参数的操作系统,也可以针对MySQL进行操作系统优化。

15.2.1、优化服务器硬件

服务器的硬件性能直接决定着MySQL数据库的性能。硬件的性能瓶颈直接决定MySQL数据库的运行速度和效率。针对性能瓶颈提高硬件配置,可以提高MySQL数据库查询、更新的速度。

  • 配置较大的内存:足够大的内存是提高MySQL数据库性能的方法之一。内存的速度比磁盘I/O快得多,可以通过增加系统得缓冲区容量使数据库在内存中停留的时间更长,以减少磁盘I/O
  • 配置高速磁盘系统:以减少读盘的等待时间,提高响应速度。磁盘的I/O能力,也就是他的寻道能力,目前的SCSI告诉旋转的是7200转/分钟,这样的速度,一旦访问量上去,磁盘的压力就会过大,如果每天的网站pv(page view)在150w,这样的一般配置就无法满足这样的需求了。现在SSD盛行,在SSD上随机访问和顺序访问性能几乎差不多,使用磁盘I/O可以减少随机I/O带来的性能损耗。
  • 合理分布磁盘I/O:把磁盘I/O分散在多个设备上,以减少资源竞争,提高并行操作能力。
  • 配置多处理器:MySQL是多线程的数据库,多处理器可同时执行多个线程。

15.2.2、MySQL的参数优化

通过优化MySQL的参数可以提高资源利用率,从而达到提高MySQL服务器性能的目的。

MySQL服务的配置参数都在my.cnf或者my.ini文件的[mysqld]组中。配置完参数以后,需要重新启动MySQL服务才会生效。

下面对几个对性能影响比较大的参数进行详细介绍。

  • innodb_buffer_pool_size :这个参数是Mysql数据库最重要的参数之一,表示InnoDB类型的 表和索引的最大缓存 。它不仅仅缓存 索引数据 ,还会缓存 表的数据 。这个值越大,查询的速度就会越快。但是这个值太大会影响操作系统的性能。
  • key_buffer_size :表示 索引缓冲区的大小 。索引缓冲区是所有的 线程共享 。增加索引缓冲区可以得到更好处理的索引(对所有读和多重写)。当然,这个值不是越大越好,它的大小取决于内存的大小。如果这个值太大,就会导致操作系统频繁换页,也会降低系统性能。对于内存在 4GB 左右的服务器该参数可设置为 256M384M
  • table_cache :表示 同时打开的表的个数 。这个值越大,能够同时打开的表的个数越多。物理内存越大,设置就越大。默认为2402,调到512-1024最佳。这个值不是越大越好,因为同时打开的表太多会影响操作系统的性能。
  • query_cache_size :表示 查询缓冲区的大小。可以通过在MySQL控制台观察,如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,就要增加Query_cache_size的值;如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多。MySQL8.0之后失效。该参数需要和query_cache_type配合使用。
  • query_cache_type的值是0时,所有的查询都不使用查询缓存区。但是query_cache_type=0并不会导致MySQL释放query_cache_size所配置的缓存区内存。
    • 当query_cache_type=1时,所有的查询都将使用查询缓存区,除非在查询语句中指定SQL_NO_CACHE ,如SELECT SQL_NO_CACHE * FROM tbl_name。
    • 当query_cache_type=2时,只有在查询语句中使用 SQL_CACHE 关键字,查询才会使用查询缓存区。使用查询缓存区可以提高查询的速度,这种方式只适用于修改操作少且经常执行相同的查询操作的情况。
  • sort_buffer_size :表示每个需要进行排序的线程分配的缓冲区的大小。增加这个参数的值可以提高 ORDER BYGROUP BY 操作的速度。默认数值是2 097 144字节(约2MB)。对于内存在4GB左右的服务器推荐设置为6-8M,如果有100个连接,那么实际分配的总共排序缓冲区大小为100 × 6= 600MB
  • join_buffer_size = 8M :表示 联合查询操作所能使用的缓冲区大小 ,和sort_buffer_size一样,该参数对应的分配内存也是每个连接独享。
  • read_buffer_size :表示 每个线程连续扫描时为扫描的每个表分配的缓冲区的大小(字节) 。当线程从表中连续读取记录时需要用到这个缓冲区。SET SESSION read_buffer_size=n可以临时设置该参数的值。默认为64K,可以设置为4M。
  • innodb_flush_log_at_trx_commit :表示 何时将缓冲区的数据写入日志文件 ,并且将日志文件写入磁盘中。该参数对于innoDB引擎非常重要。该参数有3个值,分别为0、1和2。该参数的默认值为1。
    • 值为 0 时,表示 每秒1次 的频率将数据写入日志文件并将日志文件写入磁盘。每个事务的commit并不会触发前面的任何操作。该模式速度最快,但不太安全,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。
    • 值为 1 时,表示 `每次提交事务时 将数据写入日志文件并将日志文件写入磁盘进行同步。该模式是最安全的,但也是最慢的一种方式。因为每次事务提交或事务外的指令都需要把日志写入(flush)硬盘。
    • 值为 2 时,表示 每次提交事务时 将数据写入日志文件, 每隔1秒 将日志文件写入磁盘。该模式速度较快,也比0安全,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
  • innodb_log_buffer_size :这是 InnoDB 存储引擎的 事务日志所使用的缓冲区 。为了提高性能,也是先将信息写入 Innodb Log Buffer 中,当满足 innodb_flush_log_trx_commit 参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件(或者同步到磁盘)中。
  • max_connections :表示 允许连接到MySQL数据库的最大数量 ,默认值是 151 。如果状态变量connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections 的值。在Linux 平台下,性能好的服务器,支持 500-1000 个连接不是难事,需要根据服务器性能进行评估设定。这个连接数 不是越大越好 ,因为这些连接会浪费内存的资源。过多的连接可能会导致MySQL服务器僵死
  • back_log :用于控制MySQL监听TCP端口时设置的积压请求栈大小 。如果MySql的连接数达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源,将会报错。5.6.6 版本之前默认值为 50 , 之后的版本默认为 50 + (max_connections / 5), 对于Linux系统推荐设置为小于512的整数,但最大不超过900。
  • wait_timeout :指定 一个请求的最大连接时间 ,对于4GB左右内存的服务器可以设置为5-10。
  • interactive_timeout :表示服务器在关闭连接前等待行动的秒数。

这里给出一份my.cnf的参考配置:

[mysqld]
port = 3306 
serverid = 1 
socket = /tmp/mysql.sock 
skip-locking #避免MySQL的外部锁定,减少出错几率增强稳定性。 
skip-name-resolve 
#禁止MySQL对外部连接进行DNS解析,使用这一选项可以消除MySQL进行DNS解析的时间。
#但需要注意,如果开启该选项,则所有远程主机连接授权都要使用IP地址方式,否则MySQL将无法正常处理连接请求! 
back_log = 384
key_buffer_size = 256M 
max_allowed_packet = 4M 
thread_stack = 256K
table_cache = 128K 
sort_buffer_size = 6M 
read_buffer_size = 4M
read_rnd_buffer_size=16M 
join_buffer_size = 8M 
myisam_sort_buffer_size =64M 
table_cache = 512 
thread_cache_size = 64 
query_cache_size = 64M
tmp_table_size = 256M 
max_connections = 768 
max_connect_errors = 10000000
wait_timeout = 10 
thread_concurrency = 8 #该参数取值为服务器逻辑CPU数量*2,
#在本例中,服务器有2颗物理CPU,而每颗物理CPU又支持H.T超线程,所以实际取值为4*2=8 
skip-networking #开启该选项可以彻底关闭MySQL的TCP/IP连接方式,如果WEB服务器是以远程连接的方式访问MySQL数据库服务器则不要开启该选项!否则将无法正常连接! 
table_cache=1024
innodb_additional_mem_pool_size=4M #默认为2M innodb_flush_log_at_trx_commit=1
innodb_log_buffer_size=2M #默认为1M innodb_thread_concurrency=8 #你的服务器CPU有几个就设置为几。建议用默认一般为8 
tmp_table_size=64M #默认为16M,调到64-256最挂
thread_cache_size=120
 query_cache_size=32M

这里仅供参考,具体情况具体分析。

15.3、优化数据库结构

一个好的数据库设计方案对于数据库的性能会常常起到事半功倍的效果。合理的数据库结构不仅可以使数据库占用更小的磁盘空间,而且能够使查询速度更快。数据库结构的设计需要考虑数据冗余查询和更新的速度字段的数据类型是否合理等多方面的内容。

15.3.1、拆分表:冷热数据分离

  • 拆分表的思路是,将1个包含很多字段的表拆分成2个或者多个相对较小的表
  • 因为这些表中某些字段的操作频率很高(热数据),经常要进行一些更新操作;而另一些字段的使用频率却很低(冷数据
  • 冷热数据分离,可以减少表的宽度;表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的IO。
  • 冷热数据据分离的目的是:
    • 减少磁盘IO,保证热数据的内存缓存命中率。
    • 更有效的利用缓存,避免读入无用的冷数据。

举例:
会员members表 存储会员登录认证信息,该表中有很多字段,如id、姓名、密码、地址、电话、个人描述字段。其中地址、电话、个人描述等字段并不常用,可以将这些不常用的字段分解出另一个表。将这个表取名叫members_detail,表中有member_id、address、telephone、description等字段。这样就把会员表分成了两个表,分别为 members表 和 members_detail表 。

创建这两个表的SQL如下:

CREATE TABLE members (
	id int(11) NOT NULL AUTO_INCREMENT,
	username varchar(50) DEFAULT NULL,
	password varchar(50) DEFAULT NULL,
	last_login_time datetime DEFAULT NULL,
	last_login_ip varchar(100) DEFAULT NULL,
	PRIMARY KEY(Id)
);

CREATE TABLE members_detail (
	Member_id int(11) NOT NULL DEFAULT 0,
	address varchar(255) DEFAULT NULL,
	telephone varchar(255) DEFAULT NULL,
	description text
);

如果需要查询会员的基本信息或详细信息,那么可以用会员的id来查询。如果需要将会员的基本信息和详细信息同时显示,那么可以将members表和members_detail表进行联合查询,查询语句如下:
SELECT * FROM members LEFT JOIN members_detail on members.id =members_detail.member_id;

通过这种分解可以提高表的查询效率。对于字段很多且有些字段使用不频繁的表,可以通过这种分解的方式来优化数据库的性能。

15.3.2、增加中间表

对于经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,把需要经常联合查询的数据插入中间表中,然后将原来的联合查询改为对中间表的查询,以此来提高查询效率

  • 首先,分析经常联合查询表中的字段;
  • 然后,使用这些字段建立一个中间表;
  • 最后,使用中间表来进行查询。

举例: 学生信息表 和 班级表 的SQL语句如下:

CREATE TABLE `class` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`className` VARCHAR(30) DEFAULT NULL,
	`address` VARCHAR(40) DEFAULT NULL,
	`monitor` INT NULL ,
	PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE `student` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`stuno` INT NOT NULL ,
	`name` VARCHAR(20) DEFAULT NULL,
	`age` INT(3) DEFAULT NULL,
	`classId` INT(11) DEFAULT NULL,
	PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

现在有一个模块需要经常查询带有学生名称(name)、学生所在班级名称(className)、学生班级班长(monitor)的学生信息。根据这种情况可以创建一个 temp_student 表。temp_student表中存储学生名称(stu_name)、学生所在班级名称(className)和学生班级班长(monitor)信息。创建表的语句如下:

CREATE TABLE `temp_student` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`stu_name` INT NOT NULL ,
	`className` VARCHAR(20) DEFAULT NULL,
	`monitor` INT(3) DEFAULT NULL,
	PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

接下来,从学生信息表和班级表中查询相关信息存储到临时表中:

insert into temp_student(stu_name,className,monitor)
	select s.name,c.className,c.monitor
	from student as s,class as c
	where s.classId = c.id

如果用户修改了,是不是会导致temp_vip中的数据不一致的问题呢?如何同步数据呢?
方式1:先清空数据,再重新添加数据。
方式2:使用视图。

15.3.3、增加冗余字段

设计数据库表时应尽量遵循范式理论的规约,尽可能减少冗余字段,让数据库设计看起来精致、优雅。但是,合理地加入冗余字段可以提高查询速度。

表地规范化程度越高,表与表之间的关系就越多,需要连接查询的情况也就越多。尤其在数据量大,而且需要频繁进行连接的时候,为了提升效率,我们也可以考虑增加冗余字段来减少连接。

这部分在反范式化有具体讲解。

15.3.4、数据类型优化

情况1:对整数类型数据进行优化
遇到整数类型的字段可以用INT型。因为INT型数据又足够大的取值范围,不用担心数据超出取值范围的问题。刚开始做项目的时候,首先要保证系统的稳定性,这样设计字段类型是可以的。但在数据量很大的时候,数据类型的定义,在很大程度上会影响到系统整体的执行效率。

对于非负型的数据(如自增ID、整型IP)来说,要优先使用无符号整型UNSIGNED来存储。因为无符号整型比有符号整型存储数值的范围更大。如tinyint有符号为“-128 ~ 127”,无符号为“0~255”,多出了一倍的存储空间。

情况2:既可以使用文本类型也可以使用整数类型的字段,要选择整数类型
跟文本类型数据相比,大整数往往占用更少的存储空间,因此,在存取和对比的时候,可以占用更少的内存空间。所有,在二者皆可用的情况下,尽量使用整数类型,这样可以提高查询的效率。如:将IP地址转换成整型数据。

情况3:避免使用TEXT、BLOB数据类型
MySQL内存临时表不支持TEXT、BLOB这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。并且对于这种数据,MySQL还是要进行二次查询,会使SQL性能变得很差,但是,不是说一定不能使用这样得数据类型。

情况4:使用TINYINT来代替ENUM类型
修改ENUM值需要使用ALTER语句。

ENUM类型得ORDER BY操作效率低,需要额外操作。

使用TIMESTAMP存储时间
TIMESTAMP存使用4字节,DATETIME使用8字节,同时TIMESTAMP具有自动赋值以及自动更新的特性。

情况6:用DECIMAL代替FLOAT和DOUBLE存储精确浮点数

  • 非精准浮点:float、double
  • 精准浮点:decimal

Decimal类型为精准浮点数,在计算时不会丢失精度,尤其是财务相关的金融类数据。占用空间由定义的宽度决定,每4个字节可以存储9位数字,并且小数点要占用一个字节。可用于存储比BIGINT更大的整型数据。

总之,遇到数据量大的项目时,一定要在充分了解业务需求的前提下,合理优化数据类型,这样才能充分发挥资源的效率,时系统达到最优。

15.3.5、优化INSERT插入记录的速度

插入记录时,影响插入速度的主要是索引、唯一性校验、一次插入记录条数等。根据这些情况可以分别进行优化。这里我们分为MyISAM引擎和InnoDB存储引擎来讲。

  1. MyISAM引擎表:

禁用索引

对于非空表,插入记录时,MySQL会根据表的索引对插入的记录建立索引。如果插入大量数据,建立索引就会降低插入记录的速度。为了解决这种情况,可以在插入记录之前禁用索引,数据插入完毕后再开启索引。禁用索引的语句如下:
ALTER TABLE 表 DISABLE KEYS;

重新开启索引的语句如下:
ALTER TABLE 表 ENABLE KEYS;

禁用唯一性检查
插入数据时,MySQL会对插入的记录进行唯一性校验。这种唯一性校验会降低插入记录的速度。为了降低这种情况对查询速度的影响,可以在插入记录之前禁用唯一性检查,等到插入完毕后再开启。禁用唯一性检查语法如下:
SET UNIQUE_CHECKS=0;

开启唯一性检查的语句如下:
SET UNIQUE_CHECKS=1;

使用批量插入
插入多条记录时,可以使用一条INSERT语句插入一条记录,也可以使用一条INSERT语句插入多条记录。插入一条记录的INSERT语句情形如下:

insert into student values(1,'zhangsan',18,1);
insert into student values(2,'lisi',17,1);
insert into student values(3,'wangwu',17,1);
insert into student values(4,'zhaoliu',19,1);

使用一条INSERT插入多条记录的情形如下:

insert into student values
	(1,'zhangsan',18,1),
	(2,'lisi',17,1),
	(3,'wangwu',17,1),
	(4,'zhaoliu',19,1);

第2种情形的插入速度要比第1种情形快。

使用LOAD DATA INFILE批量导入
当需要批量导入数据时,如果能用LOAD DATA INFILE语句,就尽量使用。因为LOAD DATA INFILE语句导入数据的速度比INSERT语句快

  1. InnoDB引擎表:

禁用唯一性检查
在插入数据前执行set unique_checks=0来禁止唯一性检查,在导入数据完成之后再开启唯一性检查set checks_unique=1。这个和MyISAM引擎的使用方法一样。

禁用外键检查
在插入数据前执行set foreign_key_checks=0;禁用外键检查,在插入完成后执行set foreign_key_checks=1;恢复外键检查。

禁止自动提交
插入数据前禁用事务自动提交,再数据插入完成后再开启。
禁用:set autocommit=0;
开启:set autocommit=1;

15.3.6、尽量使用非空约束

在设计字段的时候,如果业务允许,建议尽量使用非空约束。这样做的好处是:
①进行比较和计算时,省去判断可为NULL值字段是否为空的开销,提高存储效率。
②非空字段也容易创建索引。因为索引NULL列需要额外的空间来保存,所以要占用更多的空间。使用非空约束,就可以节省存储空间(每个字段1个bit)

15.3.7、分析表、检查表、优化表

  1. 分析表

MySQL中提供了ANALYZE TABLE语句分析表,ANALYZE TABLE语句的基本语法如下:
ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE 表1,表2;

默认的,MySQL服务会将ANALYZE TABLE语句写到binlog中,以便在主从架构中,从服务能够同步数据。可以添加参数LOACL或者NO_WRITE_TO_BINLOG取消语句写到binlog中。

使用ANALYZE TABLE分析表时,数据库系统会自动对表加上一个只读锁。在分析期间,只能读取表中的记录,不能更新和插入记录。ANALYZE TABLE语句能够分析InnoDB和MyISAM类型的表,但是不能作用于视图

ANALYZE TABLE分析表后的统计结果会反应到cardinality的值,该值统计了表中某一键所在的列不重复的值的个数该值越接近表中的总行数,则在表连接查询或者索引查询时,就越优先被优化器选择使用。 也就是索引列的cardinality的值与表中数据的总条数差距越大,即使查询的时候使用了该索引作为查询条件,存储引擎实际查询的时候使用的概率就越小。下面通过例子来验证下。cardinality可以通过SHOW INDEX FROM表;查看。

  1. 检查表

MySQL中可以使用CHECK TABLE语句来检查表。CHECK TABLE语句能够检查InnoDB和MyISAM类型的表是否存在错误。CHECK TABLE语句在执行过程中也会给表加速只读锁

对于MyISAM类型的表,CHECK TABLE语句还会更新关键字统计数据。而且,CHECK TABLE也可以检查视图是否有错误,比如视图中被引用的表已不存在。该语句的基本语法如下:
CHECK TABLE 表1,表2 [option]

option有五个取值:

  • QUICK:不扫描行,不检查错误连接。
  • FAST:只检查没有被正确关闭的表。
  • CHANGED:只检查上次检查后被更改的和没有被正确关闭的表。
  • MEDIUM:扫描行,以验证被删除的连接是有效的。也可以计算各行的关键字校验和,并使用计算出的校验和的时间验证这一点。
  • EXTENDED:对每行的所有关键字进行一个全面的关键字查找。这可以确保表是100%一致的,但是花的时间较长。

option只对MyISAM类型的表有效,对InnoDB类型的表无效.

该语句对于检查的表可能会产生多行信息。最后一行有一个状态的 Msg_type 值,Msg_text 通常为 OK。如果得到的不是 OK,通常要对其进行修复;是 OK 说明表已经是最新的了。表已经是最新的,意味着存储引擎对这张表不必进行检查。

  1. 优化表

方式1: OPTIMIZE TABLE
MySQL中使用 OPTIMIZE TABLE 语句来优化表。但是,OPTILMIZE TABLE语句只能优化表中的VARCHAR BLOBTEXT 类型的字段。一个表使用了这些字段的数据类型,若已经 删除了表的一大部分数据,或者已经对含有可变长度行的表(含有VARCHAR、BLOB或TEXT列的表)进行了很多 更新 ,则应使用OPTIMIZE TABLE来重新利用未使用的空间,并整理数据文件的 碎片

OPTIMIZE TABLE 语句对InnoDB和MyISAM类型的表都有效。该语句在执行过程中也会给表加上 只读锁

OPTILMIZE TABLE语句的基本语法如下:
OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...

LOCAL | NO_WRITE_TO_BINLOG关键字的意义和分析表相同,都是指定不写入二进制日志。

方式2: mysqlcheck命令
在终端中使用如下指令来优化表:
mysqlcheck -o 数据库名 表名 -u root -p密码;
mysqlchenk是Linux中的rompt,-o是代表Optimize。

优化所有表:
mysqlckeck -o 数据库 -u root -p密码;

mysqlcheck -o --all-databases -u -p密码;

15.3.8、小结

上述这些方法都是有利有弊的。比如:

  • 修改数据类型,节省存储空间的同时,你要考虑到数据不能超过取值范围;
  • 增加冗余字段的时候,不要忘了确保数据一致性;
  • 把大表拆分,也意味着你的查询会增加新的连接,从而增加额外的开销和运维的成本。

因此,你一定要结合实际的业务需求进行权衡。

15.4、大表优化

当MySQL单表记录数过大时,数据库的CURD性能会明显下降,一些常见的优化措施如下:

15.4.1、限定查询范围

禁止不带任何限制数据范围条件的查询语句。比如:当用户查询历史订单的时候,我们可以将范围控制在一个月内。

15.4.2、读/写分离

经典的数据库拆分方案,主库负责写,从库负责读。

  • 一主一从模式
    MySQL高级(SQL优化)_第33张图片
  • 双主双从模式
    因为一主一从模式一旦出现服务器宕机就完了,因而出现了双主双从模式,即对主服务器进行复制备份,当主机宕机就启用备份的服务器。
    MySQL高级(SQL优化)_第34张图片

15.4.3、垂直分表

当数据量级达到千万级以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。

  • 如果数据库中的表过多,可以采用垂直分库的方式,将关联的数据表都部署到同一个数据库上。
  • 或将功能相同的放到一个数据库中。
    MySQL高级(SQL优化)_第35张图片
  • 如果表中的列过多,可以采用垂直分表的方式,将一张表拆分成多张表,把经常一起使用的列放到同一张表中。
    MySQL高级(SQL优化)_第36张图片
    垂直拆分的优点:
    可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。

垂直拆分的缺点:
主键会出现冗余,需要管理冗余列,并会引起JOIN操作。此外,垂直拆分会让事务变得更加复杂。

15.4.4、水平拆分

  • 尽量控制单表数据量的大小,建议控制在1000万以内。1000万并不是MySQL数据库的限制,过大会造成修改表结构、备份、恢复都会有很大的问题。此时可以用历史数据归档(应用于日志数据),水平分表(应用于业务数据)等手段来控制数据量大小。
  • 这里我们主要考虑业务数据的水平分表策略。将大的数据表按照某个属性维度分拆成不同的小表,每张小表保持相同的表结构。比如你可以按照年份来划分,把不同年份的数据放到不同的数据表中。
  • 水平分表仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以水平拆分最好分库,从而达到分布式的目的。

MySQL高级(SQL优化)_第37张图片
水平拆分能够支持非常大的数据量存储,应用端改造也少,但分片事务难以解决跨节点JOIN性能较差,逻辑复杂。《Java工程师修炼之道》作者推荐尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果是在要分片,尽量选择客户端分片架构,这样可以减少一次的中间件的网络I/O。

下面补充一下数据库分片的两种常见方案:

  • 客户端代理·: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
  • 中间件代理·: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat、360的Atlas、网易的DDB等等都是这种架构的实现。

15.5、其他调优策略

15.5.1、服务器语句超时处理

在MySQL 8.0中可以设置 服务器语句超时的限制 ,单位可以达到 毫秒级别 。当中断的执行语句超过设置的毫秒数后,服务器将终止查询影响不大的事务或连接,然后将错误报给客户端。

设置服务器语句超时的限制,可以通过设置系统变量 MAX_EXECUTION_TIME 来实现。默认情况下,MAX_EXECUTION_TIME=0,代表没有时间限制。 例如:
SET GLOBAL MAX_EXECUTION_TIME=2000;
SET SESSION MAX_EXECUTION_TIME=2000; #指定该会话中SELECT语句的超时时间

15.5.2、创建全局通用表空间

MySQL8.0 使用CREATE TABLESPACE语句来创建一个全局通用表空间。全局表空间可以被所有的数据库的表共享,而且相比于独享表空间,使用手动创建共享表空间可以节约元数据方面的内存 。可以在创建表的时候,只读属于哪个表空间,也可以对已有表进行表空间修改等 。

比如创建名为tbtest的共享表空间:
CREATE TABLESPACE tbtest ADD datafile 'tbtest.idb' file_block_size=16k;

指定已有表空间为共享表空间:
CREATE TABLE test(id int,name varchar(10)) engine=innodb default cherset utf8mb4 tablespace tbtest;

也可以通过ALTER来指定已有表空间为共享表空间:
ALERT TABLE 表 tbtest;

如何删除创建的共享表空间?因为是共享表空间,所有不能直接通过drop table来删除,这样并不能回收空间。当确定共享表空间的数据都没用,并且依赖该表空间的表均已经被删除时,可以通过drop tablespace删除共享表空间来释放空间,如果依赖该共享表空间的表存在,就会删除失败。

所以应该先删除依赖该表空间的数据表:
DROP TABLE 表;

最后即可删除共享表空间:
DROP TABLESPACE 共享表空间名;

15.5.3、隐藏索引对调优的帮助(MySQL 8.0 新特性)

不可见索引的特性对于性能调试非常有用。在MySQL 8.0 中,索引可以被“隐藏”和“显示”。当一个索引被invisible隐藏时,它不会被查询优化器所使用 。也就是说,管理员可以隐藏一个索引,然后观察对数据库的影响。如果数据库性能有所下降,就说明这个索引时有用的,于是将其“恢复显示”即可;如果数据库性能看不出变化,就说明这个索引时多余的,可以删掉了。

需要注意的是,当所有被隐藏时,它的内容仍然时和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么就可以将其删除,因为索引的存在会影响插入、更新、和删除的性能。

表中的主键不能被设置为invisible。

十六、事务

16.1、数据库事务概述

事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

可以通过SHOW ENGINES,来查看哪些数据库支持事务。

16.1.1、什么是事务?

事务:一组逻辑操作单元,是数据从一种状态变成另一种状态。
事务处理原则 :保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保持下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态。

如:AA用户给BB用户转账100,当AA转账成功,且BB收款成功,这个事务才算完成。

# AA转账
update account set money = money - 100 where name = 'AA';

# BB收款,此过程若出现服务器宕机,则事务操作不成功
update account set money = money + 100 where name ='BB';

16.1.2、事务的ACID特性

  1. 原子性(atomicity):
  • 原子性是指事务时一个不可分割的工作单元,要么全部提交,要全部回滚。
  • 比如转账操作,要么转账成功,要么转账失败,是不存在中间状态的。
  • 如果无法保证原子性会怎么样?就会出现数据不一样的情况。如A用户减去100,而B用户增加100失败,则系统无故丢失了100元。
  1. 一致性(consistency):
  • 一致性是指业务上的。
  • 根据定义,一致性是指事务执行前后,数据从一个合法性状态转换到另一个合法性状态。这种状态是语义上的而不是语法上的,跟具体业务有关。
  • 那什么是合法的数据状态呢?满足预定的约束的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。

举例1
A账户有200元,转账300元出去,此时A账户余额为“-100元”。这就不满足一致性,因为数据是不一致的,因为你定义了一个状态,余额这列必须为“>=0”。

举例2
A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求A+B的总余额必须不变。

举例3
在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。

  1. 隔离性(isolation)
  • 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能相互干扰。
  • 如果无法保证隔离性会怎样?假设A账户有200元,B账户有0元。A账户往B账户转账两次,每次金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形,两条事务同时进行,就像多线程操作共享数据一样,会出现数据不同步,导致数据错误。
    update accounts set money=money-50 where name='AA';
    update accounts set money=money+50 where name='BB';
    MySQL高级(SQL优化)_第38张图片
  1. 持久性(durability)
  • 持久性是指一个事务一旦被提交,它对数据中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。也就是事务被提交就会永久保存到磁盘。
  • 持久性是通过事务日志来保证的。日志包括了重做日志回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对于的进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

16.1.3、小结

  • ACID使事务的四大特性,在这四个特性中,原则性是基础,隔离性是手段,一致性是条件,持久性是我们的目的。
  • 数据库事务,其实就是数据库设计者为了方便起见,把需要满足原子性、隔离性、一致性、持久性的一个或多个数据库操作称为一个事务。

16.1.4、事务的状态

我们现在知道事务是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把事务大致划分成几个状态:

  • 活动的(active)
    事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的状态。
  • 部分提交的(partially)
    当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘(刷盘)时,我们就说该事务处在部分提交的状态。注意:部分提交是没有提交到磁盘,不是只提交了一部分。事务的操作是执行完了的
  • 失败的(failed)
    当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误、断电等)而无法继续执行,或者认为的停止当前正在执行的事务,我们就说该事务在失败的状态。
  • 中止的(aborted)
    如果事务执行了一部分而变为失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。我们把这个还原的过程称为回滚。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处于中止的状态。
  • 提交的(committed)
    当一个处在部分提交的状态的事务将修改过的数据都通过不到磁盘上之后,我们就可以说该事务处在了提交的状态。

一个基本的状态转换图如下:
MySQL高级(SQL优化)_第39张图片
由图可知,一个事务只有提交的中止的两种结果。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效;对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。

16.2、事务的使用

使用事务有两种方式,分别为 显式事务隐式事务

事务的完成过程:

  1. 开启事务
  2. DML操作,即对记录的增删等
  3. 事务的结束状态:提交的(commit)、中止的(rollback)

16.2.1、显式事务

  1. 开启事务:BEGINSTART TRANSACTION
    begin和start transaction的区别在于,后者后边能跟几个修饰符:
  • read only:标记当前事务为只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
  • read write:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
  • with consistent snapshot:启动一致性读。
  1. 提交事务:COMMIT;,事务提交后,对数据库的修改是永久性的。
  2. 中止事务(即回滚事务):ROLLBACK,回滚事务,即撤销正在进行的所有没有提交的修改。
  • 将事务回滚到某个保存点:ROLLBACK TO 保存点名称;
  • 创建保存点:SQVEPOINT 保存点名称;,创建保存点,方便针对保存点进行回滚。一个事务可创建多个保存点。
  • 删除保存点:RELEASE SAVEPOINT 保存点名称;

16.2.2、隐式事务

隐式事务也就是事务的自动提交。

MySQL系统中有一个系统变量autocommit;,来开启或关闭事务自动提交。

默认情况下,每一个DML语句都会自动提交。

我们可以通过show variables like 'autocommit';来查看自动提交是否开启。

开启隐式事务:set autocommit=on;

关闭隐式事务:

  • 当显式的使用BEGINSTART TRANSACTION;开启显式事务,这样,在本次事务提交或者回滚前会暂时关闭掉自动提交功能。
  • 把系统变量autocommit设置为OFF
    SET autocommit=OFF;

    SET autocommit=ON;

这样的话我们写入的多条DML语句就算同一个事务,不会将每一个DML操作都自动提交。

补充:Oracle默认不会自动提交,需要手动COMMIT;而MySQL默认会自动提交。

16.2.3、会隐式提交事务的情况

  1. 数据定义语言(Data definition language,即DDL)

数据库对象,指的就是数据库视图存储过程等结构。当我们使用CREATEALTERDROP等语句去修改数据库对象时,就会隐式的提交前边语句所属于的事务

  1. 隐式使用或修改MySQL数据库中的表

当我们使用ALERT USERCREATE USERDROP USERGRANTRENAMEREVOKESET PASSWORD等语句时,也会隐式的提交前面的语句所属的事务

  1. 事务控制或关于锁的语句

①当我们在一个事务还没提交或者回滚时又使用START TRANSACTION或者 BEGIN语句开启了另一个事务时,会隐式的提交上一个事务
②开启autocommit,也会隐式的提交前面的语句所属的事务
③使用LOCK TABLESUNLOCK TABLES等关于锁定的语句也会隐式的提交前面语句所属的事务

  1. 加载数据的语句

使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前面语句所属的事务。

  1. 关于MySQL复制的一些语句

使用START SLAVESTOP SLAVERESET SLAVECHANGE MASTER TO等语句时,也会隐式的提交前面语句所属的事务。

  1. 其他一些语句

使用ANALYZE TABLECHECK INDEXCHECK TABLEFLUSHLOAD INDEX INTO CACHEOPTIMIZE TABLEREPAIR TABLERESET等语句一户隐式提交前面语句所属的事务。

16.2.4、completion_type的作用

  1. set @@completion_type=0,这时默认情况当我们使用COMMIT来显式提交事务,在执行下一个事务时,就需要使用START TRANSACTION 或者 BEGIN来开启事务。
  2. set @@completion_type=1,此时,我们提交事务后就相当于执行了 COMMIT AND CHAIN,开启了一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务,每条事务都会自动提交
  3. set @@completion_type=2,此时COMMIT=COMMIT AND RELEASE,也就是我们提交后,会自动于服务器断开连接。

当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。
当我们设置 autocommit=1 时,每条 SQL 语句都会自动进行提交。 不过这时,如果你采用 START TRANSACTION 或者 BEGIN 的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效,在 ROLLBACK 时才会回滚。

16.3、事务隔离级别

MySQL是一个 客户端/服务器 架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session )。

每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能 同时处理多个事务

事务有 隔离性的特性,理论上在某个事务 对某个数据进行访问 时,其他事务应该进行 排队 ,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对 性能影响太大 ,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些 ,那就看二者如何权衡取舍了。

16.3.1、数据并发问题

针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在 不保证串行执行 (也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:

  1. 脏写( Dirty Write ):修改了别人未提交的事务。
    对于两个事务 Session A、Session B,如果事务Session A 修改了另一个未提交事务Session B修改过的数据,那就意味着发生了 脏写

  2. 脏读( Dirty Read ):读取了别人未提交的事务。
    对于两个事务 Session A、Session B,Session A读取了已经被 Session B 更新但还没有被提交的字段。之后若 Session B 回滚 ,Session A 读取 的内容就是临时且无效的。

  3. 不可重复读( Non-Repeatable Read ):读取一个数据之后,这个数据又被更新了(相当于读取的不是最新的值)。
    对于两个事务Session A、Session B,Session A 读取了一个字段,然后Session B更新了该字段。 之后Session A 再次读取 同一个字段, 值就不同 了。那就意味着发生了不可重复读。

  4. 幻读( Phantom ):一个表被读取后,新增了几条记录,再次读取的时候就会多出几条记录来。
    对于两个事务Session A、Session B, Session A从一个表中读取了一个记录, 然后Session B在该表中插入了一些新的记录。 之后, 如果 Session A再次读取同一个表,就会多出几条记录。那就意味着发生了幻读。新增加的记录称为幻影记录

注意新增字段才是幻读,字段被删除,减少了不是幻读。

16.3.2、SQL中的四种隔离级别

隔离级别就是解决数据并发问题的。

上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:

脏写 > 脏读 > 不可重复读 > 幻读

我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。 SQL标准 中设立了4个隔离级别 :

  • READ UNCOMMITTED读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
  • READ COMMITTED读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
  • REPEATABLE READ可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
  • SERIALIZABLE可串行化(序列化),确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。

SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
其中,每一种隔离级别都解决了脏写的问题,因为脏写问题太严重。
MySQL高级(SQL优化)_第40张图片
脏写 怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。

不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4种事务隔离级别与并发性能的关系如下:
MySQL高级(SQL优化)_第41张图片

16.3.3、如何设置事务的隔离级别

不同的数据库厂商对SQL标准中归档的四种激励级别的支持情况不一样。比如Oracle只支持READ COMMITTED(默认)和SERIALIZABLE。MySQL支持SQL标准的4种隔离级别,但和SQL标准会有些许不同,后边会讲。

查看隔离级别:

  • 查看隔离级别,MySQL 5.7.20的版本之前:SHOW VARIABLES LIKE 'tx_isolation';

  • 查看隔离级别,MySQL 5.7.20的版本及之后:SHOW VARIABLES LIKE 'transaction_isolation';

  • 所有版本通用的查看方式:SELECT @@transaction_isolation;

设置隔离级别:
通过下面的语句修改事务的隔离级别:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
#其中,隔离级别取值:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE

或者

SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别取值:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE

关于设置时使用GLOBAL或SESSION的影响:

  • 使用 GLOBAL 关键字(在全局范围影响):
    SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';

    • 当前已经存在的会话无效
    • 只对执行完该语句之后产生的会话起作用
  • 使用 SESSION 关键字(在会话范围影响):
    SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';

    • 对当前会话的所有后续的事务有效
    • 如果在事务之间执行,则对后续的事务有效
    • 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
  • 在服务器启动时修改事务的默认隔离级别:transantion_ioslation=隔离级别

小结:
数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

16.3.4、事务分类

从事务理论的角度来看,可以把事务分为以下几种类型:

  1. 扁平事务(Flat Transactions):最简单,使用最频繁的事务。其操作是原子的,要么都执行,要么都回滚。
  2. 带有保存点的扁平事务(Flat Transactions with Savepoints):带有保存点savepoint的扁平事务。
  3. 链事务(Chained Transactions):即一个事务在执行时会对数据加锁,其他事务在该事务提交之前不能操作该事务涉及的数据。事务是一个一个链式执行。
  4. 嵌套事务(Nested Transactions):就是事务里边嵌套事务。
  5. 分布式事务(Distributed Transactions):在分布式环境下运行的扁平事务。

十七、MySQL事务日志

事务有4种特性:原子性、一致性、隔离性、持久性。那么事务的4种特性到底是基于什么机制实现的呢?

  • 事务的隔离性由锁机制实现。
  • 事务的原子性、一致性、持久性由事务的redo日志和undo日志来保证。
    • redo log重做日志,主要针对部分提交的状态,事务的操作在内存中进行,还没有写入到磁盘。记录的是物理级别的页上的操作,即数据在物理磁盘上的具体存储位置,以及sql操作。若出现宕机,可通过重做日志将事务操作恢复到磁盘,用来保证事务的持久性。
    • undo log回滚日志,回滚记录到某个特定版本,记录的是逻辑操作 ,即具体的sql语句,如一条记录进行了insert操作,那么undo log就记录一条与之相反的delete操作。主要用于事务的回滚(undo log记录的是每个修改操作的逆操作)和一致性非锁定读(undo log回滚行记录到某种特定的版本–MVCC,即多版本并发控制)。用来保证事务的一致性、原子性。

17.1、redo日志

InnoDB存储引擎是以页为单位来管理存储空间的。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘(checkPoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快。

17.1.1、为什么需要redo日志

对数据增删改等操作都是在内存中进行的,在内存中COMMIT了,不等于已经刷盘到磁盘了,因为从内存将数据刷盘到磁盘是以一定频率进行的。 如果在刷盘之前服务器出现了宕机等问题,内存中的数据就会丢失,无法保证事务的持久性。

那么如何保证事务的持久性呢?

  • 把事务每一次所修改的所有页面都刷新到磁盘(实时刷)。这个方案简单,但是会有一些问题:
    • 修改量于刷新磁盘工作量不成比例
      因为InnoDB是以页为单位来进行磁盘IO的,如果我们仅仅修改了某一条记录,它也会将这条记录所在的页进行IO,太小题大作了。
    • 随机IO刷新较慢
      一个事务可能包含多条语句,即使一条语句页可能修改许多页面,这些页面不可能都是连续的。因而在将事务修改的页刷新到磁盘时,需要进行很多随机IO,随机IO比顺序IO慢。
  • 只让事务所修改的数据永久生效,即使系统崩溃,在重启后也能把这种修改恢复到磁盘。
    使用WAL技术(Write-Ahead Logging),这种技术思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功。这里的日志就是redo log。当服务器宕机或数据未刷新到磁盘的时候,可以通过redo log来恢复数据到磁盘,保证ACID中的D,就是redo log的作用。

17.1.2、redo日志的优点、特点

优点:

  • redo日志降低了磁盘刷新频率。不用每次提交事务都进行刷盘。
  • redo日志占用的空间很小。存储表空间ID、页号、偏移量以及需要更新的值,所需的存储空间是很小的,刷盘快。

特点:

  • redo日志是顺序写入磁盘的。
    在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO,比随机IO快。

  • 事务执行过程中,redo log不断记录
    redo log与bin log的区别就是:
    redo log是在存储引擎层产生的,每一条sql对数据的修改都会不断记录到redo log。
    而bin log是数据库层产生的,在提交事务的时候才会将事务对数据的修改提交到bin log。

17.1.3、redo日志的组成

redo log可以简单分为以下两个部分:

  1. 重做日志的缓冲(redo log buffer),保存在内存中,容易丢失。
    在服务器启动时就向操作系统申请了一大片连续内存空间名为redo log buffer,即redo日志缓冲区。这片内存空间被划分为若干个连续的redo log block。一个redo log block占用512字节空间。
    MySQL高级(SQL优化)_第42张图片
  2. 重做日志文件(redo log file),保存在磁盘中,是持久化的。
    在终端通过var/lib/mysql/ll目录中可查看,其中ib_logfile0ib_logfile1都是redo日志。其占用空间是固定的,不会变大或变小。

17.1.4、redo日志执行过程

以一个更新事务为例,redo log 流转过程,如下图所示:
1. 先从磁盘读取数据到内存,放在buffer pool中。
2. 执行事务操作。每执行一条sql语句,就记录到内存中的redo log buffer中。(事务边执行边记录,而不是执行完再记录)。
3. 将内存中redo log buffer中的记录采用追加的方式写入磁盘redo log file中。
4. 将内存中事务对数据的修改更新到磁盘中。
MySQL高级(SQL优化)_第43张图片

体会:
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。

17.1.5、redo log刷盘策略

这里的刷盘策略不是指将事务对数据的修改更新到磁盘,而是将内存中的redo log buffer更新到磁盘中的redo log file。

刷盘策略是指何时将内存中的redo log buffer更新到磁盘中的redo log file。
MySQL高级(SQL优化)_第44张图片
注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
  • 设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
  • 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。

我们可以通过show variables like 'innodb_flush_log_at_trx_commit';来查看当前值,默认为1.

注意:

  • InnoDB存储引擎有一个后台线程,每隔1s,就会把redo log buffer中的内容写到文件系统缓存(page cache),然后调用刷盘操作。
  • 当我们没有主动设置刷盘的时候,就会执行这个后台线程,每1s刷盘一次到文件系统缓存。
  • 也就是说,一个没有提交的事务的redo log记录,也可能会被刷盘,因为后台会默认刷盘。
  • 除了后台线程每1s默认进行刷盘,还有当redo log buffer占用空间即将达到innodb_log_buffer_size(这个参数默认是16M)的一半的时候,后台线程也会自动刷盘
  • innodb_log_buffer_size大小可通过show variables like '%innodb_log_buffer_size%';来查看,默认为16M,最大值为4096M,最小为1M。

17.1.6、不同策略刷盘流程图

MySQL高级(SQL优化)_第45张图片

MySQL高级(SQL优化)_第46张图片

MySQL高级(SQL优化)_第47张图片

17.1.7、redo log file

1. 相关参数设置

  • innodb_log_group_home_dir :指定 redo log 文件组所在的路径,默认值为./,表示在数据库的数据目录下。MySQL的默认数据目录( var/lib/mysql )下默认有两个名为 ib_logfile0ib_logfile1 的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。此redo日志文件位置还可以修改。
  • innodb_log_files_in_group:指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1…iblogfilen。默认2个,最大100个。

MySQL高级(SQL优化)_第48张图片

  • innodb_flush_log_at_trx_commit:控制 redo log 刷新到磁盘的策略,默认为1。
  • innodb_log_file_size:单个 redo log 文件设置大小,默认值为 48M 。最大值为512G,注意最大值指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大于最大值512G。

MySQL高级(SQL优化)_第49张图片
根据业务修改其大小,以便容纳较大的事务。编辑my.cnf文件并重启数据库生效,如下所示
在这里插入图片描述

2. 日志文件组

从上边的描述中可以看到,磁盘上的redo日志文件不止一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字](数字可以是0、1、2…)的形式命名,每个的redo日志文件大小都是一样的。

在将日志写入文件组时,是从ib_logfile0开始写,如果ib_logfile0写满了,就接着ib_logfile1写。以此类推。如果写到最后一个文件,就重新转到ib_logfile0继续写,流程图如下:
MySQL高级(SQL优化)_第50张图片
总共的redo日志文件大小其实就是: innodb_log_file_size × innodb_log_files_in_group

采用循环使用的方式向redo日志文件组里写数据的话,会导致后写入的redo日志覆盖掉前边写的redo日志?当然!所以InnoDB的设计者提出了checkpoint的概念。

3. checkpoint

在整个日志文件组中还有两个重要的属性,分别是write pos、checkpoint。

  • write pos:是当前记录的位置,一边写一边后移。
  • checkpoint:是当前要擦除的位置,也是往后推移。

每次刷盘redo log记录到日志文件组中,write pos位置就会后移更新。每次MySQL加载日志文件组恢复数据时,会清空加载过的redo log记录,并把checkpoint后移更新。write pos 和checkpoint之间的还空着的部分可以用来写入新的redo log记录。
MySQL高级(SQL优化)_第51张图片
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
MySQL高级(SQL优化)_第52张图片

redo log小结

InnoDB的更新采用的是Write Ahead Log(预先日志持久化)策略,即先写日志,再写入磁盘)。
MySQL高级(SQL优化)_第53张图片

17.2、undo日志

redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据前置操作 其实是要先写入一个 undo log

17.2.1、什么是undo日志

事务需要保证 原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:

  • 情况一:事务执行过程中可能遇到各种错误,比如 服务器本身的错误操作系统错误 ,甚至是突然 断电 导致的错误。
  • 情况二:程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前事务的执行。

以上情况出现,我们需要把数据改回原先的样子,这个过程称之为 回滚 ,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合 原子性 要求。

  • 插入一条记录时,至少要把这条记录的主键值记录下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。(对于每个INSERT,InnoDB存储引擎会完成一个DELETE)
  • 删除一条记录时,至少要把这条记录中的内容都记下来,这样在回滚时把再把这些内容组成的记录插入到表中就好了。(对于每个DELETE,InnoDB存储引擎会执行一个INSERT )
  • 修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样在回滚时再把这条记录更新为旧值即可。(对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将值还原到修改前)

此外,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,因为undo log也需要持久化的保护。

17.2.2、undo日志的作用

  • 作用1:回滚数据
    注意:回滚数据只是逻辑上的回滚,比如插入了数据可能会新增数据页,在回滚操作时,只是将数据页中的数据删了,但数据页还存在,不会被删除。因为若有多个事务插入的数据都放到了这个数据页中,因为一个事务的回滚而删了数据页,那不就完蛋了。

  • 作用2:MVCC多版本并发控制
    在InnoDB存储引擎中MVCC的实现是通过undo来完成的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以实现非锁定读取。

17.2.3、undo的存储结构

1. 回滚段与undo页

回滚段:
InnoDB对undo log的管理采用段的方式,也就是回滚段(rollback segment) 。每个回滚段记录了1024undo log segment ,而在每个undo log segment段中进行 undo页 的申请。

  • InnoDB1.1版本之前 (不包括1.1版本),只有一个rollback segment,因此支持同时在线的事务限制为 1024 。虽然对绝大多数的应用来说都已经够用。
  • 从1.1版本开始InnoDB支持最大 128个rollback segment ,故其支持同时在线的事务限制提高到了 128*1024
  • 通过show variables like 'innodb_undo_logs';可以查看同时在线的事务限制个数。

undo页重用:
当事务提交时,并不会立刻删除undo页。因为可能会被重用,undo log在commit后,会被放到一个链表中,然后判断,若undo页的使用空间小于3/4,则会被重用,不会被回收。其他事务的undo log可以记录在当前undo页的后面。由于undo log是离散的,所有清理对应的磁盘空间时,效率不高。

2. 回滚段与事务

  1. 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。

  2. 当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。

  3. 在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用。

  4. 回滚段存在于undo表空间中,在数据库中可以存在多个undo表空间,但同一时刻只能使用一个undo表空间。
    MySQL高级(SQL优化)_第54张图片

  5. 当事务提交时,InnoDB存储引擎会做以下两件事情:

    • 将undo log放入列表中,以供之后的purge操作。
    • 判断undo log所在的页是否可以重用,若可以分配给下个事务使用。

3. 回滚段中的数据分类

  1. 未提交的回滚数据(uncommitted undo information):该数据所关联的事务并未提交,用于实现读一致性,所有该数据不能被其他事务的数据覆盖。
  2. 已经提交但未过期的回滚数据(committed undo information):该数据关联的事务已经提交,但是仍受到undo retention参数的保持时间的影响。
  3. 事务已经提交并过期的数据(expired undo information):事务已经提交,而且数据保存时间已经超过undo retention参数指定的时间,属于已经过期的数据。当回滚段满了之后,有优先覆盖“事务已经提交并过期的数据”。

事务提交后不能马上删除undo log及undo log所在的页。因为可能还有其他事务需要通过undo log来得到行记录之前的版本。故事务提交时将undo log放入一个链表中,是否可以最终删除undo log及undo log所在页由purge线程来判断。

4. undo的类型

在InnoDB存储引擎中,undo log分为:

  • insert undo log
    insert undo log是指事务在insert操作中产生的undo log。因为Insert操作的记录,只对事务本身可见,对其他事务不可见(这时事务隔离性的要求),故undo log可以在事务提交后直接删除。不需要进行purge操作。
  • uptate undo log
    update undo log记录的是对delete和update操作产生的undo’ log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge进行最后的删除。

purge线程

purge线程两个主要作用是:清理undo页清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删掉数据行,而是一种delete mark操作,在记录上标识delete_bit,而不是删除记录。是一种“假删除”,只是做了个标记,真正的删除工作需要后台purge线程去完成。

17.3、总结

MySQL高级(SQL优化)_第55张图片

undo log是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。
redo log是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程。

十八、锁

事务的隔离性是由锁来实现的。

18.1、什么是锁?

是计算机协调多个进程或线程并发访问某一资源的机制。在程序开发中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等),我们就需要保证这个数据在任何时刻最多只有一个线程在访问,保证数据的完整性一致性安全性。在开发中,加锁是为了保证数据的一致性,这个思想在数据库领域同样很重要。

在数据库中,除传统的计算机资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。为保证数据的一致性,需要对并发操作进行控制,因此产生了锁。同时锁机制也为实现MySQL个各个隔离级别提供了保证。锁冲突也是影响数据库并发访问性能的一个重要因素。

18.2、MySQL并发事务访问相同记录

18.2.1、读-读情况

读-读情况,即并发事务同时读取同一条记录。因为读操作不会对记录产生影响,所有这种情况是允许发生的

18.2.2、写-写情况

写-写情况。即并发事务同时对同一条记录进行修改

这种这种情况会发生脏写问题,任何一种隔离级别都不允许脏写的出现,所有在多个未提交事务同时对一条记录做改动时,需要让它们排队执行,这个排队的过程是通过来实现的。锁其实是一个内存中的结构,事务在执行前本来没有锁的,也就是说一开始是没有锁结构和记录进行关联的。

锁结构里由很多属性,这里描述两个重要的属性:

  • trx信息:代表这个锁结构是由哪个事务生成的。
  • is_waiting:代表当前事务是否在等待执行,正在执行的事务会被加锁。

小结几种说法:

  • 不加锁
    不需要再内存中生成相应的锁结构,直接执行操作。
  • 加锁成功、获取锁成功
    再内存中生成了相应的锁结构,二七锁结构的is_waiting属性为false,也就是事务可以继续执行操作。
  • 加锁失败、获取锁失败
    在内存中生成了相应的锁结构,不过is_waiting属性为true,也就是事务需要等待执行。

18.2.3、读-写情况

读-写,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读不可重复读幻读 的问题。

18.3、并发问题的解决方案

怎么解决读-写(也就是脏读、不可重复读、幻读)情况,这里提供了两种解决方案:

  • 方案一:读操作利用MVCC,写操作进行加锁

    • MVCC就是生成一个Read View,通过read view找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能督导生成read view之前的已提交事务所做的更改,在生成read view之前,未提交的事务或者之后才开启的事务所做的更改是看不到的。
    • 写操作是针对最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。

    普通的select语句在read committed和repeatable隔离级别下会使用到MVCC读取记录。

    • read committed隔离级别下,一个事务在执行过程中每次执行select操作时都会生成一个read view,read view的存在本身就保证了事务不可以读取未提交的事务所做的更改
      repeatable read隔离级别下,一个事务在执行过程中只有第一次执行select操作才会生成一个readview,之后的select操作都会复用这个readview,这样就避免了不可重复读和幻读的问题。
  • 方案二:读、写都进行加锁

    • 读和写都进行加锁,就意味着在执行的时候都需要排队,同时也解决了脏读不可重复读幻读的问题。
      • 脏读是因为当前事务读取了另一个未提交的事务写的记录而造成的。如果另一个事务在写记录的时候给这条记录进行加锁,那么当前事务就无法读取该记录了,所有就不会有脏读的问题。
      • 不可重复读是因为当前事务先读取一条记录,另一个事务对该记录修改并提交后,当前事务再次读取时会获得不同的值。如果当前事务在读记录时就给记录进行加锁,那么另一个事务就无法修改记录,自然也不会发生不可重复读。
      • 幻读是因为当前事务读取了一个范围的记录,然后另一个事务向该范围插入了新的记录,当前事务再次读取的时候就会多出几条记录。采用加锁的方式解决幻读就有一些问题,因为当前事务在第一次读记录时幻影记录并不存在,所以第一次读的时候加锁,插入的新记录是没有锁的。

总结:

  • 采用MVCC方式,读-写操作彼此不冲突,性能更高。
  • 采用加锁方式,读-写操作彼此需要排队,影响性能。

一般情况下使用MVCC更好,但是某些特殊情况下,会要求必须加锁执行。

18.4、锁的分类

锁的分类图,如下:

MySQL高级(SQL优化)_第56张图片

从数据操作的类型划分:读锁、写锁

  • 读锁(read lock):也称为 共享锁(Share Lock,S Lock) 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
  • 写锁(write lock) :也称为 排他锁(Exclusive Lock,X Lock) 、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。

1. 锁定读

有时候,我们需要在读取记录的时候来给这条记录加锁,来禁止别的事务读写该记录。

  • 对读取的记录加共享锁:允许记录被多个事务同时读取
    select...lock in share mode;
    或8.0新语法
    select...for share;

  • 对读取的记录加排他锁:一次只允许一个事务读取,会阻塞其他事务
    select...for update;

MySQL8新特性:

  • 在5.7之前的版本,select…for update,如果一直获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。
  • 在8.0中,在select…for update或selete…for share后边添加nowaitskip locked,会跳过锁等待,或跳过锁定。
    • nowait:若查询已被加锁,立刻返回报错信息。
    • skip locked:若查询已被加锁,立刻返回未被锁定的记录;若全被锁定,则返回empty。

2. 锁定写

平时常用的写操作无非就是delete、update、insert这三种。

  • delete
    删除一条记录时,会先在B+树种定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark操作。
  • update:对一条记录进行update操作时分为三种情况
    • 情况1:未修改该记录的键值,且被操作的列在磁盘上存储位置未发生变化。
      先在B+树中定位到这条记录,再获取X锁,最后再原记录的位置进行修改操作。
    • 情况2:未修改该记录的键值,但只有有一列被操作的记录在磁盘上存储位置发生了变化。
      先在B+树中定位到这条记录,再获取X锁,将该记录彻底删除(移入垃圾链表),最后插入一条新记录。
    • 情况3:修改了该记录的键值。
      相当于再原记录上做delete操作之后再进行insert。
  • insert
    一般情况下,插入一条新的记录并显示不加锁,通过一种隐式锁的结构来保护这条新插入的记录在本事务提交之前不被别的事务访问

从数据操作的粒度划分:表级锁、页级锁、行锁

为了尽可能地提高数据库地并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应系统性能两方面进行平衡,这样就产生了”锁粒度“的概念

1. 表锁(Table Lock)

表锁会锁定整张表,它是MySQL最基本的开销策略,并不依赖于存储引擎(MySQL所有引擎都支持表锁)。由于表级锁一次会将整张表都锁住,所以可以很好的避免死锁问题。

① 表级共享锁、排他锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的 S锁 或者 X锁 的。在对某个表执行一些诸如 ALTER TABLE 、 DROP TABLE 这类的 DDL 语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。这个过程其实是通过在 server层 使用一种称之为 元数据锁 (英文名: Metadata Locks ,简称 MDL )结构来实现的。

一般情况下,不会使用InnoDB存储引擎提供的表级别的S锁和X锁。只会在一些特殊情况下,比方说 崩溃恢复 过程中用到。比如,在系统变量 autocommit=0innodb_table_locks = 1 时, 手动 获取InnoDB存储引擎提供的表t 的S锁或者X锁可以这么写:

  • 加锁:
    • LOCK TABLES t READ; :InnoDB存储引擎会对表 t 加表级别的S锁,读锁
    • LOCK TABLES t WRITE; :InnoDB存储引擎会对表 t 加表级别的X锁,写锁
  • 释放锁:
    • unlock tables;:释放当前表的锁。
  • 查看当前表加过的锁
    • show open tables;
    • show open tables where 条件; 不过尽量避免在使用InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的 行锁`。

总结:
MyISAM在执行查询语句(select)前,

  • 会给涉及的所有表加读锁。
  • 在执行增删改前,会给涉及的表加写锁。

InnoDB不会为这个表添加读锁或写锁。

MySQL的表锁有两种模式:以MyISAM为例

  • 表共享读锁(Table Read Lock)
  • 表独占写锁(Table Write Lock)
锁类型 自己可读 自己可写 自己可操作其他表 他人可读 他人可写
读锁 否、等待
写锁 否,等待 否等待
② 意向锁 (intention lock)

InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁表级锁共存,而意向锁就是其中一种表锁

  • 意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(行锁与表锁)的锁共存。
  • 意向锁是一种不与行级锁冲突的表级锁,这一点很重要。

意向锁分为两种:

  • 意向共享锁(intention shared lock,IS):事务有意向对表中的某些行加共享锁(S锁)。
    select 列 from 表 ... lock in share mode;,事务要获取某些行的S锁,必须先获取表的IS锁。
  • 意向排他锁(intention exclusive lock,IX):事务有意向对表中的某些行加排他锁(X锁)。
    select 列 from table ... for update;,事务要获取某些行的S锁,必须先获取表的IX锁。

即:意向锁是存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB会获取该数据行所在数据表的对应意向锁。

意向锁解决的问题:
现在有两个事务a,b,其中a想在表级别上加共享锁或排他锁,若没有意向锁存在,那么a就需要遍历每一页或每一行是否存在锁。
如果存在意向锁,那么a就会受到b控制的表级别意向锁的阻塞。a在锁定表前不必检查各行或个页是否存在锁,只需要检查表上的意向锁。
简单的锁就是给更大一级别的空间示意里面是否已经上过锁

如果我们给某一行数据加上了排他锁,数据库会自动地给更大一级地空间加上意向锁,以告诉他人该表或页已经有人用过排他锁了。 这样当有人想要获取数据表排他锁的时候,只需要了解是否有人已经获取了这个数据表地意向排他锁即可。

意向锁的并发性:

  • 意向锁不会与行级的共享 / 排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。(不然我们直接用普通的表锁就行了)
  • 一条数据从被锁定到被释放的过程中,可能存在多种不同锁,但是这里我们只着重表现意向锁。
③ 自增锁(AUTO-INC锁)

插入数据的方式总共分为三类,分别是:

  1. “Simple inserts” (简单插入)
    可以 预先确定要插入的行数 (当语句被初始处理时)的语句。也就是在插入时,确定了要插入的值的个数。
  2. “Bulk inserts” (批量插入)
    事先不知道要插入的行数 (和所需自动递增值的数量)的语句。即向已有表添加值。InnoDB在每处理一行记录,会为AUTO_INCREMENT列分配一个新值。
  3. “Mixed-mode inserts” (混合模式插入)
    这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如 INSERT INTO teacher (id,name)VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); 只是指定了部分id的值。另一种类型的“混合模式插入”是 INSERT ... ON DUPLICATE KEY UPDATE

自增锁是,当向使用有auto_increment列的表中插入数据时需要获取的一种特殊的表级锁,在执行插入语句时就在表级别加一个AUTO_INC锁,然后然后为每条待插入记录的AUTO_INCREMENT修饰的列分配日递增的值,在该语句执行结束后,再把AUTO_INC锁释放掉。
一个事务在持有AUTO_INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。也正因为如此,其处理并发性的能力并不高当我们向一个有AUTO_INCREMENT关键字的主键插入值得时候,每条语句都要对这个表锁进行竞争。所以innodb通过innodb_autoinc_lock_mode得不同取值来提供不同得锁定机制。来显著提高SQL语句得可伸缩性和性能。

innodb_autoinc_lock_mode有三种取值,分别对应与不同锁定模式:

(1)innodb_autoinc_lock_mode = 0(“传统”锁定模式)
在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁,用于插入具有AUTO_INCREMENT列的表。每当执行insert的时候,都会得到一个表级锁(AUTO-INC锁),使得语句中生成的auto_increment是有序的,且在binlog中重放的时候,可以保证master与slave中数据的auto_increment是相同的。因为是表级锁,当在同一时间多个事务中执行insert的时候,对于AUTO-INC锁的争夺会 限制并发能力

(2)innodb_autoinc_lock_mode = 1(“连续”锁定模式)
在 MySQL 8.0 之前,连续锁定模式默认的。

在这个模式下,“bulk inserts”仍然使用AUTO-INC表级锁,并保持到语句结束。这适用于所有INSERT …SELECT,REPLACE … SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁

对于“Simple inserts”(要插入的行数事先已知),则通过在 mutex(轻量锁)的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁,它只在分配过程的持续时间内保持,而不是直到语句完成。不使用表级AUTO-INC锁,除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁,则“Simpleinserts”等待AUTO-INC锁,如同它是一个“bulk inserts”。

(3)innodb_autoinc_lock_mode = 2(“交错”锁定模式)
从 MySQL 8.0 开始,交错锁模式是 默认 设置。

在此锁定模式下,自动递增值保证在所有并发执行的所有类型的insert语句中是唯一 单调递增的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的

④ 元数据锁(MDL锁)

MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL 的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

因此,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁

不需要我们手动指定,在访问表的时候会被自动加上。

2. 行锁(Row Lock)

行锁也称记录锁,就是锁住某一行(某条)记录。但是MySQL服务层没有实现行锁机制,行锁只存在于存储引擎层

优点:锁定粒度小,发生锁冲突概率低,可以实现的大量的并发
缺点:对于锁的开销大加锁会比较慢,容易出现死锁

Innodb于myisam最大不同有两点,一是支持事务;二是支持行锁。

注意: 所有类型的行锁其实就是共享锁和排他锁作用的位置不一样,故名称和功能不一样,其开启语法就是共享锁和排他锁的语法

① 记录锁(Record Locks)

记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP 。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
MySQL高级(SQL优化)_第57张图片
记录锁是有S锁和X锁之分的,称之为 S型记录锁X型记录锁

  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
  • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
② 间隙锁(Gap Locks)

间隙锁是为了解决无法给幻影记录加锁的问题 。InnoDB提出了一种称之为Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁,即间隙锁 。

gap锁的提出仅仅是为了防止插入幻影记录而提出的。

什么是间隙锁?
如图:当我们向id为3和15之间插入一条记录时加上gap锁,那么就不允许再插入id在(3~15)之间的记录。

MySQL高级(SQL优化)_第58张图片
如果我们们是给id=25的记录添加间隙锁,那么id大于20的记录都不允许被插入,因为此时间隙锁范围是(20~∞)。

间隙锁会导致死锁问题

查看有哪些间隙锁:select * from performance_schema.data_locks\G;

注意: 间隙锁是开区间

③ 临键锁(Next-Key Locks)

有时候我们既想 锁住某条记录 ,又想 阻止 其他事务在该记录前边的 间隙插入新记录 ,所以InnoDB就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为next-key锁 。Next-Key Locks是在存储引擎 innodb 、事务级别在 可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

也就是将间隙锁变成闭区间,让区间边界也被锁住。 是记录锁和间隙锁的合体。

④ 插入意向锁(Insert Intention Locks)

InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构, 插入意向锁就是给在等待锁的记录添加的,表名有个事务想在某个间隙插入记录,但是现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION ,我们称为 插入意向锁 。插入意向锁是一种 Gap锁 ,不是意向锁,在insert操作时产生。

插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁 。

事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。

插入意向锁是存在于间隙锁和临键锁的基础上的。

3. 页锁

页锁就是在 页的粒度 上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般

每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

从对待锁的态度划分:乐观锁、悲观锁

从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的 设计思想

1. 悲观锁

悲观锁是一种思想,悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 `阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronizedReentrantLock 等独占锁就是悲观锁思想的实现。

注意:
select…for update语句执行过程中所有被扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住

悲观锁的并发性能不高,对数据库性能开销也很大,因而很少使用。特别是长事务而言,这样的开销往往无法承受,因而就需要乐观锁。

2. 乐观锁

乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁 ,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用 版本号机制 或者CAS机制实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的。

① 乐观锁的版本号机制

在表中设计一个 版本字段 version ,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行 UPDATE ... SET version=version+1 WHERE version=version 。此时如果已经有事务对这条数据进行了更改,修改就不会成功,因为条件是当读取时的version和更新时的version相同才会执行。

这种方式类似我们属性的SVN、CVS版本管理系统,当我们修改了代码进行提交时,首先会检查当前版本号与服务器的版本号是否一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。

② 乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。

你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

两种锁的使用场景

从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:

  • 乐观锁 适合读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  • 悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写写 - 写 的冲突。
    MySQL高级(SQL优化)_第59张图片

按加锁的方式划分:显式锁、隐式锁

1. 隐式锁

  • 情景一:对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的 事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构,is_waiting属性是 true )。
  • 情景二:对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的PageHeader部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复 情景一 的做法。

隐式锁是一种延迟加锁的机制,因为是后边的事务给正在活跃的事务加锁,自己排队等待,从而减少加锁数量

隐式锁在实际内存对象中并不含有这个锁信息。只有当产生锁等待时,隐式锁才会转换为显示锁。

2. 显式锁

通过特定的语句进行显示加锁,我们一般称之为显示加锁,例如:

  • 显示加共享锁:select .... lock in share mode;
  • 显示加排它锁:select .... for update;

全局锁

全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份

全局锁的命令:Flush tables with read lock;

死锁

1. 概念:

死锁是指两个或多个事务都持有对方需要的数据,并且在等待对方释放,但双方都不会释放自己的锁

2. 死锁举例:
事务1 事务2
1 start transaction;
update account set money=10 where id=1;
start transaction;
2 update account set money=10 where id=2;
3 update account set money=20 where id=2;
4 update account set money=20 where id=1;

这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有 两种策略 ,见下文。

3. 如何解决死锁问题:
  • 方式1:直接进入等待,直到超时
    这个超时时间可以通过参数innodb_lock_wait_timeout 来设置,若等待超时则将事务回滚,默认等待时间为50s
  • 方式2:死锁检测机制
    发起发死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on ,表示开启这个逻辑。
    缺点:每个新的被阻塞的线程,都要判断是不是由于自己的加入导致的死锁,这个操作时间复杂度是O(n)。
    如何解决
    • 方式1:关闭死锁检测,但意味着可能会出现大量的超时,会导致业务有损。
    • 方式2:控制并发数量。比如在中间件中实现对于相同行的更新,在进入引擎之前排队,这样InnoDB内部就不会有大量的死锁检测工作。
      进一步的思路:可以考虑通过一行改成多行来减少锁冲突。比如连锁超市账户总额的记录,不要每个分店的收支记录都在总店的服务器进行,而是在每个分店分别进行,然后再在某一天某一时刻将其汇总即可。
4. 如何避免死锁
  • 合理设计索引,是SQL尽可能通过索引定位更少的行,减少锁竞争。
  • 调整业务逻辑SQL执行顺序,避免update/delete长时间持有锁的sql事务在前面。
  • 避免大事务,尽量将大事务拆分为小事务来处理,小事务占用时间短,发生锁冲突的记录也更小。
  • 在并发较高的系统中,不要显示加锁,特别是在事务里显示加锁。如select…for update语句,如果是在事务里运行了start transaction或设置了autocommit=0,那么就会锁定锁查找到的记录,因为需要手动提交。
  • 降低隔离级别。如果业务允许,降低隔离级别是较好的选择。

18.5 、锁的内存结构

锁的内存结构也就是前面提到的锁结构

对一条记录加锁的本质就是创建锁结构与之关联。那么一个事务对多条记录加锁是不是就要创建多个锁结构呢?比如:
select * from user lovk in share mode;
理论上创建多个锁结构是没问题的,但是如果有10000条记录,就要创建10000个锁结构的话也太崩溃了,所以决定在对不同记录加锁时,将符合以下条件的记录放到同一个锁结构中:

  • 在同一个事务中进行加锁操作
  • 被加锁的记录在同一个数据页中
  • 加锁的类型一样
  • 等待的状态一样(is_waiting的值一样)

InnoDB 存储引擎中的 锁结构 如下:
MySQL高级(SQL优化)_第60张图片
结构解析:

  1. 锁所在的事务信息
    不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个 锁结构 ,这里就记录这个事务的信息。
    锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比方说事务id等。

  2. 索引信息
    对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。

  3. 表锁/行锁信息
    表锁结构行锁结构 在这个位置的内容是不同的:

    • 表锁:
      记载着是对哪个表加的锁,还有其他的一些信息。
    • 行锁:
      记载了三个重要的信息:
      • Space ID :记录所在表空间。
      • Page Number :记录所在页号。
      • n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits 属性代表使用了多少比特位。
  4. type_mode
    这是一个32位的数,被分成了 lock_mode lock_typerec_lock_type 三个部分,如图所示:
    MySQL高级(SQL优化)_第61张图片

  • 锁的模式( lock_mode ),占用低4位,可选的值如下:
    • LOCK_IS (十进制的 0 ):表示共享意向锁,也就是 IS锁
    • LOCK_IX (十进制的 1 ):表示独占意向锁,也就是 IX锁
    • LOCK_S (十进制的 2 ):表示共享锁,也就是 S锁
      - LOCK_X (十进制的 3 ):表示独占锁,也就是 X锁
    • LOCK_AUTO_INC (十进制的 4 ):表示 AUTO-INC锁
  • 锁的类型( lock_type ),占用第5~8位,不过现阶段只有第5位和第6位被使用:
    • LOCK_TABLE (十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁。
    • LOCK_REC (十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁。
  • 行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在 lock_type 的值为LOCK_REC 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
    • LOCK_ORDINARY (十进制的 0 ):表示 next-key锁 。
    • LOCK_GAP (十进制的 512 ):也就是当第10个比特位置为1时,表示 gap锁 。
    • LOCK_REC_NOT_GAP (十进制的 1024 ):也就是当第11个比特位置为1时,表示正经 记录锁 。
      - LOCK_INSERT_INTENTION (十进制的 2048 ):也就是当第12个比特位置为1时,表示插入意向锁。其他的类型:还有一些不常用的类型我们就不多说了。
  • is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode 这个32位的数字中:
    • LOCK_WAIT (十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为false ,也就是当前事务获取锁成功。
  1. 其他信息
    为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。

  2. 一堆比特位
    如果是 行锁结构 的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits 属性表示的。InnoDB数据页中的每条记录在 记录头信息 中都包含一个 heap_no 属性,伪记录 Infimum 的heap_no 值为 0 , Supremum 的 heap_no 值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no ,即一个比特位映射到页内的一条记录。

18.6、锁监控

关于MySQL锁的监控,我们一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况,语法:
show status like 'innodb_row_lock%';
结果集:
MySQL高级(SQL优化)_第62张图片

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
  • Innodb_row_lock_time :从系统启动到现在锁定总时间长度;(等待总时长)
  • Innodb_row_lock_time_avg :每次等待所花平均时间;(等待平均时长)
  • Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
  • Innodb_row_lock_waits :系统启动后到现在总共等待的次数;(等待总次数)

其他监控方法:
MySQL把事务和锁的信息记录在了 information_schema 库中,涉及到的三张表分别是INNODB_TRX INNODB_LOCKSINNODB_LOCK_WAITS

MySQL5.7及之前 ,可以通过information_schema.INNODB_LOCKS查看事务的锁情况,但只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。

MySQL8.0删除了information_schema.INNODB_LOCKS,添加了 performance_schema.data_locks ,可以通过performance_schema.data_locks查看事务的锁情况,和MySQL5.7及之前不同,
performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁。

同时,information_schema.INNODB_LOCK_WAITS也被 performance_schema.data_lock_waits 所代替。

查询正在被锁阻塞的sql语句: SELECT * FROM information_schema.INNODB_TRX\G;
查询锁等待情况: SELECT * FROM data_lock_waits\G;,结果集中REQUESTING_ENGINE_TRANSACTION_ID是被阻塞的事务ID,BLOCKING_ENGINE_TRANSACTION_ID是正在执行的事务ID。
查询锁的情况:SELECT * from performance_schema.data_locks\G;

18.7、间隙锁加锁规则(附录)
间隙锁是在可重复读隔离级别下才会生效的: next-key lock 实际上是由间隙锁加行锁实现的,如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分。而在读提交隔离级别下间隙锁就没有了,为了解决可能出现的数据和日志不一致问题,需要把binlog 格式设置为 row 。也就是说,许多公司的配置为:读提交隔离级别加 binlog_format=row。业务不需要可重复读的保证,这样考虑到读提交下操作数据的锁范围更小(没有间隙锁),这个选择是合理的。

next-key lock的加锁规则,总结的加锁规则里面,包含了两个 “ “ 原则 ” ” 、两个 “ “ 优化 ” ” 和一个 “bug” 。

  1. 原则 1 :加锁的基本单位是 next-key lock 。 next-key lock 是前开后闭区间。
  2. 原则 2 :查找过程中访问到的对象才会加锁。任何辅助索引上的锁,或者非索引列上的锁,最终都要回溯到主键上,在主键上也要加一把锁。
  3. 优化 1 :索引上的等值查询,给唯一索引加锁的时候, next-key lock 退化为行锁。也就是说如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁
  4. 优化 2 :索引上(不一定是唯一索引)的等值查询,向右遍历时且最后一个值不满足等值条件的时候, next-keylock 退化为间隙锁。
  5. 一个 bug :唯一索引上的范围查询会访问到不满足条件的第一个值为止。

十九、MVCC多版本并发控制

19.1、什么是MVCC

MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

MVCC没有正式的标准,在不同的数据库管理系统中MVCC的实现方式可能是不同的,页不是普遍使用的,这里主要讲解InnoDB中MVCC的实现机制,MySQL其他存储引擎不支持。

19.2、快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁非阻塞并发读 ,而这个读指的就是 快照读 , 而非 当前读 。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。

19.2.1、快照读

  • 快照读又称一致性读,读取的是快照数据。
  • 不加锁的简单的select语句都属于快照读,即不加锁的非阻塞语句。
  • 快照读的实现是基于MVCC的。
  • 优点:避免了加锁的操作,降低了开销。
  • 缺点:既然是基于多版本,那么快照读读到的数据就不一定是数据的最新版本,可能是历史版本。
  • 快照读的前提是隔离级别不是串行级别(排队执行),串行级别下的快照读会退化成当前读。

19.2.2、当前读

当前读读取的是记录的最新版本 (是最新数据,而不是历史版本的数据),读取时还有保证其他并发性事务不能修改当前记录,会对读取进行加锁。

加锁的select,或对数据进行增删改都会进行当前读。比如:
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁

即加锁的是当前读,不加锁的是快照读

19.3、回顾 隔离级别、 隐藏字段、Undo Log版本链

** 隔离级别:**
事务有 4 个隔离级别,可能存在三种并发问题:
MySQL高级(SQL优化)_第63张图片
串行化就是挨个排队执行。在隔离级别中,可重复读不能解决幻读问题,但MVCC可以。
MVCC可以不采用锁机制,而是通过乐观锁的方式来解决不可重复读和幻读问题。它可以在大多数情况下代替行锁,降低系统的开销。

** 隐藏字段、Undo Log版本链:**
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。

  • trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给trx_id 隐藏列。
  • roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本链的头节点就是当前记录最新的值。第一次插入的数据的roll_pointer是没有值的,因为本来就是第一个数据没法回滚。

每个版本中还包含生成该版本时对应的 事务id

19.4、MVCC实现原理之ReadView

MVCC 的实现依赖于:隐藏字段Undo LogRead View,也称MVCC三剑客。面试可围绕这句话展开。

19.4.1、什么是ReadView

在MVCC机制中,多个事务对同一条记录进行更新会产生多个历史快照(一个事务产生一个快照),这些历史快照都保存到了Undo log里。如果一个事务想要查询某一条记录,需要读取哪个版本的那条记录就不要ReadView来管理。

Readview就是一个事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的id(活跃是指启动了但还没提交)。

19.4.2、设计思路

ReadView主要解决的核心问题是,判断版本链中哪个版本是当前事务可见的,这是ReadView要解决的主要问题。

使用READ COMMITTEDREPEATABLE READ隔离级别的事务,都必须保证读到的是已经提交了的事务修改过的记录。因为提交过的记录才有版本信息,ReadView才有价值。

ReadView主要包含4个比较重要的内容:

  • creator_trx_id:创建这个事务的id。

    说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。

  • trx_ids:当前系统中活跃(已开启未提交)的事务id。
  • up_limit_id:活跃事务中的最小事务的id。
  • low_limit_id:当前系统(包括活跃和非活跃)中最大的事务id+1,即应该分配给下一个事务的id。

    注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。

19.4.3、ReadView的规则

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。

  • 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的 up_limit_id low_limit_id 之间,那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。
    • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
    • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

19.4.4、MVCC整体执行流程

当查询一条记录的时候,系统如何通过MVCC找到它:

  1. 首先获取事务自己的版本号,也就是事务 ID;
  2. 获取 ReadView;
  3. 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
  4. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
  5. 最后返回符合规则的数据。

如果某个版本的数据对当前事务不可见的话,就顺着版本链找到前一个版本的数据(从新版本往旧版本开始找),继续按照上面的步骤判断可见性,以此类推,一直到版本链中第一个版本。如果第一个版本页不可见的话,那就意味着该记录对该事务完全不可见,查询结果接不包含该记录。

InnoDB中,MVCC通过Undo log + ReadView 进行数据读取,Undo log 保存了历史快照,而ReadView规则帮我们判断当前版本的数据是否可见。

在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次ReadView。
两条一样的查询语句也会重新获取一次ReadView,因为每次获取到的ReadView可能不同,不同就可能出现不可重复读或者幻读的情况。当隔离级别为可重复读的时候,就避免不了不可重复读,因为一个事务只在第一次select的时候会获取一次ReadView,后面的所有select都会复用这个ReadView

每次读取已提交的事务,都会生成一个ReadView。

19.4.5、MVCC如何解决幻读

可重复读读已提交才会出现幻读问题。而可重复读只在第一次读的时候才会生成ReadView,后面会复用这个ReadView,读已提交在每一次读都会生成ReadView。

由ReadView的规则可知,只有事务的trx_id=ReadView的creator_trx_id,版本才可以被访问。因为可重复读和读已提交都只在第一次读的时候才会生成ReadView,因而一定一直满足trx_id=creator_trx_id,意味着当前事务在访问它自己修改过的记录,因而不会出现幻读。

19.5、总结

这里介绍了 MVCCREAD COMMITTDREPEATABLE READ 这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的 读-写写-读 操作并发执行,从而提升系统性能。

核心点在于 ReadView 的原理, READ COMMITTD 、 REPEATABLE READ 这两个隔离级别的一个很大不同就是生成ReadView的时机不同:

  • READ COMMITTD每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了.

说明:
删除操作的delete mark标记就是为MVCC服务的,方便回滚等操作。

通过MVCC我们可以解决:

  • 读写之间阻塞问题。通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样可以提升事务并发处理能力。
  • 降低了死锁的概率。因为MVCC采用了乐观锁的方式,读取数据时不需要加锁,对于写操作,页只锁定必要的行。
  • 解决快照读的问题。当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前提交更新的结果,而不能看到这个时间点之后提交的更新结果。

二十、其他数据库日志

20.1、日志类型

MySQL有不同类型的日志文件,用来存储不同类型的日志,分为 二进制日志错误日志通用查询日志慢查询日志 ,这也是常用的4种。MySQL 8又新增两种支持的日志: 中继日志数据定义语句日志 。使用这些日志文件,可以查看MySQL内部发生的事情。这6类日志分别为:

  • 慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便我们对查询进行优化。在第十二章性能分析的时候已经介绍过了。
  • 通用查询日志:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。
  • 错误日志:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便我们了解服务器的状态,从而对服务器进行维护。
  • 二进制日志:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。
  • 中继日志:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。
  • 数据定义语句日志:记录数据定义语句执行的元数据操作。

除二进制日志外,其他日志都是 文本文件 。默认情况下,所有日志创建于 MySQL数据目录 中。

20.2、日志的缺点

每个日志的作用就是日志的优点。

  • 日志功能会降低MySQL数据库的性能。因为MySQL在要边执行sql语句边进行日志记录。
  • 日志会占用大量的磁盘空间。对于操作非常频繁,用户量非常大的数据库,日志文件的存储空间回避数据库需要的存储空间还大。

20.3、通用查询日志(general query log)

通用查询日志用来 记录用户的所有操作 ,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL 数据库服务器的所有 SQL 指令等。当我们的数据发生异常时,查看通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。

20.3.1、查看当前状态(是否开启)

查看当前状态:SHOW VARIABLES LIKE '%general%';,默认关闭。
MySQL高级(SQL优化)_第64张图片

20.3.2、启动日志

方式1:永久性方式
修改my.cnf或者my.ini配置文件来设置。在[mysqld]组下加入log选项,并重启MySQL服务。格式如下:

[mysqld]
general_log=ON
general_log_file=[path[filename]] #日志文件所在目录路径,filename为日志文件名

如果不指定目录和文件名,通用查询日志将默认存储在MySQL数据目录中的hostname.log文件中,hostname表示主机名。

方式2:临时性方式
SET GLOBAL general_log=on; 开启通用查询日志
SET GLOBAL general_log_file=’path/filename’; 设置日志文件保存位置

关闭日志:
SET GLOBAL general_log=off; 关闭通用查询日志
SHOW VARIABLES LIKE 'general_log%'; 查看设置后情况

20.3.3、查看日志

通用查询日志是以 文本文件 的形式存储在文件系统中的,可以使用 文本编辑器 直接打开日志文件。每台MySQL服务器的通用查询日志内容是不同的。

  • 在Windows操作系统中,使用文本文件查看器;
  • 在Linux系统中,可以使用vi工具或者gedit工具查看;
  • 在Mac OSX系统中,可以使用文本文件查看器或者vi等工具查看。

SHOW VARIABLES LIKE 'general_log%'; 结果中可以看到通用查询日志的位置。

20.3.4、关闭日志

方式1:永久性方式
修改 my.cnf 或者 my.ini 文件,把[mysqld]组下的 general_log 值设置为 OFF 或者把general_log一项注释掉。修改保存后,再 重启MySQL服务 ,即可生效。

[mysqld]
general_log=OFF
或,注释掉
#general_log=ON

方式2:临时性方式
停止MySQL通用查询日志功能:SET GLOBAL general_log=off;
查询通用日志功能:SHOW VARIABLES LIKE 'general_log%';

20.3.5、 删除\刷新\备份日志

如果数据的使用非常频繁,那么通用查询日志会占用服务器非常大的磁盘空间。数据管理员可以删除很长时间之前的查询日志,以保证MySQL服务器上的硬盘空间。

手动删除文件

通过SHOW VARIABLES LIKE 'general_log%';找到日志文件位置,并手动删除。

使用如下命令重新生成查询日志文件,具体命令如下。刷新MySQL数据目录,发现创建了新的日志文件。前提一定要开启通用日志:
mysqladmin -uroot -p flush-logs

如果希望备份旧的通用查询日志,旧必须先将旧的日志文件复制出来后者改名,具体操作如下:

cd mysql-data-director #输入自己的通用日志文件所在目录
mv mysql.general.log mysql.general.log.old #指明旧的文件名,和新的文件名
mysqladmin -uroot -p flush-logs 

20.4、错误日志(error log)

错误日志记录了MySQL服务器启动、停止运行的时间,以及系统启动、运行和停止过程中的诊断信息,包括错误警告提示等。

通过错误日志可以查看系统的运行状态,便于及时发现故障、修复故障。如果MySQL服务出现异常,错误日志是发现问题、解决故障的首选

20.4.1、启动日志

在MySQL数据库中,错误日志功能是 默认开启 的。而且,错误日志 无法被禁止 。默认情况下,错误日志存储在MySQL数据库的数据文件夹下,名称默认为mysqld.log (Linux系统)或hostname.err (mac系统)。如果需要制定文件名,则需要在my.cnf或者my.ini中做如下配置:

[mysqld]
log-error=[path/[filename]] #path为日志文件所在的目录路径,filename为日志文件名

修改配置项后,需要重启MySQL服务以生效。

20.4.2、查看日志

MySQL错误日志是以文本文件形式存储的,可以使用文本编辑器直接查看。

查询错误日志的存储路径:SHOW VARIABLES LIKE 'log_err%';

20.4.3、删除\刷新日志

对于很久以前的错误日志,数据库管理员查看这些错误日志的可能性不大,可以将这些错误日志删除,以保证MySQL服务器上的 硬盘空间 。MySQL的错误日志是以文本文件的形式存储在文件系统中的,可以直接删除

  • 第一步(方式1):删除
    rm -f /var/lib/mysql/mysqld.log,在运行状态下删除错误日志后,MySQL不会自动创建日志文件。

  • 第一步(方式2):重命名文件
    mv /var/log/msqld.log /var/log/mysqld.log.old

  • 第二步:重建日志
    mysqladmin -u root -p flush-logs
    可能会报错

    [root@atguigu01 log]# mysqladmin -uroot -p flush-logs
    Enter password:
    mysqladmin: refresh failed; error: 'Could not open file '/var/log/mysqld.log' for
    error logging.'
    

    此时执行一遍install -omysql -gmysql -m0644 /dev/null /var/log/mysqld.log
    再执行mysqladmin -u root -p flush-logs 即可。

    flush-logs指令介绍:

    • mysql 5.5.7以前,flush-logs将错误日志文件重命名为filename.err_old,并创建新的日志文件。
    • 从MySQL5.5.7开始,flush-logs只是重新打开日志文件,并不做日志备份和创建的操作。因而会报上面的错误。
    • 如果日志文件不存在,MySQL启动或执行flush-logs时会自动创建新的日志文件。重新创建错误日志,大小为0字节。

20.4.4、MySQL 8 新特性

MySQL 8 里对错误日志进行了改进,可以理解为一个全新的错误日志。再这个版本里接受了来自社区的广泛批评意见,再这些意见和建议的基础上形成了新的日志。

下面时来自社区的意见:

  • 默认情况下内容过于冗长
  • 遗漏了有用信息
  • 难以过滤某些信息
  • 没有标识错误信息的子系统源
  • 没有错误代码,解析消息需要识别错误
  • 引导消息可能会丢失
  • 固定格式

针对这些意见,MySQL做了如下改变:

  • 采用组件架构,通过不同的组件执行日志的写入和过滤功能
  • 写入错误日志的全部信息都具有唯一的错误代码从10000开始
  • 增加了一个新的消息分类《system》用于在错误日志中始终可见的非错误但服务器状态更改时间的消息
  • 增加了额外的附加信息,例如关机时的版本信息,谁发起的关机等等
  • 两种过滤方式,internal和Dragnet
  • 三种写入形式:经典、JSON、syseventlog

小结:
通常情况下,管理员不需要查看错误日志,但是,MySQL服务器发生异常时,管理员可以从错误日志中找到发生异常的时间、原因,然后跟具这些信息来解决异常。

20.5、二进制日志(bin log)

binlog可以说是MySQL中比较重要的日志了,在日常开发及运维过程中,经常会遇到。binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的DDLDML 等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)。

它以事件形式记录并保存在二进制文件中。通过这些信息,我们可以再现数据更新操作的全过程。

binlog主要应用场景:

  • 一是用于 数据恢复,如果MySQL数据库意外停止,可以通过二进制文件来查看用户执行了哪些操作,对数据库服务器文件做了哪些修改,然后根据二进制日志文件中的记录来恢复数据库服务器。
  • 二是用于 数据复制,由于日志的延续性和时效性,master把它的二进制日志传递给slaves来达到master-slave数据一致性的目的。

可以说MySQL数据库的数据备份、主备、主主、主从都离不开bin log,需要依靠bin log来同步数据,保证数据一致性。

20.5.1、查看状态(是否开启)

在MySQL8中默认情况下,二进制文件是开启的。

查看记录二进制日志是否开启:show variables like '%log_bin%';

  • log_bin_basename:是binlog日志的基本文件名,后面会追加标识来标识每一个文件。
  • long_bin_index:是binlog文件的索引文件,这个文件管理了所有的binlog文件的目录。
  • long_bin_trust_function_creators:限制存储过程,因为存储函数中可能涉及now()这样调用时间的函数,而在主从复制的时候,now()获取的当前时间肯定不一致,而导致数据不一致。所以当开启二进制日志后,需要限制存储函数的创建、修改、调用。
  • log_bin_use_v1_row_events:此只读系统变量已弃用。ON表示使用版本1二进制日志行,OFF表示使用版本2二进制日志行(MySQL 5.6 默认为2)。

20.5.2、日志参数设置

方式1:永久性方式
修改MySQL的 my.cnf 或 my.ini 文件可以设置二进制日志的相关参数:

[mysqld]
log-bin=xxx-bin #打开日志(主机需要打开),设置日志名称
binlog_expire_logs_seconds=600 #日志保留的时常,单位是s
max_binlog_size=1000M #文件上限大小,是弹性的。最大和默认值是1G
#比如一个事务的日志没记录完,但是日志内存已经用完,不会新开一个日志继续记录,而是拓展内存将当前事务操作记录完。

然后重启MySQL服务器,查询二进制文件信息:show variables like '%log_bin%';

设置带文件夹的bin-log日志存放目录
如果想改变日志文件的目录和名称,可以对my.cnf或my.ini中的log_bin参数修改如下:

[mysqld]
log-bin="/var/lib/mysql/binlog/xxx-bin"

注意:新建的文件夹需要使用mysql用户,使用下面的命令即可。
chown -R -v mysql:mysql binlog

提示:
数据库文件和日志文件不要放在同一磁盘上,这样当数据库所在磁盘发生故障时,可以使用日志文件恢复数据。

方式2:临时性方式
如果不希望通过修改配置文件并重启的方式设置二进制日志的话,还可以使用如下指令,需要注意的是在mysql8中只有会话级别 的设置,没有了global级别的设置。

session级别:SET sql_log_bin=0;
global 级别:set global sql_log_bin=0;

20.5.3、查看日志

当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。

MySQL服务 重新启动一次 ,以“.000001”为后缀的文件就会增加一个,并且后缀名按1递增。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过了 max_binlog_size 的上限(默认是1GB),就会创建一个新的日志文件。

查看当前的二进制日志文件列表及大小。指令如下:SHOW BINARY LOGS;

查看日志信息:mysqlbinlog -v "bin log路径",路径要具体到文件名,“-v”是将二进制转为文本。

前面的命令同时显示binlog格式的语句,使用如下命令不显示它:mysqlbinlog -v --base64-output=DECODE-ROWS "/var/lib/mysql/binlog/atguigu-bin.000002"

关于mysqlbinlog工具的使用技巧还有很多,例如只解析对某个库的操作或者某个时间段内的操作等。简单分享几个常用的语句,更多操作可以参考官方文档。

  • 可查看参数帮助:mysqlbinlog --no-defaults --help
  • 查看最后100行:mysqlbinlog --no-defaults --base64-output=decode-rows -vv atguigu-bin.000002 |tail-100
  • 根据position查找:mysqlbinlog --no-defaults --base64-output=decode-rows -vv atguigu-bin.000002 |grep -A20 '4939002'

上面这种办法读取出binlog日志的全文内容比较多,不容易分辨查看到pos点信息,下面介绍一种更为方便的查询命令:
show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];

  • IN 'log_name' :指定要查询的binlog文件名(不指定就是第一个binlog文件)
  • FROM pos :指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算)
  • LIMIT [offset] :偏移量(不指定就是0)
  • row_count :查询总条数(不指定就是所有行)

查看binlog格式:show variables like 'binlog_format';

  • Statement:每一条会修改数据的sql都会记录在binlog中。
    优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
  • Row默认格式:5.1.5版本的MySQL才开始支持row level 的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。
    优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。
  • Mixed:从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

下章节会详细讲解。

20.5.4、使用日志恢复数据

mysqlbinlog恢复数据的语法如下:
mysqlbinlog [option] filename|mysql –uuser -ppass;
这个命令可以这样理解:使用mysqlbinlog命令来读取filename中的内容,然后使用mysql命令将这些内容恢复到数据库中。

  • filename :是日志文件名。
  • option :可选项,比较重要的两对option参数是–start-date、–stop-date 和 --start-position、–stop-position。
    • --start-date--stop-date :可以指定恢复数据库的起始时间点和结束时间点。时间必须是事务BEGIN前的 set timestamp的时间。
      通过mysqlbinlog '文件路径'; 查看。
    • --start-position--stop-position :可以指定恢复数据的开始位置和结束位置。位置必须是指令所在事务BEGIN开始的位置
      位置通过show binlog events in '文件名'查看Pos。

注意:
使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如atguigu-bin.000001必须在atguigu-bin.000002之前恢复。

20.5.5、删除二进制日志

MySQL的二进制文件可以配置自动删除,同时MySQL也提供了安全的手动删除二进制文件的方法。PURGE MASTER LOGS 只删除指定部分的二进制日志文件, RESET MASTER 删除所有的二进制日志文件。具体如下:

  1. PURGE MASTER LOGS:删除指定部分日志文件
    删除指定文件之前的文件:PURGE {MASTER | BINARY} LOGS TO ‘指定日志文件名’
    删除指定日期文件:PURGE {MASTER | BINARY} LOGS BEFORE ‘时间’,查看日志日期:mysqlbinlog --no-defaults "文件路径"
    示例:删除创建日期为2022-1-5日创建的日志:purge master logs before "20220105";

  2. RESET MASTER:删除所有二进制日志文件
    使用RESET MASTER语句清空所有的binlog日志。MySQL会重新创建二进制文件,新的日志文件拓展名从000001开始编号。
    语法:RESET MASTER;

20.5.6、其他场景

二进制日志可以通过数据库的 全量备份 和二进制日志中保存的 增量信息 ,完成数据库的 无损失恢复 。但是,如果遇到数据量大、数据库和数据表很多(比如分库分表的应用)的场景,用二进制日志进行数据恢复,是很有挑战性的,因为起止位置不容易管理。

在这种情况下,一个有效的解决办法是 配置主从数据库服务器 ,甚至是 一主多从 的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。

20.6、二进制日志底层原理

20.6.1、写入机制

binlog的写入时机也非常简单,事务执行过程中,先把日志写到 binlog cache ,事务提交的时候,再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。

我们可以通过binlog_cache_size参数控制单个线程binlog cache大小,如果储存内容超过了这个参数,就要暂存到磁盘(Swap)。binlog日志刷盘流程如下:
MySQL高级(SQL优化)_第65张图片
write和fsync的时机,可以由参数 sync_binlog 控制,默认是0

  • sync_binlog=0,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。
    MySQL高级(SQL优化)_第66张图片

  • sync_binlog=1,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样。

  • 最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。
    MySQL高级(SQL优化)_第67张图片

在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

20.6.2、binlog与redolog对比

  • redo log 它是 物理日志 ,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎层产生的。
  • binlog逻辑日志 ,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。
  • 虽然它们都属于持久化的保证,但是侧重点不同:
    • redo log让InnoDB存取引擎拥有了崩溃恢复能力。
    • binlog保证了MySQL集群架构的数据一致性。

20.6.3、两阶段提交

在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的 写入时机 不一样。
MySQL高级(SQL优化)_第68张图片
假如执行过程中写完了redo log日志后,在写binlog期间发生了异常,会出现什么情况呢?(redo log边执行边写,binlog在事务提交时才写)

  • redo log已经写完了,所以恢复主机数据是没问题的。
  • 但是binlog没有写完,从机在同步数据时就会出现问题,导致数据不一致。

为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。即:将binlog分为两个阶段写入。MySQL高级(SQL优化)_第69张图片
如果bin log在提交事务阶段出现了问题,主机在恢复数据时发现在提交阶段没有binlog则对主机进行回滚操作,不再恢复数据。
MySQL高级(SQL优化)_第70张图片
另一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?

并不会回滚事务,虽然redo log是处于prepare阶段,但是redo log是边执行边记录的,即使提交时出现问题,之前的操作也能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。
MySQL高级(SQL优化)_第71张图片

20.7、中继日志

20.7.1、介绍

中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入 本地的日志文件 中,这个从服务器本地的日志文件就叫中继日志 。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的 数据同步

搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。

文件名的格式是: 从服务器名 -relay-bin.序号 。中继日志还有一个索引文件:从服务器名 -relay-bin.index,用来定位当前正在使用的中继日志。

20.7.2、查看中继日志

中继日志与二进制日志的格式相同,可以用 mysqlbinlog 工具进行查看,下一章会具体讲解。

20.7.3、恢复的典型错误

如果从服务器宕机,有的时候为了系统恢复,要重装操作系统,这样就可能会导致你的 服务器名称 与之前 不同 。而中继日志里是 包含从服务器名 的。在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是名称不对了。

解决的方法也很简单,把从服务器的名称改回之前的名称。

二十一、主从复制

21.1、概述

21.1.1、 如何提升数据库并发能力

一般应用对数据库而言都是“ 读多写少 ”,也就说对数据库读取数据的压力比较大,有一个思路就是采用数据库集群的方案,做 主从架构 、进行 读写分离 ,这样同样可以提升数据库的并发处理能力。但并不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的。

如果我们的目的在于提升数据库高并发访问的效率,那么首先考虑的是如何 优化SQL和索引 ,这种方式简单有效;其次才是采用 缓存的策略 ,比如使用 Redis将热点数据保存在内存数据库中,提升读取的效率;最后才是对数据库采用 主从架构 ,进行读写分离。
MySQL高级(SQL优化)_第72张图片

21.1.2 主从复制的作用

主从同步设计不仅可以提高数据库的吞吐量,还有以下 3 个方面的作用。

第一个作用: 读写分离。我们可以通过主从复制的方式来同步数据,然后通过读写分离提高数据库并发处理能力。

  • 写操作(写库)在主机中进行,读操作(读库)在从机中进行。
  • 由于读库比较多,因而可以搭建多个从机,进行读库操作。
  • 当主库的数据更新时,会自动将数据复制到从库中,当客服端读取数据的时候,会从从机进行读取。
  • 面对“读多写少”的需求,采用读写分离的方式,可以实现更高的并发访问。同时,我们还能对从服务器进行负载均衡,让不同的读请求按照策略均匀地分发到不同地从服务器上,让读取更加顺畅。读取顺畅地另一个原因,就是减少了锁表地影响,比如我们让主库负责写,当主库出现写锁的时候,不会影响到从库进行SELECT的读取。
    MySQL高级(SQL优化)_第73张图片
    第二个作用: 数据备份。我们通过主从复制将主库上的数据复制到了从库上,相当于是一种热备份机制,也就是在主库正常运行的情况下进行的备份,不会影响到服务。
    第三个作用: 高可用性。当主服务器宕机故障,可以切换使用从机服务器上,保证服务正常运行。
    高可用度的指标正常可用时间/全年可用时间

21.2、主从复制的原理

Slave 会从 Master 读取 binlog 来进行数据同步。

21.2.1、三个线程

实际上主从同步的原理就是基于 binlog 进行数据同步的。在主从复制过程中,会基于 3 个线程 来操作,一个主库线程,两个从库线程。

  • 二进制日志转储线程 (Binlog dump thread)是一个主库线程。当从库线程连接的时候, 主库可以将二进制日志发送给从库,当主库读取事件(Event)的时候,会在 Binlog 上 加锁 ,读取完成之后,再将锁释放掉。
  • 从库 I/O 线程 会连接到主库,向主库发送请求更新 Binlog。这时从库的 I/O 线程就可以读取到主库的二进制日志转储线程发送的 Binlog 更新部分,并且拷贝到本地的中继日志 (Relay log)。
  • 从库 SQL 线程 会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步。
    MySQL高级(SQL优化)_第74张图片详细流程图:
    MySQL高级(SQL优化)_第75张图片

注意:
不是所有版本的MySQL都默认开启服务器的二进制日志。在进行主从同步的时候,我们需要先检查服务器是否已经开启了二进制日志。
除非特殊指定,默认情况下从服务器会执行所有主服务器中保存的事件。也可以通过配置,使从服务器执行特定的事件。

21.2.2、复制三步骤

步骤1:Master 将写操作记录到二进制日志( binlog )。
步骤2:Slave 将 Master 的binary log events拷贝到它的中继日志( relay log );
步骤3:Slave 重做中继日志中的事件,将改变应用到自己的数据库中。 MySQL复制是异步的且串行化的,而且重启后从 接入点 开始复制。

21.2.3、复制的问题

复制的最大问题: 延时

21.2.4、复制的基本原则

  • 每个 Slave 只有一个 Master
  • 每个 Slave 只能有一个唯一的服务器ID
  • 每个 Master 可以有多个 Slave

21.3、一主一从架构搭建

一台 主机 用于处理所有 写请求 ,一台 从机 负责所有 读请求 ,架构图如下:
读写分离的操作一般都是通过Mycat这样的中间件完成的。
MySQL高级(SQL优化)_第76张图片
需要两台虚拟机,每台虚拟机上需要安装好MySQL,具体搭建不做介绍

21.3.1、主机配置文件

建议mysql版本一致且后台以服务运行,主从所有配置项都配置在 [mysqld] 节点下,且都是小写字母。具体参数配置如下:

  • 必选
#[必须]主服务器唯一ID
server-id=1
#[必须]启用二进制日志,指名路径。比如:自己本地的路径/log/mysqlbin
log-bin=atguigu-bin
  • 可选
#[可选] 0(默认)表示读写(主机),1表示只读(从机)
read-only=0
#设置日志文件保留的时长,单位是秒
binlog_expire_logs_seconds=6000
#控制单个二进制日志大小。此参数的最大和默认值是1GB
max_binlog_size=200M
#[可选]设置不要复制的数据库
binlog-ignore-db=test
#[可选]设置需要复制的数据库,默认全部记录。比如:binlog-do-db=atguigu_master_slave
binlog-do-db=需要复制的主数据库名字
#[可选]设置binlog格式
binlog_format=STATEMENT

重启MySQL后台服务,使配置生效。

注意:
先搭建完主从复制,再创建数据库。
MySQL主从复制时,从机不继承主机数据。

21.3.2、从机配置文件

要求主从所有配置项都配置在 my.cnf 的 [mysqld] 栏位下,且都是小写字母。

  • 必选
#[必须]从服务器唯一ID
server-id=2
  • 可选
#[可选]启用中继日志
relay-log=mysql-relay

重启后台mysql服务,使配置生效。

注意:主从机都关闭防火墙
service iptables stop #CentOS 6
systemctl stop firewalld.service #CentOS 7

21.3.3、主机:建立账户并授权

#在主机MySQL里执行授权主从复制的命令
GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'从机器数据库IP' IDENTIFIED BY 'abc123';
#5.5,5.7

注意:如果使用的是MySQL8,需要如下的方式建立账户,并授权slave:

CREATE USER 'slave1'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'%';
#此语句必须执行。否则见下面。
ALTER USER 'slave1'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
flush privileges;

注意:在从机执行show slave status\G时报错:
Last_IO_Error: error connecting to master ‘[email protected]:3306’ - retry-time: 60 retries: 1message: Authentication plugin ‘caching_sha2_password’ reported error: Authentication requiressecure connection.

查询Master的状态,并记录下File和Position的值:show master status;
MySQL高级(SQL优化)_第77张图片

  • 记录下File和Position的值

注意:执行完此步骤后不要再操作主服务器MySQL,防止主服务器状态值变化。

21.3.4、从机:配置需要复制的主机

**步骤1:**从机上复制主机的命令

CHANGE MASTER TO
MASTER_HOST='主机的IP地址',
MASTER_USER='主机用户名',
MASTER_PASSWORD='主机用户名的密码',
MASTER_LOG_FILE='mysql-bin.具体数字',
MASTER_LOG_POS=具体值;

步骤2:
启动slave同步:START SLAVE;
如果报错:
在这里插入图片描述
可以执行如下操作,删除之前的relay_log信息。然后重新执行 CHANGE MASTER TO …语句即可。
reset slave; ,删除SLAVE数据库的relaylog日志文件,并重新启用新的relaylog文件

接着,查看同步状态:SHOW SLAVE STATUS\G;
在这里插入图片描述
显式如下的情况,就是不正确的。可能错误的原因有:

  1. 网络不通
  2. 账户密码错误
  3. 防火墙
  4. mysql配置文件问题
  5. 连接服务器时语法
  6. 主服务器mysql权限

在这里插入图片描述
测试:主机新建库、新建表、insert记录,从机复制。

21.3.5、停止主从同步

  • 停止主从同步命令:stop slave;
  • 如何重新配置主从:
    如果停止从服务器复制功能,再使用需要重新配置主从。否则会报错如下:
    在这里插入图片描述
    重新配置主从,需要在从机上执行:
stop slave;
reset master; #删除Master中所有的binglog文件,并将日志索引文件清空,重新开始所有新的日志文件(慎用)

21.3.6、binlog格式

格式1:STATEMENT模式(基于SQL语句的复制(statement-based replication, SBR))

binlog_format=STATEMENT
每一条会修改数据的sql语句会记录到binlog中。这是默认的binlog格式。

  • SBR 的优点:
    • 历史悠久,技术成熟
    • 不需要记录每一行的变化,减少了binlog日志量,文件较小
    • binlog中包含了所有数据库更改信息,可以据此来审核数据库的安全等情况
    • binlog可以用于实时的还原,而不仅仅用于复制
    • 主从版本可以不一样,从服务器版本可以比主服务器版本高
  • SBR 的缺点:
    • 不是所有的UPDATE语句都能被复制,尤其是包含不确定操作的时候
  • 使用以下函数的语句也无法被复制:LOAD_FILE()、UUID()、USER()、FOUND_ROWS()、SYSDATE()(除非启动时启用了 --sysdate-is-now 选项)
    • INSERT … SELECT 会产生比 RBR 更多的行级锁
    • 复制需要进行全表扫描(WHERE 语句中没有使用到索引)的 UPDATE 时,需要比 RBR 请求更多的行级锁
    • 对于有 AUTO_INCREMENT 字段的 InnoDB表而言,INSERT 语句会阻塞其他 INSERT 语句
    • 对于一些复杂的语句,在从服务器上的耗资源情况会更严重,而 RBR 模式下,只会对那个发生变化的记录产生影响
    • 执行复杂语句如果出错的话,会消耗更多资源
    • 数据表必须几乎和主服务器保持一致才行,否则可能会导致复制出错

格式2:ROW模式(基于行的复制(row-based replication, RBR))

binlog_format=ROW

5.1.5版本的MySQL才开始支持,不记录每条sql语句的上下文信息,仅记录哪条数据被修改了,修改成什么样了。

  • RBR 的优点:
    • 任何情况都可以被复制,这对复制来说是最 安全可靠 的。(比如:不会出现某些特定情况下的存储过程、function、trigger的调用和触发无法被正确复制的问题)
    • 多数情况下,从服务器上的表如果有主键的话,复制就会快了很多
    • 复制以下几种语句时的行锁更少:INSERT … SELECT、包含 AUTO_INCREMENT 字段的 INSERT、没有附带条件或者并没有修改很多记录的 UPDATE 或 DELETE 语句
    • 执行 INSERT,UPDATE,DELETE 语句时锁更少
    • 从服务器上采用 多线程 来执行复制成为可能
  • RBR 的缺点:
    • binlog 大了很多
    • 复杂的回滚时 binlog 中会包含大量的数据
    • 主服务器上执行 UPDATE 语句时,所有发生变化的记录都会写到 binlog 中,而 SBR 只会写一次,这会导致频繁发生 binlog 的并发写问题
    • 无法从 binlog 中看到都复制了些什么语句

格式3:MIXED模式(混合模式复制(mixed-based replication, MBR))

binlog_format=MIXED

从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

在Mixed模式下,一般的语句修改使用statment格式保存binlog。如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog。

MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。

21.4、主从复制:双主双从

一个主机m1用于处理所有写请求,它的从机s1和另一台主机m2还有它的从机s2负责所有读请求。当m1主机宕机后,m2主机负责写请求,m1、m2互为备机。架构图如下:
MySQL高级(SQL优化)_第78张图片

21.5、同步数据一致性问题

主从同步的要求:

  • 读库和写库的数据一致(最终一致);
  • 写数据必须写到写库;
  • 读数据必须到读库(不一定);

21.5.1、理解主从延迟问题

进行主从同步的内容是二进制日志,它是一个文件,在进行 网络传输 的过程中就一定会 存在主从延迟(比如 500ms),这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的 数据不一致性 问题。

21.5.2、主从延迟问题原因

在网络正常的时候,日志从主库传给从库所需的时间是很短的,即T2-T1的值是非常小的。即,网络正常情况下,主备延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差。主备延迟最直接的表现是,从库消费中继日志(relay log)的速度,比主库生产binlog的速度要慢。造成原因:

  • 从库的机器性能比主库要差
  • 从库的压力大
  • 大事务的执行

21.5.3、如何减少主从延迟

若想要减少主从延迟的时间,可以采取下面的办法:

  1. 降低多线程大事务并发的概率,优化业务逻辑
  2. 优化SQL,避免慢SQL, 减少批量操作 ,建议写脚本以update-sleep这样的形式完成。
  3. 提高从库机器的配置 ,减少主库写binlog和从库读binlog的效率差。
  4. 尽量采用 短的链路 ,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输
    的网络延时。
  5. 实时性要求的业务读强制走主库,从库只做灾备,备份。

21.5.4、如何解决一致性问题

如果操作的数据存储在同一个数据库中,那么对数据进行更新的时候,可以对记录加写锁,这样在读取的时候就不会发生数据不一致的情况。但这时从库的作用就是 备份 ,并没有起到 读写分离 ,分担主库读压力的作用。
MySQL高级(SQL优化)_第79张图片
读写分离情况下,解决主从同步中数据不一致的问题, 就是解决主从之间 数据复制方式 的问题,如果按照数据一致性从弱到强 来进行划分,有以下 3 种复制方式。

方法 1:异步复制

异步模式就是主机在完成操作后,先响应操作结果给客户端,再进行从机复制。

  • 优点:不会影响主库写的效率。
  • 缺点:若主机宕机或故障,而binlog还没有同步到从库,则导致主机和从机数据不一致。此时备机会变成主机,备机中也会缺少原来主机中已提交的事务。

方法 2:半同步复制

半同步复制就是主机完成操作后,先进行至少一个从机的复制操作(不会等所有备机同步数据完成),再向客户端响应操作结果。

  • 优点:因为由部分从机已经完成数据同步,若主机故障,可以将已同步数据的从机作为主机,保证数据一致性。
  • 缺点:至少增加了一个网络延迟,降低了主库写的效率。

再MySQL5.7中还增加了一个rpl_semi_sync_master_wait_for_slave_count参数,可以设置半同步从机数量,默认为1。如果将参数调大,可以提升数据一致性的轻度,但会增加主库等待从库响应的时间。
MySQL高级(SQL优化)_第80张图片

方法 3:组复制

组复制技术,简称 MGR(MySQL Group Replication)。是 MySQL 在 5.7.17 版本中推出的一种新的数据复制技术,这种复制技术是基于 Paxos 协议的状态机复制。

首先我们将多个节点共同组成一个复制组,在 执行读写(RW)事务 的时候,需要通过一致性协议层(Consensus 层)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应 Node 节点)的同意,大多数指的是同意的节点数量需要大于 (N/2+1),这样才可以进行提交,而不是原发起方一个说了算。而针对 只读(RO)事务 则不需要经过组内同意,直接 COMMIT 即可。

也就是将事务分组,当大多事务都提交的时候才将改组的所有事务进行同步复制。

在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据的一致性。

MGR 将 MySQL 带入了数据强一致性的时代,是一个划时代的创新,其中一个重要的原因就是MGR 是基于 Paxos 协议的。Paxos 算法是由 2013 年的图灵奖获得者 Leslie Lamport 于 1990 年提出的,有关这个算法的决策机制可以搜一下。事实上,Paxos 算法提出来之后就作为 分布式一致性算法 被广泛应用,比如Apache 的 ZooKeeper 也是基于 Paxos 实现的。

21.6、读写分离

在主从架构的配置中,如果想要采取读写分离的策略,我们可以 自己编写程序 ,也可以通过 第三方的中间件 来实现。

  • 自己编写程序的好处就在于比较自主,我们可以自己判断哪些查询在从库上来执行,针对实时性要求高的需求,我们还可以考虑哪些查询可以在主库上执行。同时,程序直接连接数据库,减少了中间件层,相当于减少了性能损耗。
  • 采用中间件的方法有很明显的优势, 功能强大使用简单 。但因为在客户端和数据库之间增加了中间件层会有一些 性能损耗 ,同时商业中间件也是有使用成本的。我们也可以考虑采取一些优秀的开源工具。
    MySQL高级(SQL优化)_第81张图片
    Cobar 属于阿里B2B事业群,始于2008年,在阿里服役3年多,接管3000+个MySQL数据库的schema,集群日处理在线SQL请求50亿次以上。由于Cobar发起人的离职,Cobar停止维护。
    Mycat 是开源社区在阿里cobar基础上进行二次开发,解决了cobar存在的问题,并且加入了许多新的功能在其中。青出于蓝而胜于蓝。
    OneProxy 基于MySQL官方的proxy思想利用c语言进行开发的,OneProxy是一款商业 收费 的中间件。舍弃了一些功能,专注在性能和稳定性上 。
    kingshard 由小团队用go语言开发,还需要发展,需要不断完善。
    Vitess 是Youtube生产在使用,架构很复杂。不支持MySQL原生协议,使用 需要大量改造成本 。
    Atlas 是360团队基于mysql proxy改写,功能还需完善,高并发下不稳定。
    MaxScale 是mariadb(MySQL原作者维护的一个版本) 研发的中间件
    MySQLRoute 是MySQL官方Oracle公司发布的中间件

21.7、主备切换

  • 主动切换
  • 被动切换
  • 如何判断主库出问题了?如何解决过程中的数据不一致性问题?
    MySQL高级(SQL优化)_第82张图片

二十二、数据库备份与恢复

在任何数据库环境中,总会有不确定的意外发生,导致数据丢失服务器瘫痪等严重的后果。存在多个服务器时会出现主从数据同步的问题

为了有效防止数据丢失,并将损失降到最低,应定期对MySQL数据库服务器做备份。若数据库出现故障可以使用备份的数据进行恢复。主从服务器之间的数据同步问题可以通过复制功能实现。

22.1、物理备份与逻辑备份

物理备份:备份数据文件,转储数据库物理文件到某一目录。物理备份恢复速度比较快,但占用空间比较大,MySQL中可以用 xtrabackup 工具来进行物理备份。

逻辑备份:对数据库对象利用工具进行导出工作,汇总入备份文件内。逻辑备份恢复速度慢,但占用空间小,更灵活。MySQL 中常用的逻辑备份工具为 mysqldump 。逻辑备份就是 备份sql语句 ,在恢复的时候执行备份的sql语句实现数据库数据的重现。

22.2、mysqldump实现逻辑备份

mysqldump命令执行时,可以将数据库备份成一个文本文件,该文件中实际上包含多个CREATE和INSERT语句,使用这个文件可以重新执行sql语句,生成相应的表和数据。

  • 查出需要备份的表的结构,在文本文件中生成一个CREATE语句。
  • 将表中的所有记录准换成一条INSERT语句。

22.2.1、备份一个数据库

语法:mysqldump –u 用户名称 –h 主机名称 –p密码 待备份的数据库名称 [表名1,表名2]> 备份文件名称.sql/绝对路径;
说明:

  • 备份的文件并非一定要求后缀名为.sql,例如后缀名为.txt的文件也是可以的。
  • ">"不是大于符号,而是表示将待备份数据库备份到哪个数据库,也可以是绝对路径(具体到文件名),若为文件名则在当前目录下的文件中备份。

22.2.2、备份全部数据库

若想用mysqldump备份整个实例,可以使用 --all-databases 或 -A 参数:
mysqldump -uroot -pxxxxxx --all-databases > 备份文件名称.sql/绝对路径;
mysqldump -uroot -pxxxxxx -A > 备份文件名称.sql/绝对路径;

22.2.3、备份部分数据库

使用 --databases-B 参数了,该参数后面跟数据库名称,多个数据库间用空格隔开。如果指定databases参数,备份文件中会存在创建数据库的语句,如果不指定参数,则不存在。语法如下:
mysqldump –u user –h host –p --databases [数据库的名称1,数据库的名称2] > 备份文件名称.sql;

22.2.4、备份部分表

在表变更前做个备份。语法如下:
mysqldump –u user –h host –p 数据库的名称 [表名1 ,表名2] > 备份文件名称.sql;

22.2.5、备份单表的部分数据

有些时候一张表的数据量很大,我们只需要部分数据。这时就可以使用 --where 选项了。where后面附带需要满足的条件。

举例:备份student表中id小于10的数据:
mysqldump -uroot -p 数据库 student --where="id < 10 " > 备份文件名称.sql;

22.2.6、排除某些表的备份

如果我们想备份某个库,但是某些表数据量很大或者与业务关联不大,这个时候可以考虑排除掉这些表,同样的,选项 --ignore-table 可以完成这个功能。
mysqldump -uroot -p 数据库名 --ignore-table=数据库.表 > 备份文件名称.sql

通过如下指定判定文件中没有student表结构:grep "表" 备份文件名称.sql

22.2.7、只备份结构或只备份数据

只备份结构的话可以使用 --no-data 简写为 -d 选项;只备份数据可以使用 --no-create-info 简写为-t 选项。

  • 只备份结构
mysqldump -uroot -p 库名 --no-data > 备份文件名称.sql
#使用grep命令,没有找到insert相关语句,表示没有数据备份。
[root@node1 ~]# grep "INSERT" 备份文件名称.sql
[root@node1 ~]#
  • 只备份数据
mysqldump -uroot -p 库名 --no-create-info > 备份文件名称.sql
#使用grep命令,没有找到create相关语句,表示没有数据结构。
[root@node1 ~]# grep "CREATE" 备份文件名称.sql
[root@node1 ~]#

22.2.8、备份 存储过程、函数、事件

mysqldump备份默认是不包含存储过程,自定义函数及事件的。可以使用 --routines-R 选项来备份存储过程及函数,使用 --events-E参数来备份事件。

  • 查看当前库有哪些存储过程或函数
    SELECT SPECIFIC_NAME,ROUTINE_TYPE ,ROUTINE_SCHEMA FROM information_schema.Routines WHERE ROUTINE_SCHEMA="库名";
  • 备份函数及存储过程
    mysqldump -uroot -p -R -E --databases 库名 > 备份文件名称.sql;

22.2.9、mysqldump常用选项

mysqldump其他常用选项如下:

--add-drop-database:在每个CREATE DATABASE语句前添加DROP DATABASE语句。
--add-drop-tables:在每个CREATE TABLE语句前添加DROP TABLE语句。
--add-locking:用LOCK TABLES和UNLOCK TABLES语句引用每个表转储。重载转储文件时插入得更快。
--all-database, -A:转储所有数据库中的所有表。与使用--database选项相同,在命令行中命名所有数据库。
--comment[=0|1]:如果设置为0,禁止转储文件中的其他信息,例如程序版本、服务器版本和主机。--skipcomments与--comments=0的结果相同。默认值为1,即包括额外信息。
--compact:产生少量输出。该选项禁用注释并启用--skip-add-drop-tables、--no-set-names、--skipdisable-keys和--skip-add-locking选项。
--compatible=name:产生与其他数据库系统或旧的MySQL服务器更兼容的输出,值可以为ansi、MySQL323、
#MySQL40、postgresql、oracle、mssql、db2、maxdb、no_key_options、no_table_options或者no_field_options。
--complete_insert, -c:使用包括列名的完整的INSERT语句。
--debug[=debug_options], -#[debug_options]:写调试日志。
--delete,-D:导入文本文件前清空表。
--default-character-set=charset:使用charsets默认字符集。如果没有指定,就使用utf8。
--delete--master-logs:在主复制服务器上,完成转储操作后删除二进制日志。该选项自动启用-masterdata。
--extended-insert,-e:使用包括几个VALUES列表的多行INSERT语法。这样使得转储文件更小,重载文件时可以加速插入。
--flush-logs,-F:开始转储前刷新MySQL服务器日志文件。该选项要求RELOAD权限。
--force,-f:在表转储过程中,即使出现SQL错误也继续。
--lock-all-tables,-x:对所有数据库中的所有表加锁。在整体转储过程中通过全局锁定来实现。该选项自动关闭--single-transaction和--lock-tables。
--lock-tables,-l:开始转储前锁定所有表。用READ LOCAL锁定表以允许并行插入MyISAM表。对于事务表(例如InnoDB和BDB),--single-transaction是一个更好的选项,因为它根本不需要锁定表。
--no-create-db,-n:该选项禁用CREATE DATABASE /*!32312 IF NOT EXIST*/db_name语句,如果给出--database或--all-database选项,就包含到输出中。
--no-create-info,-t:只导出数据,而不添加CREATE TABLE语句。
--no-data,-d:不写表的任何行信息,只转储表的结构。
--opt:该选项是速记,它可以快速进行转储操作并产生一个能很快装入MySQL服务器的转储文件。该选项默认开启,但可以用--skip-opt禁用。
--password[=password],-p[password]:当连接服务器时使用的密码。
-port=port_num,-P port_num:用于连接的TCP/IP端口号。
--protocol={TCP|SOCKET|PIPE|MEMORY}:使用的连接协议。
--replace,-r –replace和--ignore:控制替换或复制唯一键值已有记录的输入记录的处理。如果指定--replace,新行替换有相同的唯一键值的已有行;
#如果指定--ignore,复制已有的唯一键值的输入行被跳过。如果不指定这两个选项,当发现一个复制键值时会出现一个错误,并且忽视文本文件的剩余部分。
--silent,-s:沉默模式。只有出现错误时才输出。
--socket=path,-S path:当连接localhost时使用的套接字文件(为默认主机)。
--user=user_name,-u user_name:当连接服务器时MySQL使用的用户名。
--verbose,-v:冗长模式,打印出程序操作的详细信息。
--xml,-X:产生XML输出。

运行帮助命令 mysqldump --help ,可以获得特定版本的完整选项列表。

提示 如果运行mysqldump没有–quick或–opt选项,mysqldump在转储结果前将整个结果集装入内存。如果转储大数据库可能会出现问题,该选项默认启用,但可以用–skip-opt禁用。如果使用最新版本的mysqldump程序备份数据,并用于恢复到比较旧版本的MySQL服务器中,则不要使用–opt或-e选项。

22.3、从mysqldump备份恢复数据

使用mysqldump命令可以将数据库中的数据备份成一个文本文件,需要恢复时,可以使用mysql命令从备份的mysqldump文件恢复数据。

mysql命令可以执行备份文件中的sql语句来重新建表和添加数据。

语法:mysql –u root –p [库名] < 备份文件/绝对路径;

  • 其中库名时可选项,若不指定,则恢复文件中所有数据库。

22.3.1、单库备份中恢复单库

  • 如果备份文件中包含了创建数据库的语句,则恢复的时候不需要指定数据库名称
    mysql -uroot -p < 备份文件;
  • 否则需要指定数据库名称
    mysql -uroot -p 库名< 备份文件;

22.3.2、全量备份恢复

如果我们现在有昨天的全量备份,现在想整个恢复,则可以这样操作:
mysql –u root –p < 备份文件
mysql -uroot -pxxxxxx < 备份文件
执行完后,MySQL数据库中就已经恢复了备份文件中的所有数据库。

补充:
如果使用–all-database参数备份了所有的数据库,那么恢复时不需要指定数据库。对应的sql文件包含有CREATE DATABASE语句,可通过该语句创建数据库。创建数据库后,可以执行sql文件中的USE语句选择数据库,再创建表并插入记录。

22.3.3、从全量备份中恢复单库

可能有这样的需求,比如说我们只想恢复某一个库,但是我们有的是整个实例的备份,这个时候我们可以从全量备份中分离出单个库的备份。
语法:

sed -n '/^-- Current Database: `库名` /,/^-- Current `/p' 备份文件> 备份到哪个文件.sql

分离完成后我们再导入到某个库的备份文件(备份到哪个文件)即可恢复单个库,若该文件没有会自动新建一个新的。

22.3.4、从单库备份中恢复单表

这个需求还是比较常见的。比如说我们知道哪个表误操作了,那么就可以用单表恢复的方式来恢复。

cat 备份文件 | sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `表名`/!d;q' > 备份后生成的文件1

cat 备份文件 | grep --ignore-case 'insert into `表名`' > 备份后生成的文件2
#用shell语法分离出创建表的语句及插入数据的语句后 再依次导出即可完成恢复

use 数据库;
source 备份后生成的文件1;
source 备份后生成的文件2;

22.4、物理备份:直接复制整个数据库

直接将MySQL中的数据库文件复制出来。这种方法最简单,速度也最快。MySQL的数据库目录位置不一定相同:

  • 在Windows平台下,MySQL 8.0存放数据库的目录通常默认为 “ C:\ProgramData\MySQL\MySQLServer 8.0\Data ”或者其他用户自定义目录;
  • 在Linux平台下,数据库目录位置通常为/var/lib/mysql/
  • 在MAC OSX平台下,数据库目录位置通常为“/usr/local/mysql/data

但为了保证备份的一致性。需要保证:

  • 方式1:备份前,将服务器停止。
  • 方式2:备份前,对相关表执行 FLUSH TABLES WITH READ LOCK 操作。这样当复制数据库目录中的文件时,允许其他客户继续查询表。同时,FLUSH TABLES语句来确保开始备份前将所有激活的索引页写入硬盘。

这种方式方便、快速,但不是最好的备份方法,因为实际情况可能 不允许停止MySQL服务器 或者 锁住表 ,而且这种方法 对InnoDB存储引擎 的表不适用。对于MyISAM存储引擎的表,这样备份和还原很方便,但是还原时最好是相同版本的MySQL数据库,否则可能会存在文件类型不同的情况。

注意,物理备份完毕后,执行 UNLOCK TABLES 来结算其他客户对表的修改行为。

说明: 在MySQL版本号中,第一个数字表示主版本号,主版本号相同的MySQL数据库文件格式相同。

此外,还可以考虑使用相关工具实现备份。比如,MySQLhotcopy工具。MySQLhotcopy是一个Perl脚本,它使用LOCK TABLES、FLUSH TABLES和cp或scp来快速备份数据库。它是备份数据库或单个表最快的途径,但它只能运行在数据库目录所在的机器上,并且只能备份MyISAM类型的表。多用于mysql5.5之前。

22.5、物理恢复:直接复制到数据库目录

步骤:

  1. 演示删除备份的数据库中指定表的数据
  2. 将备份的数据库数据拷贝到数据目录下,并重启MySQL服务器
  3. 查询相关表的数据是否恢复。需要使用下面的 chown 操作。

要求:

  • 必须确保备份数据的数据库和待恢复的数据库服务器的主版本号相同。
    • 因为只有MySQL数据库主版本号相同时,才能保证这两个MySQL数据库文件类型是相同的。
  • 这种方式对MyISAM类型的表比较有效,对于InnoDB类型的表则不可用。
    • 因为InnoDB表的表空间不能直接复制。
  • 在Linux操作系统下,复制到数据库目录后,一定要将数据库的用户和组变成mysql,命令如下:
    chown -R mysql.mysql /var/lib/mysql/dbname

其中,两个mysql分别表示组和用户;“-R”参数可以改变文件夹下的所有子文件的用户和组;“dbname”参数表示数据库目录。

提示 Linux操作系统下的权限设置非常严格。通常情况下,MySQL数据库只有root用户和mysql用户组下的mysql用户才可以访问,因此将数据库目录复制到指定文件夹后,一定要使用chown命令将文件夹的用户组变为mysql,将用户变为mysql。

22.6、表的导出与导入

22.6.1、表的导出

1. 使用SELECT…INTO OUTFILE导出文本文件

在MySQL中,可以使用SELECT…INTO OUTFILE语句将表的内容导出成一个文本文件。

  1. mysql默认对导出的目录有权限限制,也就是说使用命令行进行导出的时候,需要指定目录进行操作。
  • 查询secure_file_priv值:SHOW GLOBAL VARIABLES LIKE '%secure%';

MySQL高级(SQL优化)_第83张图片

  1. 上面结果中显示,secure_file_priv变量的值为/var/lib/mysql-files/,导出目录设置为该目录,SQL语句如下。
    SELECT * FROM account INTO OUTFILE "/var/lib/mysql-files/account.txt";

  2. 查看 /var/lib/mysql-files/account.txt`文件。

2. 使用mysqldump命令导出文本文件

举例1: 使用mysqldump命令将将atguigu数据库中account表中的记录导出到文本文件:
mysqldump -uroot -p -T "/var/lib/mysql-files/" atguigu account
mysqldump命令执行完毕后,在指定的目录/var/lib/mysql-files/下生成了account.sql和account.txt文件。

举例2: 使用mysqldump将atguigu数据库中的account表导出到文本文件,使用FIELDS选项,要求字段之间使用逗号“,”间隔,所有字符类型字段值用双引号括起来:
mysqldump -uroot -p -T "/var/lib/mysql-files/" atguigu account --fields-terminated-by=',' --fields-optionally-enclosed-by='\"'
语句mysqldump语句执行成功之后,指定目录下会出现两个文件account.sql和account.txt。

3. 使用mysql命令导出文本文件

**举例1:**使用mysql语句导出atguigu数据中account表中的记录到文本文件:
mysql -uroot -p --execute="SELECT * FROM account;" atguigu> "/var/lib/mysqlfiles/account.txt"

**举例2:**将atguigu数据库account表中的记录导出到文本文件,使用–veritcal参数将该条件记录分为多行显示:
mysql -uroot -p --vertical --execute="SELECT * FROM account;" atguigu >"/var/lib/mysql-files/account_1.txt"

**举例3:**将atguigu数据库account表中的记录导出到xml文件,使用–xml参数,具体语句如下。
mysql -uroot -p --xml --execute="SELECT * FROM account;" atguigu>"/var/lib/mysqlfiles/account_3.xml"

说明:如果要将表数据导出到html文件中,可以使用 --html 选项。然后可以使用浏览器打开。

22.6.2、表的导入

1. 使用LOAD DATA INFILE方式导入文本文件

举例1:

  • 使用SELECT…INTO OUTFILE将atguigu数据库中account表的记录导出到文本文件
    SELECT * FROM atguigu.account INTO OUTFILE '/var/lib/mysql-files/account_0.txt';
  • 删除account表中的数据:
    DELETE FROM atguigu.account;
  • 从文本文件account.txt中恢复数据:
    LOAD DATA INFILE '/var/lib/mysql-files/account_0.txt' INTO TABLE atguigu.account;

举例2:

  • 选择数据库atguigu,使用SELECT…INTO OUTFILE将atguigu数据库account表中的记录导出到文本文件,使用FIELDS选项和LINES选项,要求字段之间使用逗号","间隔,所有字段值用双引号括起来:
    SELECT * FROM atguigu.account INTO OUTFILE '/var/lib/mysql-files/account_1.txt' FIELDS TERMINATED BY ',' ENCLOSED BY '\"';
  • 删除account表中的数据:DELETE FROM atguigu.account;
  • 从/var/lib/mysql-files/account.txt中导入数据到account表中:
    LOAD DATA INFILE '/var/lib/mysql-files/account_1.txt' INTO TABLE atguigu.account FIELDS TERMINATED BY ',' ENCLOSED BY '\"';

2. 使用mysqlimport方式导入文本文件

举例:

  • 导出文件account.txt,字段之间使用逗号","间隔,字段值用双引号括起来:
    SELECT * FROM atguigu.account INTO OUTFILE '/var/lib/mysql-files/account.txt' FIELDS TERMINATED BY ',' ENCLOSED BY '\"';
  • 删除account表中的数据:DELETE FROM atguigu.account;
  • 使用mysqlimport命令将account.txt文件内容导入到数据库atguigu的account表中:
    mysqlimport -uroot -p atguigu '/var/lib/mysql-files/account.txt' --fields-terminatedby=',' --fields-optionally-enclosed-by='\"'

22.7、数据库迁移

22.7.1、概述

数据迁移(data migration)是指选择、准备、提取和转换数据,并将数据从一个计算机存储系统永久地传输到另一个计算机存储系统的过程。此外, 验证迁移数据的完整性退役原来旧的数据存储 ,也被认为是整个数据迁移过程的一部分。

数据库迁移的原因是多样的,包括服务器或存储设备更换、维护或升级,应用程序迁移,网站集成,灾难恢复和数据中心迁移。

根据不同的需求可能要采取不同的迁移方案,但总体来讲,MySQL 数据迁移方案大致可以分为 物理迁移逻辑迁移 两类。通常以尽可能 自动化 的方式执行,从而将人力资源从繁琐的任务中解放出来。

22.7.2、迁移方案

  • 物理迁移
    物理迁移适用于大数据量下的整体迁移。使用物理迁移方案的优点是比较快速,但需要停机迁移并且要求 MySQL 版本及配置必须和原服务器相同,也可能引起未知问题。

    物理迁移包括拷贝数据文件和使用 XtraBackup 备份工具两种。

    不同服务器之间可以采用物理迁移,我们可以在新的服务器上安装好同版本的数据库软件,创建好相同目录,建议配置文件也要和原数据库相同,然后从原数据库方拷贝来数据文件及日志文件,配置好文件组权限,之后在新服务器这边使用 mysqld 命令启动数据库。

  • 逻辑迁移
    逻辑迁移适用范围更广,无论是 部分迁移 还是 全量迁移 ,都可以使用逻辑迁移。逻辑迁移中使用最多的就是通过 mysqldump 等备份工具。

22.7.3、迁移注意点

1. 相同版本的数据库之间迁移注意点

指的是在主版本号相同的MySQL数据库之间进行数据库移动。
方式1: 因为迁移前后MySQL数据库的 主版本号相同 ,所以可以通过复制数据库目录来实现数据库迁移,但是物理迁移方式只适用于MyISAM引擎的表。对于InnoDB表,不能用直接复制文件的方式备份数据库。

方式2: 最常见和最安全的方式是使用 mysqldump命令 导出数据,然后在目标数据库服务器中使用MySQL命令导入。
举例:

#host1的机器中备份所有数据库,并将数据库迁移到名为host2的机器上
mysqldump –h host1 –uroot –p –-all-databases|
mysql –h host2 –uroot –p

在上述语句中,“|”符号表示管道,其作用是将mysqldump备份的文件给mysql命令;“–all-databases”表示要迁移所有的数据库。通过这种方式可以直接实现迁移。

2. 不同版本的数据库之间迁移注意点

例如,原来很多服务器使用5.7版本的MySQL数据库,在8.0版本推出来以后,改进了5.7版本的很多缺陷,因此需要把数据库升级到8.0版本

旧版本与新版本的MySQL可能使用不同的默认字符集,例如有的旧版本中使用latin1作为默认字符集,而最新版本的MySQL默认字符集为utf8mb4。如果数据库中有中文数据,那么迁移过程中需要对 默认字符集进行修改 ,不然可能无法正常显示数据。

高版本的MySQL数据库通常都会 兼容低版本 ,因此可以从低版本的MySQL数据库迁移到高版本的MySQL数据库。

3. 不同数据库之间迁移注意点

不同数据库之间迁移是指从其他类型的数据库迁移到MySQL数据库,或者从MySQL数据库迁移到其他类型的数据库。这种迁移没有普适的解决方法。

迁移之前,需要了解不同数据库的架构, 比较它们之间的差异 。不同数据库中定义相同类型的数据的 关键字可能会不同 。例如,MySQL中日期字段分为DATE和TIME两种,而ORACLE日期字段只有DATE;SQLServer数据库中有ntext、Image等数据类型,MySQL数据库没有这些数据类型;MySQL支持的ENUM和SET类型,这些SQL Server数据库不支持。

另外,数据库厂商并没有完全按照SQL标准来设计数据库系统,导致不同的数据库系统的 SQL语句 有差别。例如,微软的SQL Server软件使用的是T-SQL语句,T-SQL中包含了非标准的SQL语句,不能和MySQL的SQL语句兼容。

不同类型数据库之间的差异造成了互相 迁移的困难 ,这些差异其实是商业公司故意造成的技术壁垒。但是不同类型的数据库之间的迁移并 不是完全不可能 。例如,可以使用 MyODBC 实现MySQL和SQL Server之间的迁移。MySQL官方提供的工具 MySQL Migration Toolkit 也可以在不同数据之间进行数据迁移。MySQL迁移到Oracle时,需要使用mysqldump命令导出sql文件,然后, 手动更改 sql文件中的CREATE语句。

22.8、小结

MySQL高级(SQL优化)_第84张图片

二十三、误删了库怎么办

传统的高可用架构是不能预防误删数据的,因为主库的一个drop table命令,会通过binlog传给所有从库和级联从库,进而导致整个集群的实例都会执行这个指令。

为了找到解决误删数据的更高效的方法,我们需要先对和MySQL相关的误删数据,做下分类:

  • 使用delete语句误删数据行;
  • 使用drop table或者truncate table语句误删数据表;
  • 使用drop database语句误删数据库;
  • 使用rm命令误删整个MySQL实例。

23.1、delete:误删行

处理措施1: 数据恢复
使用Flashback工具恢复数据。

原理:修改binlog内容,拿回原库重放。如果误删数据涉及到了多个事务的话,需要将事务的顺序调过来再执行。

使用前提:binlog_format=rowbinlog_row_image=FULL

处理措施2: 预防

  • 代码上线前,必须SQL审查、审计
  • 建议可以打开安全模式,把sql_safe_updates参数设置为on。强制要求加where条件且where后需要是索引字段,否则必须使用limit。否则就会报错。

23.2、truncate/drop:误删库/表

truncate/drop table删除的数据是没法通过Flashback来恢复,因为即使我们配置了binlog_format=row,执行这三个命令时,记录的binlog还是statement格式。binlog里面就只有一个truncate/drop语句,这些信息是恢复不出数据的。

解决方案:
这种情况下,要想恢复数据,就需要使用 全量备份 ,加 增量日志 的方式了。这个方案要求线上有定期的全量备份,并且实时备份binlog。
在这两个条件都具备的情况下,假如有人中午12点误删了一个库,恢复数据的流程如下:

  1. 取最近一次 全量备份 ,假设这个库是一天一备,上次备份是当天 凌晨2点 ;
  2. 用备份恢复出一个 临时库 ;
  3. 从日志备份里面,取出凌晨2点之后的日志;
  4. 把这些日志,除了误删除数据的语句外,全部应用到临时库;
  5. 最后恢复到主库。

23.3、预防truncate/drop:误删库/表

  1. 权限分离
  • 限制账户权限,核心的数据库,一般都不能随便分配写权限,想要获取写权限需要审批。比如只给业务开发人员DML权限,不给truncate/drop权限。即使是DBA团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号。
  • 不同的账号,不同的数据之间要进行权限分离,避免一个账号可以删除所有库。
  1. 制定操作规范
    比如在删除数据表之前,必须先对表做改名操作(比如加_to_be_deleted)。然后,观察一段时间,确保对业务无影响以后再删除这张表。

  2. 设置延迟复制备库
    简单的说延迟复制就是设置一个固定的延迟时间,比如1个小时,让从库落后主库一个小时。出现误删操作1小时内,到这个备库上执行stop slave,再通过之前介绍的方法,跳过这个误操作命令,就可以恢复出需要的数据。这里通过CHANGE MASTER TO MASTER_DELAY=N命令,可以指定这个备库持续保持跟主库有N秒的延迟。比如把N设置为3600,即1小时。

此外,延迟复制还可以用来解决以下问题:
① 用来做延迟测试,比如做的好的数据库读写分离,把从库作为读库,那么想知道当数据产生延迟的时候到底会发生什么,就可以使用这个特性模拟延迟。
② 用于老数据的查询等需求,比如你经常需要查看某天前一个表或者一个字段的数值,你可能需要把备份恢复后进行查看,如果有延迟从库,比如延迟一周,那么就可以解决这样类似的需求。

23.4、rm:误删MySQL实例

对于一个有高可用机制的MySQL集群来说,不用担心 rm删除数据 了。只是删掉了其中某一个节点的数据的话,HA系统就会开始工作,选出一个新的主库,从而保证整个集群的正常工作。我们要做的就是在这个节点上把数据恢复回来,再接入整个集群。

但如果是恶意地把整个集群删除,那就需要考虑跨机房备份,跨城市备份。

你可能感兴趣的:(后端,mysql,数据库,sql)