MySQL初学者文档

1、了解数据库

1.1 为什么要用数据库?

为了数据的持久化,将数据保存起来以供之后使用

相对于保存在文件中而言,使用数据库可以更快的查询,同时也能保存更多类型的数据

1.2 什么是数据库?

DBS:数据库系统,它是数据库、数据库管理系统、数据库管理员等的统称

DB:数据库,它本质上是一个文件系统,用来存储一系列有组织的数据

DBMS:数据库管理系统,它是操作和管理数据库的软件,MySQL 属于数据库管理系统

注意,这里不能说 MySQL 是数据库,我们是通过 MySQL 来创建和操作数据库的

SQL:结构化查询语言,就是我们要写的代码

我们作为数据库管理员,使用 SQL 语句,通过数据库关系系统 ( MySQL 等 ) 来创建和操作数据库

1.3 数据库的重要性

数据库是所有软件体系中最核心的存在,因为大部分软件都需要将数据保存在数据库当中。

2、下载安装

下载

在官网下载压缩版本即可,下载完成后,解压到自己想要的目录

官网下载页面:https://www.mysql.com/downloads/

在页面选择: MySQL Community (GPL) Downloads ->> MySQL Community Server

安装

1、配置初始化配置文件

解压目录下( bin 目录同级),创建 my.ini 文件,文件内容如下

ini 复制代码
[mysqld]
# 这个是我本地的安装目录
basedir=D:\DevTools\MySQL
# 这个是我本地的数据存放目录
datadir=D:\DevTools\MySQL\data
# 端口号
port=3306
# 服务端使用的字符集默认为UTF8mb4
character-set-server=utf8mb4
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用"mysql_native_password"插件认证
default_authentication_plugin=mysql_native_password
# mysql 8.0 设置时区
default-time-zone='+8:00'
[mysql]
# 客户端默认字符集
default-character-set=utf8mb4

注意:第一步这里不需要创建 data 文件夹,初始化的时候会自动生成的

2、初始化

  • ①. 用管理员身份打开 cmd 命令行

  • ②. 命令行进入 MySQLbin 目录

    bash 复制代码
    # 默认显示的是 C 盘,直接输入 d: 进入 D盘
    d:
    # 切换到 bin 目录
    cd D:\DevTools\MySQL\bin
  • ③. 进行初始化,输入命令:mysqld --initialize --console

    • 初始化正常

      初始化正常情况下,最后一行会生成一个随机密码,格式大概如下

      bash 复制代码
      A temporary password is generated for root@localhost: SGa2Nolwsi+t

      这个随机密码需要记住,第一次登录要用到!

    • 初始化异常情况

      初始化这里可能会遇到问题,我之前遇到过 vcruntime140_1.dll 缺失 这个问题

      解决方法:百度之后,是缺少了一个配置文件,下载安装后保存在 C:\Windows\System32 即可

      这个文件我放在云盘了,一般也不会遇到问题,我就遇到一次这个文件缺失的问题,之后就没遇到过了~

      至于其他问题,可以自行百度

3、安装 MySQL 服务

  • ①. 安装服务,输入命令:mysqld --install

    • 如果提示下面这一行,就表示成功了

      bash 复制代码
      Service successfully installed.
    • 如果出现了下面这两行,表示电脑上已经有了 MySQL 服务,需要删掉(可能是之前安装过但没删除干净)

      bash 复制代码
      The service already exists!
      The current server installed: "(bin目录)\mysqld" MySQL

      解决办法:删除之前的 MySQL 服务

      bash 复制代码
      # 查看是否还有 MySQL 服务没删除
      sc query mysql
        
      # 删除 MySQL 服务
      sc delete mysql
  • ②. 启动服务,输入命令:net start mysql

  • ③. 正常修改密码

    因为服务已经启动了,所以就可以连接数据库了,使用:mysql -u root -p

    回车之后输入上面的临时密码 ( 如果已经改过密码,就输入当前密码 )

    如果开头变成了 mysql> 就表示成功连接了,接下来通过下面的语句修改密码

    sql 复制代码
    -- 下面随便一行都可以,只是为了表示大小写都可以,仅此而已
    -- 我这里是吧我 root 账号的密码修改为 root 了,最后面的单引号包裹的就是准备修改后的密码
    alter user 'root'@'localhost' identified by 'root';
    
    ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
  • ④. 修改成功后通过 exit; 退出

  • ⑤. 关闭服务,输入命令:net stop mysql

4、修改密码其他方法 ( 如果第3步不能正常修改密码,可以参考这个步骤进行修改 )

修改密码的时候可能会遇到提示,说输入的密码错误 ( 我只遇到过一次这个错误 )

  • 可能是真的输错了

  • 遇到其他情况了

针对遇到其他情况提出的解决方法如下:

bash 复制代码
# 1.先停止 mysql 服务(管理员模式命令行)
net stop mysql

# 2.进入安装的 bin 目录,根据版本不同输入下面的命令用来跳过密码验证
# mysql 5 版本
mysqld --skip-grant-tables
# mysql 8 版本
mysqld --console --skip-grant-tables --shared-memory

# 重点,保持这个命令行窗口不关闭,新开一个命令行窗口

# 3.在另一个命令行进入 bin 目录,逐步进行以下操作

# 进入 mysql
mysql
# 使用数据库 mysql
use mysql
# 刷新权限表
flush privileges;
# 修改密码
alter user'root'@'localhost' IDENTIFIED BY '123456'; 
# 刷新权限
flush privileges;

# 关闭两个命令行

5、设置环境变量

目的是为了在命令行任何地方都可以通过 mysql -u root -p 连接数据库

在系统变量 Path 中添加:D:\DevTools\MySQL\bin

6、密码插件

如果不想设定太简单的密码,可以安装密码插件,设置密码时们就需要满足其中的规则了

相关内容如下:

bash 复制代码
#首先需要登录 mysql
mysql -u root -p

# 然后安装插件
install plugin validate_password soname 'validate_password.so';

# 安装之后就可以查看规则了
show variables like 'validate_pass%';

# 然后可以通过 set global 查出的结果 = 想要给的值; 进行修改,这个就不细说了
# 相关的结果是什么意思,可以直接百度

# 不想要了可以直接删除
uninstall plugin validate_password;

密码插件

install plugin validate_password soname 'validate_password.so';

show variables like 'validate_pass%';

然后可以通过 set global 查出的结果 = 想要给的值; 进行修改

要关闭插件,需要先停止 mysql 服务,然后修改 my.cnf 文件

3、语句

这个文档里面所有的表都保存在 learn_mysql 这个数据库中,对应的建表语句也都在这个库中

写文档的时候,对应的 sql 都可以正确执行,较为复杂的语句会展示运行结果,简单的就不展示运行的结果了

3.1 数据库相关语句
sql 复制代码
-- 查看所有数据库
show databases;

-- 创建数据库
-- 1.创建时不判断,直接创建 learn_mysql,如果数据库不存在,直接创建;如果存在了,则会报错
create database learn_mysql;

-- 2.创建时判断,如果 learn_mysql 不存在,进行创建
create database if not exists learn_mysql;

-- 查看创建数据库的语句
show create database learn_mysql;

-- 使用某一个数据库
use learn_mysql;

-- 查看当前使用的数据库
select database();

-- 删除数据库
-- 1.不判断直接删除
drop database learn_mysql;

-- 2.判断后删除,如果存在 learn_mysql,对其进行删除
drop database if exists learn_mysql;

-- 根据脚本导入数据库
-- 如果用的是可视化界面,就自行百度查阅用法,我用的 DataGrip 是把文件拖进去运行就好了
-- 如果用的是原生的导入,就需要在命令行进入 mysql 后执行下面的语句
source D:\temp.sql
3.2 表相关语句

创建表常用的列的类型

  • 数值类型

    • int:标准整数

    • decimal:计算价格

  • 字符类型

    • char:固定长度字符串

    • varchar:可变长字符串

    • text:用于大文本

  • 时间类型

    • Date:YYYY-MM-DD

    • datetime:YYYY-MM-DD HH:mm:ss

    • timestamp:时间戳

  • 空类型

    • null:表示没有值或者表示未知

相关语句:

sql 复制代码
-- 进入数据库
use learn_mysql;

-- 查看数据库所有表
show tables;

