MySQL浅谈

MySQL

TODO-LIST

  • lsm
  • 为什么b+ tree 就是比btree 快
    • 为什么b+ tree 能够减少io次数,明明要至少三次啊(看来我的为什么还是不够多)
      说明 —finish

Github上更新的较这里勤


索引

  • 什么是索引:索引是指数据库管理系统中的一个排序的数据结构,索引的类型有b tree,hash,lsm树
    • hash索引: 基于hash的特性,检索效率非常的高,一次定位,不需要像b tree一样多次io
      • 缺点:
        • 仅仅只能满足"=",“IN”,和"<=>"查询,不能使用范围查询,因为hash当数据变更之后并不能保证变更后的hash与变更前的一致
        • Hash索引无法用来排序,原因一样,hash一旦数据变更之后就不能保证前后一致
        • Hash索引不能利用部分查询,当通过组合索引的时候,hash索引会将索引和相加,而不是单独计算,组合索引也就无效了
        • 无法避免全局扫描,因为可能不同的值具有相同的hash索引
        • 大量hash碰撞后,并不能保证效率比b tree 高
    • b tree索引(是一种k,v结构的树): b tree 是一种多路平衡二叉树,k,v在同一个节点上
      • 具有如下特征:对于m阶的b tree
        • 根节点的孩子数目为[ceil(m/2),m],关键字的数目为[ceil(m/2)-1,m-1]
        • 非叶节点的根节点至少有2个孩子节点
        • 节点与数据相绑定
          btree中叶子节点和根节点以及分支节点都是同一种类型(class)
    • b+ tree索引(也是一种k,v结构的树): b+ tree 也是一种多路平衡二叉树,但是与b tree的不同点在于,非叶子节点不存放值v,只存放键k,值都在叶子节点上,且链表相连
      • 具有如下的特征:对于m阶的b+ tree
        • 根节点的孩子数目为:[ceil(m/2),m],关键字的数目为[ceil(m/2)-1,m]
        • 根节点和分支节点只用来保存关键字(索引),数据地址都存放在叶子节点上

          叶子节点中的5,8,9分别就是数据库中的记录,因此b+tree中叶子节点与根节点和分支节点不是同一种类型(class)
    • lsm(Log Structured Merge Tree)树索引:
      • 就是将对数据的修改尽量保存在内存中,当达到一定量的时候批量写入磁盘,读取的时候合并磁盘的和内存中的最近修改记录
    • 为什么b+tree 更适合作为索引:原因很简单,在内存方面b+tree 毫无优势,但是当涉及磁盘io的时候,b+tree的优势就体现了
      • 在mysql中每次读取数据都是以页为单位,而每页的数据由操作系统而定,4K(WIP这里需要我有空去官网上看文档补齐证据),但是磁盘io是昂贵的操作,因而操作系统会预读,既多读相连的几页数据
      • b+tree因为数据都在叶子节点,且叶子节点都是有序的,这也就导致了范围查询的时候b+tree比btree要快太多了,也因为如此使得数据页中的数据量尽可能的多,从而减少了io次数
    • b+tree 索引可以分为聚集索引和辅助索引,聚集索引的b+tree存放的是行记录数据,而辅助索引存放的是,当通过辅助索引的时候先通过辅助索引找到聚集索引键,然后聚集索引键在聚集索引中找到数据
    • 索引设置的疑问点:
      • 为什么索引要尽可能的小:因为io次数与b+tree的高度有关,原因在于每个磁盘页的大小是一定的,数据项占的空间越小->从而每页占据的数据量也就越多->树的高度也就会越低,这同时也是为什么将数据值都放在叶子节点的缘故
      • 索引的最左匹配原则: b+树的key项是复合索引结构,是按照从左到右建立搜索树的,如(a,b,c)索引,优先比较a,然后b,最后c
  • 索引的分类:一般来说除了聚集索引之外就是非聚集索引
    • 聚集索引:表中各行的物理顺序与物理磁盘键值的逻辑(索引)顺序一致,表中只能包含一个聚集索引,主键列默认为聚集索引
      • 如果主键被定义了: 则这个主键就是聚集索引
      • 若主键未定义,则这个表的唯一非空索引作为聚集索引
      • 若没有主键也没有唯一非空索引,则会自动生成一个隐藏的主键,会自动递增
    • 非聚集索引,也可以称为辅助索引:非聚集索引是指,叶子节点上存放的不是完整的记录,而是指向某个数据块的值,如有username作为非聚集索引,则叶子节点还指向了聚集索引中username为xxx的一个数据块,同时还包含了主键
      • 普通索引(name): 仅用户加速查找
      • 唯一索引:
        • 唯一索引: unique(uuid) 加速查找且约束:唯一
      • 联合索引(组合索引,覆盖索引):
        • 联合主键索引:(id,name)
        • 联合唯一索引:(uuid,name)
        • 联合普通索引:(name,asd)
      • 全文索引: 用于搜索一篇很长的文章的时候
    • 关于索引的注意点:
      • 二次查询问题:从非聚集索引定义可知,叶子节点有指向聚簇索引值的指针,因而如果查询列中还有其他的属性,则会发生二次查询(通过查询出来的主键再查询数据),也就是说会访问数据表
        • 如: select name,score from user where name="qwe"的时候,会发生二次查询,因为只有name是个索引
        • 避免的方法就是通过组合索引: 为score也创建索引,这样就是覆盖索引了,不会二次查询
    • 关于建立聚集索引还是非聚集索引的建议:
动作 是否推荐使用聚簇索引 是否推荐使用非聚簇索引
列经常被分组排序
返回某范围内的数据
一个或极少不同值
小数目的不同值
大数目的不同值
频繁更新的列
外键列
主键列
频繁修改索引列

判断是否要用聚集索引其实只需要判断这个属性列的值是否频繁改变,如果频繁改变不推荐,因为需要树中移动和呼唤

  • 关于索引的建议:
    • 重复值少的建立索引
    • Where字句条件频繁的建立索引
    • 在有原生函数:distint,min,max,order by,group by操作的列和join连接列建立索引
    • 不要用select *,或者尽量少用查询的列不仅仅包含索引:这样会导致二次查询,从而会访问数据表,因而有可能会造成全表扫描

SQL语句编写的建议

  • 不要使用 select *

    • 数据太多: 会返回所有的业务字段,通信时间会增长
    • 性能可能低下: 原因在于非聚簇索引上存放着的是指向这个索引的值和指向聚簇索引的指针,而一个表往往不会对所有的属性列都做索引,就会导致二次查询
    • 可能业务层会崩溃:select * 可能会获取到自己不需要的列,如果以后表结构修改了,同样也可能会对代码产生影响。比如表增加了一个字段,而我代码与其对接的对象属性里没有这个字段,select * 就会导致报错
  • 不要返回大批量的数据,让业务做筛选

  • 擅用,慎用索引

  • 复合索引的最左匹配原则

MySQL的存储引擎

  • InnoDB(默认引擎)

    • 支持事务
    • 最小锁为行级锁,支持外键
    • 聚集索引:叶子节点存放数据,非聚集索引:叶子节点不存放数据,存放指向数据的指针
      (图在上面)
  • MyISAM

    • 不支持事务,但是每次操作都是原子性的
    • 最小锁为表级锁
    • 每个表都会在磁盘中存储三个文件:.frm:存储表的定义,.MYD:存储数据,.MYI:存储索引;既myisam数据文件,索引文件,表文件分开存储
    • 聚集索引:类似于InnoDB的非聚集索引,叶子节点存放指向数据的指针,但是这个数据是全部数据,非聚集索引与聚集索引一样,但是非聚集索引不用保证一致性
    • 聚集索引结构如下:
    • 非聚集索引结构:
  • MyISAM和InnoDB,左侧为innodb的索引查找过程:
  • Memory

  • 存储引擎总结:

MySQL的事务:

  • MySQL中的锁可以分为两类:
    • 共享锁: 既可以认为是读锁,读锁共享
      • select name from user lock in share mode
    • 排它锁: 既可以认为是写锁
      • 行锁: recordLock 锁定之后就不能再操作了
      • 表锁: tableLock 对整个表锁定
      • 间隙锁: gapLock 既对复合记录的所有行数加锁,这种锁也就彻底避免了幻读

事务之前需要了解2pl(Two-Phase Locking)

  • 2pl: Two-Phase Locking ;既2阶段加锁,与Java类似,对于lock需要加锁和解锁,具体如下图所示:

    2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。

