mysql
1 基本知识
- 支持分区
- 默认innodb引擎
1-1 常用引擎
- myisam
my-z[ei]m - innodb
in-no-db
2 数据类型
- 整数,tiny int 1位 127/255,small int 2位,medium int 3位,int 4位,big int 8位
2-1 定点数
- decimal(m,d) m 1~65,d 0~30 默认0
- numeric(m,d)
2-2 浮点数
- m 0~255,d 0~30
- double(m,d) 只保证16位有效数字,超出=decimal
- float(m,d) =double real_as_float
- real(m,d) =double
2-3 二进制
- bit(m) m 1~64
- binary
- varbinary
- blob
2-4 字符串
- char(m) m 0~255 固定长度,不够右边空格填充,取出时截掉
- varchar(m) m 0~65535 ,与字符集有关系
- text
2-5 enum
- 单选字符串数据类型
- 最多 65535
2-6 set
- 多选字符串数据类型
- 最多64个选项
2-7 时间日期类型
- date,time,datatime,timestamp
datatime
- 范围 全范围
- 8个字节存储
- 与时区无关
timestamp
- 范围 1970 ~ 2037
- 4位存储
- 与时区有关,不同时区数据不一样
毫秒,微秒问题
- mysql 没有毫秒,微秒数据,需要程序自行处理
- mysql5.6.4 后支持,datetime(3/6)或者timestamp(3/6)
3 约束
3-1 主键 ,联合主键
- 会生成唯一索引
- 加no null 约束
- 产生聚集索引
3-2 外键
- 会生成索引
3-3 约束
唯一约束
- 会生成唯一索引
- 可以为空
- 产生非聚集索引
- 区别:目的不同,唯一约束约束数据,唯一索引优化查询
- no null
- 高可用方式
- sql优化
4 索引
4-1 底层结构
背景知识
- 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一磁盘块中的数据会被一次性读取出来,而不是按需读取。
- innodb 存储引擎使用页作为数据读取单位,页是其磁盘管理的最小单位,默认 page 大小是 16k。
- 系统的一个磁盘块的存储空间往往没有这么大,因此 innodb 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16kb。
btree
- 如比查找29关键字
模拟查找关键字 29 的过程:
- 1.根据根节点找到磁盘块 1,读入内存。【磁盘 i/o 操作第 1 次】
- 2.比较关键字 29 在区间(17,35),找到磁盘块 1 的指针 p2。
- 3.根据 p2 指针找到磁盘块 3,读入内存。【磁盘 i/o 操作第 2 次】
- 4.比较关键字 29 在区间(26,30),找到磁盘块 3 的指针 p2。
- 5.根据 p2 指针找到磁盘块 8,读入内存。【磁盘 i/o 操作第 3 次】
- 6.在磁盘块 8 中的关键字列表中找到关键字 29。
特性
- 1、关键字集合分布在整颗树中;
- 2、任何一个关键字出现且只出现在一个节点中;
- 3、每个节点存储data和key;
- 4、搜索有可能在非叶子节点结束;
- 5、一个节点中的key从左到右非递减排列;
- 6、所有叶节点具有相同的深度,等于树高h。
- 每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。
b+tree(innodb举例)
设计初衷
- innodb 存储引擎在设计时是将根节点常驻内存的,力求达到树的深度不超过 3,也就是说 i/o 不需要超过 3 次。
- 分析b-tree的查找过程,发现需要3次磁盘 i/o 操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。
- 在 b-tree 中,每个节点中有 key,也有 data,而每一个页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,会导致 b-tree 的深度较大,增大查询时的磁盘 i/o 次数,进而影响查询效率。。
数据结构图
- b+tree的检索过程
在b-tree的基础上有两点变化:
- 数据是存在叶子节点中的;
- 数据节点之间是有指针指向的。
page结构
page是整个innodb存储的最基本构件,也是innodb磁盘管理的最小单位,与数据库相关的所有内容都存储在这种page结构里。
page常见页类型
- 数据页(b-tree node)
- undo页(undo log page)
- 系统页(system page)
- 事务数据页(transaction system page)等。
- 单个page的大小是16k(编译宏univ_page_size控制),每个page使用一个32位的int值来唯一标识,这也正好对应innodb最大64tb的存储容量(16kib * 2^32 = 64tib)。
page的基本结构
- page的头部保存了两个指针,分别指向前一个page和后一个page,page链接起来就是一个双向链表的结构。
主体内容
page的主体内容中主要关注行数据和索引的存储,都位于page的user records部分在一个page内部
- user records占据page的大部分空间,user records由一条一条的record组成,每条记录代表索引树上的一个节点(非叶子节点和叶子节点)。
- 单链表的头尾由固定内容的两条记录来表示,字符串形式的"infimum"代表开头,"supremum"代表结尾。这两个用来代表开头结尾的record存储在system records的段里,这个system records和user records是两个平行的段。
- innodb存在4种不同的record,它们分别是1主键索引树非叶节点 2主键索引树叶子节点 3辅助键索引树非叶节点 4辅助键索引树叶子节点。
user record在page内以单链表的形式存在,最初数据是按照插入的先后顺序排列的,但是随着新数据的插入和旧数据的删除,数据物理顺序会变得混乱,但他们依然保持着逻辑上的先后顺序。
4-2 常见索引类型(逻辑分类)
- 主键索引
- 常规索引(非主键)
- 唯一索引
复合索引
- 由多列创建的索引称为符合索引
前缀索引
- 当索引的字符串列很大时,创建的索引也就变得很大,为了减小索引体积,提高索引的扫描速度,就用索引的前部分字串索引,这样索引占用的空间就会大大减少,并且索引的选择性也不会降低很多。而且是对blob和text列进行索引,或者非常长的varchar列,就必须使用前缀索引,因为mysql不允许索引它们的全部长度。
全文索引
- innodb引擎对fulltext索引的支持是mysql5.6新引入的特性,之前只有myisam引擎支持fulltext索引。对于fulltext索引的内容可以使用match()…against语法进行查询。
mysql 5.6以前版本
- 仅myisam引擎支持full text索引,可以使用match()...against语法进行查询
- 只支持英文。缘由是他从来都使用空格来作为分词的分隔符,而对于中文来讲,显然用空格就不合适,需要针对中文语义进行分词。
mysql 5.6
- innodb支持full text索引
mysql 5.7以后版本
- myisam、innodb均支持full text索引
- 支持中文索引
外键索引
- 它可以提高查询效率,外键会自动和对应的其他表的主键关联。外键的主要作用是保证记录的一致性和完整性。
- 不推荐使用外键,在应用层使用外键的思想来保证数据的一致性和完整性。
覆盖索引
- 使用覆盖索引扫描的查询可以直接使用节点页中的主键值。(具体参照下图中的索引数据结构)
4-3 索引类型-叶子节点数据分类
聚簇索引
- 叶子节点中存放的是索引和数据记录行
- 聚簇索引的每一个叶子节点都包含了主键值、事务id、用于事务和mvcc(多版本控制)的回滚指针以及所有的剩余列。
优点
- 提高数据访问性能:聚簇索引把索引和数据都保存到同一棵b+树数据结构中,并且同时将索引列与相关数据行保存在一起。这意味着,当你访问同一数据页不同行记录时,已经把页加载到了buffer中,再次访问的时候,会在内存中完成访问,不必访问磁盘。
- 不同于myisam引擎(非聚簇索引),它将索引和数据没有放在一块,放在不同的物理文件中,索引文件是缓存在key_buffer中,索引对应的是磁盘位置,不得不通过磁盘位置访问磁盘数据。
缺点
基于聚簇索引的表在插入新行,或者主键被更新,导致需要移动行的时候,可能面临“页分裂(page split)”的问题。页分裂会导致表占用更多的磁盘空间。
- 建议在大量插入新行后,选在负载较低的时间段,通过optimize table优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片
表如果使用uuid作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更新
- 建议使用int的auto_increment作为主键
- 如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值;过长的主键值,会导致非叶子节点占用更多的物理空间
非聚簇索引(辅助索引)
- 叶子节点中存放的是索引以及对应的数据记录指针(或主键)
4-4 索引扫描方式
紧凑索引扫描(dense index)
- 为了定位数据需要做权表扫描,为了提高扫描速度,把索引键值单独放在独立的数据的数据块里,并且每个键值都有个指向原数据块的指针,因为索引比较小,扫描索引的速度就比扫描全表快,这种需要扫描所有键值的方式就称为紧凑索引扫描
松散索引扫描(sparse index)
- 为了提高紧凑索引扫描效率,通过把索引排序和查找算法(b+trre),发现只需要和每个数据块的第一行键值匹配,就可以判断下一个数据块的位置或方向,因此有效数据就是每个数据块的第一行数据,如果把每个数据块的第一行数据创建索引,这样在这个新创建的索引上折半查找,数据定位速度将更快。这种索引扫描方式就称为松散索引扫描。
覆盖索引扫描(covering index)
- 包含所有满足查询需要的数据的索引称为覆盖索引,即利用索引返回select列表中的字段,而不必根据索引再次读取数据文件
- 针对某种sql覆盖掉了某种操作
4-5 innodb
一级索引/主键索引/聚簇索引
- 数据结构
- 聚簇索引中的每个叶子节点包含主键值、事务id、回滚指针(rollback pointer用于事务和mvcc)和余下的列(如col2)。
innodb默认创建聚簇索引
-
- 有主键时,根据主键创建聚簇索引
-
- 没有主键时,会用一个唯一且不为空的索引列做为主键,成为此表的聚簇索引
-
- 如果以上两个都不满足那innodb自己创建一个虚拟的聚集索引
二级索引/非主键索引/非聚簇索引
- 数据结构
- innodb的二级索引(非聚簇索引)的叶子节点中存储的不是“行指针”,而是主键值。
- 故通过二级索引查找数据时,会进行两次索引查找。存储引擎需要先查找二级索引的叶子节点来获得对应的主键值,然后根据这个主键值到聚簇索引中查找对应的数据行。
4-6 myisam
同innodb数据结构比较
- 分支主题
- myisam的二级索引叶子节点存放的还是列值与行号的组合,叶子节点中保存的是数据的物理地址。
- myisam的主键索引和二级索引没有任何区别,主键索引仅仅只是一个叫做primary的唯一、非空的索引,且myisam引擎中可以不设主键。
5 mvcc
多版本一致性控制
5-1 隐含字段
- db_trx_id 6字节
事务id,随事务增加 - db_roll_ptr 7字节
回滚指针 - db_row_id
隐藏的id,随记录自然增长
5-2 过程
select
- 当前session 事务版本号>=db_trx_id
- delete_id is null or 当前事务号
insert
- 新插入的行,db_trx_id=当前事务号
update
- 更新行的时候,innodb会把原来行复制一份,db_trx_id=当前事务号
delete
- 设置删除版本号为当前事务id (db_trx_id=当前事务号??) delete_id = 当前事务号
- 相当于标记为删除,而不是物理的删除。真是的删除是在innodb的purge线程去做的。
commit
- 修改事务状态为commit,在 redo log 中写入 commit 记录
rollback
- 根据当前回滚指针从undo log中找出事务修改前的版本,并恢复
6 事务的实现
6-1 相关概念
- redo log
binlog
- 主要用于主从复制
- undo log 存在表空间中,会有purge线程清理
- rollback segment
- 锁
- 隔离级别
6-2 事务执行过程
- 用排他锁锁定该行
- 记录 undo/redo log,确保日志刷到磁盘上持久存储。
- 更新数据记录,缓存操作并异步刷盘。
- 将事务日志持久化到 binlog。
- 回滚事务:根据当前回滚指针从undo log中找出事务修改前的版本,并恢复
- 提交事务,在 redo log 中写入commit记录。
- 释放锁资源
- 异步清理undo段信息
- 清理保存点列表
6-3 恢复
- 通过 redo log 来重做事务或通过 undo log 来回滚
- 以binlog 来确定是提交还是回滚
7 行锁实现
7-1 快照读
snapshot read
- 定义:读取记录的可见版本(可能是历史版本),不加锁
表现
- 普通 select
7-2 当前读
current read
- 定义:读取记录最新版本,会加锁保证不并发修改
表现
- 特殊查询 select .. where ? for update 加s锁
- select .. where ? lock in share mode 加x锁
- insert,update,delete等 dml操作 加x锁
7-3 特点
- 针对当前读,innodb交互是一条一条的进行,加锁也是一条一条
7-4 过程
- 发送一个update语句
- 使用当前读取出第一条并加锁
- mysql 收到会再发一个update请求,更新这条记录
- 完成后再读下一条重复上述过程
7-5 两阶段锁实现
- 2pl就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁
- 过程
8 锁机制
8-1 锁信息查看
- show status like 'innodb_row_lock%';
8-2 无锁
- 普通select 不加任何锁,因此行锁无阻塞,但page锁和表锁会阻塞
8-3 共享锁(读写锁,s锁)
共享读,ix,x锁会阻塞
- select * from table where ? lock in share mode
8-4 当前读加排他锁(写锁,x锁)
排他锁即其他任何锁都会阻塞
- select * from table where ? for update
- insert,update,delete
8-5 gap间隙锁
- 范围条件的行锁会有
- 或=条件但数据不存在
- 意向共享锁
- 意向排他锁
8-6 范围
表锁
- 没有用到索引查询时
行级锁
- 通过索引+mvcc 实现
8-7 replace into
- 导致主从数据不一致
- 丢失更新问题
9 隔离级别
- 丢失更新,脏读,可重复读(数据变化),幻读(行数变化)
- 读未提交(read uncommited) yes,yes,yes
9-1 读提交(read commited) no,yes,yes
- 大部分数据库的默认值
- 一个事务内,数据会变,数据数量也会变
- 针对当前读:加记录锁,可幻读
9-2 可重复读(repeatable read)no,no,yes
- mysql默认值,含义是限制其他事务写(排他锁,写锁)
- 一个事务内数据,数据不变但数量会变
- 解决幻读,要加范围锁或表锁
- 当前读:加记录锁,范围锁,不能插入(间隙锁),无幻读
9-3 串行(serializable)no,no,no
- 从mvcc 退化到基于锁的并发控制,全是当前读,读+读锁,写+写锁
10 mysql 优化
10-1 字段优化
1.更小的最好(相同类型)
- 选择一个你认为的默认不会超过的类型的下限
2.简单的越好(不同类型)
- int比string好
3.避免null
- null的索引以及比较更耗时
10-2 范式优化
- 采用范式与非范式的结合,如采用一些赘余避免不必要的查询,一个字段包含多个含义
- 1.所有字段值都是不可分解的原子值
- 2.数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关
- 3.每一列数据都和主键直接相关,而不能间接相关
10-3 缓存与汇总表
- 1.redis缓存
- 2.汇总访问次数,每次访问的时候都修改,如果汇总一个月的,数据量太大
- 计数器表
- 高并发时,计数器表100行,随机访问一行,减少冲突,提高并发处理能力
10-4 join优化
- 确保on和using字句有索引,考虑关联顺序
- 索引where和排序冲突的时候,改成关联表方式或者子查询
- mysql5.6之前的一般需要改子查询为关联表
- 走索引不一定都快,区分度不够的索引可用考虑用复合索引
- 多值 max,改为group后max
- join 时要考虑类型一致,char型要考虑字符集一致,时间类型没走索引,要注意隐世转换
- select in 会被改写为exists,造成查询缓慢,可改成关联join查询
- 驱动表的选择,认清关联字段索引,可用straight_join验证猜想
- query cache并不适合更新,插入,删除非常大的应用
- group by,order by 只涉及表中一个列
- order by +limit 将limit,order by内嵌,限制join数量和保证index排序
- join时 尽量将条件内嵌,先过滤后join。
- 知道仅有一条时,用limit 1 限制
- not in或not exits 可转换为外连接或等值连接
- 某些情况下 or 可用用union all 或union 代替
10-5 排序优化
- 单表查询,若file sort 则可以用强制使用索引排序,或者用自关联方式
- 需要where+sort 或者多个sort 做联合索引,并考虑加上排序
- sort buffer
- 与group by 连用时 用order by null 禁用排序
- 别名 若和字段重复也会影响排序优化
- 索引长度导致不能优化
相关参数
- a. sort_buffer_size 排序缓存。
- b. read_rnd_buffer_size 第二次排序缓存。
- c. max_length_for_sort_data 带普通列的最大排序约束。
file sort过程-普通&双路排序
- 取出字段a和每行的物理id(tid),然后在sort_buffer_size 中排序
- 根据排序好的tid从磁盘中拿到行数据,放到read_rnd_buffer_size中
file sort过程-冗余单路排序
- 区别是第一次是拿到所有相关数据而不只是tid,所以不用二次去磁盘取
file sort 优化
- 增加相关参数大小
- 增强tempdir 指向的文件系统io
10-6 limit优化
- 当与order by 字段联合使用时,若排序字段有重复,需要增加排序列,否则数据会错
10-7 索引问题
复合索引
- 最常用的放在最前面,无where顺序无关
10-8 常规优化
explain
select_type
- simple,没有union或子查询
- primary,嵌套中最外层
[ˈpraɪmeri] - union/union result/dependent union/dependent union
- derived 衍生,导出,子查询产生临时表
[di`raivd] - subquery 子查询第一个select语句
type
- const,最多一条,system 特例仅有一条
- eq_ref,一种索引访问,仅返回1条,且索引为unique,primary
- ref,返回多条,索引的是最左前缀,可以用于=<>
- ref_or_null, 针对null
index_merger,索引合并优化
- 5.1以后支持的,一个表可以使用多个索引
- index intersect
- unique_subquery,替换in子查询
- index_subquery 替换in子查询
- range,用index检索范围的行
- index,全扫描索引
- all
extra
- using filesort ,没有用索引次序读到排序
- using temporary
- not exists
- using index
- using index condition
- using where
possible_keys
- key 实际使用的索引
- key_len,key的长度
- ref 使用哪个列与key一起选择行
- profile
11 mysql锁机制
1-1 innodb
- 常用
行级锁
特征
- 锁定粒度小
- 锁获取释放复杂度高
- 并发度高
- 易出现死锁
分类
行级别
- 共享锁
- 排他锁
列级别
- 意向共享锁
- 意向排它锁
优化
- 查询尽量用索引,防止锁升级为表锁
- 控制事务大小(数据量操作)
- 减少锁定资源数量
- 减少锁定时间
减少死锁的发生
- 类似业务模块中,尽可能按照相同的访问顺序来访问
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率
1-2 myisam
- 不常用
表级锁
特征
- 锁定粒度大
- 锁获取释放简单
- 并发度低
- 不易出现死锁
读写互阻塞
- 读取表的时候,另一线程无法进行读或写
- 写入表的时候,另一线程无法进行读或写
分类
- 表级别的锁
优化
- 缩短锁定时间
- 分离并行的操作