-- 创建表
-- 注意:ENGINE 和 default charset 在 my.ini 已经指定了默认值,这里可以不写
--       collate 也有默认值,默认是 utf8mb4_0900_ai_ci,也可以不写
create table if not exists user (
    id int(4) not null auto_increment comment '主键id',
    name varchar(3) not null comment '姓名',
    age int(3) comment '年龄',
    birthday datetime default null comment '生日',
    primary key (id)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

-- 下面几种都是基于现有表创建新的表的方式,问题就是:我测试了之后,下面这几种方式创建的表没有主键!

-- 创建表的方式二,如果表中有数据,会复制表的数据
create table if not exists temp_user1
as
select id, name from user;

-- 创建表的方式三,复制表结构,但是不复制数据
-- 这里 sql 只需要查不出内容就可以不复制数据了,所以 where 写一个不可能的条件就好了
create table if not exists temp_user2
as
select * from user where 1 = 2;

-- 删除这两个多余的表
drop table temp_user1, temp_user2;

-- 查看表的结构
show columns from user;
-- 下面这个是查看表结构的简写模式
desc user; 

-- 查看创建表的语句
show create table user;

-- 给表添加列 (add)
alter table user add sex int(1) comment '人的性别';

-- 在表中修改列的类型 (modify)
alter table user modify sex char(1) comment '性别';

-- 修改列名 (change),修改列名的时候也可以修改列的类型
alter table user change sex person_sex varchar(1);

-- 删除列 (drop)
alter table user drop person_sex;

-- 修改表名
rename table user to student;

-- 修改自增的数字
-- 修改的原因是自增在删除记录后并不会改变
alter table student auto_increment = 3;

-- 删除表
drop table if exists student;
3.3 数据相关语句

这一块是对一个表里面的数据进行增删改查的相关语句

sql 复制代码
-- 使用数据库
use learn_mysql;

-- 依旧使用 user 表,前面删掉了,这里重新创建一下
create table if not exists user (
    id int(4) not null auto_increment comment '主键id',
    name varchar(3) not null comment '姓名',
    age int(3) comment '年龄',
    birthday datetime default null comment '生日',
    primary key (id)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

-- 插入一条数据
insert into user (id, name, age, birthday)
values(1, '小一', 15, '2021-01-01 13:33:33');

-- 插入多条数据,如果 id 是(int)自增的话,可以不插入 id ,但是需要注意 int 类型的 id 自增是有上限的
insert into user (name, age, birthday)
values('小二',15,'2021-01-02 12:12:12'),('小三',16,'2021-01-03 12:13:32');

-- 更新数据
update user
set birthday = '1999-2-22 10:10:10'
where id = 1;

-- 从表中查询某一列数据
select name from user;

-- 从表中查询多列数据
select name, age from user;

-- 查询表中所有数据
select * from user;

-- 查询时给所有结果的每一行都添加一个常量列
select 'xx学校', name, age, birthday
from user;

-- 查询数据并去除重复
-- distinct要在最前面,同时如果后面有多个字段,会根据多个字段去重,就相当于两个字段内容都一样才会去重
select distinct age
from user;

-- 条件查询,给查询结果起别名
select name as "姓名", age as "年龄"
from user;

-- 查询的时候使用别名
select u.name, u.age
from user u;

-- 条件查询 (查询名字是"小一"的人)
select name, age
from user
where name = '小一';

-- 条件查询 (查询 age < 18 的人)
select name, age, birthday
from user
where age < 18;

-- 模糊查询 (查询名字为"_一"的人,_ 只能匹配一个字符)
select name, age, birthday
from user
where name like '_一';

-- 模糊查询 (查询名字中带有 "小" 的人,% 可以代表多个字符)
select name, age, birthday
from user
where name like '%小%';

-- 匹配查找 (查找年龄为 15 或者 18 的人)
select name, age, birthday
from user
where age in (15, 18);

-- 查找年龄不为空的人
select name, age, birthday
from user
where age is not null;

-- 范围查找 (查找年龄在 [15,18] 的人)
select name, age, birthday
from user
where age between 15 and 18;

-- 查找并排序 (asc:升序,升序是默认的,可以不写;如果要降序,可以使用 desc )
select name, age, birthday
from user
order by age asc;

-- 按年龄和id排序
-- asc 和 desc 都是针对某一列的,如果有多个列,需要分别指定 asc 和 desc
select id, name, age, birthday
from user
order by age asc, id desc;

-- 查找部分结果,返回至多前 2 行的数据 (为什么用至多,因为如果只有一条数据,就只能返回一个数据呀~)
select name, age, birthday
from user
limit 2;

-- 查找部分结果,返回指定行数 m 开始的 n 条数据,下面意思就是:查找第一行开始的两条数据
-- 注意:第一个数字的索引是从 0 开始的,所以这里找的是表里面第二行和第三行的数据
select name, age, birthday
from user
limit 1, 2;

-- 查找并排序的中文问题
-- 这里需要注意中文的问题,由于是按照 ASCII 码进行排序的,所以出现中文可以用函数进行排序
-- convert(column using gbk) 使得中文可以按照拼音字符的 ASCII 码进行排序
-- convert() 函数:将某一列的类型转化成其他类型的值
select name, age, birthday
from user
order by age desc, CONVERT(name using gbk) desc;

-- 使用正则表达式查询,需要了解一下正则表达式的相关内容
-- 这里的 "小." 中的 . 表示匹配所以 匹配除换行符 \n 之外的任何单字符
select name, age, birthday
from user
where name REGEXP '小.';

-- 查询年龄大于平均值的人,注意 where 语句中不能使用函数,所以这里用了子查询
select name, age, birthday
from user
where age > (select avg(age) from user);

-- 查询并分组 (返回 年龄-人数 表)
select age, count(*) as num
from user
group by age;

-- 查询并进行字符串拼接 concat(x,y) 拼接 x 和 y
select id, CONCAT('姓名:', name) as name
from user;

-- having 和 where 的区别
-- where 条件中不能直接出现函数,在分组之前使用
-- having 条件可以使用函数,但是必须在分组之后

-- select 语句的前后次序,次序不对会报错的
select distinct 字段
from 表 
where ...
group by ...
having ...
order by ...
limit ...

-- 删除数据
delete from user where id = 5;

-- 删除的区别
-- delete: 一条一条删除数据,删除部分数据时使用
-- drop: 删除表,不再需要某个表时使用
-- truncate: 清空表,不能回滚
--           清空表时,能针对具有自动递增值的字段,自动递增会重新从1开始递增

4、简单的多表语句

4.1 外键

多表查询时候,可能会用到相互关联的表,表和表关联的时候会用到外键

  • 物理外键:数据库级别的外键,不建议使用,因为会显得很乱

  • 逻辑外键:数据库就是单纯的表,只用来保存数据,想使用外键,通过程序来实现,即逻辑外键

这里先写一下建表的时候添加物理外键的语句,分为情况一和情况二两种方式

sql 复制代码
-- 建立班级表
create table if not exists class (
    id int(2) not null auto_increment comment '主键id',
    name varchar(11) not null comment '班级名字',
    primary key(`id`)
);

-- 情况一:建立学生表的时候就定义外键
create table if not exists student (
    id int(2) not null auto_increment comment '主键id',
    name varchar(10) not null comment '学生姓名',
    age int(2) not null default '18' comment '学生年龄',
    class_id int(2) not null comment '班级id',
    primary key(id),
    -- 定义外键 key
    key FK_class_id (class_id),
    -- 约束外键,外键 class_id 引用 班级表的 id 字段
    constraint FK_class_id foreign key (class_id) references class(id)
);

-- 情况二:建立学生表之后再定义外键
create table if not exists student (
    id int(2) not null auto_increment comment '主键id',
    name varchar(10) not null comment '学生姓名',
    age int(2) not null default '18' comment '学生年龄',
    class_id int(2) not null comment '班级id',
    primary key(id)
);

-- 添加外键
-- 添加约束,外键 class_id 引用班级表的 id 字段
alter table student add constraint FK_class_id foreign key(class_id) references class(id);

-- 删除外键约束
alter table student drop foreign key FK_class_id;

-- 删除外键索引 (这里创建外键时引用的是班级表的主键,有索引,所以创建外键时 mysql 自动给外键列创建了索引)
alter table student drop index FK_class_id;

外键约束性很强,在添加数据、删除数据时有很强的约束,操作时不注意的容易报错

  • 添加外键约束后,添加外键的表 ( 从表,这里就相当于是学生表 ) 便不能随意插入或者删除数据了

    插入或者删除数据的时候,需要结合主表 ( 班级表 ) 来进行

逻辑外键则比较自由,最起码添加、删除数据的时候比较方便

  • 逻辑外键在建表的时候不定义外键,转而用代码中的逻辑来维持表与表的关系
4.2 多表关联查询

笛卡尔积和连接查询

sql 复制代码
-- 说明,这里我用逻辑外键了,因为物理外键增删改比较麻烦,但他们理论上作用一样
-- 这里图一个方便

-- 删除之前创建的班级表和学生表,这里重新创建
drop table if exists student, class;

-- 学生表,一个学生只有一个班级,所以要创建一个 class_id 来建立逻辑关联
create table if not exists student (
    id int(4) not null auto_increment comment '主键id',
    name varchar(3) not null comment '姓名',
    age int(4) comment '年龄',
    birthday datetime default null comment '生日',
    class_id int(4) comment '班级id',
    primary key (id)
);

-- 老师表,一个老师可能会带多个班级,所以不需要关联
create table if not exists teacher (
    id int(4) not null auto_increment comment '主键id',
    name varchar(3) not null comment '姓名',
    age int(4) comment '年龄',
    birthday datetime default null comment '生日',
    primary key (id)
);

-- 班级表
create table if not exists class (
    id int(4) not null auto_increment comment '主键id',
    name varchar(3) not null comment '班级名',
    student_id int(4) comment '班长id',
    teacher_id int(4) comment '班主任id',
    primary key (id)
);

-- 创建完成后,往里面添加一些数据
insert into class (name, student_id, teacher_id)
values ('一班', 1, 1),
       ('二班', 4, 2),
       ('三班', 6, 2);

insert into student (name, age, birthday, class_id)
values ('小明', 15, '2020-12-12', 1),
       ('小王', 16, '1993-12-21', 1),
       ('小赵', 15, '2021-03-11', 2),
       ('小孙', 14, '2002-06-29', 2),
       ('小张', 15, '2013-08-16', 3),
       ('小陈', 15, '2014-06-18', 3),
       ('小李', 16, '2000-01-21', 3);

insert into teacher (name, age, birthday)
values ('高老师', 38, '1973-02-02'),
       ('侯老师', 39, '1978-03-09');


-- 笛卡尔积:左表每一行记录与右表所有记录进行匹配,返回的结果是两个表的乘积
-- 结果是:student 表的每一行记录都会匹配 class 的每一行,结果就是 7×3=21 条记录
select * from student, class;

-- 针对笛卡尔积,可以通过过滤得到正确有用的数据
-- 过滤的方式有内连接、左外连接、右外连接三种,这三种连接的过滤都建立在笛卡尔积的基础之上
-- 什么意思呢?就是这三种连接方式,都是在上面的笛卡尔积的结果上进行过滤的

-- 内连接: 在笛卡尔积的基础上,筛选出满足 on 条件的记录
-- 结果是:上面的 21 条记录中,只返回了 s.class_id = c.id 的结果
select * from student as s inner join class as c on s.class_id = c.id;

-- 左外连接:在笛卡尔积的基础上,以左表的匹配字段为基准,筛选出右表符合条件的记录
-- 结果是:上面的 21 条记录中,以左表的 class_id 为基准,筛选出右表中 id = s.class_id 的记录
select * from student as s left outer join class as c on s.class_id = c.id;

-- 右外连接: 在笛卡尔积的基础上,以右表的匹配字段为基准,筛选出左表符合条件的记录
-- 结果是:上面的 21 条记录中,以右表的 id 为基准,筛选出左表中 class_id = c.id 的记录
select * from student as s right outer join class as c on s.class_id = c.id;

-- 嵌套查询
select s.name as '姓名', (select c.name from class as c where s.class_id = c.id) as '班级'
from student as s;

-- 子查询 查询二班学生的信息
select name, age, class_id
from student
where class_id = (
	select id from class
    where name = '二班'
);

unionunion all

当需要把多个 select 语句的结果作为一个整体时,可以使用下面这俩关键字:

  • union 会返回多个结果集 ( 最少两个 ) 的并集,对结果集重复的部分,会进行去重

  • union all 会返回多个结果集 ( 最少两个 ) 的并集,对结果集重复的部分,不会去重,所以这个实现起来效率高

注意这两个关键字使用的前提:防止引起不必要的麻烦,结果集查询出的的列最好是一样的

sql 复制代码
-- 查询 student 表的 id 和 teacher 表的 id,对一样的进行去重
select id from student
union
select id from teacher

-- 不去重
select id from student
union all
select id from teacher

-- 应用:可以 union all 两个或多个多表关联的结果以实现查询所有数据
-- 这个例子并不恰当,仅做一个演示用,查询一班学生的姓名和班级,查询二班的学生的姓名和班级,将这俩结果合并一下
select s.name, c.name from student as s right outer join class as c on s.class_id = c.id where c.name = '一班'
union all
select s.name, c.name from student as s left outer join class as c on s.class_id = c.id where c.name = '二班';

naturalusing

natural 会自动查询两个表中所有相同的那个字段 ( 两个表中只能有一个字段相同,不然查询不到结果 ),然后将那一个字段作为内连接的 on 条件

using 也是类似的作用,不过需要在后面指定具体的字段名称

感觉这两个了解一下就好了,using() 看起来可以简化代码,但 natural 貌似没什么用

sql 复制代码
-- 假设 a 表字段为 a_id, temp_id, other_id
-- 假设 b 表字段为 b_id, temp_id, other_id

-- 此时根据 temp_id 和 other_id 查询时,下面两个语句是等价的
select t1.*, t2.* from a as t1 join b as t2 on t1.temp_id = t2.temp_id and t1.other_id = t2.other_id;
select t1.*, t2.* from a as t1 join b as t2 using(temp_id, other_id);

5、函数

函数有好多,这里就是随便写几个

5.1 简单函数
sql 复制代码
-- 首先创建一个用来测试的表
create table if not exists learn_simple_fun (
    id int(2) not null auto_increment comment '主键id',
    test_number decimal(10, 2) comment '测试数字',
    test_str varchar(10) comment '测试字符串',
    test_date date comment '测试时间1',
    test_datetime datetime comment '测试时间2',
    primary key(id)
);

-- 一、数字相关
-- 添加测试数据
insert into learn_simple_fun(id, test_number)
values (1, 123.456),
       (2, 223.45),
       (3, 323.4),
       (4, 4230);

-- 查询结果
-- 会发现小数点后面的数字都是两位,对于不足两位的会补0,超出两位的会四舍五入保存
select * from learn_simple_fun;

-- 1.截断小数位数,不会四舍五入
select id,
       truncate(test_number, 0) as '整数位',
       truncate(test_number, 1) as '保留一位小数',
       truncate(test_number, -3) as '去除小数并从个位开始将数字替换成0'
from learn_simple_fun;

-- 2.四舍五入并保留指定位数
select round(test_number) as '四舍五入保留整数位',
       round(test_number, 1) as '四舍五入保留一位小数'
from learn_simple_fun;

-- 二、字符串相关
-- 1.判断字符的长度,length() 判断时会有中文的问题,所以可以用 char_length() 判断
select length(test_str) as len from learn_simple_fun;
select char_length(test_str) as len from learn_simple_fun;

-- 2.连接多个字符串, concat_ws 可以指定连接符
select concat(test_str, ' ', test_number) as res from learn_simple_fun;
select concat_ws(' -> ', id, test_str) as res from learn_simple_fun;

-- 替换字符串,这里是查询,对数据库中的字段无影响,只是返回一个结果
-- MySQL 的索引是从 1 开始的!!!
-- 3.insert(str, index, len, newStr): 从 str 的第 index 位置开始,将 len 长度的字符替换为 newStr
select insert(test_str, 1, 3, '**') as res from learn_simple_fun;

-- 4.replace(str, old, new): 将 str 中的 old 字符替换为 new 字符,没有的话就不替换
select replace(test_str, '你', '*') as res from learn_simple_fun;

-- 5.substr(str, index, len): 截取字符串 str,从 index 的位置开始,截取长度为 len
select substr(test_str, 1, 2) as res from learn_simple_fun;

-- 6.left(): 返回最左边的几个字符
select left(test_str, 2) as res from learn_simple_fun;
-- right(): 返回最右边的几个字符
select right(test_str, 2) as res from learn_simple_fun;

-- 7.lpad(str, len, pad): 从左侧开始填充字符串 str 直到长度为 len,填充内容为 pad
--   如果字符串长度超过 len,结果表现为:从左边开始截取 len 长度的字符串
select lpad(test_str, 8, '#') as res from learn_simple_fun; -- hello 会变成 ###hello
--   如果 pad 很长,假如是 '#123123',说起来难以理解,直接看下面,注释就是对应的结果
select lpad(test_str, 8, '#123123') as res from learn_simple_fun;  -- hello 会变成 #12hello
-- rpad(str, len, pad): 从右侧开始填充字符串 str 直到长度到 len,填充内容为 pad
select rpad(test_str, 10, ' ') as res from learn_simple_fun;

-- 8.trim(): 去掉前后的空格
select concat('  ', test_str, '  ') as old, trim(concat('  ', test_str, '  ')) as new from learn_simple_fun;

-- 9.reverse(): 反转
select reverse(test_str) as res from learn_simple_fun;


-- 三、时间相关
-- 添加测试数据
update learn_simple_fun set test_date = '2100-01-01 11:11:11', test_datetime = '2100-01-01 11:11:11' where id = 1;
update learn_simple_fun set test_date = '2200-02-02 12:12:12', test_datetime = '2200-02-02 12:12:12' where id = 2;
update learn_simple_fun set test_date = '2300-03-03 13:13:13', test_datetime = '2300-03-03 13:13:13' where id = 3;
update learn_simple_fun set test_date = '2400-04-04 14:14:14', test_datetime = '2400-04-04 14:14:14' where id = 4;

-- 查看系统当前的 年月日、时分秒、时间
select curdate() as '年月日', curtime() as '时分秒', now() as '当前时间1', sysdate() as '当前时间2';

-- 时间计算,对时间进行操作
-- date_add(日期字段, interval 数字 单位):用于给日期字段增加,下面是增加一天的写法,20天,100年都类似
select test_datetime as now, date_add(test_datetime, interval 1 day ) as future from learn_simple_fun;
-- date_sub():用于给日期字段减少
select test_datetime as now, date_sub(test_datetime, interval 1 day ) as future from learn_simple_fun;

-- 日期间隔天数
select datediff(test_datetime, '2022-10-10') as res from learn_simple_fun;
select datediff(now(), '2022-10-10') as res;

-- 时分秒间隔,注意这个有上限,日期不要相差太大
select timediff(now(), '2022-11-11 10:10:10') as res;

-- 给给定时间增加多少时间,只能加时分秒
select addtime('2022-11-11 10:10:10', 10) as res;
select addtime('2022-11-11 10:10:10', '12:00:00') as res;
select addtime(test_datetime, '12:00:00') as res from learn_simple_fun;

-- 给给定时间减少多少时间
select subtime('2022-11-11 10:10:10', '239:45:02') as res;

-- 时间格式化
select date_format(now(), '%Y-%m-%d %H:%i:%s') as res;
select time_format(now(), '%H:%i:%s') as res;


-- 四、流程控制
-- IF(value, res1, res2):如果 value 为真,返回 res1,否则返回 res2
select id, name, if(age >= 18, '成年人', '未成年') as type from student;

-- ifnull(value, value1):如果 value 是 null,返回 value1
select id, name, ifnull(student_id, '没有班长') as '班长', teacher_id from class;

-- case when 条件1 then 结果1 when 条件2 then 结果2 else 结果3 end,相当于 if...else if...else if...
select name, case when age >= 18 then '成年'
                  when age >= 16 then '比16岁大'
                  else '小孩' end as type
from student;

-- case key when 常量1 then 操作1 when 常量2 then 结果2 end,相当于 switch(key) case...
-- 给 18,19,20 岁的孩子年龄进行不同操作(不影响数据库)
select name, age, case age when 13 then age * 3
                           when 14 then age * 2
                           when 15 then age * 1
                           end as newAge
from student
where age in (13, 14, 15);


-- 五、md5 加密
select md5('123') as res;
5.2 聚合函数

聚合函数是针对于一组数据,返回一个值,会配合 group by 分组和 having 过滤使用

这里随便列举几个函数:

  • avg():求平均值

  • sum():求和

  • max():求最大值

  • min():求最小值

  • count():求数量

sql 复制代码
-- 查询出学生年龄的平均值
select avg(age) as '平均值' from student;

-- 查询学生年龄累加的总和
select sum(age) as '总和' from student;

-- 查询学生年龄最大值和最小值
select max(age) as '最大值', min(age) as '最小值' from student;

-- 查询日期最大值和最小值
select max(test_date) as '最大日期', min(test_date) as '最小日期' from learn_simple_fun;

-- 查询表一共有多少条记录
-- 其中根据字段统计时,如果字段为null,则会过滤掉,所以不一定准确
select count(1) from student;
select count(*) from student;
select count(id) from student;

6、子查询

子查询的本质是将两个查询关联起来,写成一个查询

sql 复制代码
-- 查询年龄大于一班平均年龄的所有学生

-- 先用一个查询求出一班的平均年龄
select avg(age) from student where class_id = 1;
-- 再执行查询,查询年龄大于上面查询结果的
select * from student where age > 15;

-- 写成一个就是这样了
select * from student
where age > (
    select avg(age)
    from student
    where class_id = 1
    );

-- 子查询有多个结果时,需要用到如下的操作符:
-- in:主查询的条件 in 子查询的结果,就跟之前的 where age in (10,20,30) 一样的效果
-- any:用在比较时,子查询有多个结果时,主查询的条件满足其中一个就好
-- all:用在比较时,子查询有多个结果时,主查询的条件需要全部满足才行
-- some:同 any

-- 查询年龄跟一班学生年龄一样的所有学生信息
select * from student
where age in (
    select age
    from student
    where class_id = 1
    );

-- 查询年龄比一班学生年龄小的学生的信息
select * from student
where age < any (
    select age
    from student
    where class_id = 1
  );

-- 查询年龄比一班所有学生年龄都小的学生的信息
select * from student
where age < all (
    select age
    from student
    where class_id = 1
  );

-- 使用时,可以把子查询的结果当做表来查询
-- 查询不同班级的平均年龄中的最小值

-- 本想嵌套一下,发现 mysql 的聚合函数不能嵌套,所以这样写不对
-- select min(avg(age)) from student group by class_id;

-- 正确的写法
select min(avg_age) as '最小平均值'
from (
    select avg(age) as avg_age
    from student
    group by class_id
    ) temp;

7、数据类型

建表时,不同字段需要用到不同的数据类型

7.1 整型

整型使用时候分为下面五个:

  • tinyint:用于枚举的数据

  • smallint:用于小范围统计数据,统计教室的桌子数量啥的

  • middleint:用于较大整数的计算,统计车站每日客流量啥的

  • int:用的最多,一般不考虑超过范围的问题

  • bigint:超级无敌大的范围,用于大型网站点击量啥的

具体情况具体分析啦,用的时候从存储空间和数字范围两方面考量,确保不会超过范围的情况下,再考虑存储空间的问题

高版本的 MySQL 不推荐在使用整型时指定宽度了,即 int(4)

zerofill:0 填充。这个不推荐使用啊!

使用了 zerofill 之后,int 会强制变成 unsigned ( 无符号 ),只能添加正数了

sql 复制代码
-- 创建临时表,看一下 zerofill 的用法
-- 下面的例子里面使用了 zerofill 之后,如果数字宽度不够 10,就会在前面补充 0

-- 注意:对于高版本的 mysql,int 不推荐指定宽度,带来的效果是 zerofill 也失去了相应的意义
-- 无论 int(1),还是 int(10),表示的数字范围都一样,宽度也一样,至于补充0,还是用其他函数去实现吧
create table if not exists temp (
    test_a int,
    test_b int zerofill
);

insert into temp(test_a, test_b)
values (1, 1),
       (12345, 12345),
       (-123, 123);

select * from temp;

drop table temp;
7.2 浮点类型

浮点数支持小数,能使用的场景就比整型要多一些

float:单精度

double:双精度

浮点数存在精度问题,例如下面的例子

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a float,
    test_b double
);

-- 插入数据
insert into temp(test_a, test_b)
values (0.1, 0.47), (0.43, 0.19), (0.57, 0.44);

select * from temp;

-- 求和,发现结果都不是 1.1
select sum(test_a), sum(test_b) from temp;

-- 删除临时表
drop table temp;

造成上面的原因是:MySQL 存储浮点类型时用的是二进制,无法精确表示一些数字,所以进行运算时会有精度问题

一般进行计算时,用 decimal 代替浮点型进行运算,以避免精度缺失

但是如果对精度需求不是很高时,使用 double 能表示的范围更大

7.3 位类型

bit 类型存储的是二进制,范围是:[0, 64],不指定长度时,默认是 bit(1)

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a bit,
    test_b bit(2)
    -- test_c bit(65) 后面的数字超过了 64 会报错,因为最大是 64
);