事务之前需要先了解:MVCC

  • 什么是MVCC: MVCC既多版本控制,读不加锁,与MVCC相对的就是基于锁的并发控制
    • 在InnoDB中,MVCC的实现是通过,在每一行中添加2个属性,一个是行创建的系统版本号,另外一个则是删除/修改的系统版本号,开启事务的时候,系统版本号会增加,并且会使用当前系统版本号作为此次的事务号,当InnoDB读取数据的时候,
    • 行的系统版本号<=当前事务号
      • 这样可以确保读取到的数据是事务开始之前就已经是最新的
    • 删除版本号要么未定义,要么读取的就是>当前事务版本号
      • 这样可以确保数据在开始之前未被删除
    • 在MVCC中,读可以分为两类:快照读(snapshot read)当前读(current read)
      • 快照读(一致性读): 只会查询当前版本号<=当前版本号的数据行(既读取到的数据要么已经commit了的,要么就是当前事务的数据)
        • 定义: 指的是读取到的是可见版本,可能是历史版本
        • 默认的select 操作是快照读,不加锁
          • 当insert的时候: 保存当前事务版本号的行作为行的行创建版本号:既当前事务的版本号作为这条记录的版本号
          • update: 会产生两条数据;然后会执行2个操作: 将当前事务的版本号作为新的记录的版本号,将当前版本号作为老数据的要更新的版本号
          • delete: 同理也是用当前版本号的行作为删除记录
      • 当前读:
        • 定义: 读取到的是最新的记录,最新的,最新的并返回,会加上锁
        • select * from table where ? lock in share mode;
        • select * from table where ? for update;
        • insert into table values (…);
        • update table set ? where ?;
        • delete from table where ?;
        • 注意这些使用到的都是当前读,这里可能会有疑惑,明明还有写操作,这里的读是指定位这条数据记录行的时候是当前读,除了in share mode使用的是共享锁,其他的都是默认加的排它锁,流程大致如下图所示:
        • 可以发现是先where定位返回然后锁住的,
          并且是一条一条交互的
      • 总结: 注意,这里的读不是仅仅的指select,这里的读是每个操作都要用到的,无论是select,update,delete,insert,都要用到的,这里的读特指定位到数据的之后的操作
  • 什么是事务:事务是指一段连续的操作,要么全部成功,要么全部失败
  • 事务的特性:ACID
    • A: Atomic: 原子性,要么成功,要么失败
    • C: Cosistent: 一致性,既事务开始前和结束后数据只有预期的变化
    • I: Isolation: 隔离性,指的不是不同的事务不会产生影响
    • Durable: 持久化: 既持久化到数据库中
  • 事务的并发:
    • 脏读: 指的是读取到其他事务失败前的数据,注意其他事务回滚导致读取到的数据其实是失败的数据

    • 不可重复读: 读取到其他事务提交修改之后的数据,注意是修改,与原来读的数据不一致,再注意一点:是同一事务第二次读取,第二次,第二次例如:事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。

    • 幻读: 指的是读取到其他事务提交的新增的数据,注意是添加,添加,与原来读取到的数目不一致

  • 事务的隔离级别:
    • 未提交读: 事务中的修改,即使没有提交,对其他会话也是可见的,啥并发问题都不能解决,只能解决更新丢失

    • 提交读: 保证了一个事务如果没有完全成功(未执行commit),事务中的操作对其他会话是不可见的 。,因而能够解决不可重复读(因为不可重复读是这条记录的数据改变,而刚还哦RC是加行锁)

      • 原理: 快照度不考虑(因为没必要,不会加锁),当使用当前读的时候,会对读到的记录加锁(行锁),如下面的那幅图,但是第二次读的时候,第一次和第二次这个间隔不能保证
        其实就想volatile 的int变量一样,+1是原子性的,但是++不是原子性的一样,因而还是会存在幻读的情况
    • 重复读: 一个事务中多次执行统一读SQL,返回结果一样

      • 原理: 快照读同样忽略,当前读的时候,读取到的记录加锁(行锁),以及当where条件是一个范围的时候,会加间隙锁,如:delete from t1 where id = 10 就会在符合记录之间加锁,如果是范围的话(where id >10 and id<20 )也一样会加间隙锁(10与20之间),如下图所示:
        既通过间隙锁+写锁的形式消除幻读
    • 串行读: 隔离级别最高的,在RC,RR隔离级别中,select默认都是快照读,而在这个隔离级别,默认是当前读

    • 总结: 在MySQL/InnoDB中所谓的读不加锁,其实是针对隔离级别而言的,Serialize隔离级别所有的读操作都是需要加锁的

实例分析:

表:有属性列: id,name

sql语句为select * from t1 where id = 10;

