数据库基础知识
本文整理自 统计学面试经典问题
为什么要使用数据库
- 数据保存在内存中
- 优点: 存取速度快
- 缺点: 数据不能永久保存
- 数据保存在文件中
- 优点: 可以永久保存
- 缺点: 1. 存取速度慢, IO操作频繁; 2. 查询数据不方便
- 数据保存在数据库中
- 数据永久保存
- 使用SQL语句, 查询效率高
- 管理数据方便
什么是SQL
结构化查询语言(Stuctured Query Language), 简称SQL, 是一种数据库查询语言
作用: 用于存取数据、查询、更新和管理数据库系统
什么是MySQL
MySQL是一个关系型数据库管理系统, 开源免费, 方便扩展
数据库三大范式
第一范式(1NF):数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。
第二范式(2NF):数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选关键字。
第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如果存在”A → B → C”的决定关系,则C传递函数依赖于A。因此,满足第三范式的数据库表应该不存在如下依赖关系: 关键字段 → 非关键字段x → 非关键字段y
满足着第三范式,那么就一定满足第二范式、满足着第二范式就一定满足第一范式
第一范式:字段是最小的的单元不可再分
学生信息组成学生信息表,有年龄、性别、学号等信息组成。这些字段都不可再分,所以它是满足第一范式的
第二范式:满足第一范式,表中的字段必须完全依赖于全部主键而非部分主键。
其他字段组成的这行记录和主键表示的是同一个东西,而主键是唯一的,它们只需要依赖于主键,也就成了唯一的学号为1024的同学,姓名为Java3y,年龄是22岁。姓名和年龄字段都依赖着学号主键。
第三范式:满足第二范式,非主键外的所有字段必须互不依赖
就是数据只在一个地方存储,不重复出现在多张表中,可以认为就是消除传递依赖
比如,我们大学分了很多系(中文系、英语系、计算机系……),这个系别管理表信息有以下字段组成:系编号,系主任,系简介,系架构。那我们能不能在学生信息表添加系编号,系主任,系简介,系架构字段呢?不行的,因为这样就冗余了,非主键外的字段形成了依赖关系(依赖到学生信息表了)!正确的做法是:学生表就只能增加一个系编号字段。
MySQL有关权限的表都有哪几个
MySQL服务器通过权限表来控制用户对数据库的访问, 权限表存放在mysql数据库里, 由mysql_install_db脚本初始化, 分别是 user, db, table_priv, columns_priv, host
- user权限表: 记录允许连接到服务器的用户账号信息, 里面的权限是全局的
- db权限表: 记录各个账号在各个数据库上的权限操作
- table_priv权限表: 记录数据表级的操作权限
- columns_priv权限表: 记录数据列级的操作权限
- host权限表: 配合db权限表对给定主机上数据库级操作权限做更细致的控制, 不受GRANT和REVOKE语句的影响
MySQL的binlog有几种录入形式, 分别有什么区别
Mysql binlog是二进制日志文件,用于记录mysql的数据更新或者潜在更新
有三种形式: statement, row, mixed
- statement 模式: 每一条会修改数据的sql都会记录在binlog中. 不需要记录每一行的变化, 减少了binlog日志量, 节约了IO, 提高了性能. 由于sql的执行是有上下文的, 因此在保存的时候需要保存相关信息, 同时还有一些使用了函数之类的语句无法被记录复制, 容易出现主从复制不一致
- row模式: 不记录sql语句上下文相关信息, 仅保存哪条记录被修改. 记录单元为每一行的改动, 基本是可以全部记下来, 但是由于很多操作, 会导致大量行的改动(比如alter table), 因此这种模式的文件保存的信息太多, 日志量太大
- mixed 模式: 一种折中的方案, 普通操作使用statement记录, 当无法使用statement时使用row
新版的MySQL对row级别做了一些优化, 当表结构发生变化时, 会记录语句而不是逐行记录
数据类型
分类 | 类型名称 | 说明 |
---|---|---|
整数类型 | tinyint | 很小的整数(8位二进制) |
smallint | 小的整数(16位二进制) | |
mediumint | 中等大小的数(24位二进制) | |
int(integer) | 普通大小的整数(32位二进制) | |
小数类型 | float | 单精度浮点数 |
double | 双精度浮点数 | |
decimal(m,d) | 压缩严格的定点数 | |
日期类型 | year | YYYY 1901~2155 |
time | HH:MM:SS -838:59:59~838:59:59 | |
date | YYYY-MM-DD 1000-01-01~9999-12-3 | |
datetime | YYYY-MM-DD HH:MM:SS | |
timestamp | YYYY-MM-DD HH:MM:SS | |
文本、二进制类型 | CHAR(M) | M为0~255的整数 |
VARCHAR(M) | M为0~65535的整数 | |
TINYBLOB | 允许长度0~255字节 | |
BLOB | 允许长度0~65535字节 | |
MEDIUMBLOB | 0~167772150字节 | |
LONGBLOB | 0~4294967295字节 | |
TINYTEXT | 同BLOB | |
TEXT | ||
MEDIUMTEXT | ||
LONGTEXT | ||
VARBINARY(M) | ||
BINARY(M) |
-
整数类型
- 包括tinyint(1字节), smallint(2字节), mediumint(3字节), int(4字节), bigint(8字节)整数, 加上
UNSIGNED
, 表示无符号的, 即非负整数 - 整数类型可以被指定长度, 即
INT(5)
, 表示长度为5的int型, 如果配合UNSIGNED ZEROFILL
, 如果用户插入数据12, 那么数据库实际存储数据为00012
- 包括tinyint(1字节), smallint(2字节), mediumint(3字节), int(4字节), bigint(8字节)整数, 加上
-
实数类型
- 包括 float, double, decimal
- decimal 可以用于存储比bigint还大的整型, 也能存储精确的小数
- float和double有取值范围, 支持使用标准的浮点进行近似预算, 并且效率比decimal更高一些, decimal相当于用字符串进行处理
-
字符串类型
- 包括varchar, char, text, blob
- varchar用于存储可变长字符串
- 比定长类型更节省空间, 使用额外1或2个字节存储字符串长度.
- 列长度小于255字节时, 使用1字节表示, 否则2字节
- 当存储内容超出设置长度时, 内容会被截断
- char是定长的
- 根据定义的字符串长度分配足够的空间.
- 会根据需要使用空格进行填充方便比较.
- 适合存储很短的字符串, 或者所有值都接近同一个长度
- 存储内容超出长度设置, 同样会被截断
使用策略
- 对于经常变更的数据来说, CHAR比VARCHAR好, 因为不容易产生碎片
- 对于非常短的列, CHAR比VARCHAR在存储空间上更有效率
- 使用时要注意只分配需要的空间, 更长的列排序时会消耗更多内存
- 尽量避免使用TEXT/BLOB类型, 查询时会使用临时表, 导致严重的性能开销
- 枚举类型(ENUM)
- 把不重复的数据存储为一个预定义的集合
- 有时可以使用ENUM代替常用的字符串类型
- ENUM存储非常紧凑, 会把列表值压缩到一个或两个字节
- ENUM在内部存储时, 其实存的是整数
- 尽量避免使用数字作为ENUM枚举的常量, 容易混乱
- 排序是按照内部存储的整数
- 日期和时间类型
- timestamp的空间效率高于datatime
- 用整数保存时间戳通常不方便处理
- 需要存储微秒时, 用bigint存储
varchar 和 char
char的特点
- char表示定长字符串, 长度固定
- 如果插入数据的长度小于char的固定长度时, 用空格填充
- 因为长度固定, 所以 存取速度 比varchar快很多, 但是会占据更多的空间, 是 空间换时间 的做法
- 最多存放的字符数是255, 和编码无关
varchar的特点
- varchar表示可变长字符串, 长度是可变的
- 插入数据是多长就按照多长来存储
- varchar在存取方面与char相反, 因其长度不固定, 所以存取速度慢, 但是不占用多余空间, 属于 时间换空间 的做法
- 最多存放字符个数为65532
总之,结合性能角度(char更快)和节省磁盘空间角度(varchar更小),具体情况还需具体来设计数据库才是妥当的做法。
mysql中int(10)和char(10)以及varchar(10)的区别
-
int(10)的10表示显示的数据的长度,不是存储数据的大小;chart(10)和varchar(10)的10表示存储数据的大小,即表示存储多少个字符。
-
int(10) 10位的数据长度 9999999999,占32个字节,int型4位
-
char(10) 10位固定字符串,不足补空格, 最多10个字符, 表示存储定长的10个字符,不足10个就用空格补齐,占用更多的存储空间
-
varchar(10) 10位可变字符串,不足补空格 , 最多10个字符, 表示存储10个变长的字符,存储多少个就是多少个,空格也按一个字符存储,这一点是和char(10)的空格不同的,char(10)的空格表示占位不算一个字符
FLOAT和DOUBLE的区别
- FLOAT类型可以存储至多8为十进制数, 并在内存中占4字节
- DOUBLE类型数据可以存储至多18位十进制数, 并在内存中占8字节
引擎
MySQL存储引擎MyISAM与InnoDB区别
存储引擎Storage engine: MySQL中的数据、索引以及其他对象是如何存储的, 是一套文件系统的实现
- Innodb引擎: Innodb引擎提供了对数据库ACID事务的支持. 并且提供了行级锁和外键的约束. 其设计的目标就是处理大数据容量的数据库系统
- MyISAM引擎(原本MySQL的默认引擎): 不提供事务的支持, 也不支持行级锁和外锁
- MEMORY引擎: 所有数据都在内存中, 数据处理速度快, 但是安全性低
MyISAM索引和Innodb索引的区别
- Innodb索引是聚簇索引, MyISAM是非聚簇索引
- InnoDB的主键索引的叶子结点存储着行数据, 因此主键索引非常高效
- MyISAM索引的叶子节点存储的是行数据地址, 需要再寻址一次才能得到数据
- Innodb非主键索引的叶子节点存储的是主键和其他带索引的列数据, 因此查询时做到覆盖索引会非常高效
InnoDB引擎的4大特性
- 插入缓冲
- 二次写
- 自适应哈希索引
- 预读
存储引擎选择
如无特殊需求, 使用 Innodb
即可
- MyISAM: 以读写插入为主的应用程序(博客系统, 新闻门户网站)
- Innodb: 更新(删除)操作频率高, 或者要保证数据的完整性; 并发量高, 支持事务和外键(OA自动化办公系统)
索引
什么是索引?
索引是一种特殊的文件(Innodb数据表上的索引是表空间的一个组成部分), 它们包含着对数据表里所有记录的引用指针, 是要占据物理空间的
索引是一种数据结构. 数据库索引, 是数据库管理系统中一个排序的数据结构, 以协助快速查询、更新数据库表中数据. 索引的实现通常使用B树及其变种B+树
索引就相当于目录, 为了方便查找书中的内容, 通过内容建立索引形成目录
索引的优缺点
索引的优点
- 加快数据的检索速度
- 使用索引, 可以在查询的过程中, 使用优化隐藏器, 提高系统的性能
索引的缺点
- 时间方面: 创建索引和维护索引要耗费时间. 当对表中的数据进行增删改时, 索引也要动态维护, 会降低增删改的执行效率
- 空间方面: 索引要占物理空间
索引使用场景
where
给定一个table, 当我们想找出符合条件的记录时, 使用语句
select * from table1 where id < 20
可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(alter table 表名 add index(字段名)
),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。
现在对 sex
列做索引, 并找到 sex=‘男’
的数据
alter table table1 add sex char(1);
select * from table1 where sex='男'
order by
当使用 order by
将查询结果按照某个字段排序时, 如果该字段没有建立索引, 那么会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用馁不排序, 最后合并排序结果), 该方法效率低下, 因为需要将查询涉及到的所有数据从磁盘中读到内存
但是如果对该字段建立索引 alter table 表名 add index(字段名)
, 那么由于索引本身有序, 因此直接按照索引的顺序和映射关系逐条取出数据即可. 对待分页数据, 只用取出索引表某个范围内的索引对应的数据, 而不用再取出所有数据进行排序在返回某个范围内的数据
join
对 join
语句匹配关系(on
)设计的字段建立索引能够提高效率
索引覆盖
如果要查询的字段都建立过索引, 那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描), 这叫做索引覆盖
不要想着为每个字段建立索引, 优先使用索引的优势就在于其体积小
索引的类型
主键索引: 数据列不允许重复, 不允许为NULL, 一个表只能有一个主键
唯一索引: 数据列不允许重复, 允许为NULL, 一个表允许多个列创建唯一索引
- 可以通过
alter table table_name add unique (column)
创建唯一索引 - 可以通过
alter table table_name add unique(column1, column2)
创建唯一组合索引
普通索引: 基本的索引类型, 没有唯一性的限制, 允许为NULL
- 可以通过
alter table table_name add index index_name(column)
创建普通索引 - 可以通过
alter table table_name add index index_name(column1, column2, column3)
创建普通组合索引
全文索引: 是目前搜索引擎使用的一种关键技术
- 可以通过
alter table table_name add fulltext (column)
创建全文索引
索引的基本原理
索引用来快速的寻找那些具有特定值的记录, 如果没有索引, 一般来说执行查询时遍历整张表
索引的原理就是把无序的数据变成有序的查询:
- 把创建了索引的列的内容进行排序
- 对排序结果生成倒排表
- 在倒排表内容上品赏数据地址链
- 在查询时, 先拿到倒排表内容, 再取出数据地址链, 从而拿到具体数据
索引算法有哪些
B树算法
不仅可以用在 对等比较, 例如: =,>,>=,<,<=和between, 还可以用于 like
操作符, 只要其查询条件是一个不以通配符开头的常量
select * from user where name like 'jack'
Hash算法
只能用于 对等比较, 例如=,<=>(相当于=), 由于是一次定位数据, 不像B树索引需要从根结点到也节点这样多次IO访问, 所以检索效率更高
索引设计的原则
- 适合索引的列是出现在where子句中的列, 或者连接子句中指定的列
- 基数较小的类, 索引效果很差, 没必要在此列建立索引
- 使用短索引. 如果对长字符串列进行索引, 应该指定一个前缀长度, 从而节省大量索引空间
- 不要过度索引. 索引需要额外的磁盘空间, 并降低写操作的性能. 在修改表内容的时候, 索引会进行更新甚至重构, 索引列越多, 用时越久, 所以只保持需要的索引有利于查询即可
创建索引的原则
- 最左匹配原则. mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
- 频繁查询的字段才去创建索引
- 频繁更新的不适合
- 不能有效区分数据的列不适合
- 尽量扩展索引, 不要新建索引. 如已经有a的索引, 要加(a,b)的索引, 只需要修改原来的索引即可
- 定义有外键的数据列一定要建立索引
- 查询中很少涉及的列、重复值多的列不要建立索引
- 定义为text、image和bit的数据类型的列不要建立索引
最左前缀原则
- 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
- 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
- =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
索引的创建和删除
创建索引
-
在执行
create table
时创建索引CREATE TABLE user_index2 ( id INT auto_increment PRIMARY KEY, first_name VARCHAR (16), last_name VARCHAR (16), id_card VARCHAR (18), information text, KEY name (first_name, last_name), FULLTEXT KEY (information), UNIQUE KEY (id_card) );
-
使用
alter table
命令去增加索引alter table table_name ADD INDEX index_name
-
使用
create index
命令创建索引create index index_name on table_name
删除索引
根据索引名删除普通索引、唯一索引、全文索引:alter table 表名 drop KEY 索引名
alter table user_index drop KEY name;
alter table user_index drop KEY id_card;
alter table user_index drop KEY information;
删除主键索引:alter table 表名 drop primary key
(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引)
创建索引时需要注意什么
- 非空字段: 应该指定列为NOT NULL, 可以用0或别的特殊值替代空值
- 取值离散大的字段(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
- 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。
使用索引查询一定能提高查询的性能吗?
通常, 索引查询快过全表扫描, 但是也引入了新的代价
索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。
使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
- 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
- 基于非唯一性索引的检索
前缀索引
语法:index(field(10))
,使用字段值的前10个字符建立索引,默认是使用字段的全部内容建立索引。
前提:前缀的标识度高。比如密码就适合建立前缀索引,因为密码几乎各不相同。
实操的难度:在于前缀截取的长度。
我们可以利用 select count(*)/count(distinct left(password,prefixLen))
;,通过从调整prefixLen
的值(从1自增)查看不同前缀长度的一个平均匹配度,接近1时就可以了(表示一个密码的前prefixLen
个字符几乎能确定唯一一条记录)
索引的数据结构(b树, hash)
MySQL中常用 hash索引, B+树索引, Innodb常用 B+树索引
对于hash索引, 底层的数据结构就是哈希表, 因此在绝大多数需求为单条记录查询的时候可以选择哈希索引, 查找性能最快, 其余大部分场景建议用B树索引
平衡二叉树
平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;平衡二叉树的数据结构组装过程有以下规则:
- 非叶子节点只能允许最多两个子节点存在。
- 每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值(这里值是基于自己的算法规则而定的,比如hash值);
平衡树的层级结构:因为平衡二叉树查询性能和树的层级(h高度)成反比,h值越小查询越快、为了保证树的结构左右两端数据大致平衡降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如Treap、红黑树,使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1.,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找;
总结平衡二叉树特点:
- 非叶子节点最多拥有两个子节点;
- 非叶子节值大于左边子节点、小于右边子节点;
- 树的左右两边的层级数相差不会大于1;
- 没有值相等重复的节点;
B树
B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个)
规则:
- 排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则
- 子节点数:非叶节点的子节点数>1,且<=M ,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉);
- 关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2, 即上取整);
- 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;
B树查询流程
如上图我要从上图中找到E字母,查找流程如下
- 获取根节点的关键字进行比较,当前根节点关键字为M,E<M(26个字母顺序),所以往找到指向左边的子节点(二分法规则,左小右大,左边放小于当前节点值的子节点、右边放大于当前节点值的子节点);
- 拿到关键字D和G,D<E<G 所以直接找到D和G中间的节点;
- 拿到E和F,因为E=E 所以直接返回关键字和指针信息(如果树结构里面没有包含所要查找的节点则返回null);
B树插入流程
遵循规则:
(1)节点拆分规则:当前是要组成一个5路查找树,那么此时m=5,关键字数必须<=5-1(这里关键字数>4就要进行节点拆分);
(2)排序规则:满足节点本身比左边节点大,比右边节点小的排序规则;
判断当前结点key的个数是否小于等于m-1,如果满足,直接插入即可,如果不满足,将节点的中间的key将这个节点分为左右两部分,中间的节点放到父节点中即可
- 插入18,70,50,40
- 插入22
插入22时,发现这个节点的关键字已经大于4了,所以需要进行分裂,分裂的规则在上面已经讲了,分裂之后,如下。
- 接着插入23,25,39
分裂,得到下面的。
B树删除流程
规则:
-
节点合并规则:当前是要组成一个5路查找树,那么此时m=5,关键字数必须大于等于ceil(5/2)(这里关键字数<2就要进行节点合并);
-
满足节点本身比左边节点大,比右边节点小的排序规则;
-
关键字数小于二时先从子节点取,子节点没有符合条件时就向向父节点取,取中间值往父节点放;
现在有一个初始状态是下面这样的B树,然后进行删除操作。
- 删除15,这种情况是删除叶子节点的元素,如果删除之后,节点数还是大于
m/2
,这种情况只要直接删除即可。
- 接着,我们把22删除,这种情况的规则:22是非叶子节点,对于非叶子节点的删除,我们需要用后继key(元素)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。对于删除22,需要将后继元素24移到被删除的22所在的节点。
此时发现26所在的节点只有一个元素,小于2个(m/2),这个节点不符合要求,这时候的规则(向兄弟节点借元素):如果删除叶子节点,如果删除元素后元素个数少于(m/2),并且它的兄弟节点的元素大于(m/2),也就是说兄弟节点的元素比最少值m/2还多,将先将父节点的元素移到该节点,然后将兄弟节点的元素再移动到父节点。这样就满足要求了。
我们看看操作过程就更加明白了。
- 接着删除28,删除叶子节点,删除后不满足要求,所以,我们需要考虑向兄弟节点借元素,但是,兄弟节点也没有多的节点(2个),借不了,怎么办呢?如果遇到这种情况,首先,还是将先将父节点的元素移到该节点,然后,将当前节点及它的兄弟节点中的key合并,形成一个新的节点。
移动之后,跟兄弟节点合并。
总结:
- 删除叶子节点时, 如果删除后节点元素大于
m/2
, 则可以直接删除 - 删除非叶子节点时, 如果删除后节点元素小于
m/2
, 则需要将删除元素的后继节点中的元素(比如比22大的最小的元素是24)拿到删除元素的位置上 - 删除叶子节点时, 节点元素数小于
m/2
, 如果兄弟节点的元素大于m/2
, 则将父节点中的元素(最大的)挪到删除元素的叶子节点中, 再将兄弟节点中的最大元素放到父节点中 - 删除叶子节点时, 节点元素数小于
m/2
且兄弟节点的元素等于m/2
, 即兄弟节点无法借出节点时, 可以将父节点的值放到删除的位置上, 同时合并该节点及兄弟节点形成新节点
B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制充分使用在磁盘块大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;
B+树
B+树其实和B树是非常相似的,我们首先看看相同点。
- 根节点至少一个元素
- 非根节点元素范围:m/2 <= k <= m-1
不同点。
- B+树有两种类型的节点:内部结点(也称索引结点)和叶子结点。内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存储在叶子节点。这样使得B+树每个非叶子节点所能保存的关键字大大增加
- 内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
- B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
- 每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。左边结尾数据都会保存右边节点开始数据的指针。
- 父节点存有右孩子的第一个元素的索引。
特点
- B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
- B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
- B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
- B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
- B+ 树中,数据对象的插入和删除仅在叶节点上进行。
- B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点
B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
B+树插入流程
当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的。(同B树)
B+树删除流程
对于删除操作是比B树简单一些的,因为叶子节点有指针的存在,向兄弟节点借元素时,不需要通过父节点了,而是可以直接通过兄弟节移动即可(前提是兄弟节点的元素大于m/2),然后更新父节点的索引;如果兄弟节点的元素不大于m/2(兄弟节点也没有多余的元素),则将当前节点和兄弟节点合并,并且删除父节点中的key
- 初始状态
- 删除10,删除后,不满足要求,发现左边兄弟节点有多余的元素,所以去借元素,最后,修改父节点索引
- 删除元素5,发现不满足要求,并且发现左右兄弟节点都没有多余的元素,所以,可以选择和兄弟节点合并,最后修改父节点索引
- 发现父节点索引也不满足条件,所以,需要做跟上面一步一样的操作
hash
哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
对于hash相同的,采用链表的方式解决冲突。类似于hashmap。因为索引的结构是十分紧凑的,所以hash索引的查询很快。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OKazzihU-1616748487465)(https://pics7.baidu.com/feed/2fdda3cc7cd98d103d39d390e0672b0b7aec9004.jpeg?token=53a5ae65bb75624dfb8bd1ceec7f04de&s=89215D3095B064234C6CD9C2020080B2)]
Hash索引和B+树所有有什么区别
hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。
- hash索引进行等值查询更快, 但是无法进行范围查询(因为在hash索引中经过hash函数建立索引之后, 索引的顺序与原顺序无法保持一致, 无法支持范围查询, 而B+树的所有节点皆遵循(左节点小于父节点, 多叉树也类似), 天然支持范围查询)
- hash索引不支持使用索引进行排序(因为打乱顺序)
- hash索引不支持模糊查询以及多列索引的最左前缀匹配
- hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询
- hash索引虽然在等值查询上较快,但是不稳定。性能不可预测,当某个键值存在大量重复的时候,发生hash碰撞,此时效率可能极差。而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低。
为什么用B+树不用B树
- B树只适合 随机检索, 而B+树同时支持 随机检索和顺序检索
- B+树空间利用率更高, 可以减少IO次数, 磁盘读写代价更低.索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗。B+树的内部结点并没有指向关键字具体信息的指针,只是作为索引使用,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素
- B+树查询效率更稳定
- B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作。
- 增删文件(节点)时,效率更高。因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。
聚簇索引和非聚簇索引
- 聚簇索引: 将 数据存储和索引放在一起, 找到索引也就找到了数据
- 非聚簇索引: 索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值
非聚簇索引一定会回表查询吗
不一定, 如果查询语句所要求的字段全部命中索引, 就不必回表查询
假设在员工表的年龄上建立了索引, 当进行 select age from employee where age < 20
的查询时, 在索引的叶子节点上, 已经包含了age信息, 就不必回表查询
联合索引及其顺序
MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
具体原因为:
MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。
事务
什么是数据库事务
事务是一个不可分割的数据库操作序列, 也是数据库并发控制的基本单位, 其执行的结果必须使数据库从一种一致性状态变成另一种一致性状态. 事务是逻辑上的一组操作, 要么都执行, 要么都不执行
以转账为例
如果小明转给小红1000元, 涉及到的两个操作就是: 将小明的余额减少1000元, 将小红的余额增加1000元. 万一在这两个操作之间突然出现错误比如银行系统崩溃, 导致小明余额减少而小红余额没有增加, 这样就是错误的.
事务就是保证这两个关键操作要么都成功, 要么都失败
事务的四大特性(ACID)
- 原子性: 事务是最小的执行单位, 不允许分割. 事务的原子性确保动作要么都成功, 要么都失败
- 一致性: 执行事务前后, 数据保持一致, 多个事务对同一个数据读取的结果是相同的
- 隔离性: 并发访问数据库时, 一个用户的事务不被其他事务所干扰, 各并发事务之间数据库是独立的
- 持久性: 一个事务被提交之后, 他对数据库中数据的改变是持久的, 即使数据库发生故障也不应该对其有任何影响
转账就是原子性体现
转账结束前后, 两人的账户余额就是一致性
同时有多人转账, 也不会对二人的转账过程有影响, 就是隔离性
转账的结果是永久保留的, 银行系统故障, 也不会影响二人余额, 就是持久性
脏读、幻读、不可重复读
- 脏读: 某个事务已更新一份数据, 另一个事务在此时读取了同一份数据, 由于某些原因, 前一个rollback了操作, 则后一个事务所读取的数据就不会是正确的
- 幻读: 一个事务的两次查询中数据列数不一致. 例如有一个事务查询了几列数据, 而另一个事务却在此时插入了新的几列数据. 先前的事务在接下来的查询中, 就会发现有几列数据是它先前所没有的
- 前提条件:InnoDB引擎,可重复读隔离级别,使用当前读时。
- 表现:一个事务(同一个read view)在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看到的行。两点需要说明:
1、在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只在当前读下才会出现。
2、幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有已经提交记录的最新值。
- 不可重复读: 在一个事务的两次查询之中数据不一致, 这可能是两次查询过程中间插入了一个事务, 更新了原有的数据
事务的隔离级别, MySQL的默认隔离级别
为了达到事务的四大特性, 数据库定义了4种不同的事务隔离级别, 由低到高依次为 read uncommitted, read committed, repeatable read, serializable, 这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题
- Read Uncommitted(已读未提交): 最低的隔离级别, 允许读取尚未提交的数据变更, 可能会导致脏读、幻读或不可重复读
- Read Committed(读取已提交): 允许读取并发事务已经提交的数据, 阻止了脏读(因为拒绝了rollback), 但是幻读或不可重复读仍有可能发生
- Repeatable Read(可重复读): 对同一字段的多次读取结果都是一致的, 除非数据是被本身事务自己所修改, 可以阻止脏读和不可重复读, 但仍有可能发生幻读
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read Uncommitted | √ | √ | √ |
Read Committed | x | √ | √ |
Repeatable Read | x | x | √ |
Serializable | x | x | x |
MySQL及InnoDB默认采用 Repeatable Read 隔离级别
InnoDB存储引擎在 分布式事务 的情况下一般会用到 Serializable 隔离级别
Oracle及大多数数据库系统默认采用 Read Committed 隔离级别
事务隔离机制的实现基于 锁机制和并发调度, 其中并发调度使用 MVVC(多版本并发控制), 通过保存修改的旧版本信息来支持并发一致性读和回滚等特性
锁
MySQL的锁
当数据库有并发事务时, 可能会导致数据不一致, 这时候需要一些机制来保证访问的次序, 锁机制就是这样的一个机制
就像酒店的房间, 如果大家随意进出, 就会出现多人抢夺同一个房间的情况, 而在房间装上锁之后, 申请到钥匙的人才可以入住并且将房间锁起来, 其他人只能等他使用完毕才可以再次使用
隔离级别与锁的关系
- 在Read Uncommitted级别下, 读取数据不需要加共享锁, 这样就不会跟被修改的数据上的排他锁冲突
- 在Read Committed级别下, 读操作不需要加共享锁
- 在Repeatbale Read级别下, 读操作需要加共享锁, 但是在事务提交之前并不释放共享锁, 也就是必须等事务执行完毕以后才释放共享锁
- 在Serializable级别下, 其是限制性最强的隔离级别, 因为该级别锁定整个范围的键, 并一直持有锁, 直到事务完成
按照锁的粒度分数据库锁有哪些
在关系型数据库中, 可以按照锁的粒度把数据库锁分为 行级锁(Innodb引擎), 表级锁(MyISAM引擎), 页级锁(BDB引擎)
行级锁
行级锁是MySQL中锁定粒度最细的一种锁, 表示只针对当前操作的行进行加锁
行级锁能大大减少数据库操作的冲突, 其粒度最小, 但是开销最大
行级锁分为 共享锁和排他锁
特点:
- 开销大, 加锁慢
- 会出现死锁(一个操作只占一行, 需要的另外行被别的操作占住)
- 锁定粒度最小, 发生锁冲突的概率最低, 并发度也最高
表级锁
表级锁是MySQL中锁定粒度最大的一种锁, 表示对当前操作的整张表加锁
它实现简单, 资源消耗较少, 被大部分MySQL引擎支持, 最常用的Innodb和MyISAM都支持表级锁
表级锁分为表共享读锁(共享锁)与表独占写锁(排他锁)
特点:
- 开销小, 加锁快
- 不会出现死锁(一个操作独享一张表, 不会被死锁)
- 锁定粒度大, 发生锁冲突的概率最高, 并发读最低
页级锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁
表级锁速度快, 但冲突多, 行级冲突少, 但速度慢
所以取了折中的页级, 一次锁定相邻的一组记录
特点:
- 开销和加锁时间介于表锁和行锁止键
- 会出现死锁
- 锁定粒度介于表锁和行锁止键, 并发度一般
从锁的类别上分有哪些锁
有共享锁和排他锁
共享锁: 又叫做 读锁, 当用户要进行数据读取时, 对数据加上共享锁, 共享锁可以同时加上多个
排他锁: 又叫做 写锁, 当用户要进行数据写入时, 对数据加上排他锁, 排他锁只可以加一个, 和其他的排他锁、共享锁都互斥
举例: 一种是来看房, 多个用户一起看房是可以接受的, 一种是真正的入住一晚, 在这期间, 无论是想入住的还是想看房的都不可以
锁的粒度取决于具体的存储引擎
MySQL中InnoDB引擎的行锁如何实现
InnoDB是基于索引来完成行锁
select * from table1 where id = 1 for update;
for update
可以根据条件来完成行锁定, 并且 id
是有索引键的列, 如果 id
不是索引键那么InnoDB将完成表锁, 并发将无从谈起
InnoDB存储引擎的锁的算法
- Record lock: 单个行记录上的锁
- Gap lock: 间隙锁, 锁定一个范围, 不包括记录本身
- Next-key lock: record+gap, 锁定一个范围, 包括记录本身
- innodb对于行的查询使用next-key lock
- Next-locking keying为了解决Phantom Problem幻读问题
- 当查询的索引含有唯一属性时,将next-key lock降级为record key
- Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
- 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数
innodb_locks_unsafe_for_binlog
设置为1
死锁
死锁 是指两个或多个事务在统一资源上相互占用, 并请求锁定对方的资源, 从而导致恶性循环的现象
常见的解决死锁的方法:
- 如果不同程序会并发存取多个表, 尽量约定以相同的顺序访问表, 可以大大降低死锁机会
- 在同一个事务中, 尽可能做到一次锁定所需要的所有资源, 减少死锁产生概率
- 对于非常容易产生死锁的业务部分, 可以使用升级锁定颗粒度, 通过表级锁定来减少死锁产生的概率
如果业务处理不好可以使用分布式事务锁或者乐观锁
乐观锁和悲观锁
数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性和数据库的统一性.
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段
悲观锁:
- 假定会发生并发冲突, 屏蔽一切可能违反数据完整性的操作
- 在查询完数据的时候就把事务锁起来, 直到提交事务
- 实现方式: 使用数据库中的锁机制
乐观锁:
- 假定不会发生并发冲突, 只在提交操作时检查是否违反数据完整性
- 在修改数据的时候把事务锁起来, 通过version的方式来进行锁定
- 实现方式: 一般使用版本号机制或CAS算法实现
两种锁的使用场景
乐观锁适用于写较少的情况(多读场景), 这样可以省去锁的开销, 加大了系统的整个吞吐量
悲观锁适用于多写的场景
视图
视图的概念
为了提高复杂SQL语句的复用性和表操作的安全性, MySQL数据库管理系统提供了视图特性
视图本质上是一种 虚拟表, 在物理上是不存在的, 其内容与真实的表相似, 包含一系列带有名称的列和行数据
视图并不在数据库中以存储的数据值形式存在
行和列数据来自定义视图的查询所引用的基本表, 并且在具体引用视图时动态生成
视图使开发者只关心感兴趣的某些特定数据和所负责的特定任务, 只能看到视图中所定义的数据, 而不是视图所引用表中的全部数据, 从而提高了数据库中数据的安全性
视图的特点
- 视图的列可以来自不同的表, 是表的抽象和在逻辑意义上建立的新关系
- 视图是由基本表(实表)产生的表(虚表)
- 视图的建立和删除不影响基本表
- 对视图内容的更新(增删改)直接影响基本表
- 当视图来自多个基本表时, 不允许增加和删除数据
视图的操作包括创建视图、查看视图、删除和修改视图
视图的使用场景
视图根本用途
- 简化sql查询, 提高开发效率
- 兼容老的表结构
常见场景
- 重用SQL语句
- 简化复杂的SQL操作, 在编写查询后, 可以方便地重用它而不必知道它的基本查询细节
- 使用表的组成部分而不是整个表
- 保护数据. 可以给用户授予表的特定部分的访问权限而不是整个表的访问权限
- 更改数据格式和表示。视图可返回与地层表的表示和格式不同的数据
视图的优缺点
优点
- 查询简单化。视图能简化用户操作
- 数据安全性。视图使用户能以多种角度看待同一数据, 能够对机密数据提供安全保护
- 逻辑数据独立性。视图对重构数据库提供了一定程度的逻辑独立性
缺点
-
性能。数据库必须把视图的查询转化为表的查询, 如果这个视图由一个复杂的多表查询所定义, 那么, 即使是视图的一个简单查询, 数据库也把它变成一个复杂的结合体, 需要花费一定的时间
-
修改限制。当用户视图修改视图的某些行时, 数据库必须把它转化为对基本表的某些行的修改。事实上, 当从视图插入或者删除时, 情况也是这样. 对于简单视图, 这是很方便的, 但是对于比较复杂的视图, 可能是不能修改的
这些视图有如下特征:
- 有
UNIQUE
等集合操作符的视图 - 有
GROUP BY
子句的视图 - 有诸如
AVG\SUM\MAX
等聚合函数的视图 - 使用
DISTINCT
关键字的视图 - 连接表的视图
- 有
游标
游标是系统为用户开设的一个数据缓冲区, 用于存放SQL语句的执行结果, 每个游标区都有一个名字
用户可以通过游标逐一获取记录并赋给主变量, 交由主语言进一步处理
存储过程与函数
存储过程及其优缺点
存储过程是一个预编译的SQL语句, 优点是允许模块化的设计, 就是说只需要创建一次, 以后在该程序中就可以调用多次
如果每次操作需要执行多次SQL, 使用存储过程比单纯SQL语句执行更快
优点
- 存储过程是预编译的, 执行效率高
- 存储过程的代码直接放在数据库中, 通过存储过程名直接调用, 减少网络通讯
- 安全性高, 执行存储过程需要有一定权限的用户
- 存储过程可以重复使用, 减少数据库开发人员的工作量
缺点
- 调试麻烦
- 移植问题
- 重新编译问题. 因为后端代码是运行前编译的, 如果带有引用关系的对象发生改变时, 受影响的存储过程、包将需要重新编译(不过也可以设置成运行时刻自动编译)
- 如果在一个程序系统中大量使用存储过程, 到程序交付使用的时候随着用户需求的增加会导致数据结构的变化, 接着就是系统的相关问题了, 用户很难维护该系统
触发器
触发器及其使用场景
触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程
触发器是指一段代码, 当触发某个事件时, 自动执行这些代码
使用场景:
- 可以通过数据库中的相关表实现级联更改
- 实时监控某张表中的某个字段的更改而需要做出相应的处理
- 例如可以生成某些业务的编号
- 不要滥用, 否则会造成数据库及应用程序的维护困难
MySQL中有哪些触发器
- Before Insert
- After Insert
- Before Update
- After Update
- Before Delete
- After Delete
常用SQL语句
SQL语句类别
-
数据定义语言DDL(Data Definition Language)
CREATE、DROP、ALTER
对逻辑结构有操作的, 其中包括表结构、视图和索引
-
数据查询语言DQL(Data Query Language)
SELECT
查询操作, 以SELECT关键字, 各种简单查询、连接查询等
-
数据操纵语言DML(Data Manipulation Language)
INSERT、UPDATE、DELETE
对数据进行操作
-
数据控制功能DCL(Data Control Language)
GRANT、REVOKE、COMMIT、ROLLBACK
对数据库安全性完整性进行操作, 即权限控制
超键、候选键、主键、外键
- 超键: 在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以作为一个超键, 多个属性组合在一起也可以作为一个超键。超键包含候选键和主键
- 候选键: 最小超键, 没有冗余元素的超键
- 主键: 数据库表中对存储数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键, 且主键的取值不能缺失, 既不能为空值(NULL)
- 外键: 在一个表中存在的另一个表的主键称此表的外键
SQL的约束
- NOT NULL: 用于控制字段的内容一定不能为空
- UNIQUE: 控制字段内容不能重复, 一个表允许有多个UNIQUE约束
- PRIMARY KEY: 也是用于控制字段不能重复, 但是在表中只能出现一次
- FOREIGN KEY: 用于预防破坏表之间连接的动作, 也能防止非法数据插入外键列, 因为它必须是它指向的表中的值之一
- CHECK: 用于控制字段的值范围
关联查询
交叉连接(cross join)
返回左表中的所有行,左表中的每一行与右表中的所有行组合。交叉联接也称作笛卡尔积
举例:
A = {0,1} B = {2,3,4}
集合 A×B 和 B×A的结果集就可以分别表示为以下这种形式:
A×B = {(0,2),(1,2),(0,3),(1,3),(0,4),(1,4)};
B×A = {(2,0),(2,1),(3,0),(3,1),(4,0),(4,1)};
得出以下两点结论:
1,两个集合相乘,不满足交换率,即 A × B ≠ B × A A×B ≠ B×A A×B=B×A;
2,A集合和B集合相乘,包含了集合A中元素和集合B中元素相结合的所有的可能性。既两个集合相乘得到的新集合的元素个数是 A集合的元素个数 × B集合的元素个数;
隐式的交叉连接
SELECT O.ID, O.ORDER_NUMBER, C.ID, C.NAME
FROM ORDERS O, CUSTOMERS C
WHERE O.ID=1;
显式的交叉连接, 使用了 CROSS JOIN
SELECT O.ID,O.ORDER_NUMBER,C.ID,
C.NAME
FROM ORDERS O CROSS JOIN CUSTOMERS C
WHERE O.ID=1;
SELECT * FROM A,B(,C)或者SELECT * FROM A CROSS JOIN B (CROSS JOIN C)#没有任何关联条件,结果是笛卡尔积,结果集会很大,没有意义,很少使用内连接(INNER JOIN)SELECT * FROM A,B WHERE A.id=B.id或者SELECT * FROM A INNER JOIN B ON A.id=B.id多表中同时符合某种条件的数据记录的集合
内连接
内连接分为三类
- 等值连接
ON A.id = B.id
- 不等值连接
ON A.id>B.id
- 自连接
SELECT * FROM T1 INNER JOIN T2 ON T1.id=T2.id
返回两张表都满足条件的部分
inner join 就等于 join
我们在进行表连接查询的时候一般都会使用JOIN xxx ON xxx的语法,ON语句的执行是在JOIN语句之前的,也就是说两张表数据行之间进行匹配的时候,会先判断数据行是否符合ON语句后面的条件,再决定是否JOIN。
因此,有一个显而易见的SQL优化的方案是,当两张表的数据量比较大,又需要连接查询时,应该使用 FROM table1 JOIN table2 ON xxx的语法,避免使用 FROM table1,table2 WHERE xxx 的语法,因为后者会在内存中先生成一张数据量比较大的笛卡尔积表,增加了内存的开销。
内连接和where在结果上没有区别仅仅在于性能上的区别
外连接
外连接分为左外连接和右外连接
-
左外连接: LEFT OUTER JOIN, 以左表为主, 先查询出左表, 按照
ON
后的关联条件匹配右表, 没有匹配到的用NULL填充, 可以简写成LEFT JOIN
select * from A left join B on A.id=B.id #相当于包含A的全部,在B中找等号
-
右外连接: RIGHT OUTER JOIN, 以右表为主, 先查询出右表, 按照
ON
后的关联条件匹配左表, 没有匹配到的用NULL填充, 可以简写成RIGHT JOIN
select * from A right join B on A.id=B.id #相当于包含B的全部, 在A中找等号
全联接
- MySQL不支持全联接(FULL JOIN)
- 全外连接是在结果中除了显示满足连接的条件的行外,还显示了join两侧表中所有满足检索条件的行
- 把左联结果表+右联结果表组合在一起,然后过滤掉重复的。
联合查询
select * from A
union
select * from B
- UNION
- UNION 操作符用于合并两个或多个 SELECT 语句的结果集。
- 请注意,UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同。
- UNION 结果集中的列名总是等于 UNION 中第一个 SELECT 语句中的列名。
- UNION 操作符选取不同的值
- UNION ALL: 不会合并重复的记录行
- UNION ALL效率更高, 因为UNION还有一个相当于去重的操作
子查询
- 条件: 一条SQL语句的查询结果作为另一条查询语句的条件或查询结果
- 嵌套: 多条SQL语句嵌套使用, 内部的SQL查询语句称为子查询
单行单列子查询
结果是一个值, 父查询使用 =,<,>
等运算符
# 查询工资最高的员工
select * from employee
where
salary = (select max(salary) from employee)
多行单列子查询
结果集类似于一个数组, 父查询用 in
运算符
select * from t_book
where
bookType in(select id from t_bookType);
多行多列子查询
结果集类似于一张虚拟表, 不能用于 where
条件, 用于 select
子句中作为主表
-- 1) 查询出2011年以后入职的员工信息
-- 2) 查询所有的部门信息,与上面的虚拟表中的信息比对,找出所有部门ID相等的员工。
select * from dept d, (select * from employee where join_date > '2011-1-1') e where e.dept_id = d.id;
-- 使用表连接:
select d.*, e.* from dept d inner join employee e on d.id = e.dept_id where e.join_date > '2011-1-1'
常用查询语句
-
查询一张表:
select * from 表名
; -
查询指定字段:
select 字段1,字段2,字段3....from 表名
; -
where
条件查询:select 字段 from 表名 where 条件表达式
;select * from t_studect where id=1; select * from t_student where age>22;
-
带
in
关键字查询:select 字段 from 表名 where 字段 [not]in(元素1,元素2);
select * from t_student where age in (21,23); select * from t_student where age not in (21,23);
-
带
between and
的范围查询:select 字段1,字段2 from 表名 where 字段 [not]between 取值1 and 取值2
;select * frome t_student where age between 21 and 29; select * frome t_student where age not between 21 and 29;
-
带
like
的模糊查询:select 字段1 from 表名 where 字段 [not] like '字符串';
“%”代表任意字符;“_"代表单个字符;select * frome t_student where stuName like '张三''; select * frome t_student where stuName like '张三%''; select * frome t_student where stuName like '%张三%'';//含有张三的任意字符 select * frome t_student where stuName like '张三_''
-
空值查询:
select 字段 from 表名 where 字段 is[not] null
; -
带
and
的多条件查询:
select 字段1,字段2... from 表名 where 条件表达式1 and 条件表达式2 [and 条件表达式n]
select * frome t_student where gradeName='一年级' and age=23;
-
带
or
的多条件查询
select 字段 from 表名 where 条件表达式1 or 条件表达式2 [or 条件表达式n]
select * frome t_student where gradeName='一年级' or age=23;//或者,条件只要满足一个
-
distinct
去重复查询:select distinct 字段名 from 表名
;
11.对查询结果排序order by
:select 字段1 from 表名 order by 属性名 [asc|desc]
select * frome t_student order by age desc;//降序,从大到小
select * frome t_student order by age asc;//升序,asc默认可以不写
-
分组查询
group by
group by 属性名 [having 条件表达式][with rollup]
-
单独使用(毫无意义,不能单独使用);
-
与
group_concat()
函数一起使用;group_concat() 函数就是把stuName按照gradeName进行合并
select gradeName,group_concat(stuName) from t_student group by gradeName;
-
与聚合函数一起使用;
select gradeName,count(stuName) from t_student group by gradeName;
-
与
having
一起使用(显示输出的结果);select gradeName,count(stuName) from t_student group by gradeName having count(stuName)>3 ;
-
与
with rollup
一起使用(最后加入一个总和行);select gradeName,group_concat(stuName) from t_student group by gradeName with rollup;
-
-
limit 分页查询:
select 字段 from 表名 limit 初始位置,记录数;
select * from t_student limit 0,5;
MySQl中in和exists区别
mysql中的in语句是把外表和内表作hash连接, 而exist语句是对外表作loop循环, 每次loop循环再对内表进行查询
需要分情况讨论in和exist的效率问题
- 如果查询的两个表大小相当, 那么in和exist差别不大
- 如果两个表一大一小, 则子查询表大的用exists, 小的用in
- not in 和not exists: 如果查询语句使用了 not in, 那么内外表都进行全表扫描, 没有用到索引; 而not exists的子查询依然能用到索引, 所以无论哪个表大, not exists都比not in要快
drop, delete, truncate
delete | trancate | drop | |
---|---|---|---|
类型 | DML | DDL | DDL |
回滚 | 可回滚 | 不可回滚 | 不可回滚 |
删除内容 | 表结构还在,删除表的全部或者一部分数据行 | 表结构还在, 删除表中的所有数据 | 从数据库中删除表, 所有的数据行, 索引和权限也会被删除 |
删除速度 | 删除速度慢, 需要逐行删除 | 删除速度快 | 删除速度最快 |
不再需要一张表的时候,用drop;
在想删除部分数据行时候,用delete;
在保留表而删除所有数据的时候用truncate。
SQL优化
如何定位及优化SQL语句的性能
使用 执行计划
MySQL提供了 explain
命令来查看语句的 执行计划
对于查询语句, 最重要的优化方式就是使用索引
执行计划就是现实数据库引擎对于SQL语句的执行的详细情况, 其中包含了是否使用索引, 使用什么索引, 使用索引的相关信息等
信息 id
有一组数字组成, 表示一个查询中各个子查询的执行顺序
- id相同执行顺序由上至下
- id不同, 值越大优先级越高, 越先被执行
- id为null时表示一个结果集, 不需要使用它查询, 常出现在包含union等查询语句中
select type
指每个子查询的查询类型, 常出现在包含union的查询语句中
id | select_type | description |
---|---|---|
1 | SIMPLE | 不包含任何子查询或union等查询 |
2 | PRIMARY | 包含子查询最外层查询就显示为PRIMARY |
3 | SUBQUERY | 在select或where子句中包含的查询 |
4 | DERIVED | from子句中包含的查询 |
5 | UNION | 出现在union后的查询语句 |
6 | UNION RESULT | 从union中获取结果集 |
type
指访问类型
- ALL 扫描全表数据
- index 遍历索引
- range 索引范围查找
- index_subquery 在子查询中使用ref
- Unique_subquery 在子查询中使用eq_ref
- Ref_or_null 对NULL进行索引的优化ref
- fulltext 使用全文索引
- ref 使用非唯一索引查找数据
- eq_ref 在join查询中使用PRIMARY KEY 或者 UNIQUE NOT NULL 索引关联
possible_keys
可能使用的索引, 注意不一定会使用。查询涉及到的字段上若存在索引, 则该索引将被列出来。当该列为NULL时就要考虑当前的SQL是否需要优化了
key
现实MySQL在查询中实际使用的索引, 若没有使用索引, 显示为NULL
key_length
索引长度
ref
表示上述表的连接匹配条件, 即哪些列霍常亮被用于查找索引列上的值
rows
返回估算的结果集
extra
:
- Using index, 使用覆盖索引
- Using where, 使用where子句来过滤结果集
- Using filesort, 使用文件排序, 使用非索引列进行排序时出现, 非常消耗性能
- Using temporary, 使用临时表
SQL的生命周期
- 应用与数据库服务器建立连接
- 数据库进程拿到请求sql
- 解析并生成执行计划, 执行
- 读取数据到内存并进行逻辑处理
- 通过步骤一的连接, 发送结果到客户端
- 关掉连接, 释放资源
为什么要尽量设定一个主键
主键时数据库确保数据行在整张表唯一性的保障, 即使业务上本张表没有主键, 也建议添加一个自增长的ID列作为主键
设定主键之后, 在后续的增删改也能更加快速以及确保操作数据范围安全
字段为什么要求定义为not null
null值会占用更多字节, 且会在程序中早晨跟多与预期不符的情况
优化查询过程中的数据访问
- 访问数据太多导致查询性能的下降
- 确定应用程序是否存在检索大量超过需要的数据, 可能是太多行或列
- 确认MySQL服务器是否在分析大量不必要的数据行
- 避免SQL语句错误
- 对于查询到不需要的数据, 可以使用
limit
解决 - 对于多表关联返回全部列, 可以用 指定列名 解决
- 总是返回全部列, 避免使用
select *
- 对于重复查询相同的数据, 可以缓存数据, 下次直接读取缓存
- 使用exlain进行分析
- 使用索引覆盖扫描, 把所有的列都放到索引中, 这样存储引擎不需要回表获取对应行就可以返回结果
- 改变数据库和表的结构, 修改数据表范式
- 重写SQL语句, 让优化器可以以更优的方式执行查询
优化长难的查询语句
- 将一个复杂查询变成多个简单查询
- 一次性删除1000万的数据要比一次删除1万,暂停一会的方案更加损耗服务器开销。
- 执行单个查询可以减少锁的竞争
- 尽量避免全表差扫描, 优先考虑在where和order by设计的列上建立索引
- 在使用where子句时, 尽量避免使用 !=<>, or, null值判断和in, not in, 这样会导致全表扫描
- 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引
数据库优化
为什么要优化
- 系统的吞吐量瓶颈往往出现在数据库的访问速度上
- 随着应用程序的运行, 数据库中的数据会越来越多, 处理时间会相应变慢
- 数据是存放在磁盘上的, 读写速度无法和内存相比
优化原则: 减少系统瓶颈, 减少资源占用, 增加系统的反应速度
数据库结构优化
将字段很多的表分解成多个表
-
对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。
-
因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
增加中间表
-
对于需要经常联合查询的表,可以建立中间表以提高查询效率。
-
通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。
增加冗余字段
- 设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。
- 表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。
注意:
冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。
主从复制
将数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上, 然后将这些日志重新执行, 从而使得从数据库的数据与主数据库的数据保持一致
主从复制的作用:
- 主数据库出现问题, 可以切换到副数据库
- 可以进行数据库层面的读写分离
- 可以在从数据库上进行日常备份
MySQL主从复制解决的问题
- 数据分布: 随意开始或停止复制, 并在不同地理位置分布数据备份
- 负载均衡: 降低单个服务器的压力
- 高可用和故障切换: 帮助应用程序避免单点失败
- 升级测试: 可以用更高版本的MySQL作为从库
MySQL主从复制工作原理
- 在主库上把数据更高记录到二进制日志
- 从库将主库的日志复制到自己的中继日志
- 从库读取中继日志的事件, 将其重放到从库数据中
基本流程
主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;
从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中;
从:sql执行线程——执行relay log中的语句;
Binary log:主数据库的二进制日志
Relay log:从服务器的中继日志
第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。
第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。
第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。
读写分离解决方案
读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。因为主从复制要求slave不能写只能读(如果对slave执行写操作,那么show slave status将会呈现Slave_SQL_Running=NO,此时你需要按照前面提到的手动同步一下slave)。
方案一
使用mysql-proxy代理
优点:直接实现读写分离和负载均衡,不用修改代码,master和slave用一样的帐号,mysql官方不建议实际生产中使用
缺点:降低性能, 不支持事务
方案二
使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。
如果采用了mybatis, 可以将读写分离放在ORM层,比如mybatis可以通过mybatis plugin拦截sql语句,所有的insert/update/delete都访问master库,所有的select 都访问salve库,这样对于dao层都是透明。 plugin实现时可以通过注解或者分析语句是读写方法来选定主从库。不过这样依然有一个问题, 也就是不支持事务, 所以我们还需要重写一下DataSourceTransactionManager, 将read-only的事务扔进读库, 其余的有读有写的扔进写库。
方案三
使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务.
缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。