-- 添加测试数据
insert into temp(test_a, test_b)
values (0, 0), (1, 1);

-- 查找数据
-- 我在 DataGrip 中执行后,看到的是二进制的数据
-- 在命令行中执行时,数据则是以 16 进制存储的
select * from temp;

-- 删除临时表
drop table temp;
7.4 时间类型

时间类型也是有好几种,但是常用的感觉就是 datetime

year:年份,默认是 year(4),所以用的话直接写 year 就可以

date:年月日,常用的方式就是以 YYYY-MM-DD 的格式插入年月日

time:时分秒

datetime:年月日 时分秒,常用的方式就是 YYYY-MM-DD HH:mm:ss

timestamp:年月日 时分秒 时区,会根据时区显示不同的结果

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a year,
    test_b date,
    test_c time,
    test_d datetime,
    test_e timestamp
);

-- 测试 year
-- 年份必须在 [1901, 2155] 中
insert into temp(test_a)
values ('1901'), ('2155');

-- 测试 date
-- 时间必须是正常的时间,你写 2022-33-2 就不行,因为没有 33 月份
insert into temp(test_b)
values ('2000-1-1'), ('2222-11-6');

-- 测试 time
-- 只有一个数字就默认是秒,就会按照 00:00:xx 来显示
-- xx:xx 的格式会被识别为 小时:分钟,就会按照 xx:xx:00 来显示
-- 如果有空格,前面的数字会被当作天数,结果按照这个规则计算再保存:(3*24+xx):xx:xx
insert into temp(test_c)
values ('15'), ('12:23'), ('12:23:34'), ('3 12'), ('3 12:24');
-- 这里的 3 12 ,会把 12 当成小时

-- 测试 datetime
insert into temp(test_d)
values ('2222-12-12 12:12:12');

-- 测试 timestamp
insert into temp(test_e)
values (now());

-- 用于显示测试的时间
select * from temp;

-- 修改时区后,会发现 timestamp 的时间发生了变化
select test_e from temp;
set time_zone = '+9:00';
select test_e from temp;

-- 恢复原先的时区,删除临时表
set time_zone  = '+8:00';

select * from temp;

drop table temp;
7.5 字符串文本类型

文本类型有很多:charvarchartinytexttextmediumtextlongtextenumset

常用的 charvarchar 对比:

  • char 是固定长度字符串,一般保存长度固定且比较简短的字符串

    因为长度固定,所以占用存储空间大,但是由于是固定空间,所以检索时效率更高

  • varchar 是可变长字符串,一般存非 cahr 类型的字符串

    由于长度不固定,所以有可能会节省空间,但是空间不固定,检索时需要计算长度,所以效率较低

InnoDB 引擎中,建议使用 varchar,因为这个引擎的行存储个数没有区分固定长度和可变长度,主要影响性能的是数据行使用的存储总量,就平均值而言,varchar 更小

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a char(3),
    test_b varchar(3)
);

-- char 保存数据时,长度不足时会在末尾添加空格,查询时会将末尾的空格去掉
insert into temp(test_a)
values ('a'), ('ab '), ('abc');

select * from temp;

select char_length(test_a) from temp;  -- 'ab ' 的长度也是 2,说明他在查询时也去掉了我们字符某末尾的空格

-- varchar 在建表定义时就需要指明长度,否则会报错
insert into temp(test_b)
values ('a'), ('ab '), ('abc');

select * from temp;

select char_length(test_b) from temp;  -- 'ab ' 的长度是 3,说明他在查询时没有去掉尾部的空格

-- 删除临时表
drop table temp;

text 主要是存储文本了,它不会删除尾部的空格,但是由于它存储的是大文本,所以查询速度自然就慢了

如果是频繁使用的表,可以将 text 类型的字段分出去做个单独的表保存

enum 枚举类型,需要在建表的时候定义

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a enum('A', 'B', 'C', 'D')
);

-- 存储枚举值 或者 null
insert into temp(test_a)
values ('A'), ('B'), (null);

-- 存储枚举值的索引也可以
insert into temp(test_a)
values (1), (2);

-- 存储其他值就不行了,会报错
insert into temp(test_a)
values ('666');

-- 查询
select * from temp;

-- 删除临时表
drop table temp;

setenum 类似,都是在一个类似于集合中选择,区别在于:

  • set 可以选择集合中的多个保存,同时对于重复的还会去重

  • enum 只能选择一个进行保存

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a set('A', 'B', 'C', 'D')
);

-- 存储 set 值 或者 null 值,存储重复的值会去重
insert into temp(test_a)
values ('A'), ('A,B'), (null), ('A,B,C,D,a,A');

-- 存储索引也可以,只不过也只能一个一个存
insert into temp(test_a)
values (1), (2);

-- 存储其他值就不行了,会报错
insert into temp(test_a)
values ('666');

-- 存储小写居然也可以,我很惊讶!
insert into temp(test_a)
values ('a');

-- 查询
select * from temp;

-- 删除临时表
drop table temp;
7.6 二进制字符串类型和JSON

二进制类型主要存储图片、视频、音频等二进制数据

binaryvarbinary 就跟 charvarchar 的关系一样,我也没用过,所以这里就不再说了

主要说一下 blob,它可以容纳可变数量的数据,其中又根据存储大小分为 tinyblobblobmediumbloblongblob

但是实际中,并不会直接将图片、音频、视频直接存储在数据库中,而是将其存储在服务器的磁盘上,然后把地址存到数据库中

json 格式也能存储在数据库中,还可以通过特定方式取出 json 中的指定数据

sql 复制代码
-- 临时表
create table if not exists temp (
    test_a json
);

-- 存储 json 格式的数据
insert into temp(test_a)
values ('{"name": "小明" , "age": "18", "home": {"country": "China"}}');

-- 查询
select * from temp;

-- 使用 '->' 获取 json 中的值
select test_a -> '$.name' as name, test_a -> '$.age' as age, test_a -> '$.home.country' as country
from temp;

-- 删除临时表
drop table temp;

8、约束

约束主要是表的创建相关,对表中的字段进行限制

  • 可以在建表时定义约束

  • 建表完成后,可以通过 alter table 定义约束

8.1 非空约束

非空约束指的是表中某个字段不能为 NULL,需要留意的是,虽然不能为 NULL,但是可以是空字符串

sql 复制代码
-- 临时表
create table if not exists temp (
    id int not null,  -- 非空约束,该字段在数据库中不能为 NULL
    name varchar(10) not null,  -- 非空约束,该字段在数据库中不能为 NULL
    age int
);

-- 测试添加数据,age 可以为 null
insert into temp(id, name, age)
values (1, '小一', 18), (2, '', null);

-- 下面这条插入语句就会报错,因为 id 不能是 null
insert into temp(id, name, age)
values (null, '小三', 20);

-- 下面这样也会报错,因为我们不赋值时,默认就是给其他字段一个 NULL 了,于是乎就违反了约束
-- 这里 name 被赋值 NULL,所以报错了
insert into temp(id)
values (1);

