数据库
通用语法及分类
- DDL: 数据定义语言,用来定义数据库对象(数据库、表、字段)
- DML: 数据操作语言,用来对数据库表中的数据进行增删改
- DQL: 数据查询语言,用来查询数据库中表的记录
- DCL: 数据控制语言,用来创建数据库用户、控制数据库的控制权限
DDL(数据定义语言)
数据库操作
# 1. 查询所有数据库:
show databases;
# 2. 查询当前所在数据库:
select database();
# 3. 创建数据库:
create database [if not exists] 数据库名 [default charset 字符集] [collate 排序规则];
# 4. 删除数据库:
drop database [if exists] 数据库名;
# 5. 使用数据库:
use 数据库名;
注意事项: UTF8字符集长度为3字节,有些符号占4字节,所以推荐用utf8mb4字符集
表操作
# 1. 查询当前数据库所有表:
show tables;
# 2. 查询表结构:
desc 表名;
# 3. 查询指定表的建表语句:
show create table 表名;
# 4. 创建表
create table 表名(
字段1 字段1类型 [comment 字段1注释],
字段2 字段2类型 [comment 字段2注释],
字段3 字段3类型 [comment 字段3注释],
...
字段n 字段n类型 [comment 字段n注释]
) [comment 表注释];
# 5. 添加字段
alter table 表名 add 字段名 类型(长度) [comment 字段注释];
# 6. 修改数据类型
alter table 表名 modify 字段名 新数据类型(长度);
# 7. 修改字段名和字段类型
alter table 表名 change 旧字段名 新字段名 新数据类型(长度) [comment 字段注释];
# 8. 删除字段
alter table 表名 drop 字段名;
# 9. 修改表名
alter table rename to 新表名;
# 10. 删除表
drop table [if exists] 表名;
# 11. 删除表,并重新创建该表
truncate table 表名;
MySql数据类型
数值类型
| 类型 | 大小 | Signed范围 | Unsigned范围 | 描述 |
|---|---|---|---|---|
| TINYINT | 1byte | (-128, 127) | (0, 1255) | 小整数值 |
| SMALLINT | 2 bytes | (-32768, 32767) | (0, 65535) | 大整数值 |
| MEDIUMINT | 3 bytes | (-8388608, 8388607) | (0, 16777215) | 大整数值 |
| INT/INTEGER | 4 bytes | (-2147483648, 2147483647) | (0, 4294967265) | 大整数值 |
| BIGINT | 8 bytes | … | … | 极大整数值 |
| FLOAT | 4 bytes | … | … | 单精度浮点数 |
| DOUBLE | 8 bytes | … | … | 双精度浮点数 |
| DECIMAL | 依赖于M(精度)和D(标度)的值 | 依赖于M(精度)和D(标度)的值 | 小数值(精确定点数) |
字符串类型
| 类型 | 大小 | 描述 |
|---|---|---|
| CHAR | 0 - 255 bytes | 定长字符串(需要指定长度) |
| VARCHAR | 0 - 65535 bytes | 变长字符串(需要指定长度) |
| TINYBLOB | 0 -255 bytes | 不超过255个字符的二进制数据 |
| TINYTEXT | 0 -255 bytes | 短文本字符串 |
| BLOB | 0 - 65535 bytes | 二进制形式的长文本数据 |
| TEXT | 0 - 65535 bytes | 长文本数据 |
| MEDIUMBLOB | 0 - 16777215 bytes | 二进制形式的中等长度极大文本数 |
| MEDIUMTEXT | 0 - 16777215 bytes | 中等长度文本数据 |
| LONGBLOB | 0 - 4294967295 bytes | 二进制形式的极大文本数 |
| LONGTEXT | 0 - 4294967295 bytes | 极大文本数据 |
日期时间类型
| 类型 | 大小 | 范围 | 格式 | 描述 |
|---|---|---|---|---|
| DATE | 3 | 1000-01-01 至 9999-12-31 | YYYY-MM-DD | 日期值 |
| TIME | 3 | -838:59:59 至 838:59:59 | HH:MM:SS | 时间值或持续时间 |
| YEAR | 1 | 1901 至 2155 | YYYY | 年份值 |
| DATETIME | 8 | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间 |
| TIMESTAMP | 4 | 1970-01-01 00:00:01 至 2038-01-19 03:14:07 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值,时间戳 |
DML(数据操作语言)
增
# 指定字段
insert into 表名 (字段1, 字段2...) values (值1, 值2...), (值1, 值2...);
# 全部字段
insert into 表名 values (值1, 值2...), (值1, 值2...);
# 注意事项
# 字符串和日期类型数据应该包含在引号中
# 插入的数据大小应该在字段的规定范围内
改删
# 修改数据
update 表名 set 字段1=值1, 字段2=值2,... [where 条件];
# 删除数据
delete from 表名 [where 条件];
# 只能删除整行数据,如果删除单个数据可以通过update修改为null
DQL(数据查询语言)
基础查询
# 查询多个字段
select 字段1, 字段2 from 表名;
#查询所有数据
select * from 表名;
# 设置别名 as可以省略
select 字段1 [as 别名1], 字段2 [as 别名2] from 表名;
#去除重复记录
select distinct 字段列表 from 表名;
条件查询
# 语法
select 字符列表 from 表名 where 条件列表;
| 比较运算符 | 功能 |
|---|---|
| > | 大于 |
| >= | 大于等于 |
| < | 小于 |
| <= | 小于等于 |
| = | 等于 |
| <> 或 != | 不等于 |
| BETWEEN … AND … | 在某个范围内(含最小、最大值) |
| IN(…) | 在in之后的列表中的值,多选一 |
| LIKE 占位符 | 模糊匹配(_匹配单个字符,%匹配任意个字符) |
| IS NULL | 是NULL |
| 逻辑运算符 | 功能 |
|---|---|
| AND 或 && | 并且(多个条件同时成立) |
| OR 或 || | 或者(多个条件任意一个成立) |
| NOT 或 ! | 非,不是 |
# 例子
# 年龄大于等于18岁的员工
select * from employee where age >= 18;
# 没有填写身份证号的员工
select * from employee where idcard is null or idcard = '';
# 有身份证号的员工
select * from employee where idcard is not null;
# 年龄在20岁到30岁之间的员工(between 小值 and 大值)
select * from employee where age between 20 and 30;
select * from employee where age >= 20 and age <= 30;
# 年龄等于25或30或35
select * from employee where age = 25 or age = 30 or age = 35;
select * from employee where age in (25, 30, 35);
# 姓名为两个字
select * from employee where name like '__';
# 身份证最后为X
select * from employee where idcard like '%X';
聚合查询
# 语法
select 聚合函数(字段名) from 表名;
# 统计安徽省的有多少人
select count(id) from employee where workaddress='安徽省';
| 函数 | 功能 |
|---|---|
| count | 统计数量 |
| max | 最大值 |
| min | 最小值 |
| avg | 平均值 |
| sum | 求和 |
分组查询
# 语法
select 字段列表 from 表名 where 条件列表 group by 分组字段名 [having 分组后的过滤条件];
# where 和 having 的区别:
# 执行时机不同:where是分组之前进行过滤,不满足where条件不参与分组;having是分组后对结果进行过滤
# 判断条件不同:where不能对聚合函数进行判断,而having可以
# 例子
# 根据性别分组,统计男性和女性数量
select gender count(*) from employee group by gender;
# 年龄小于45,并根据工作地址分组,获取员工数量大于等于3的工作地址
select workaddress count(id) from employee where age < 45 group by workaddress having count(id) >= 3;
# count(id) 也可以起个别名,having过滤时可以通过别名过滤
# 注意事项
# 执行顺序:where > 聚合函数 > having
# 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义
排序查询
# 语法
select 字段列表 from 表名 order by 字段1 排序方式1, 字段2 排序方式2;
# 排序方式
# asc 升序(默认)
# desc 降序
# 例子
# 根据年龄升序排序,年龄相同根据入职时间降序排序
select * from employee order by age, entrydata desc;
分页查询
# 语法
select 字段列表 from 表名 limit 起始索引, 查询记录数;
# 例子
# 查询第二页
select * from employee limit 10, 10;
# 扩展
# 分页查询是数据库的方言,不同数据库有不同实现,MySQL是LIMIT
# 如果查询的是第一页数据,起始索引可以省略,直接简写 LIMIT 10
DQL执行顺序
FROM -> WHERE -> GROUP BY -> SELECT -> ORDER BY -> LIMIT
DCL(数据控制语言)
管理用户
# 查询用户
use mysql;
select * from user;
# 创建用户
create user '用户名'@'主机名' identified by '密码';
# 修改用户密码
alter user '用户名'@'主机名' identified with mysql_native_password by '新密码';
# 删除用户
drop user '用户名'@'主机名';
# 例子
# 创建用户test,只能在当前主机localhost访问
create user 'test'@'localhost' identified by '123456';
# 创建用户test,能在任意主机访问
create user 'test'@'%' identified by '123456';
# 修改密码
alter user 'test'@'localhost' identified with mysql_native_password by '1234';
# 删除用户
drop user 'test'@'localhost';
# 扩展
# 主机名可以使用 % 通配
权限控制
| 权限 | 说明 |
|---|---|
| ALL, ALL PRIVILEGES | 所有权限 |
| SELECT | 查询数据 |
| INSERT | 插入数据 |
| UPDATE | 修改数据 |
| DELETE | 删除数据 |
| ALTER | 修改表 |
| DROP | 删除数据库/表/视图 |
| CREATE | 创建数据库/表 |
# 查询权限
show grants from '用户名'@'主机名';
# 授予权限
grant 权限列表 on 数据库名.表名 to '用户名'@'主机名';
# 撤销权限
revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名';
# 注意事项
# 多个权限用逗号分隔
# 授权时,数据库名和表名可以用 * 进行通配,代表所有
函数
字符串函数
| 函数 | 功能 |
|---|---|
| CONCAT(s1, s2, …, sn) | 字符串拼接,将s1, s2, …, sn拼接成一个字符串 |
| LOWER(str) | 将字符串全部转为小写 |
| UPPER(str) | 将字符串全部转为大写 |
| LPAD(str, n, pad) | 左填充,用字符串pad对str的左边进行填充,达到n个字符串长度 |
| RPAD(str, n, pad) | 右填充,用字符串pad对str的右边进行填充,达到n个字符串长度 |
| TRIM(str) | 去掉字符串头部和尾部的空格 |
| SUBSTRING(str, start, len) | 返回从字符串str从start位置起的len个长度的字符串 |
| REPLACE(column, source, replace) | 替换字符串 |
-- 拼接
SELECT CONCAT('Hello', 'World');
-- 小写
SELECT LOWER('Hello');
-- 大写
SELECT UPPER('Hello');
-- 左填充
SELECT LPAD('01', 5, '-');
-- 右填充
SELECT RPAD('01', 5, '-');
-- 去除空格
SELECT TRIM(' Hello World ');
-- 切片(起始索引为1)
SELECT SUBSTRING('Hello World', 1, 5);
数值函数
| 函数 | 功能 |
|---|---|
| CEIL(x) | 向上取整 |
| FLOOR(x) | 向下取整 |
| MOD(x, y) | 返回x/y的模 |
| RAND() | 返回0~1内的随机数 |
| ROUND(x, y) | 求参数x的四舍五入值,保留y位小数 |
日期函数
| 函数 | 功能 |
|---|---|
| CURDATE() | 返回当前日期 |
| CURTIME() | 返回当前时间 |
| NOW() | 返回当前日期和时间 |
| YEAR(date) | 获取指定date的年份 |
| MONTH(date) | 获取指定date的月份 |
| DAY(date) | 获取指定date的日期 |
| DATE_ADD(date, INTERVAL expr type) | 返回一个日期/时间值加上一个时间间隔expr后的时间值 |
| DATEDIFF(date1, date2) | 返回起始时间date1和结束时间date2之间的天数 |
# 例子
select date_add(now(), interval 70 year);
流程函数
| 函数 | 功能 |
|---|---|
| IF(value, t, f) | 如果value为true,则返回t,否则返回f |
| IFNULL(value1, value2) | 如果value1不为空,返回value1,否则返回value2 |
| CASE WHEN [ val1 ] THEN [ res1 ] … ELSE [ default ] END | 如果val1为true,返回res1,… 否则返回default默认值 |
| CASE [ expr ] WHEN [ val1 ] THEN [ res1 ] … ELSE [ default ] END | 如果expr的值等于val1,返回res1,… 否则返回default默认值 |
# 例子
# 查看员工的名字和年龄段,大于30岁记为中年,否则记为青年
select name (case when age > 30 then '中年' else '青年' end) from employee;
# 查看员工的姓名和工作地址,如果是北京市和上海市就记为一线城市,否则记为二线城市
select name (case workaddress when '北京市' then '一线城市' when '上海市' then '一线城市' else '二线城市' end) as '工作地址' from employee;
约束
常用约束
| 约束 | 描述 | 关键字 |
|---|---|---|
| 非空约束 | 限制该字段的数据不能为null | NOT NULL |
| 唯一约束 | 保证该字段的所有数据都是唯一、不重复的 | UNIQUE |
| 主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | PRIMARY KEY |
| 自动增长 | 用于主键自增,录入数据时可以直接不录主键 | AUTO_INCREMENT |
| 默认约束 | 保存数据时,如果未指定该字段的值,则采用默认值 | DEFAULT |
| 检查约束(8.0.1版本后) | 保证字段值满足某一个条件 | CHECK |
| 外键约束 | 用来让两张图的数据之间建立连接,保证数据的一致性和完整性 | FOREIGN KEY |
# 例子
create table user(
id int primary key auto_increment,
name varchar(10) not null unique,
age int check(age > 0 and age < 120),
status char(1) default '1',
gender char(1)
);
# 增加数据时,如果仅不满足unique条件,会报错,但是会占用一个主键序号,可以理解为加入到表中才能对比
外键约束
# 创建表时添加外键
create table 表名(
字段名 字段类型 约束 [comment 字段注释],
...
[constraint] 外键名称 foreign key (外键字段名) references 主表(主表列名)
) [comment 表注释];
# 单独添加外键
alter table 表名 add [constraint] 外键名称 foreign key (外键字段名) references 主表名(主表列名)
# 例子
alter table worker add constraint fk_worder_dept_id foreign key (dept_id) references dept(id);
# 删除外键
alter table 表名 drop foreign key 外键名称;
删除/更新行为
| 行为 | 说明 |
|---|---|
| NO ACTION | 当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除/更新(与RESTRICT一致) |
| RESTRICT | 当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除/更新(与NO ACTION一致) |
| CASCADE | 当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则也删除/更新外键在子表中的记录 |
| SET NULL | 当在父表中删除/更新对应记录时,首先检查该记录是否有对应外键,如果有则设置子表中该外键值为null(要求该外键允许为null) |
| SET DEFAULT | 父表有变更时,子表将外键设为一个默认值(Innodb不支持) |
# 修改删除更新行为
alter table 表名 add constraint 外键名称 foreign key 外键字段名 references 主表名(主表列名) on update 行为 on delete 行为;
多表查询
多表关系
- 一对多(多对一)
- 多对多
- 一对一
一对多
案例:部门与员工
关系:一个部门对应多个员工,一个员工对应一个部门
实现:在多的一方(员工)建立外键,指向一的一方的主键
多对多
案例:学生与课程
关系:一个学生可以选多门课程,一门课程也可以供多个学生选修
实现:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
一对一
案例:用户与用户详情
关系:一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他详情字段放在另一张表中,以提升操作效率
实现:在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)
查询
笛卡尔积:两个集合A集合和B集合的所有组合情况(在多表查询时,需要消除无效的笛卡尔积)
# 合并查询(笛卡尔积,会展示所有组合结果):
select * from employee, dept;
# 消除无效笛卡尔积
select * from employee, dept where employ.dept_id = dept.id;
内连接查询
# 内连接查询的是两张表交集的部分
# 隐式内连接
select 字段列表 from 表名1, 表名2 where 条件列表;
# 显示内连接
select 字段列表 from 表名1 inner join 表名2 on 连接条件;
# 例子
# 查询员工姓名,及关联的部门的名称
# 隐式
select e.name, d.name from employee as e, dept as d where e.dept = d.id;
# 显式
select e.name, d.name from employee as e inner join dept as d on e.dept = d.id;
外连接查询
# 左外连接: 查询左表所有数据,以及两张表交集部分数据
select 字段列表 from 表1 left outer join 表2 on 连接条件;
# 右外连接: 查询右表所有数据,以及两张表交集部分数据
select 字段列表 from 表1 right outer join 表2 on 连接条件;
# 例子
# 左
select e.*, d.name from employee as e left outer join dept as d on e.dept = d.id;
select d.name, e.* from dept d left outer join emp e on e.dept = d.id; # 这条语句与下面的语句效果一样
# 右
select d.name, e.* from employee as e right outer join dept as d on e.dept = d.id;
# 注意事项
# 左连接可以查询到没有dept的employee,右连接可以查询到没有employee的dept
# 内连接会过滤掉值为空的数据
自连接查询
# 当前表与自身的连接查询,自连接必须使用表别名
select 字段列表 from 表名 别名A join 表名 别名B on 连接条件;
# 自连接查询,可以是内连接查询,也可以是外连接查询
# 例子
# 查询员工及其所属领导的名字
select a.name, b.name from employee a, employee b where a.manager = b.id;
# 没有领导的也查询出来
select a.name, b.name from employee a left join employee b on a.manager = b.id;
联合查询 UNION, UNION ALL
# 把多次查询的结果合并,形成一个新的查询集
# 语法
select 字段列表 from 表A
union [all]
select 字段列表 from 表B;
# 注意事项
# union all 可能会有重复结果 union不会
# 联合查询比使用or效率高,不会使索引失效
子查询
- 标量子查询(子查询结果为单个值)
- 列子查询(子查询结果为一列)
- 行子查询(子查询结果为一行)
- 表子查询(子查询结果为多行多列)
标量子查询
# 例子
# 查询销售部的所有员工
select * from employee where dept = (select id from dept where name = '销售部');
# 查询xxx入职之后的员工信息
select * from employee where entrydate > (select entrydate from employee where name = 'xxx');
列子查询
| 操作符 | 描述 |
|---|---|
| IN | 在指定的集合范围内,多选一 |
| NOT IN | 不在指定的集合范围内 |
| ANY | 子查询返回列表中,有任意一个满足即可 |
| SOME | 与ANY等同,使用SOME的地方都可以使用ANY |
| ALL | 子查询返回列表的所有值都必须满足 |
# 例子
# 查询销售部和市场部的所有员工信息
select * from employee where dept in (select id from dept where name = '销售部' or name = '市场部');
# 查询比财务部所有人工资都高的员工信息
select * from employee where salary > all(select salary from employee where dept = (select id from dept where name = '财务部'));
# 查询比研发部任意一人工资高的员工信息
select * from employee where salary > any (select salary from employee where dept = (select id from dept where name = '研发部'));
行子查询
# 例子
# 查询与xxx的薪资及直属领导相同的员工信息
select * from employee where (salary, manager) = (select salary, manager from employee where name = 'xxx');`
表子查询
# 查询与xxx1,xxx2的职位和薪资相同的员工`
select * from employee where (job, salary) in (select job, salary from employee where name = 'xxx1' or name = 'xxx2');
# 查询入职日期是2006-01-01之后的员工,及其部门信息
select e.*, d.* from (select * from employee where entrydate > '2006-01-01') as e left join dept as d on e.dept = d.id;
事务
事务是一组操作的集合,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
# 例子: 银行转账
create table account(
id int primary key auto_increment comment '主键',
name varchar(10) comment '姓名',
money int comment '余额'
) comment '账户表';
insert into account (name, money) values ('张三', 2000), ('李四', 2000);
-- 恢复数据
update account set money = 2000 where name = '张三' or name = '李四';
/* 方式一:设置手动提交,一系列操作执行后观察是否出错,出错则回滚,否则提交 */
select @@autocommit; -- 默认为1,自动提交
set @@autocommit = 0; -- 设置为手动提交
-- 转账操作
-- 1. 查询张三账户余额
select money from account where name = '张三';
-- 2. 将张三账户余额 -1000
update account set money = money - 1000 where name = '张三';
-- 3. 将李四账户余额 +1000
update account set money = money + 1000 where name = '李四';
-- 提交事务
commit;
-- 回滚事务
rollback ;
/* 方式二:开启事务 执行操作*/
start transaction; -- 开启事务
-- 转账操作
-- 1. 查询张三账户余额
select money from account where name = '张三';
-- 2. 将张三账户余额 -1000
update account set money = money - 1000 where name = '张三';
-- 3. 将李四账户余额 +1000
update account set money = money + 1000 where name = '李四';
-- 提交事务
commit;
-- 回滚事务
rollback ;
/* 查看事务隔离级别 */
select @@transaction_isolation;
-- 设置事务隔离级别
set session transaction isolation level read uncommitted;
set session transaction isolation level repeatable read;
四大特性ACID
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败
- 一致性(Consistency):事务完成时,必须使所有数据都保持一致状态
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
并发事务
| 问题 | 描述 |
|---|---|
| 脏读 | 一个事务读到另一个事务还没提交的数据 |
| 不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同 |
| 幻读 | 一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在 |
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read uncommitted | √ | √ | √ |
| Read committed | × | √ | √ |
| Repeatable Read(默认) | × | × | √ |
| Serializable | × | × | × |
- √ 表示在当前隔离级别下该问题会出现
- Repeatable Read可以理解为快照读
- Serializable加了表级锁,第二个事务的操作会阻塞,等待第一个事务操作完
- Serializable 性能最低;Read uncommitted 性能最高,数据安全性最差
# 查看事务隔离级别:
select @@transaction_isolation;
# 设置事务隔离级别:
set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable };
# SESSION 是会话级别,表示只针对当前会话有效,GLOBAL 表示对所有会话有效
存储引擎
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的,所以存储引擎也可以被称为表引擎。默认存储引擎是InnoDB。
show create table employee; -- 查询建表语句,默认存储引擎为InnoDB
show engines; -- 查询支持的存储引擎
-- 创建表并指定存储引擎
create table my_memory(
name varchar(20),
age tinyint unsigned
) engine = MyISAM;
InnoDB
InnoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎
特点:
- DML 操作遵循 ACID 模型,支持事务
- 行级锁,提高并发访问性能
- 支持外键约束,保证数据的完整性和正确性
文件:
- xxx.ibd: xxx代表表名,InnoDB 引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm、sdi)、数据和索引。 参数:innodb_file_per_table(默认打开),决定多张表共享一个表空间还是每张表对应一个表空间(默认后者)
# 查看MySql中的变量
show variables like 'innodb_file_per_table';
MyISAM
MyISAM 是 MySQL 早期的默认存储引擎 特点:
- 不支持事务,不支持外键
- 支持表锁,不支持行锁
- 访问速度快
文件:
- xxx.sdi: 存储表结构信息
- xxx.MYD: 存储数据
- xxx.MYI: 存储索引
Memory
Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
特点:
- 存放在内存中,速度快
- hash索引(默认)
文件:
- xxx.sdi: 存储表结构信息
存储引擎的特点
| 特点 | InnoDB | MyISAM | Memory |
|---|---|---|---|
| 存储限制 | 64TB | 有 | 有 |
| 事务安全 | 支持 | - | - |
| 锁机制 | 行锁 | 表锁 | 表锁 |
| B+tree索引 | 支持 | 支持 | 支持 |
| Hash索引 | - | - | 支持 |
| 全文索引 | 支持(5.6版本之后) | 支持 | - |
| 空间使用 | 高 | 低 | N/A |
| 内存使用 | 高 | 低 | 中等 |
| 批量插入速度 | 低 | 高 | 高 |
| 支持外键 | 支持 | - | - |
存储引擎的选择
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
InnoDB: 如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,则 InnoDB 是比较合适的选择- MyISAM: 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不高,那这个存储引擎是非常合适的。
- Memory: 将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。Memory 的缺陷是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性
电商中的足迹和评论适合使用 MyISAM 引擎,缓存适合使用 Memory 引擎。
索引
索引是帮助 MySQL 高效获取数据的数据结构(有序) 。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。
优点:
- 提高数据检索效率,降低数据库的IO成本
- 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗
缺点:
- 索引列也是要占用空间的
- 索引大大提高了查询效率,但降低了更新的速度,比如 INSERT、UPDATE、DELETE
索引结构
| 索引结构 | 描述 |
|---|---|
| B+Tree | 最常见的索引类型,大部分引擎都支持B+树索引 |
| Hash | 底层数据结构是用哈希表实现,只有精确匹配索引列的查询才有效,不支持范围查询 |
| R-Tree(空间索引) | 空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少 |
| Full-Text(全文索引) | 是一种通过建立倒排索引,快速匹配文档的方式,类似于 Lucene, Solr, ES |
| 索引 | InnoDB | MyISAM | Memory |
|---|---|---|---|
| B+Tree索引 | 支持 | 支持 | 支持 |
| Hash索引 | 不支持 | 不支持 | 支持 |
| R-Tree索引 | 不支持 | 支持 | 不支持 |
| Full-text | 5.6版本后支持 | 支持 | 不支持 |
BTree
红黑树也存在大数据量情况下,层级较深,检索速度慢的问题。
为了解决上述问题,可以使用 B-Tree 结构。
B-Tree (多路平衡查找树) 以一棵最大度数(max-degree,指一个节点的子节点个数)为5(5阶)的 b-tree 为例(每个节点最多存储4个key,5个指针)
B-Tree 的数据插入过程动画参照:www.bilibili.com/video/BV1Kr…
演示地址:www.cs.usfca.edu/~galles/vis…
B+Tree
与 B-Tree 的区别:
- 所有的数据都会出现在叶子节点
- 叶子节点形成一个单向链表
MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。(双向循环链表)
Hash
哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。
特点:
- Hash索引只能用于对等比较(=、in),不支持范围查询(betwwn、>、<、…)
- 无法利用索引完成排序操作
- 查询效率高,通常只需要一次检索就可以了,效率通常要高于 B+Tree 索引
存储引擎支持:
- Memory
- InnoDB: 具有自适应hash功能,hash索引是存储引擎根据 B+Tree 索引在指定条件下自动构建的
面试题
- 为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?
- 相对于二叉树,层级更少,搜索效率高
- 对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低
- 相对于 Hash 索引,B+Tree 支持范围匹配及排序操作
索引分类
| 分类 | 含义 | 特点 | 关键字 |
|---|---|---|---|
| 主键索引 | 针对于表中主键创建的索引 | 默认自动创建,只能有一个 | PRIMARY |
| 唯一索引 | 避免同一个表中某数据列中的值重复 | 可以有多个 | UNIQUE |
| 常规索引 | 快速定位特定数据 | 可以有多个 | |
| 全文索引 | 全文索引查找的是文本中的关键词,而不是比较索引中的值 | 可以有多个 | FULLTEXT |
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:
| 分类 | 含义 | 特点 |
|---|---|---|
| 聚集索引(Clustered Index) | 将数据存储与索引放一块,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个 |
| 二级索引(Secondary Index) | 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 | 可以存在多个 |
聚集索引选取规则:
- 如果存在主键,主键索引就是聚集索引
- 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引
- 如果表没有主键或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引
思考题
- 以下 SQL 语句,哪个执行效率高?为什么?
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引
答:第一条语句,因为第二条需要回表查询,相当于两个步骤。
语法
# 创建索引:
create [unique | fulltext] index 索引名 on 表名 (表中的列名, ...);
# 如果不加 CREATE 后面不加索引类型参数,则创建的是常规索引
# 可以一个索引对应多列,如果对应单列就叫单列索引
# 查看索引:
show index from 表名;
# 删除索引:
drop index 索引名 on 表名;
# 例子
-- name字段为姓名字段,该字段的值可能会重复,为该字段创建索引
create index idx_user_name on tb_user(name);
-- phone手机号字段的值非空,且唯一,为该字段创建唯一索引
create unique index idx_user_phone on tb_user (phone);
-- 为profession, age, status创建联合索引
create index idx_user_pro_age_stat on tb_user(profession, age, status);
-- 为email建立合适的索引来提升查询效率
create index idx_user_email on tb_user(email);
-- 删除索引
drop index idx_user_email on tb_user;
使用规则
最左前缀法则
如果索引关联了多列(联合索引),要遵守最左前缀法则,最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。
如果跳跃某一列,索引将部分失效(后面的字段索引失效)。
联合索引中,出现范围查询(<, >),范围查询右侧的列索引失效。可以用>=或者<=来规避索引失效问题。
# 例子
给 name, age, idcard 三个建立联合索引(顺序)
# 通过索引查询,三个都用上
slect * from employee where name = 'hsy', age = 24, idcard = '3411...';
# 通过索引查询,只用上name
select * from employee where name = 'hsy';
# 通过索引查询,用上name和age
select * from employee where name = 'hsy', age = 24;
# 通过索引查询,只用上name
select * from employee where name = 'hsy', idcard = '3411...';
# 通过索引查询,三个都用上
select * from employee where idcard = '3411...name = 'hsy', age = 24;
# 通过索引查询,只用上name和age
slect * from employee where name = 'hsy', age > 24, idcard = '3411...';
# 总结来说,不取决于写的顺序,而是取决于有没有出现
索引失效情况
- 在索引列上进行运算操作,索引将失效。如:
explain select * from tb_user where substring(phone, 10, 2) = '15'; - 字符串类型字段使用时,不加引号,索引将失效。如:
explain select * from tb_user where phone = 17799990015;,此处phone的值没有加引号 - 模糊查询中,如果仅仅是尾部模糊匹配,索引不会是失效;如果是头部模糊匹配,索引失效。如:
explain select * from tb_user where profession like '%工程';,前后都有 % 也会失效。 - 用 or 分割开的条件,如果 or 其中一个条件的列没有索引,那么涉及的索引都不会被用到。
- 如果 MySQL 评估使用索引查询比全表查询更慢,则不使用索引。
SQL 提示
是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。
# 例如,使用索引:
explain select * from tb_user use index(idx_user_pro) where profession="软件工程";
# 不使用哪个索引:
explain select * from tb_user ignore index(idx_user_pro) where profession="软件工程";
# 必须使用哪个索引:
explain select * from tb_user force index(idx_user_pro) where profession="软件工程";
use 是建议,实际使用哪个索引 MySQL 还会自己权衡运行速度去更改,force 就是无论如何都强制使用该索引
覆盖索引&回表查询
尽量使用覆盖索引(查询使用了索引,并且需要返回的列,在该索引中已经全部能找到),减少 select *
explain 中 extra 字段含义:
using index condition:查找使用了索引,但是需要回表查询数据
using where; using index;:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询
如果在聚集索引中直接能找到对应的行,则直接返回行数据,只需要一次查询,哪怕是select *;如果在辅助索引中找聚集索引,如select id, name from xxx where name='xxx';,也只需要通过辅助索引(name)查找到对应的id,返回name和name索引对应的id即可,只需要一次查询;如果是通过辅助索引查找其他字段,则需要回表查询,如select id, name, gender from xxx where name='xxx';
所以尽量不要用select *,容易出现回表查询,降低效率,除非有联合索引包含了所有字段
面试题:一张表,有四个字段(id, username, password, status),由于数据量大,需要对以下SQL语句进行优化,该如何进行才是最优方案:
select id, username, password from tb_user where username='itcast';
解:给username和password字段建立联合索引,则不需要回表查询,直接覆盖索引
前缀索引
当字段类型为字符串(varchar, text等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘IO,影响查询效率,此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。
# 语法:
create index idx_xxxx on 表名(列名(n));
前缀长度:可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
# 求选择性公式:
select count(distinct email) / count(*) from tb_user;
select count(distinct substring(email, 1, 5)) / count(*) from tb_user;
show index 里面的sub_part可以看到接取的长度
前缀索引搜索到数据以后,还会比对整个数据是否相同(之前只比对了前缀),然后再从二级索引里面向下查找看看是否还有满足条件的
单列索引&联合索引
单列索引:即一个索引只包含单个列
联合索引:即一个索引包含了多个列
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
# 单列索引情况:
explain select id, phone, name from tb_user where phone = '17799990010' and name = '韩信';
# 这句只会用到phone索引字段
多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询
设计原则
- 针对于数据量较大,且查询比较频繁的表建立索引
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高
- 如果是字符串类型的字段,字段长度较长,可以针对于字段的特点,建立前缀索引
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价就越大,会影响增删改的效率
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询
SQL优化
插入数据
普通插入:
- 采用批量插入(一次插入的数据不建议超过1000条)
- 手动提交事务
- 主键顺序插入
大批量插入:
如果一次性需要插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令插入。
# 客户端连接服务端时,加上参数 --local-infile(这一行在bash/cmd界面输入)
mysql --local-infile -u root -p
# 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
select @@local_infile;
# 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table 'tb_user' fields terminated by ',' lines terminated by '\n';
主键优化
数据组织方式:在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(Index organized table, IOT)
页分裂:页可以为空,也可以填充一半,也可以填充100%,每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。
页合并:当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。当页中删除的记录到达 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前后)看看是否可以将这两个页合并以优化空间使用。
MERGE_THRESHOLD:合并页的阈值,可以自己设置,在创建表或创建索引时指定
文字说明不够清晰明了,具体可以看视频里的PPT演示过程:www.bilibili.com/video/BV1Kr…
主键设计原则:
- 满足业务需求的情况下,尽量降低主键的长度
- 插入数据时,尽量选择顺序插入(否则容易页分裂),选择使用 AUTO_INCREMENT 自增主键
- 尽量不要使用 UUID 做主键或者是其他的自然主键,如身份证号(长且无序)
- 业务操作时,避免对主键的修改
order by优化
- Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区 sort buffer 中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序
- Using index:通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要额外排序,操作效率高(需要建立索引)
建立索引的顺序和排序顺序要一致(eg create index ... (age, name)),则查询时也要先按年龄再按名字排序
如果order by字段全部使用升序排序或者降序排序,则都会走索引,但是如果一个字段升序排序,另一个字段降序排序,则不会走索引,explain的extra信息显示的是Using index, Using filesort,如果要优化掉Using filesort,则需要另外再创建一个索引,如:create index idx_user_age_phone_ad on tb_user(age asc, phone desc);,此时使用select id, age, phone from tb_user order by age asc, phone desc;会全部走索引
总结:
- 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则
- 尽量使用覆盖索引
- 多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)
- 如果不可避免出现filesort,大数据量排序时,可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)
group by优化
- 在分组操作时,可以通过索引来提高效率
- 分组操作时,索引的使用也是满足最左前缀法则的
如索引为idx_user_pro_age_stat,则句式可以是select ... where profession order by age,这样也符合最左前缀法则
limit优化
常见的问题如limit 2000000, 10,此时需要 MySQL 排序前2000000条记录,但仅仅返回2000000 - 2000010的记录,其他记录丢弃,查询排序的代价非常大。
优化方案:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化
# 例子
# 此语句耗时很长
select * from tb_sku limit 9000000, 10;
# 通过覆盖索引加快速度,直接通过主键索引进行排序及查询
select id from tb_sku order by id limit 9000000, 10;
# 下面的语句是错误的,因为 MySQL 不支持 in 里面使用 limit
select * from tb_sku where id in (select id from tb_sku order by id limit 9000000, 10);
# 通过内连接多表查询即可实现第一句的效果,并且能达到第二句的速度
select s.* from tb_sku as s, (select id from tb_sku order by id limit 9000000, 10) as a where s.id = a.id;
count优化
MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高(前提是不使用where)
InnoDB 在执行 count(*) 时,需要把数据一行一行地从引擎里面读出来,然后累计计数。
优化方案:自己计数,如创建key-value表存储在内存或硬盘,或者是用redis
count的几种用法:
- 如果count函数的参数(count里面写的那个字段)
不是NULL(字段值不为NULL),累计值就加一,最后返回累计值 - 用法:count(*)、count(主键)、count(字段)、count(1)
- count(主键)跟count(*)一样,因为主键不能为空;count(字段)只计算字段值不为NULL的行;count(1)引擎会为每行添加一个1,然后就count这个1,返回结果也跟count(*)一样;count(null)返回0
各种用法的性能:
- count(主键):InnoDB引擎会遍历整张表,把每行的主键id值都取出来,返回给服务层,服务层拿到主键后,直接按行进行累加(主键不可能为空)
- count(字段):没有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加;有not null约束的话,InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加
- count(1):InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一层,放一个数字 1 进去,直接按行进行累加
- count(*):InnoDB 引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加
按效率排序:count(字段) < count(主键) < count(1) < count(*),所以尽量使用 count(\*)
update优化(避免行锁升级为表锁)
InnoDB 的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁。
如以下两条语句:
update student set no = '123' where id = 1; 由于id有主键索引,所以只会锁这一行;
update student set no = '123' where name = 'test';,由于name没有索引,所以会把整张表都锁住进行数据更新,解决方法是给name字段添加索引
视图
视图(view)是一种虚拟存在的表,视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且在使用视图时动态生成的
通俗地讲,视图只保存了查询的SQL逻辑,不保存查询结果,所以我们在创建视图的时候,主要的工作就落在创建这条SQL查询语句上
-- 创建
create [or replace] view 视图名称[(列名列表)] as select语句 [with[cascaded | local] check option];
-- 例子
create or replace view stu_v_1 as select id name from student where id <= 10;
-- 查询
-- 查看创建视图语句
show create view 视图名称;
-- 查看视图数据
select * from 视图名称;
-- 修改
-- 方式1:
create [or replace] view 视图名称[(列名列表)] as select语句 [with[cascaded | local] check option];
-- 方式2:
alter view 视图名称[(列名列表) as select语句 [with[cascaded | local] check option]
--删除
drop view [if exists] 视图名称;
视图也支持数据插入操作,但是视图本身是不存储数据的,所以其实还是会插入到视图的基表当中
检查选项
当使用 with check option 子句创建视图时,MySql会通过视图检查正在更改的每个行,例如插入,更新,删除,以使其符合视图的规定。MySql允许基于另一个视图创建视图,他还会检查依赖视图中的规则以保持一致性。为了确定检查的范围,MySql提供了两个选项:cascaded 和 local,默认值为cascade。例如上面要求id小于10,那么就不可以通过视图插入id大于10的数据
-- cascaded(级联) 和 local区别
-- cascaded
create view v1 as select id name from student where id <= 20;
create view v2 as select id name from v1 where id >= 10 with check cascaded option;
那么在对v2做插入操作时,也会检查v1的条件
insert into v2 (25, 'hsy'); -- 会报错,不满足v1要求
create view v3 as select id name from v2 where id <= 15;
insert into v3 (28, 'yt');
-- 会报错,不满足v1要求,检查选项存在继承,即使不检查自己的,如果父视图有检查,也要去检查
-- local
create view v1 as select id name from student where id <= 15;
create view v2 as select id name from v1 where id >= 10 with local check cascaded option;
create view v3 as select id name from v2 where id < 20;
insert into v2 (25, 'hsy'); -- 不会报错
insert into v3 (25, 'yt'); -- 不会报错
可以理解为 cascaded 会为祖先视图添加检查选项,而locla不会,local也会检查祖先视图是否有检查选项,有就检查,没有就不检查
视图的更新
要使视图可更新,视图中的行与基础表中的行必须存在一对一的关系。如果视图包含以下任何一项,则该视图不可更新:
- 聚合函数 sum(), min(), max, count()等
- distinct
- group by
- having
- union 或 union all
视图的作用
- 简单:视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件。(封装?)
- 安全:数据库可以授权,但不能授权到数据库特定的行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据。
- 数据独立:视图可帮助用户屏蔽真实表结构变化带来的影响。(? 没讲明白)