RC级别

  1. id为主键,且rc隔离级别: 走聚簇索引定位之后之后加行锁
  2. id为unique,name为主键,rc级别:先走非聚簇索引id=10之后,得到name主键,然后走聚簇索引定位到name匹配的记录,所以会加2个锁,一个是非聚簇索引上的锁,一个则是聚簇索引上的记录
  3. id非唯一索引,name为主键,rc级别: 与上述类似,但是存在多条记录的可能,因此会对所有服务的加行锁(注意是行锁)
  4. id无索引,级别为rc:会上表锁,但是会有优化,既mysql会对不匹配的记录解锁(违背了2pl)

    RR级别
  5. id为主键: 与1一样,聚簇中加行锁
  6. id唯一索引:与2一样,对满足的记录加行锁
  7. id非唯一索引,name为注解,rr级别: 与3类似,但是加的锁不同,加的是间隙锁,对范围加锁
  8. id非索引: 与4类似,加表锁,同时会加间隙锁,效率非常低下,但是myslq也会优化,对于不满足记录的条件会自动解锁(同样违背2pl)

疑问点:

  • 为什么要最左匹配原则: 假设有表复合索引(name,cid)

    • 首先:什么是最左匹配原则,最左匹配原则是指where条件中,必须存在复合索引中的第一个索引,至于是不是第一个条件,其实不是必然,但是最好还是放在第一个,因为有的sql查询器可能并不会优化(使之成为第一个条件)
    • 提出这个疑问之前我们需要明白:通过索引查询之所以很快,是因为会通过搜索树维护索引,使得索引是有序的,是有序的,是有序的
    • 对于索引而言,单个索引结构上都是有序的,因而能快速的查找到
    • 对于复合索引而言,则在内存中索引的结构会如下:(大概通过如此建立)
    • ,从左到右建立维护索引树,既先通过name排序,name:abcdef排好序之后,再对cid进行排序,也就是说右边的总是基于左边的进行排序,只有最左边的完全的肯定有序的,这里就可以回答为什么要最左匹配了,我们从图中可以发现,只有当左侧等值©的时候,右侧才是有序的,所以这就是最左匹配的缘故
      • 因而在这种复合表索引的情况下,有如下的sql写法
      • where顺序是name,cid :先匹配name,再匹配cid,这时候是完美走索引的,name是有序的,而cid在name的基础下也是有序的,
      • where只有cid: 在这种情况下不会走索引`
      • 当有索引A,B,C的时候,会默认创建如下索引: (A),(A,B),(A,B,C)索引,所以如果想走索引,条件中必须要有A
  • 何时会使用全表扫描:

    • 首先: 什么是全表扫描:既对数据库中所有的记录一条一条匹配
    • sql编写不规范:
      • 无索引的查询

      • 使用null作为判断条件 select name from user where age=null

      • 左模糊查询 select age from user where name like %qwe% 或者是like %qwe

        • 建议使用 like qwe% 右模糊查询
      • 使用or作为条件:select name from user where age=1 or age =2

        • 建议使用 union
      • 使用in时 select name from user where age in (1,2,3)

        • 如果范围是连续的话,使用between
        • 如果是嵌套查询的话,可以使用 exist代替:但是适用于条件少的情况: select name from user where id in (select id from user where age =3)------>select name from user where id exist(select id from user where name=3)
        • 同理not in时使用not exist即可
      • 使用!=或者<>时

        • 推荐使用<,<=,>,>=,between等
      • 当=号左边有计算的时候:select name from user where age+1>3

      • 当使用参数作为查询条件的时候:select name from user where age=@age

        • 原因: 不确定性,
    • 数据量太少的时候,sql查询器认为还不如全表查询块
  • 如何避免幻读,为什么隔离级别为RC(提交读)无法避免幻读:

    • 首先: 幻读是指同一个事务中读取到的数据量不一致,既可以认为是范围中插入了数据
    • 如何避免呢,其实在mysql中幻读已经永远不会发生了,
      • MVCC机制:默认的select会使用快照读(既一致读),只能读取到事务版本号<=当前事务版本号的记录
      • 而在当前读中(insert,delete,update)则是通过间隙锁来实现的(既对这个范围加锁)
      • 还有一个很关键的因素在于:2pl,既两阶段加锁,加锁和解锁互相隔离,这也就使得间隙锁是事务结束止之后再解锁
    • RC为什么无法避免呢,究其原因在于:加的锁的姿势不对,应该加的是间隙锁而不是行锁
  • 为什么使用b+tree而不是使用btree

    • 回答这个问题首先需要先了解这些:
      • 磁盘预读+局部性原理: 磁盘预读是基于局部性原理而定的,局部性原理表示:当有部分数据被使用时,相连的数据也很可能会被使用; 系统读取数据非按需读取,就算读取1个字节,也会从当前位置多读一部分数据写入到内存中,这就是磁盘预读
    • 然后从以下几方面回答:
      • 适用方面:
        • b tree只是解决了io性能方面,但是并没有解决元素遍历的效率低下问题,需要中序遍历,范围查询性能差,b+ tree 完美解决这个问题,因为底层叶子链表存储了所有节点且有序
      • 环境方面:
        • b+ tree 在内存中相比btree 是一点优势都没有的,但是为什么要使用b+tree 呢,因为索引是很多的,数据是很大的,所有的数据都放在内存中会oom,因而部分数据会放在磁盘中,此时b+tree的优势就体现了:
          • 磁盘预读: 从磁盘中包括寻道(本人对这块不是很了解,可以理解为balabalabala),而这个时间相比内存是贼缓慢的,因而有了磁盘预读,磁盘预读是指系统不是按需读取的,会预读,既就算只读取1个字节,磁盘也会从此往后多读取一定数据的字节,并且,操作系统是根据页面置换来访问数据的,每页为4k,而b+tree设计的非常巧妙,每个节点刚好为4k(WIP待认证),也就是说不会提高缓存丢失率,从而也就不会再去读取node(io+1);
          • 通俗点说我们假设:底层数据结构为btree; btree 因为每个节点都存在了data指针,也就导致了当我们读取4k作为一页的时候,节点的数量远远比b+tree要少,也就会潜在的降低缓存命中率,当降低了缓存命中率意味着我们需要再次从硬盘读取(io)
      • 衍生的疑惑点: 这里可能会问,btree 因为数据在节点上,不是查询会更快吗
        • 答:是的,这便是btree的优点,但是这个优点要巧妙,巧妙到刚好预读的时候这个key值在内存中,如果刚好不在内存中,btree的io读取次数是很可能大于b+tree的
      • 总结: b+tree 相比btree的优点:
        • 数据结构特点使得,b+tree每个节点有更多的缓存key,也就变相的减少了io次数(原因在于:操作系统的读取数据采用页面置换,每页的大小是固定的为4k,b+tree的宗旨是在有限的空间下存储更多的数据来提高缓存命中率,避免io再读取)
        • 范围查询性能更高\

遇到的问题

  • windows环境下忘记mysql密码:

    • 解决方法: 推荐,若提示password字段不存在,则参考
      • 在这期间提示password colum不存在,则 推荐
      • 提示密码格式过于简单
  • windows下安装mysql

    • 第一次使用的时候需要先注册为服务:
      • 以管理员的模式
        • 进入mysql 的bin 目录
        • mysqld.exe --install Mysql --default-file="D:\mysql\bin\my.ini" 这里的default-file 指向的是ini文件,实际而定
    • 遇到的问题:
      • 如何卸载: SC delete Mysql
  • MySQL允许远程连接

    • mysql -u root -p 登录
    • GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;
      • 若遇到提示密码不符合要求:
        • https://blog.csdn.net/maxsky/article/details/51171474
        • 登录之后:
          • set global validate_password_policy=0;
          • set global validate_password_length=4;
    • flush privileges;
      • 明明已经授权了允许外界连接,但是外界连接还是提示connection refused:
        • 原因在于: 绑定了本地
        • 解决方法: https://blog.csdn.net/adsadadaddadasda/article/details/78829336 修改配置文件,vim /etc/mysql/mysql.conf.d/mysqld.cnf 将bind-address注释即可
  • **批量插入提示: BY clause is not in GROUP BY clause and contains nonaggregated column ‘information_schema.PROFILING.SEQ’ **

    • 原因在于full group:
    • 解决方法: 修改mysqld.conf
      • centos 在 /etc/my.cnf 下 ,添加或者修改为: 即可
      	sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
      
    • 参考
  • **提示:Incorrect datetime value: ‘0000-00-00’ for column ‘PAY_DATE’ at row 1: **

    • 原因在于: 5.7对于时间更加严格
    • 解决方法: 修改sql_mode
      • 在ubuntu下: 是/etc/mysql/my.conf 添加
      sql-mode=STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
      
  • NaviCat 提示 MySQL server has gone away

    • 解决方法
    • mac 下的mysql的my.ini 在/etc下

你可能感兴趣的:(数据库)