-- 更新时也需要遵循非空约束,所以这样也会报错
update temp
set id = null
where id = 1;

-- 可以通过 alter table 修改约束,这里就是把 id 的非空约束去掉了
alter table temp modify id int;

-- 去掉约束后,这个语句就可以执行了
update temp
set id = null
where id = 1;

-- id 此时已经是没有约束了,再给它加非空约束时报错了,是因为现在表中存在 id 为 NULL 的数据,所以无法加非空约束
alter table temp modify id int not null;

-- 删除临时表
drop table temp;
8.2 唯一性约束

唯一性约束是指表中指定的列是唯一的,不能重复,虽然不能重复,但是允许是 NULL

sql 复制代码
-- 临时表
create table if not exists temp (
    id int unique ,  -- 方式一,在这里定义唯一约束,表中 id 不能重复
    name varchar(10),
    age int,
    hobby varchar(5),

    -- 方式二,在这里定义唯一约束,这里是定义了两个列为唯一约束
    -- 什么意思呢,就是指表中的 name+age 拼接后是唯一的就行了
    constraint uk_name_age unique (name, age)
);

-- 测试
insert into temp(id, name, age, hobby)
values (1, '小一', 18, '吃饭');

-- 下面这个插入会报错,因为 id 重复,并且 name + age 合起来也重复了
insert into temp(id, name, age, hobby)
values (1, '小一', 18, '吃饭');

-- id 不能重复,但是可以是 NULL
-- 注意,此时 name + age 也都默认是 NULL
insert into temp(id)
values (null), (null);

-- 给 hobby 添加唯一性约束
alter table temp add constraint uk_hobby unique(hobby);

-- 错误的取消:将 id 的唯一性约束取消,发现这样虽然可以执行成功,但是不起作用,因为此时 id 仍然是唯一约束
alter table temp modify id int;

-- 正确的取消:创建唯一约束时会创建一个唯一索引,这两者共用一个名字
--            删除唯一约束时,只能通过删除唯一索引来实现

-- 首先查看有哪些索引
select * from information_schema.table_constraints where table_name = 'temp';
-- 删除唯一约束的方法
alter table temp drop index uk_hobby;

-- 删除临时表
drop table temp;
8.3 主键约束

主键约束用来标识表中一行记录,这个标识就是主键,它可以保证每一行都是单独的记录

实际上这个标识是 非空约束 + 唯一性约束,因为主键不允许重复,也不允许为空

在最开始之前建表的时候已经用过了主键约束,这里就不再细说了

sql 复制代码
-- 临时表
create table if not exists temp (
    id int primary key ,  -- 方式一,在这里定义 id 为主键
    name varchar(10),
    age int,
    hobby varchar(5)

    -- 方式二,在这里定义主键
    -- primary key (id)
    -- 联合主键
    -- primary key(name, age)
);

-- 测试,主键测测试就不用做了

-- 删除主键
alter table  temp drop primary key;

-- 表外添加主键
alter table temp add primary key (name, age);

-- 复合主键中的两个字段都不能为 NULL
insert into temp(id, name, age, hobby)
values (1, NULL, 18, '吃饭');

-- 删除临时表
drop table temp;

主键一般都会搭配一个 auto_increment 自增,之后添加记录就不再需要管这个主键了,每次插入数据时,它会自动增加

8.4 外键约束

外键约束在前面已经提到过了,这里就不再讲怎么创建外键了,就简单写一下怎么删除外键吧

sql 复制代码
-- 建立班级表
create table if not exists temp_class (
    id int(2) not null auto_increment comment '主键id',
    name varchar(11) not null comment '班级名字',
    primary key(`id`)
);

-- 建立学生表的时定义外键
create table if not exists temp_student (
    id int(2) not null auto_increment comment '主键id',
    name varchar(10) not null comment '学生姓名',
    age int(2) not null default '18' comment '学生年龄',
    class_id int(2) not null comment '班级id',
    primary key(id),
    -- 定义外键 key
    key FK_class_id (class_id),
    -- 约束外键,外键 class_id 引用 班级表的 id 字段
    constraint FK_class_id foreign key (class_id) references temp_class(id)
);

-- 删除外键步骤:
-- 1.通过 alter 删除外键约束
alter table temp_student drop foreign key FK_class_id;
-- 2.删除外键对应的索引
show index from temp_student;  -- 查看这个表的索引
alter table temp_student drop index FK_class_id;

-- 删除临时表
drop table temp_class, temp_student;
8.5 检查约束和 default

检查约束 check 主要用于检查某个字段的值是否符合 xx 要求

sql 复制代码
-- 临时表
create table if not exists temp (
    id int,
    name varchar(20),
    age int check ( age > 0 ),  -- 这里用了检查约束
    hobby varchar(10) default '吃饭,睡觉'  -- 这里指定了 default 默认值

    -- 也可以在这里创建约束,创建的时候可以指定约束名
    -- 所有的约束都可以在这里创建
    -- constraint ch_age check ( age > 0 )
);

-- 测试,年龄 < 0 时报错了
insert into temp(id, name, age)
values (1, '小一', -1);

-- 测试,年龄 > 0 保存成功,且 hobby 会有默认值
insert into temp(id, name, age)
values (2, '小二', 10);

-- 删除 check 约束,需要根据 check 名来删除
alter table temp drop check temp_chk_1;

-- 新增 check 约束,如果报错需要看数据库中是否有数据不满足条件
alter table temp add constraint ck_age check ( age > 0 );

-- 删除 default
alter table temp modify hobby varchar(10);

-- 测试插入数据
insert into temp(id, name, age)
values (3, '小三', 10);

-- 删除临时表
drop table temp;

9、视图

视图可以让我们使用表的一部分,它本身没有数据,它的数据来源于数据库已存在的表中

对视图显示的数据修改时,会直接反馈到原表上,同理,原表的数据修改时,视图也会发生变化

由于视图是基于查询的,在不删除时,可以将视图当作存储查询的容器

视图虽然有时候可以更新,但是不要更新视图,更新表就好了呀,视图只做查询就好了,不要给自己找麻烦

sql 复制代码
-- 创建视图是依赖于下面的查询语句的
-- 创建视图1,在 select 中使用别名
create view v_stu1
as
select id as v_id, name, age
from student;

-- 通过视图查询数据
select * from v_stu1;

-- 创建视图2,在 create view 的时候使用别名
create view v_stu2 (v_id, v_name, v_age)
as
select id, name, age
from student
where class_id = 1;

-- 通过视图查询数据
select * from v_stu2;

-- 创建视图3,通过聚合函数写一个表中没有的字段
create view v_stu3
as
select class_id as class, count(*) as num
from student
group by class_id;

-- 通过视图查询数据
select * from v_stu3;

-- 创建视图4,多表关联查询
create view v_stu4
as
select s.name, s.age, c.name as class_name
from student s left join class c on s.class_id = c.id;

select * from v_stu4;

-- 创建视图5,通过视图创建视图
create view v_stu5
as
select name, class_name from v_stu4;

select * from v_stu5;

-- 查看数据库中的表和视图
show tables;

-- 查看视图结构,写法跟查看表结构的语句一样
desc v_stu4;
desc student;

-- 查看视图的创建信息,写法跟查看创建表的创建信息一样
show create view v_stu4;
show create table student;

-- 查看视图属性信息,写法跟查看表的属性信息一样
show table status like 'v_stu4';
show table status like 'student';


-- 更新视图的数据,修改后表中的数据也会更改,反之亦然
-- 更新只限于单表,对于视图中有聚合函数对应的列或者涉及到多个表,就无法更改了
-- 所以这里只有 v_stu1、v_stu2 可以更改
update v_stu1
set name = '修改'
where age = 15;

select * from v_stu1;
select * from student;

-- 视图替换,就是说存在这个名字的视图就替换,不存在就创建
-- 这里替换了 v_stu1  这个视图
create or replace view v_stu1
as
select * from class;

-- 删除视图,不会对原表造成影响,但如果视图 A 依赖于另一个视图 B,那么删除了 B 之后,A 也无法使用
-- 这里删除了 v_stu4,那么基于 v_stu4 创建的 v_stu5 就无法使用了
drop view if exists v_stu4;

-- 删除视图
drop view if exists v_stu1, v_stu2, v_stu3, v_stu4, v_stu5;

10、存储过程

存储过程:事先经过编译并存储在数据库的一段 SQL 语句的集合

存储过程和函数能将一堆复杂的 SQL 封装在一起,应用程序无需关注存储过程和函数内部的 SQL 逻辑,只需要调用即可

至于内部的 SQL,只需要在 存储过程体函数体 中定义编写即可,使用时直接调用存储过程或者函数即可

  • 存储过程提高了 SQL 的重用性,并且只在创建时编译一次,之后都不需要编译,效率高

  • 由于封装在一起,减少了操作过程中可能带来的失误

  • 减少网络传输量,保存在服务器上,客户端不需要传输了

  • 减少了 SQL 暴露在网络上的风险,同时可以设定权限,提高了安全性

10.1 存储过程的语法规则

具体格式如下:

sql 复制代码
-- 创建
create procedure 名称 (IN|OUT|INOUT 参数名 参数的数据类型...)
[characteristics ...]
BEGIN
   存储过程体
END

-- 调用
call 名称(参数)

上面的格式可以看出,存储过程中可以包含参数和返回值

其具体规则跟 java 方法定义时参数和返回值的规则差不多,参数前面的单词含义如下:

  • IN:表示当前参数为输入参数

  • OUT:当前参数为输出参数,存储过程执行完毕后,对应的客户端或者应用程序会读取到这个返回值

  • INOUT:当前参数既可以表示为输入参数也可以表示为输出参数

除了参数外,下面的 characteristics 表示创建存储过程时对存储过程的约束条件,具体用到了可以百度查

10.2 变量

MySQL 在使用存储过程和存储函数的时候,可以使用变量存储中间数据或者输出数据

变量分类

  • 系统变量,系统自带的

    • 全局系统变量,( global ) 其中定义的变量全局有效,但是如果某些变量修改后又重启了服务,就会恢复成默认值了

    • 会话系统变量,( session ) 不同的客户端会有不同的会话连接,他们彼此之间的 会话系统变量 互不影响

  • 用户变量,用户自己定义的

    • 会话用户变量,只在当前连接的会话中有效,在新建的连接中没用

    • 局部变量,只在 beginend 之间的语句块中有效,因此可见,局部变量只能用于 存储过程和存储函数

系统变量相关

sql 复制代码
-- 查看系统全局变量
show global variables;

-- 查看会话系统变量
show session variables;

-- 系统变量以 @@ 开头,其中 @@global 是全局系统变量 @@session 是会话系统变量
-- 查看指定全局系统变量的值
select @@global.max_connections;

-- 查看指定会话系统变量的值
select @@session.character_set_client;

-- 没有指定 @@global 或 @@session 时,会默认先查询 @@session 下的变量,没有的话再查 @@global 下的
select @@character_set_client, @@max_connections;

-- MySQL 运行期间,可以通过 set 修改系统变量(全局 + 会话)的值
-- 对于全局系统变量,修改后如果重启了 MySQL,那么修改会失效哦
-- 对于会话系统变量,新建一个连接的时候,修改在新的连接里是无效的

-- 修改时有两种方式:一是通过 global 或者 session 修改,二是通过 @@global 或者 @@session 修改
set global max_connections = 161;
set @@global.max_connections = 151;

set session character_set_client = 'utf8mb3';
set @@session.character_set_client = 'utf8mb4';

用户变量相关

sql 复制代码
-- 定义用户变量
-- 方式一:可以使用 = 或者 := 进行赋值
set @name = '小赵';
set @age := 99;

-- 查看定义的变量,重新启动 mysql 之后,就查不到了
select @name, @age;

-- 方式二:通过查询表中的数据设置变量,注意这里只能用 := 符号
select @user_name := name from user where id = 1;
select @user_age := age from user where id = 1;
select avg(age) into @avg_age from user;

-- 查询变量,如果查询的变量原先并没有声明,就会获取 null,例如这里的 @null_var
select @user_name, @user_age, @avg_age, @null_var;


-- 定义局部变量,需要用到 declare 关键字,并且只能放在 begin ... end 中的语句的第一行
-- 定义时需要用到存储过程或者存储函数
delimiter $
create procedure test_var()
begin
    -- 声明局部变量,第一行
    declare one int default 0;
    declare two int;
    declare three varchar(5);

    -- 给局部变量赋值,也是通过 = 或者 := 赋值
    set one = 10;
    set two := 20;
    -- 也可以通过 sql 语句中使用 into 进行赋值
    select name into three from user where id = 1;

    -- 查看
    select one, two, three;
end $
delimiter ;

call test_var();

-- 测试,通过局部变量计算某个学生和他老师的年龄差
delimiter $
create procedure test_diff(in stu_id int, out diff int)
begin
    -- 声明局部变量
    declare stu_age int;
    declare tea_age int;

    -- 这两个是为了简化 sql 而声明的变量,不然要写子查询的
    declare cls_id int;
    declare tea_id int;

    -- 找到对应学生的年龄
    select age into stu_age from student where id = stu_id;
    -- 先根据 学生id 找到 班级id,然后在班级表通过 班级id 找到 老师id,再查询 老师age
    select class_id into cls_id from student where id = stu_id;
    select teacher_id into tea_id from class where id = cls_id;
    select age into tea_age from teacher where id = tea_id;

    set diff := tea_age - stu_age;
end $
delimiter ;

-- 调用方法一:使用之前的 @diff 变量就可以
call test_diff(1, @diff);

-- 调用方法二:再定义一个变量用于接收
set @diff_two = 0;
call test_diff(1, @diff_two);

-- 查看
select @diff, @diff_two;
10.3 使用存储过程

使用前需要注意一点,就是 MySQL 执行的时候遇到了 ; 就会直接执行分号之前的语句了,但是存储过程体中的 sql 又需要分号结尾,那怎么办呢 ?

哎嘿,别急,这里有了一个新东西:delimiter,它可以指定语句分隔符,不指定的时候默认就是 ;

具体步骤是这样的:delimiter 修改了语句分隔符后,你写你的代码,写完之后再补充一个分隔符,两个分隔符中间的代码会被认为是一块内容。最后再通过 delimiter 将分隔符修改为默认的 ; 即可

sql 复制代码
-- 1.创建一个简单的存储过程 (无参无返回值) ,查看学生表所有信息
--   在定义之前先修改一下分隔符为 $,防止 mysql 遇到主体的 sql 语句时直接执行导致报错
delimiter $
create procedure select_all_student()
begin
    select * from student;
end $
-- 定义完成之后记得改回来
delimiter ;

-- 调用存储过程
call select_all_student();


-- 2.使用 in (有参无返回值),查看某个学生所有信息
delimiter $
create procedure select_one_student(in stu_name varchar(3))
begin
    select * from student where name = stu_name;
end $
delimiter ;

-- 调用的时候传参
call select_one_student('小李');


-- 3.使用 out (无参有返回值),查看学生表的年龄最大值
delimiter $
create procedure select_max_age(out max_age int)
begin
    select max(age) into max_age from student order by age desc;
end $
delimiter ;

-- 这里用到了变量
call select_max_age(@res);
select @res;


-- 4.使用 in + out (有参数有返回值),查看某个学生的年龄
delimiter $
create procedure select_age_by_name(in stu_name varchar(3), out stu_age int)
begin
    select age into stu_age from student where name = stu_name;
end $
delimiter ;

-- 这里可以直接把 '小李' 当作参数,也可以通过 set 创建一个变量,然后把变量传进去
set @name = '小李';
call select_age_by_name(@name, @age);
select @age;


-- 5.使用 inout (有参有返回值),查看某个学生的班主任姓名
delimiter $
create procedure select_tea_name(inout args_name varchar(3))
begin
    select name into args_name
    from teacher
    where id = (
        select teacher_id
        from class
        where id = (
            select class_id
            from student
            where name = args_name
            )
        );
end $
delimiter ;

-- 这里 @name 既是参数又是返回值
set @name = '小李';
call select_tea_name(@name);
select @name;
10.4 存储函数

这里是存储函数,不是存储过程,但是由于两者比较相似,但也有区别:

  • 创建格式类似,但是用到的关键字不同,格式也有细微区别

  • 调用方式不同

  • 返回值不同

    • 存储过程可以没有,也可以有好多个

    • 存储函数必须有且只有一个返回值

  • 应用场景不同

    • 存储过程一般用于更新,能够进行事务操作,但是不能在查询语句中用到

    • 存储函数一般用于查询结果是一个值且需要返回时,可以在查询语句中使用

创建方式:

sql 复制代码
-- 创建
create function 名称 (参数名 参数的数据类型...)
returns 返回值类型
[characteristics ...]
BEGIN
   return (函数体);
END

-- 调用存储函数
select 名称(参数)

具体使用:

sql 复制代码
-- 这里需要制定 characteristics 规范,不然会报错
-- 1.创建存储函数,不传参数
delimiter $
create function select_age()
returns int
-- 下面这三行就是所谓的规范,后面是我的注释
deterministic -- 表示结果是确定的,即相同的输入会有相同的结果
contains sql -- 表示下面的代码包含了 sql 语句
reads sql data -- 表示 sql 包含了读数据的语句
begin
    return (select age from student where name = '小李');
end $
delimiter ;

select select_age();

-- 2.传入参数
delimiter $
create function age_by_name(stu_name varchar(3))
returns int
-- 下面这三行就是所谓的规范,后面是我的注释
deterministic -- 表示结果是确定的,即相同的输入会有相同的结果
contains sql -- 表示下面的代码包含了 sql 语句
reads sql data -- 表示 sql 包含了读数据的语句
begin
    return (select age from student where name = stu_name);
end $
delimiter ;

set @name = '小李';
select age_by_name(@name);
10.3 查看、修改和删除

这里就直接写例子吧,看着也不是很难的样子

sql 复制代码
-- 查看存储过程的创建
show create procedure select_max_age;

-- 查看存储函数的创建
show create function age_by_name;

-- 查看存储过程的状态信息
show procedure status like 'select_%';

-- 查看存储函数的状态信息
show function status like 'age_%';

-- 在系统的表中查看,这里注意的是后面的 TYPE 必须是大写的 FUNCTION 或者 PROCEDURE
select * from information_schema.ROUTINES
where ROUTINE_NAME like 'age_%' and ROUTINE_TYPE = 'FUNCTION';

修改的话,是不能修改已经写好的 存储过程体 和 函数体 的,只能修改它的规范,就是 characteristics

sql 复制代码
-- 修改指定的 存储过程或者存储函数的规范
-- 写法格式类似,如下:
alter function|procedure 名称
特性


-- 修改存储过程,修改成调用者可以执行
alter procedure select_all_student
sql security invoker
comment '查询所有学生信息'

-- 修改存储函数
alter function age_by_name
comment '根据姓名查找年龄'

删除就跟删除表格式一样

sql 复制代码
drop procedure if exists select_all_student;

drop function if exists age_by_name;

11、错误和流程控制

11.1 错误

这里的错误指的是:sql 执行时可能会遇到错误导致程序中断无法继续下去

感觉 sql 的错误处理机制主要是用在存储过程中,这个错误处理机制主要是有下面两个部分:

  • 定义条件:定义可能会出现的问题,给错误码命名

    • 错误码又分为:数值错误代码 mysql_error_code 和 字符串错误代码 sqlstate_value
  • 处理程序:遇到问题时采取的处理措施,并使存储过程或存储函数在遇到警告时可以继续往下执行

演示一下错误:

sql 复制代码
-- 演示错误
-- 里面的 sql 不对,因此执行这个存储过程就会报错了
delimiter $
create procedure update_user()
begin
    update user
    set id = null
    where id = 2;
end $
delimiter ;

-- 调用,会报错:[23000][1048] Column 'id' cannot be null
-- 这里的 1048 就是 mysql_error_code, '23000' 是 sqlstate_value
call update_user();

因为有两种类型的错误码,所以定义条件的写法也有两种,但需要定义条件和处理程序都需要在存储过程或存储函数中写

定义条件 的格式:

sql 复制代码
declare 错误名称 condition for 1048;  -- 这里是数字
declare 错误名称 condition for sqlstate '23000';  -- 这里是字符串

处理程序 的格式如下,但是要注意三个地方的内容

sql 复制代码
declare 处理方式 handler for 错误类型 处理语句
  • 处理方式

    • continue:表示遇到错误不处理,继续执行

    • exit:表示遇到错误马上退出

    • undo:表示遇到错误之后撤回之前的操作,但是 mysql 不支持这样的操作

  • 错误类型

    • sqlstate + sqlstate_value

    • mysql_error_code

    • 通过定义条件定义的错误名

    • sqlwarning 会匹配 01 开头的 sqlstate

    • not found 会匹配 02 开头的 sqlstate

    • sqlexception 会匹配 5 跟 6 之外的 sqlstate

  • 处理语句

    • set 变量 = 值 单条语句

    • begin ... end 复合语句

错误处理机制的示例:

sql 复制代码
-- 先定义一个存储过程,定义条件可以不写的
delimiter $
create procedure update_user()
begin

    -- 定义处理方式
    declare continue handler for 1048 set @temp = 1;
    -- 下面这个等价于上面的语句
    -- declare continue handler for sqlstate '23000' set @temp = 1;

    -- 出错的 sql
    update user
    set id = null
    where id = 2;

end $
delimiter ;

-- 调用存储过程,并查看变量
-- 会发现存储过程不再报错,而且 sql 实际也没有执行,变量值也变成了 1
call update_user();
select @temp;


-- 再写一个例子,这次使用 定义条件 + 处理程序来解决
-- 首先将 user 表的 name 设置成唯一约束
alter table user add constraint unique_name unique (name);

-- 定义存储过程
delimiter $
create procedure add_user()
begin

    -- 定义条件
    -- 定义名字不能重复的条件,其对应的码是 1062
    -- 别问我怎么知道这个码是 1062 (故意插入一条重复的记录就会报错,提示的就是 1062)
    declare name_cannot_repeat condition for 1062;

    -- 处理程序
    -- 遇到错误时插入这条数据
    declare exit handler for name_cannot_repeat
    begin
        insert into user(id, name, age, birthday) values (5, '测试2', 18, '2020-01-01 12:12:12');
    end;

    -- 插入两条重复数据制造错误 (name的唯一性约束会导致这里错误)
    insert into user(id, name, age, birthday) values (4, '测试', 18, '2020-01-01 12:12:12');
    insert into user(id, name, age, birthday) values (5, '测试', 18, '2020-01-01 12:12:12');

end $
delimiter ;

-- 调用,完成后发现 user 表多了两条数据
call add_user();
11.2 流程控制

流程控制语句在好多编程语言中都有,sql 中的流程控制语句用在存储过程中,其分为三类:顺序分支循环

其中 顺序 就是从上往下依次执行,之前例子里面的存储过程中的代码都是顺序结构

这里就主要讲一下后面两种,后两者都是在存储过程的 begin ... end 语句中写的

  • 分支

    • if 语句

      sql 复制代码
      if 表达式1 then 操作1
      elseif 表达式2 then 操作2
      else 操作3
      end if
    • case 语句

      sql 复制代码
      -- 写法一
      case 表达式
      when 值1 then 结果或语句1
      when 值2 then 结果或语句2
      else 结果或语句3
      end case
      
      -- 写法二
      case
      when 条件1 then 结果或语句1
      when 条件2 then 结果或语句2
      else 结果或语句3
      end case
  • 循环

    • loop 语句

      sql 复制代码
      名称: loop
        循环体
      end loop 名称
    • while 语句

      sql 复制代码
      名称: while 条件 do
        循环体
      end while 名称
    • repeat 语句:就好比是 java 里面的 do...while,至少会执行一次

      sql 复制代码
      名称: repeat
        循环体
      until 结束循环的条件
      end repeat 名称

分支语句

sql 复制代码
-- 1.1 if 语句 -> 存储过程中简单测试判断
delimiter $
create procedure test_if()
begin
    
    -- 定义局部变量
    declare name varchar(3);

    -- 给局部变量赋值
    set name := '小一';

    -- if 语句,名字不是 null 就按照名字查询,否则就查询所有
    if name is null
        then select * from user;
    else
        select * from user where user.name = name;
    end if;

end $
delimiter ;

-- 调用
call test_if();


-- 1.2 if 语句 -> 存储过程传入参数后进行判断
delimiter $
create procedure test_if_age(in stu_age int)
begin

    -- 没什么含义的 sql,就是为了测试 if 语句而写的
    -- 大于 20 就查询, [18,20] 就 + 1
    if stu_age > 20
        then select * from student where age = stu_age;
    elseif stu_age >= 18
        then update student set age = age + 1;
    end if;

end $
delimiter ;

call test_if_age(10);
call test_if_age(16);
call test_if_age(19);


-- 2.1 case 语句 -> 存储过程简单测试
delimiter $
create procedure test_case()
begin

    -- 定义局部变量并赋值
    declare age int;
    set age = 10;

    -- 判断,case 后面是变量,when 后面是值
    case age
        when 1 then select * from user where user.age < age;
        when 10 then select * from user where user.age = age;
    end case;
end $
delimiter ;

-- 调用
call test_case();

-- 2.2 case 语句 -> 存储过程传入参数后进行判断
delimiter $
create procedure test_case_age(in stu_age int)
begin

    -- 判断,when 后面是条件
    case
        when stu_age > 20 then select * from student where age = stu_age;
        when stu_age >= 18 then select * from student where age between 18 and 20;
    else select * from student;
    end case;

end $
delimiter ;

call test_case_age(10);
call test_case_age(18);
call test_case_age(21);

循环语句的三种循环语句写法类似

只不过在用到了 leave ( 跳出循环 ) 和 iterate ( 跳过本次循环,开始下一次循环 ) 时必须指定循环名称

同时,leave 还可以跳出存储过程体 ( 这样需要在 begin 前面定义名称 ),下面会演示用法

sql 复制代码
-- 1.1 loop 简单使用
delimiter $
create procedure loop_test()
begin
    declare count int default 0;

    test:loop
        if count >= 3
            then leave test;
        else
            set count = count + 1;
        end if;
    end loop test;

    select count;
end $
delimiter ;

call loop_test();

-- 1.2 loop -> 输出结果
-- 这里是计算每个人的平均年龄,不足 20 就继续循环,统计循环次数
delimiter $
create procedure loop_age(out num int)
begin

    -- 定义变量需要写在第一行
    declare avg_age double;
    -- 这里赋值 0
    set num = 0;
    -- 计算平均年龄
    select avg(age) into avg_age from student;

    loop_age: loop
        -- 如果大于等于 20,离开循环,否则更新年龄
        if avg_age >= 20
            then leave loop_age;
        else
            update student set age = age + 1;
        end if;

        -- 重新计算平均值
        select avg(age) into avg_age from student;
        -- 循环计数 + 1
        set num = num + 1;

    end loop loop_age;
end $
delimiter ;

-- 调用
call loop_age(@num);
select @num;


-- 2.1 while 简单使用
delimiter $
create procedure while_test()
begin
    declare count int default 0;

    while count < 3 do
        set count = count + 1;
    end while;

    select count;
end $
delimiter ;

call while_test();


-- 3. repeat 简单使用
delimiter $
create procedure repeat_test()
begin
    declare count int default 0;

    repeat
        set count = count + 1;
    until count > 3
    end repeat;

    select count;
end $
delimiter ;

call repeat_test();


-- 由于三种循环写法很类似,这里就不再给后面两种循环举例了
-- 下面展示一下 leave 和 iterate 的用法
-- 1.1 循环中使用 leave 和 iterate
delimiter $
create procedure test_one(in num int)
begin
    test:loop
        set num = num + 10;
        if num < 10
            then iterate test;
        elseif num > 20
            then select num; leave test;
        end if;

        select '测试是否开始下一次循环' + num;
    end loop test;
end $
delimiter ;

call test_one(8);

-- 1.2 leave 离开存储过程体
delimiter $
create procedure test_two(in num int)
test: begin
    if num < 0
        then leave test;
    elseif num < 100
        then select * from student where age = num;
    end if;
end $
delimiter ;

call test_two(-1);
call test_two(20);
11.3 游标

游标的内容很少,就放在存储过程和流程控制语句之后简单说明一下

先说它是干嘛的:在 存储过程存储函数 中可以定位指定结果集的每一行数据

bash 复制代码
通过 select * from user; 获取所有的结果后,可以通过在后面添加 where 语句筛选数据

游标可以不筛选而是定位结果集每一行的记录,提供给我们进行使用

就假设在一个存储过程中,我们要对公司员工的工资进行排序累加,看什么时候达到某个值的时候就咋咋咋
这个时候就可以在存储过程中通过循环游标来获取每一行记录,进行啥啥啥操作,避免写很复杂的查询语句了

感觉这个就像是:结果集就是 java 里面的集合,游标就是通过循环获取结果集的每一行数据来操作

游标为逐条读取结果集中的数据提供了解决方案

但是也有对应的问题:使用的时候会对数据加锁,业务并发量大的时候会影响效率,消耗内存资源

游标使用时的格式如下:

  • 定义游标:游标的列数跟查询语句的结果列数是一样的,在定义游标之前,可能需要定义之后要用到的局部变量

    sql 复制代码
    -- 游标是通过查询语句获取结果集的
    declare 游标名 cursor for 查询语句
  • 打开游标:打开

    sql 复制代码
    open 游标名;
  • 使用游标:使用的时候,局部变量个数要与上面查询语句的列一一对应

    sql 复制代码
    fetch 游标名 into 局部变量1, 局部变量2...;
  • 关闭游标:关闭

    sql 复制代码
    close 游标名;
sql 复制代码
-- 游标的用法
delimiter $
create procedure cursor_test(out out_age int)
begin
    -- 局部变量的定义依旧是在第一行
    declare age_total int default 0;  -- 年龄累加总和
    declare record_num int default 0;  -- 记录的行数
    declare temp_age int;  -- 用来保存游标中匹配结果集每一行的年龄

    -- 1.游标定义在局部变量之后
    declare cur_stu cursor for select age from student order by age desc;

    -- 变量赋值,这个 record_num 主要是用于后面的判断
    select count(*) into record_num from student order by age desc;

    -- 2.打开游标
    open cur_stu;

    -- 在循环中使用游标
    cur_loop:loop
        -- 3.使用游标,这样就获取到了结果集的每一行,temp_age 对应结果集的 age
        fetch cur_stu into temp_age;

        -- 进行相关操作,这里就随便写了个年龄和 >= 100 就退出循环的例子演示用法
        -- 可能会遇到加起来总和也不到 100 的情况,于是有了第二个 if
        if age_total >= 100
            then leave cur_loop;
        else
            set age_total = age_total + temp_age; -- 这里是累加年龄
            set record_num = record_num - 1;  -- 循环累加一次数量减一
        end if;

        -- 每次累加的时候都对 record_num - 1
        -- 等于 0 表示所有数据都累加了,不管有没有到 100 都得退出了
        if record_num = 0
            then leave cur_loop;
        end if;

    end loop cur_loop;

    -- 给输出赋值
    set out_age = age_total;

    -- 4.关闭游标
    close cur_stu;
end $
delimiter ;

call cursor_test(@res);
select @res;

12、触发器

触发器是在 MySQL 服务器端写的一段程序,就是说他跟存储过程一样,是写在数据库那边的一段代码

主要用于某个表发生变化时,同步更新其他表

12.1 触发器的语法规则
sql 复制代码
create trigger 触发器名
{before|after} {insert|update|delete} on 表名
for each row
触发器执行的语句;
12.2 触发器的使用

由于触发器的 before|after + insert|update|delete 写起来的步骤是一样的,这里就只选一种来写,其余的写法类似

还需要注意两个关键字:

  • NEWinsert 中表示要插入的新记录,update 中表示要修改的记录

  • OLDinsert 表示修改之前的旧记录,delete 中表示将要删除或已经删除的数据

sql 复制代码
-- 创建临时表,用于验证触发器
create table if not exists temp_log (
    id int(4) not null auto_increment comment '主键id',
    log varchar(50) not null comment '说明',
    primary key (id)
);


-- 这里用到了 delimiter 是因为下面用到了 begin...end
delimiter $
-- 创建触发器,在插入 user 表前,在 temp_log 表插入一条日志记录
create trigger before_insert_user_tri
before insert on user
for each row
begin
    insert into temp_log(log) values ('添加前...');
end $

delimiter ;


-- 测试
insert into user(name, age, birthday)
values ('t1', 23, '2021-11-11 10:10:10');

-- 这里写另一个例子:在插入前判断年龄,满足插入数据的年龄大于最小值时,才能插入数据
delimiter $
create trigger before_insert_user_age_tri
before insert on user
for each row
begin
    declare min_age int;

    select min(age) into min_age from user;

    -- 这里的 NEW 表示的是新插入的那一条数据
    if min_age > NEW.age
        then signal sqlstate 'MY666' set message_text = '年龄太小';
    end if;

end $
delimiter ;

-- 测试
insert into user(name, age, birthday)
values ('t2', 2, '2021-11-11 10:10:10');

-- 查看触发器
show triggers;

-- 删除触发器
drop trigger if exists before_insert_user_tri;
drop trigger if exists before_insert_user_age_tri;

13、窗口函数

窗口函数可以理解成一种新的查询方式。它是对数据进行分组操作的,但是和 group by 不同:

  • group by 是以后面的一个或多个字段为条件,将数据分组,最后的结果就是:同一组只会出现一条记录

  • 窗口函数 也是通过一个或多个字段分组,但是它会将同一组的数据按顺序依次显示,最后的结果是:数据的行数不变,每行的结果都会额外增加指定字段,提供给我们进行处理

窗口函数的格式

sql 复制代码
func_name over (partition by 字段名 order by 字段名 asc|desc)

func_name:窗口函数名

over:窗口范围

  • 如果是 over(),即括号里什么都不写,就跟普通语句一样,只不过结果里面会多一个的窗口函数产生的字段

  • 如果是 over(xxxxx),即括号里面有内容,可以使用下面的几个关键字

    • partition by:按照什么字段分组

    • order by:按照什么字段排序

13.1 简单使用

这里主要是通过 实现指定的查询 ,来对比 基本语句窗口函数 的写法

首先是准备工作

sql 复制代码
-- 创建表
create table if not exists learn_ck (
    id int(4) not null auto_increment comment '主键id',
    name varchar(30) not null comment '姓名',
    class_id int(4) comment '班级id',
    subject char(5) comment '科目',
    score int(3) comment '分数',
    primary key (id)
);

-- 随便插入数据
INSERT INTO learn_ck (id, name, class_id, subject, score)
VALUES (1, '小明', 1, '语文', 89),
       (2, '小明', 1, '数学', 90),
       (3, '小王', 2, '语文', 67),
       (4, '小王', 2, '数学', 25),
       (5, '小孙', 1, '语文', 85),
       (6, '小孙', 1, '数学', 64),
       (7, '小李', 3, '语文', 98),
       (8, '小李', 3, '数学', 100),
       (9, '小李', 3, '英语', 77),
       (10, '小胡', 2, '体育', 90);

这里列举几个查询,来从基本语句一步一步转成窗口函数实现

按照科目分组,每组的成绩倒序排列

基本语句写法:

sql 复制代码
select * 
from learn_ck 
order by subject asc, score desc;

结果如下:

id name class_id subject score
10 小胡 2 体育 90
8 小李 3 数学 100
2 小明 1 数学 90
6 小孙 1 数学 64
4 小王 2 数学 25
9 小李 3 英语 77
7 小李 3 语文 98
1 小明 1 语文 89
5 小孙 1 语文 85
3 小王 2 语文 67

这里接触第一个窗口函数:row_number(),它会在结果中显示当前行在同一组内的位置编号,编号从 1 开始,写法如下:

sql 复制代码
select row_number() over (partition by ck.subject order by ck.score desc) as row_num,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

-- 另一种写法,就是下面这样,把括号里面的一串写在下面了
-- 这里就是简单记录一下,用的时候两个写法都可以
select row_number() over w as row_num, ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck
window w as (partition by ck.subject order by ck.score desc);

对应的结果如下:

row_num id name class_id subject score
1 10 小胡 体育 2 90
1 8 小李 数学 3 100
2 2 小明 数学 1 90
3 6 小孙 数学 1 64
4 4 小王 数学 2 25
1 9 小李 英语 3 77
1 7 小李 语文 3 98
2 1 小明 语文 1 89
3 5 小孙 语文 1 85
4 3 小王 语文 2 67

此时又有一个查询:需要获取每个科目的第一名及其对应的 id, name, class_id

首先很容易就能想到:分组后再取 max(score) 当地一名就行了么

sql 复制代码
select subject, max(score) as score
from learn_ck
group by subject;

结果如下:

subject score
语文 98
数学 100
英语 77
体育 90

上面的结果只有 subjectscore 两个字段,但是要求的查询里面还要获取其他的字段,而用了 group by 之后,其他字段就无法写在 select 语句中了,不然会报错,这个是 sql_mode 的问题

我们学习阶段,还是尽量从语句层面解决问题,所以这个 sql_mode 暂不考虑

既然上面要查所有信息,而我们又可以通过上面的语句获取每个科目最大的成绩,那用内连接就可以查询到想要的数据了

sql 复制代码
select a.*
from learn_ck a
    inner join (select subject, max(score) as score from learn_ck group by subject) b on a.score = b.score and a.subject = b.subject;

结果如下:

id name class_id subject score
7 小李 3 语文 98
8 小李 3 数学 100
9 小李 3 英语 77
10 小胡 2 体育 90

使用窗口函数写法如下,在上面倒序排列的基础上,额外写一个 select,然后指定条件 row_num = 1 的就是第一名,row_num = 2 的就是第二名

sql 复制代码
select *
from (
    select row_number() over (partition by ck.subject order by ck.score desc) as row_num, 
        ck.id, ck.name, ck.subject, ck.class_id, ck.score
    from learn_ck ck
     ) temp
where row_num = 1;

结果如下:

row_num id name class_id subject score
1 10 小胡 体育 2 90
1 8 小李 数学 3 100
1 9 小李 英语 3 77
1 7 小李 语文 3 98

此时,小明的数学成绩是 90 分,将其修改为 100 分,数学就有 2 个 100 分了,那么这种情况下,上面两个语句结果会不会变呢

sql 复制代码
update learn_ck
set score = 100
where name = '小明' and subject = '数学';

修改小明成绩后,内连接运行结果如下:

id name class_id subject score
2 小明 1 数学 100
7 小李 3 语文 98
8 小李 3 数学 100
9 小李 3 英语 77
10 小胡 2 体育 90

窗口函数运行结果如下:

row_num id name class_id subject score
1 10 小胡 体育 2 90
1 2 小明 数学 1 100
1 9 小李 英语 3 77
1 7 小李 语文 3 98

正常认知情况下,结果应该出现 2 个数学 100 分的人,但是从运行的结果来看,窗口函数少了一行数据,这就是 row_number() 的局限性了,它在对每组的数据进行编号时,不在乎数据是否一样,顺序都是 1,2,3... 往下走

为了应对这种问题,就要介绍到另外两个函数了:rank()dense_rank(),这两个函数用法跟 row_number() 一样,区别在于排列的位置编号上,这里就用数学成绩来说明比较直观一些:

这里默认跟之前一样,分组后按照分数倒序排列,学生数学成绩分别是:小李100,小明100,小孙64,小王25

  • row_number():就直接从 1 开始编号。表现为:1-小李,2-小明,3-小孙,4-小王

  • rank():编号时会有并列效果,并且并列时编号会跳。表现为:1-小李,1-小明,3-小孙,4-小王

  • dense_rank():编号时会有并列效果,但是并列后编号不跳。表现为:1-小李,1-小明,2-小孙,3-小王

下面就小小展示一波对应的语句和结果吧

sql 复制代码
-- 根据科目分组,成绩倒序排列
-- 只是将 row_number() 修改为了 rank(),注意结果的 row_num 的变化
select rank() over (partition by ck.subject order by ck.score desc) as row_num, 
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

row_num id name class_id subject score
1 10 小胡 体育 2 90
1 2 小明 数学 1 100
1 8 小李 数学 3 100
3 6 小孙 数学 1 64
4 4 小王 数学 2 25
1 9 小李 英语 3 77
1 7 小李 语文 3 98
2 1 小明 语文 1 89
3 5 小孙 语文 1 85
4 3 小王 语文 2 67
sql 复制代码
-- 根据科目分组,成绩倒序排列
-- 只是将 row_number() 修改为了 dense_rank(),注意结果的 row_num 的变化
select dense_rank() over (partition by ck.subject order by ck.score desc) as row_num, 
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

row_num id name class_id subject score
1 10 小胡 体育 2 90
1 2 小明 数学 1 100
1 8 小李 数学 3 100
2 6 小孙 数学 1 64
3 4 小王 数学 2 25
1 9 小李 英语 3 77
1 7 小李 语文 3 98
2 1 小明 语文 1 89
3 5 小孙 语文 1 85
4 3 小王 语文 2 67

知道了上面这两个函数之后,再想要获取每个科目的第一名及其对应的 id, name, class_id 时,就可以这么写了

sql 复制代码
-- 用 rank() 来应对有多个第一
-- 但是由于数据并列后编号会跳,所以查后面的数据就不大方便
select *
from (
    select rank() over (partition by ck.subject order by ck.score desc) as row_num, ck.id, ck.name, ck.subject, ck.class_id, ck.score
    from learn_ck ck
     ) temp
where row_num = 1;

-- 用 dense_rank() 可以实现同样的效果
-- 并且由于即使数据并列了,row_num 也不会跳,所以还可以应对有多个第二第三之类的
select *
from (
    select dense_rank() over (partition by ck.subject order by ck.score desc) as row_num, ck.id, ck.name, ck.subject, ck.class_id, ck.score
    from learn_ck ck
     ) temp
where row_num = 1;
13.2 其他函数

上面到此为止,下面再介绍几个其他的函数,包括下面几个:

  • percent_rank()cume_dist()

  • first_value()last_value()nth_value()

  • lag()lead()

  • ntile()

percent_rank() 是显示当前行在组内的百分比位置。直接描述不太清除,跟 rank() 的排序结果结合着看能好一点,实际就是用 对应的 (rank_num - 1) / (组内数据的总数 - 1)

sql 复制代码
-- 根据科目分组,成绩倒序排列
select
    rank() over (partition by ck.subject order by ck.score desc) as rank_num,
    percent_rank() over (partition by ck.subject order by ck.score desc) as percent_num,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果解释一下,就是同一组里,抛去自己,看自己前面的人占总数的多少

这里用数学来说,相当于有 66% 的人分数比小孙高,100% 的人分数比小王高

rank_num percent_num id name class_id subject score
1 0 10 小胡 体育 2 90
1 0 2 小明 数学 1 100
1 0 8 小李 数学 3 100
3 0.6666 6 小孙 数学 1 64
4 1 4 小王 数学 2 25
1 0 9 小李 英语 3 77
1 0 7 小李 语文 3 98
2 0.3333 1 小明 语文 1 89
3 0.6666 5 小孙 语文 1 85
4 1 3 小王 语文 2 67

cume_dist() 表示在当前分组内,【当前行之前的行数】和【与当前行数据相同的行数】加起来占分组中数据总数的比例

sql 复制代码
-- 根据科目分组,成绩倒序排列
select
    cume_dist() over (partition by ck.subject order by ck.score desc) as cume_num,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果解释一下,就是同一组里,自己加自己前面的行数总和,占分组总行数的比例

这里用数学科目来说,因为这里是倒序排列,所以就相当于 75% 的人分数大于等于小孙的

cume_num id name class_id subject score
1 10 小胡 体育 2 90
0.5 2 小明 数学 1 100
0.5 8 小李 数学 3 100
0.75 6 小孙 数学 1 64
1 4 小王 数学 2 25
1 9 小李 英语 3 77
0.25 7 小李 语文 3 98
0.5 1 小明 语文 1 89
0.75 5 小孙 语文 1 85
1 3 小王 语文 2 67

这里首先这里需要了解一下 窗口函数的框架 这个概念,框架由两部分构成:

  • 框架单位:

    • rows:通过指定起始行和结束行来规定框架的范围

    • range:通过具有相同值的行来划定框架的范围

  • 框架范围:包括起点和终点,使用时可以只指定起点,默认终点是当前行;也可以通过 between 起点 and 终点 来指定范围

    • current row:当前行。单位是 range 时包含当前行和当前行相同的行

    • unbound preceding:窗口第一行

    • unbound following:窗口最后一行

    • n preceding:当前行的前面第 n 行。单位是 range时,包含【值 = 当前行 - n 】的行

    • n following:当前行的后面第 n 行。单位是 range时,包含【值 = 当前行 - n 】的行

上面一长串看不懂不重要,下面有例子,例子明白一二就可以了

下面几个函数都可以通过指定框架范围来实现不一样的结果,就不一一展示了

first_value() 是显示当前框架内的第一个值

sql 复制代码
-- 这里没写框架,默认的就是组内第一行到当前行
-- 于是显示的就是每组第一个值了
select
    first_value(score) over (partition by ck.subject order by ck.score desc) as first_score,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

last_val id name class_id subject score
90 10 小胡 体育 2 90
100 2 小明 数学 1 100
100 8 小李 数学 3 100
100 6 小孙 数学 1 64
100 4 小王 数学 2 25
77 9 小李 英语 3 77
98 7 小李 语文 3 98
98 1 小明 语文 1 89
98 5 小孙 语文 1 85
98 3 小王 语文 2 67

last_value() 是显示截止到当前行的框架内的最后一个值

sql 复制代码
-- 根据科目分组,成绩倒序排列
-- 这里不写框架语句,就默认是组内的第一行到当前行,然后用 last_value 获取到的就是当前行,就跟当前行的分数一样
select
    last_value(score) over (
        partition by ck.subject order by ck.score desc
        ) as first_score,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

last_val id name class_id subject score
90 10 小胡 体育 2 90
100 2 小明 数学 1 100
100 8 小李 数学 3 100
64 6 小孙 数学 1 64
25 4 小王 数学 2 25
77 9 小李 英语 3 77
98 7 小李 语文 3 98
89 1 小明 语文 1 89
85 5 小孙 语文 1 85
67 3 小王 语文 2 67

这里换一下框架的范围,换成组内的第一行到最后一行

sql 复制代码
-- 结果显示的就是每组的最小值了
select
    last_value(score) over (
        partition by ck.subject order by ck.score desc
        -- 表示范围是分组内的第一行到最后一行
        rows between unbounded preceding and unbounded following
        ) as first_score,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

last_val id name class_id subject score
90 10 小胡 体育 2 90
25 2 小明 数学 1 100
25 8 小李 数学 3 100
25 6 小孙 数学 1 64
25 4 小王 数学 2 25
77 9 小李 英语 3 77
67 7 小李 语文 3 98
67 1 小明 语文 1 89
67 5 小孙 语文 1 85
67 3 小王 语文 2 67

nth_value() 是显示当前框架内第 n 行的值

sql 复制代码
-- 这里没写框架,默认的就是组内第一行到当前行
-- 然后指定的数字是 2 ,表示显示框加第 2 行的值
select
    nth_value(score, 2) over (partition by ck.subject order by ck.score desc) as nth_val,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下,解析:这里有四门学科,所以有四组,由于要显示框架内的第2行,所以对应框架的第一行就是 null

  • 第一组只有一行数据,没有第二行,所以结果是 null

  • 第二组有四行数据,由于第一行和第二行的成绩一样,可以当作有数据,所以第一行也有了结果是 100

  • 第三组只有一行数据,所以也是 null

  • 第四组有三行数据,并且第一行和第二行成绩不一样,所以第一行会显示 null

nth_val id name class_id subject score
null 10 小胡 体育 2 90
100 2 小明 数学 1 100
100 8 小李 数学 3 100
100 6 小孙 数学 1 64
100 4 小王 数学 2 25
null 9 小李 英语 3 77
null 7 小李 语文 3 98
89 1 小明 语文 1 89
89 5 小孙 语文 1 85
89 3 小王 语文 2 67

lag() 是返回当前行前面第 n 行的结果

sql 复制代码
-- 直接写 lag(score) 默认就是前一行的数据
-- 不存在前一行的话, 就是 null,但是可以指定默认值,这里默认值是 空的
select
    lag(score, 1, '空的') over (partition by ck.subject order by ck.score desc) as lag_val,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

lag_val id name class_id subject score
空的 10 小胡 体育 2 90
空的 2 小明 数学 1 100
100 8 小李 数学 3 100
100 6 小孙 数学 1 64
64 4 小王 数学 2 25
空的 9 小李 英语 3 77
空的 7 小李 语文 3 98
98 1 小明 语文 1 89
89 5 小孙 语文 1 85
85 3 小王 语文 2 67

lead() 是返回当前行后面第 n 行的结果,和 lag() 是一对

sql 复制代码
-- 直接写 lead(score) 默认就是后一行的数据
-- 不存在后一行的话, 就是 null,但是可以指定默认值,这里默认值是 空的
select
    lead(score, 1, '空的') over (partition by ck.subject order by ck.score desc) as lead_Val,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

lead_val id name class_id subject score
空的 10 小胡 体育 2 90
100 2 小明 数学 1 100
64 8 小李 数学 3 100
25 6 小孙 数学 1 64
空的 4 小王 数学 2 25
空的 9 小李 英语 3 77
89 7 小李 语文 3 98
85 1 小明 语文 1 89
67 5 小孙 语文 1 85
空的 3 小王 语文 2 67

ntile() 就是将分组再分组

sql 复制代码
-- 就是在分组的基础上继续分组...
select
    ntile(3) over (partition by ck.subject order by ck.score desc) as ntile_val,
    ck.id, ck.name, ck.subject, ck.class_id, ck.score
from learn_ck ck;

结果如下:

ntile_val id name class_id subject score
1 10 小胡 体育 2 90
1 2 小明 数学 1 100
1 8 小李 数学 3 100
2 6 小孙 数学 1 64
3 4 小王 数学 2 25
1 9 小李 英语 3 77
1 7 小李 语文 3 98
1 1 小明 语文 1 89
2 5 小孙 语文 1 85
3 3 小王 语文 2 67

14、公共表表达式

公共表表达式也叫 CTE,这三个字母就是对应的英文的首字母缩写

咱就简单说:首先,它类似于子查询,其次,它比子查询要厉害一些,就这样~

sql 复制代码
-- 创建表用于测试
create table if not exists student(
    id int(4) not null auto_increment comment '主键id',
    name varchar(30) not null comment '学生姓名',
    class_id int(4) comment '班级id',
    primary key (id)
);

create table if not exists class(
    id int(4) not null auto_increment comment '主键id',
    name varchar(30) not null comment '班级名称',
    primary key (id)
);

-- 随便插入数据
insert into class(id, name)
values (1, '一班'),
       (2, '二班'),
       (3, '三班');

insert into student(id, name, class_id)
values (1, '小赵', 1),
       (2, '小钱', 2),
       (3, '小李', 3),
       (4, '小吴', 1),
       (5, '小郑', 1),
       (6, '小光', 1),
       (7, '小马', 2),
       (8, '小孙', 2),
       (9, '小朱', 3);

下面举例子,使用子查询和公共表表达式分别实现同样的东西

  • 子查询就是正常的写法,写就行了

  • 公共表表达式,相当于是先把子查询定义好,后面使用的时候,当作了一个临时表使用了

    但是它跟临时表的区别是,临时表相当于是创建了一个单独的表,而它后面必须跟 SQL 语句才能使用

sql 复制代码
-- 查询高中的学生信息

-- 使用子查询
select s.*
from student s
where s.class_id = (
    select c.id from class c where c.name = '高中'
    );

-- 使用 CTE
-- 需要额外注意的是:它不能单独存在,后面必须有对应的 sql 语句
with
    cls as (select id from class where name = '高中')

select s.*
from student s join cls on class_id = cls.id;

-- CTE 的其他用法
-- 可以定义好几个 as,后面的 as 语句可以用前面定义好的,像下面这样
with
    cls1 as (select id, name from class where name = '高中'),
    cls2 as (select id from cls1)

update class
set name = '修改'
where id = (select id from cls2 );


-------------------------分割线---------------------------------

-- CTE 还可以递归调用
-- 这里重新定义一个具有上下级关系的表,我们一般叫做树形结构的表
create table if not exists department(
    id int(4) not null auto_increment comment '主键id',
    name varchar(30) not null comment '名称',
    parent_id int(4) comment '父id',
    primary key (id)
);

-- 插入数据,不明白可以在纸上画一下关系
-- 最顶层是 A,第二层是 A1, A2,第三层是 A11,A21,A22,第四层是 A111,A2111
insert into department (id, name, parent_id)
values (1, 'A', null),
       (2, 'A1', 1),
       (3, 'A2', 1),
       (4, 'A11', 2),
       (5, 'A21', 3),
       (6, 'A111', 4),
       (7, 'A211', 5),
       (8, 'A22', 3);

-- 这里查询一下 A2 及其所有下属的信息
-- 因为要用到递归实现,就可以通过 CTE 写出来
-- 这里还额外用 level 表示当前的层级
with recursive
    cte as (
        select d1.id, d1.name, 0 as level
        from department d1
        where d1.id = 3
        union all
        select d2.id, d2.name, cte.level + 1 as level
        from cte join department d2 on d2.parent_id = cte.id
    )

select * from cte;

权限相关

bash 复制代码
# 切换数据库
use mysql;

# 查看用户部分信息
select host, user from user;

# 创建用户
# 第一次创建会成功,相同的语句再执行一遍就会报错了
# host 和 user 是联合逐渐,用于确保唯一性
create user 'dong'@'localhost' identified by 'dong';

# 执行完毕后,user 表中就会多出一条记录
# 然后就可以通过 dong 这个用户来进行登录了
mysql -u dong -p

# 删除用户
# 不推荐 delete 是因为直接使用 delete 删除用户表时,权限相关的内容没删掉
drop user 'dong'@'localhost';

# 如果非要使用 delete 删除,按照下面的步骤来
# 1.先查看有什么权限
show grants for 'dong'@'localhost';
# 2.撤销对应的权限
revoke xxxxx from 'dong'@'localhost';
# 3.刷新
flush privileges;
# 4.在用户表删除用户
delete from user where user = 'dong' and host = 'localhost';


# 权限
# 创建用户之后,用户就只有少部分权限,甚至没有我们业务数据库的权限,所以要进行赋权的操作

# 用户登录后,可以查看用户权限
mysql -u dong -p
show grants;

# 给用户权限,用 [有赋权权限] 的角色赋予别的用户权限
mysql -u root -p
# 这里用 root 用户给 dong 赋予可以查询 learn 库里面所有表的权限
# 具体写法:grant 权限1,权限2,... on 数据库名.表名 to 用户
grant select on learn.* to 'dong'@'localhost';

# 用 dong 登录后就可以查看 learn 下的所有表的信息了,但是没有增删改的权限
mysql -u dong -p
use learn;
select * from user;  # 可以查出结果
update user set name = 'xx' where id = 1;  # 报错,因为没有权限

# 赋予用户所有权限
# 首先用 root 用户登录,给 dong 赋予所有权限
mysql -u root -p
grant all privileges on *.* to 'dong'@'localhost';

# 上面的语句虽然赋予了所有权限,但是有一个权限没给,就是 [可以赋予别人权限的权限]
# 想要获得这个权限,就需要修改一下上面的语句,在最后面添加 with grant option 来实现
# 这个时候,dong 就与 root 权限一样了
grant all privileges on *.* to 'dong'@'localhost' with grant option;

# 收回所有权限
# 执行后,dong 就没什么权限了
revoke all privileges on *.* from 'dong'@'localhost';


# 角色相关
# 角色的作用就是方便管理具有相同权限的用户

# 创建角色 big_boss 和 small_boss
create role 'big_boss'@'%', 'small_boss'@'%';

# 给 small_boss 查看 learn 下所有表的权限
# 给 big_boss 操作 learn 下所有表的权限
grant select on learn.* to 'small_boss'@'%';
grant all privileges on learn.* to 'big_boss'@'%';

# 查看角色权限
show grants for 'small_boss';

# 给用户赋予角色身份,并进行激活
grant 'small_boss'@'%' to 'dong'@'localhost';
set default role 'small_boss'@'%' to 'dong'@'localhost';

# 查看用户权限,会发现多了一个
show grants for 'dong'@'localhost';

# 此时切换到 dong 用户,执行语句,可以看角色是否激活了
mysql -u dong -p
select current_role();  # 查看是否激活了
# 此时就可以通过 select 查看 learn 表下的信息了

# 撤销用户的角色身份
revoke 'small_boss'@'%' from 'dong'@'localhost';

# 收回角色权限
revoke select on learn.* from 'small_boss'@'%';

# 删除角色
drop role 'small_boss';

Linux 下的相关目录说明

data 数据目录

bin 相关命令目录

share、/etc/my.cnf 配置文件目录

创建数据库时,会在【数据目录】(我这里就是 data 目录) 下面生成与数据库名字相同的文件夹

如果数据库里面有了表结构,那么会在该文件夹下面生成一些文件:

  • InnoDB 引擎下

    • 5.7 版本下,会生成 db.opt、xx.frm、xx.ibd 三个文件

    • 8.0 版本下,会生成 xx.ibd 这一个文件

    其中 .opt 用来保存数据库相关设置,.frm 用来描述表结构,.ibd 用来存储数据和索引

  • MyYSAM 引擎下

    • 5.7 版本下,会生成 xx.frm、xx.MYD、xx.MYI

    • 8.0 版本下,会生成 xx_363.sdi、xx.MYD、xx.MYI 三个文件

    其中 .frm、.sdi 用来描述表结构,.MYD 用来记录数据,.MYI 用来记录索引

索引

索引有好几种类型,这里就说几个常用的:

  • 普通索引:只是用于提高查询效率,没有什么限制条件

  • 唯一性索引:索引的值必须是唯一的,一个表中可以有多个唯一性索引,值可以为空

  • 主键索引:特殊的唯一性索引,在唯一性索引的基础上添加了不为空的条件,一个表中只有一个

对于索引来说,除了上述几种类型外,还有一种分类方式是基于字段个数的,如下:

  • 单列索引:某一个字段上建立的索引

  • 多列索引,联合索引:很明显就是多个字段组合建立索引

索引的使用
bash 复制代码
# 索引的创建分为两中:一是建表的时候创建索引,另一种是有了表之后再创建索引

# 1.1  建表时候,主键约束会默认自动创建一个主键索引
#      id 字段是主键,就会默认创建一个主键索引
create table if not exists class (
    id int comment '主键id',
    name varchar(10) comment '名称',
    primary key (id)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

# 1.2  建表时候,唯一性约束和外键约束都会默认自动创建索引
#      id 字段会默认创建主键索引
#      phone 字段做了唯一性约束,所以会创建一个唯一索引
#      cls_id 有外键约束,所以也会创建一个索引
create table if not exists student (
    id int comment '主键id',
    name varchar(19) comment '姓名',
    phone varchar(11) unique comment '电话',
    cls_id int comment '班级id',
    primary key (id),
    constraint fk_stu_cls foreign key (cls_id) references class(id)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

 # 上面是在建表的时候创建外键约束,如果建表的时候不建立外键约束,反而是在建表之后通过 alter 语句添加
 # 实际上的效果是一样的,执行语句后,依旧会默认创建索引
alter table student add constraint fk_stu_cls foreign key (cls_id) references class(id);


# 1.3  建表的时候,直接创建索引
create table if not exists temp (
    id int comment '主键id',
    name varchar(19) comment '姓名',
    phone varchar(11) comment '电话',
    primary key (id),
    # 创建普通索引
    index idx_name(name),
    # 创建唯一性索引
    unique uk_idx_phone(phone),
    # 创建联合索引
    index lh_idx(id, name, phone)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

# 测试一下,因为 phone 有了唯一性索引,两条记录的 phone 一样时,无法插入数据
insert into temp(id, name, phone)
values (1, '第一', '1234'), (2, '第二', '1234');

# 2.1  已经建好的表,在表外添加索引
#      通过 alter 和 create 方式创建索引都可以
create table if not exists temp2 (
    id int comment '主键id',
    name varchar(19) comment '姓名',
    phone varchar(11) comment '电话'
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

# 创建普通索引
alter table temp2 add index idx_name(name);
create index idx_name on temp2(name);

# 创建唯一索引
alter table temp2 add unique uk_idx_id(id);
create unique index uk_idx_id on temp2(id);

# 创建联合索引
alter table temp2 add index lh_idx(id, name, phone);
create index lh_idx on temp2(id, name, phone);

# 查看一个表所有的索引
show index from temp;

# 修改索引可见性,调优时可以看索引对调优的帮助。
# 也可以当作是删除前的保险,删除前可以先隐藏,看是否有影响,没有影响就可以删除了
# mysql 8.x 不支持修改现有索引,想要修改一个索引,只能先删除再创建新的索引
# 但是有一个例外,就是【索引可见性】
# 默认索引都是可见的(visible),通过下面的语句可以设置索引为 invisible,这样查询的时候就不会走索引了
alter table temp alter index idx_name invisible;

# 删除索引,通过 alter 或者 drop 都可以实现
# 对于联合索引来说,数据库中删除掉其中的字段后,联合索引会自动去掉对应的字段
alter table temp drop index lh_idx;
drop index lh_idx on temp2;
索引的设计

索引设置的不合理,或者缺少了索引,都会对性能有一定影响,所以设计索引时候,要考虑一些准则

1、在编写 selectupdatedelete 语句时,一般都需要写 where 条件, 对经常出现在 where 条件的字段建立索引,可以提高效率

2、对于经常要用到 oder bygroup by 的字段,建立索引可以提交效率

3、对于 distinct 去重的字段,建立索引可以提高效率

4、多表连接时,尽量不要 join 超过 3 张表,严重影响效率;同时对 where 条件的字段 和 连接的条件字段创建索引,可以提交效率

5、数据类型根据实际情况优化,越小的数据类型查询起来越快;同时给小数据类型的列建立索引效率更高

6、使用最频繁的列,放在联合索引的左侧

7、多个字段要创建索引时,联合索引比单列索引好一些

8、每张表的索引个数不要太多,最多 5 ~ 6 个,因为索引不仅要占用磁盘空间,而且太多的话,增删改数据时性能索引要同步更改

不建议使用索引的情况:

  • 表的数据量小

  • 列重复数据多,例如性别

  • 经常更新的表,不要创建过多的索引,索引不要包含很多列

bash 复制代码
# sql 有多个关键字时,执行的前后顺序如下
select distinct 字段
from 表 
where ...
group by ...
having ...
order by ...
limit ...

# 例如下面三组语句中,id 使用的次数多,对 id 建立索引,效率会高
select name, age from user
where id > 10;

update user
set name = '修改'
where id = 3;

delete from user
where id > 10;

# 这个例子不符合,只是说明一下。例如对年龄进行去重,给 age 建立索引,效率就高
select distinct(age) from user;


create database learn;



use learn;

# 建表时候,主键约束会默认自动创建一个主键索引
# id 字段是主键,就会默认创建一个主键索引
create table if not exists temp (
    id int comment '主键id',
    name varchar(10) comment '名称',
    primary key (id)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

# 建表时候,唯一性约束和外键约束都会默认自动创建索引
# id 字段会默认创建主键索引
# phone 字段做了唯一性约束,所以会创建一个唯一索引
# cls_id 有外键约束,所以也会创建一个索引
create table if not exists student (
    id int comment '主键id',
    name varchar(19) comment '姓名',
    phone varchar(11) unique comment '电话',
    cls_id int comment '班级id',
    primary key (id),
    constraint fk_stu_cls foreign key (cls_id) references class(id)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

 # 上面是在建表的时候创建外键约束,如果建表的时候不建立外键约束,反而是在建表之后通过 alter 语句添加
 # 实际上的效果是一样的,执行语句后,依旧会默认创建索引
alter table student add constraint fk_stu_cls foreign key (cls_id) references class(id);

# 1.3  建表的时候,直接创建索引
create table if not exists temp (
    id int comment '主键id',
    name varchar(19) comment '姓名',
    phone varchar(11) comment '电话',
    primary key (id),
    # 创建普通索引
    index idx_name(name),
    # 创建唯一索引
    unique uk_idx_phone(phone),
    # 创建联合索引
    index lh_idx(id, name, phone)
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

# 测试一下,因为 phone 有了唯一性索引,两条记录的 phone 一样时,无法插入数据
insert into temp(id, name, phone)
values (1, '第一', '1234'), (2, '第二', '1234');

# 2.1  已经建好的表,在表外添加索引
#      通过 alter 和 create 方式创建索引都可以
create table if not exists temp2 (
    id int comment '主键id',
    name varchar(19) comment '姓名',
    phone varchar(11) comment '电话'
)ENGINE=InnoDB default charset=utf8mb4 collate=utf8mb4_general_ci;

# 创建普通索引
alter table temp2 add index idx_name(name);
create index idx_name on temp2(name);

# 创建唯一索引
alter table temp2 add unique uk_idx_id(id);
create unique index uk_idx_id on temp2(id);

# 创建联合索引
alter table temp2 add index lh_idx(id, name, phone);
create index lh_idx on temp2(id, name, phone);

# 查看一个表所有的索引
show index from temp;

# 修改索引可见性,可以当作是删除前的保险,删除前可以先隐藏,看是否有影响,没有影响就可以删了
# mysql 不支持修改现有索引,想要修改一个索引,只能先删除再创建新的索引
# 但是有一个例外,就是【索引可见性】
# 默认索引都是可见的(visible),通过下面的语句可以设置索引为 invisible,这样查询的时候就不会走索引了
alter table temp alter index idx_name invisible ;

# 删除索引,通过 alter 或者 drop 都可以实现
alter table temp drop index lh_idx;
drop index lh_idx on temp2;
相关推荐
bropro2 小时前
MySQL不使用子查询的原因
android·数据库·mysql
泯仲3 小时前
从零起步学习MySQL 第九章:从数据页的角度看B+树及MySQL中数据的底层存储原理
数据库·b树·mysql
gp3210263 小时前
开放自己本机的mysql允许别人连接
数据库·mysql·adb
原来是猿3 小时前
MySQL【表的约束下】
数据库·mysql
6+h3 小时前
【MySQL】索引原理详解
数据库·mysql
DBA小马哥3 小时前
国产数据库选型实战:MySQL迁移的兼容性、安全与性能落地
数据库·mysql·安全
sygydxfwd4 小时前
TwinCAT 3配合MySQL数据库实现ms级数据存储
数据库·mysql
happymaker06264 小时前
JDBC(MySQL)——DAY02
android·数据库·mysql
有梦想的小何5 小时前
从结算需求出发:基于库存日快照与分区的结算报表的Java实践
java·数据库·mysql