互联网环境,数据是第一生产力,数据的存储至关重要,而MySQL是大多数普通业务的第一选择。

🔖MySql基础

数据库概述

数据库的介绍

目前来说如果我们要进行数据存储,有几种方式:

  1. 我们可以使用集合等方式将数据保存在内存中,但是数据不能持久化保存,断电/程序退出,数据就清除了
  2. 我们还可以将数据保存在普通文件中,可以持久化保存,但是查找,增加,修改,删除数据比较麻烦,效率低

所以我们需要一个既可以持久化保存数据又可以方便操作的地方来存储数据,这就是我们接下来要给大家介绍的数据库

什么是数据库

​ 数据库(DataBase,DB):指长期保存在计算机的存储设备(硬盘)上,按照一定规则组织起来,可以被各种用户或应用共享的数据集合. 还是以文件的方式存在服务器的电脑上的。

​ 说白了就是数据的仓库, 用来持久化保存数据的.

数据的存储方式

  1. 数据保存在内存

    1
    2
    3
    4
    int[] arr = new int[]{1, 2, 3, 4};
    ArrayList<Integer>list = new ArrayList<Integer>();
    list.add(1);
    list.add(2);

    new出来的对象存储在堆中.堆是内存中的一小块空间

    优点:内存速度快 缺点:断电/程序退出,数据就清除了.内存价格贵

  2. 数据保存在普通文件 优点:永久保存 缺点:查找,增加,修改,删除数据比较麻烦,效率低

  3. 数据保存在数据库 优点:永久保存,通过SQL语句比较方便的操作数据库

数据库的优点

​ 数据库是按照特定的格式将数据存储在文件中,通过SQL语句可以方便的对大量数据进行增、删、改、查操作,数据库是对大量的信息进行管理的高效的解决方案。

常见的关系型数据库

  • MySql(最流行中型数据库):开源免费的数据库,中小型的数据库,已经被Oracle收购了。MySql6.x版本也开始收费。后来Sun公司收购了MySql,而Sun公司又被Oracle收购
  • Oracle(老大,最挣钱的数据库):收费的大型数据库.Oracle公司的产品.Oracle收购SUN公司,收购MySql.
  • DB2:IBM公司的数据库产品,收费的.银行系统中.
  • SQLServer(Windows上最好的数据库):MS公司.收费的中型的数据库.
  • SyBase:已经淡出历史舞台.提供了一个非常专业数据建模的工具PowerDesigner.
  • SQLite(最流行的嵌入式数据库): 嵌入式的小型数据库,应用在手机端.
  • PostgreSQL: (功能最强大的开源数据库)

image-20230927200821827

image-20230927200816947

常用数据库MYSQLOracle 在web应用中,使用的最多的就是MySQL数据库,原因如下:

  1. 开源、免费
  2. 功能足够强大,足以应付web应用开发(最高支持千万级别的并发访问

数据库结构

数据库是用来存储数据的,那么到底通过什么样的方式来存的. 结构是怎么样的呢?

​ 数据库管理程序(DBMS)可以管理多个数据库,一般开发人员会针对每一个应用创建一个数据库。为保存应用中实体的数据,一般会在数据库创建多个表,以保存程序中实体的数据。

​ 数据库管理系统、数据库和表的关系如图所示:

image-20230927200810859

  • 1.一般情况下,一个系统(软件,项目) 就设计一个数据库;
  • 2.一个数据库里面有多(>=1)张表. 一个实体(java类)对应一张表
  • 3.一张表里面有多条(>=1)记录, 一个对象对应一条记录

mysql简介

特点

  • ①MySQL数据库是用C和C++语言编写的,以保证源码的可移植性
  • ②支持多个操作系统例如:Windows、Linux、Mac OS等等
  • ③支持多线程,可以充分的利用CPU资源
  • ④为多种编程语言提供API,包括C语言,Java,PHP。Python语言等
  • ⑤MySQL优化了SQL算法,有效的提高了查询速度
  • ⑥MySQL开放源代码且无版权制约,自主性强、使用成本低。
  • ⑧MySQL历史悠久、社区及用户非常活跃,遇到问题,可以很快获取到帮助。

版本

针对不同的用户,MySQL分为两种不同的版本:

  • MySQL Community Server :社区版本,免费,但是Mysql不提供官方技术支持。
  • MySQL Enterprise Edition :商业版,该版本是收费版本,可以试用30天,官方提供技术支持
  • MySQL Cluster :集群版,开源免费,可将几个MySQL Server封装成一个Server。
  • MySQL Cluster CGE :高级集群版,需付费。
  • MySQL Workbench(GUI TOOL):一款专为MySQL设计的ER/数据库建模工具。MySQL Workbench又分为两个版本,分别是社区版(MySQL Workbench OSS)、商用版(MySQL Workbench SE)。

MySQL的命名机制使用由3个数字和一个后缀组成的版本号。例如,像mysql-8.0.26的版本号这样解释:、

  • 第1个数字(8)是主版本号,描述了文件格式。所有版本5的发行都有相同的文件格式。
  • 第2个数字(0)是发行级别。主版本号和发行级别组合到一起便构成了发行序列号。
  • 第3个数字(26)是在此发行系列的版本号,随每个新分发版递增。

目前,My SQL的最新版本为MySQL 8.0。

数据库的安装,卸载,启动,登录

实操-MySql的安装

我们要使用MySql数据库,就需要先安装. 如果第一次安装失败了,就需要卸载,再安装.

一、MYSQL的安装

1、打开下载的mysql安装文件mysql-5.5.27-win32.zip,双击解压缩,运行“setup.exe”。

image-20230927200804351

image-20230927200759672

2、选择安装类型,有“Typical(默认)”、“Complete(完全)”、“Custom(用户自定义)”三个选项,选择“Custom”,按“next”键继续。

image-20230927200753353

3、点选“Browse”,手动指定安装目录。

image-20230927200749073

4、填上安装目录,我的是“F:\Server\MySQL\MySQL Server 5.0”,也建议不要放在与操作系统同一分区,这样可以防止系统备份还原的时候,数据被清空。按“OK”继续。

image-20230927200743922

确认一下先前的设置,如果有误,按“Back”返回重做。按“Install”开始安装。

image-20230927200739498

image-20230927200734769

image-20230927200729800

image-20230927200725582

image-20230927200720955

5、正在安装中,请稍候,直到出现下面的界面, 则完成MYSQL的安装

image-20230927200716188

二、MYSQL的配置

1、安装完成了,出现如下界面将进入mysql配置向导

image-20230927200708004

image-20230927200703241

2、选择配置方式,“Detailed Configuration(手动精确配置)”、“Standard Configuration(标准配置)”,我们选择“Detailed Configuration”,方便熟悉配置过程。

image-20230927200658819

3、选择服务器类型,“Developer Machine(开发测试类,mysql占用很少资源)”、“Server Machine(服务器类型,mysql占用较多资源)”、“Dedicated MySQL Server Machine(专门的数据库服务器,mysql占用所有可用资源)”

image-20230927200654080

4、选择mysql数据库的大致用途,“Multifunctional Database(通用多功能型,好)”、“Transactional Database Only(服务器类型,专注于事务处理,一般)”、“Non-Transactional Database Only(非事务处理型,较简单,主要做一些监控、记数用,对MyISAM数据类型的支持仅限于non-transactional),按“Next”继续。

image-20230927200645509

5、选择网站并发连接数,同时连接的数目,“Decision Support(DSS)/OLAP(20个左右)”、“Online Transaction Processing(OLTP)(500个左右)”、“Manual Setting(手动设置,自己输一个数)”。

image-20230927200641219

image-20230927200636994

6、是否启用TCP/IP连接,设定端口,如果不启用,就只能在自己的机器上访问mysql数据库了,在这个页面上,您还可以选择“启用标准模式”(Enable Strict Mode),这样MySQL就不会允许细小的语法错误。如果是新手,建议您取消标准模式以减少麻烦。但熟悉MySQL以后,尽量使用标准模式,因为它可以降低有害数据进入数据库的可能性。按“Next”继续

image-20230927200632651

7、就是对mysql默认数据库语言编码进行设置(重要),一般选UTF-8,按 “Next”继续。

image-20230927200628290

8、选择是否将mysql安装为windows服务,还可以指定Service Name(服务标识名称),是否将mysql的bin目录加入到Windows PATH(加入后,就可以直接使用bin下的文件,而不用指出目录名,比如连接,“mysql.exe -uusername -ppassword;”就可以了,不用指出mysql.exe的完整地址,很方便),我这里全部打上了勾,Service Name不变。按“Next”继续。

image-20230927200624040

9、询问是否要修改默认root用户(超级管理)的密码。“Enable root access from remote machines(是否允许root用户在其它的机器上登陆,如果要安全,就不要勾上,如果要方便,就勾上它)”。最后“Create An Anonymous Account(新建一个匿名用户,匿名用户可以连接数据库,不能操作数据,包括查询)”,一般就不用勾了,设置完毕,按“Next”继续。

image-20230927200618881

10、确认设置无误,按“Execute”使设置生效,即完成MYSQL的安装和配置。

image-20230927200614056

image-20230927200609203

注意:设置完毕,按“Finish”后有一个比较常见的错误,就是不能“Start service”,一般出现在以前有安装mysql的服务器上,解决的办法,先保证以前安装的mysql服务器彻底卸载掉了;不行的话,检查是否按上面一步所说,之前的密码是否有修改,照上面的操作;如果依然不行,将mysql安装目录下的data文件夹备份,然后删除,在安装完成后,将安装生成的 data文件夹删除,备份的data文件夹移回来,再重启mysql服务就可以了,这种情况下,可能需要将数据库检查一下,然后修复一次,防止数据出错。

image-20230927200603463

解决方法:

1,卸载MySQL

2,windows Xp系统删除目录 C:\Documents and Settings\All Users\Application Data

windows 7\8\10操作系统删除目录C:\ProgramData\MySQL

3,重新安装就可以了

小结

  • 安装需要注意的地方: 安装路径不要有空格和中文,对着文档装

  • 卸载需要注意的地方

    • 去360/软件管家或者控制面板卸载(删除之前先找到两个文件夹)
    • 一定要删除两个文件夹(数据库安装路径和数据存放路径,这两个文件夹在配置文件里面my.ini)

    image-20230927202054677

实操-MySql的卸载

如何彻底的删除MySQL数据库:

以下操作以Window7操作系统为例:

1)停止window的MySQL服务。

找到“控制面板”-> “管理工具”-> “服务”,停止MySQL后台服务。

image-20230927202050127

或者可以用DOS:net stop MySQL命令停止服务(需要知道服务名,不一定是MySQL)

2)卸载MySQL安装程序。找到“控制面板”-> “程序和功能”,卸载MySQL程序。

image-20230927202045438

3)删除MySQL安装目录下的所有文件。(删除安装的文件夹)

4)删除c:/ProgramDate/MySQL隐藏目录。(删除文件存放的文件夹)

  • 打开window系统的“显示隐藏文件”功能,以便查看到系统的所有隐藏文件

image-20230927202041035

  • 找到ProgramData目录

image-20230927202036635

  • 删除MySQL目录

image-20230927202031506

  • 查看注册表: 可选
1
2
3
4
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services/Eventlog/Applications/MySQL
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services/Eventlog/Applications/MySQL
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet002\Services/Eventlog/Applications/MySQL
搜索mysql,找到一律干掉!
  • 【可选】如果配置了环境变量MYSQL_HOME和在path中使用,也需要删除
  • 删除MySQL服务,管理员启动cmd-> sc delete MySQL服务名字

实操-数据库服务的启动和登录,退出

我们刚刚把数据库安装成功了,下面就需要进行数据库的启动,登录和退出

image-20230927202025086

数据库服务的启动(界面)

image-20230927202019704

image-20230927202013823

数据库服务的启动(DOS命令)

image-20230927202008524

image-20230927201958158

​ MySql是一个需要账户名密码登录的数据库,登陆后使用,它提供了一个默认的root账号,使用安装时设置的密码即可登录.

登录

命令行

1
2
mysql -u 用户名 -p       #然后再输入密码. 
mysql -u用户名 -p密码

图形化工具

  • 安装

image-20230927201950396

image-20230927201944679

  • 连接

    image-20230927201936636

    image-20230927201930471

退出

命令行

1
输入: quit或exit

图形化工具

  • 右键关闭

小结

  1. MySql服务启动: 建议大家开机就启动
  2. dos连接
1
2
3
4
mysql -u root -p
再输入密码
-----------------------------------
mysql -uroot -p密码
  1. navicat图形化工具, 会连接就OK

实操-重置MySQL密码

  • 停止MySQL服务

image-20230927202827174

  • 在cmd下启动MySQL服务

image-20230927202840612

  • 重新开启cmd命名行,登录MySQL,不需要输入密码

image-20230927202856219

  • 修改root密码

image-20230927202907412

  • 结束mysqld的进程

image-20230927202920623

  • 重启MySQL服务

image-20230927202931357

mysql windows安装

下载:

https://dev.mysql.com/downloads/mysql/,下载zip包

初始化

管理员身份cmd进入bin,执行初始化命令:mysqld --initialize --console,记住随记生成的密码,t6olp-eQjt3j

安装服务

1
mysqld -install

启动服务

1
net start mysql

修改密码

mysqladmin -u root -p password root,输入上面的随机密码

登陆MySQL

mysql -u root -p,输入新的密码

可视化工具使用

可视化工具有很多

  • Navicat
  • SQLyog
  • MySQL Workbench
  • DataGrip

实操-可视化Navicat的使用

  • 连接数据库

image-20230927200912461

  • 对数据库的操作

    img

  • 对表的操作

  • 创建表

    image-20230927201844893

    image-20230927201838957

  • 修改表

    image-20230927201834014

    image-20230927201829136

  • 删除表

    image-20230927201823880

  • 对数据的操作

    • 插入数据

      image-20230927201818152

    • 删除数据

      image-20230927201808576

    • 修改数据

注意点:表, 记录如果创建好了, 没有展示, 需要刷新一下就可以了

SQL概述

什么是sql

  • SQL:Structure Query Language。(结构化查询语言),通过sql操作数据库(操作数据库,操作表,操作数据)
  • SQL被美国国家标准局(ANSI)确定为关系型数据库语言的美国标准,后来被国际化标准组织(ISO)采纳为关系数据库语言的国际标准
    • 美国国家标准局(ANSI)开始着手制定SQL标准,并在1986年10月公布了最早的SQL标准,扩展的标准版本是1989年发表的SQL-89,之后还有1992年制定的版本SQL-92和1999年ISO发布的版本SQL-99。
    • SQL标准几经修改和完善,其功能更加强大,但目前很多数据库系统只支持SQL-99的部分特征,而大部分数据库系统都能支持1992年制定的SQL-92。
  • 各数据库厂商(MySql,oracle,sql server)都支持ISO的SQL标准。
  • 各数据库厂商在标准的基础上做了自己的扩展。 各个数据库自己特定的语法

image-20230927203321991

sql的语法

  • 每条语句以分号结尾(命令行里面需要),如果在navicat,java代码中不是必须加的。
  • SQL在window中不区分大小写,关键字中认为大写和小写是一样的

sql的分类

  • Data Definition Language (DDL数据定义语言) 如:操作数据库,操作表
  • Data Manipulation Language(DML数据操纵语言),如:对表中的记录操作增删改 (常用)
  • Data Query Language(DQL 数据查询语言),如:对表中的记录查询操作 (常用)
  • Data Control Language(DCL 数据控制语言),如:对用户权限的设置

DDL操作数据库

我们把Sql介绍完成了, 那下面就通过DDL操作数据库

创建数据库

  • 语法
1
create database 数据库名 [character set 字符集][collate  校对规则]     注: []意思是可选的意思

字符集(charset):是一套符号和编码。

  • 练习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 直接创建数据库db1
CREATE DATABASE db1;

-- 判断是否存在并创建数据库db2
CREATE DATABASE IF NOT EXISTS db2;

-- 创建数据库db3并指定字符集为gbk【注意mysql中不支持utf-8,只能用utf8】
CREATE DATABASE db3 CHARACTER SET gbk;

-- 查看所有的数据库
SHOW DATABASES;

-- 查看某个数据库的创建信息
/*CREATE DATABASE `db3` !40100 DEFAULT CHARACTER SET gbk*/
SHOW CREATE DATABASE db3;

-- 扩展:``重音符号,里面的内容会原样显示
CREATE DATABASE `create`;

image-20230927194454139

查看所有的数据库

  • 语法
1
show databases; 
  • 查看数据库的定义结构
1
show create database 数据库名;
  • 查看web14_1这个数据库的定义
1
show create database web14_1; 

删除数据库

  • 语法
1
drop database 数据库名;
  • 删除web14_2数据库
1
2
3
drop database web14_2;
# 如果存在才删除
drop database if exists web14_2;

修改数据库编码

  • 语法
1
alter database 数据库名 character set 字符集;
  • 修改web14_1这个数据库的字符集(gbk)
1
alter database web14_1 character set gbk;

注意:

  • 是utf8,不是utf-8
  • 不是修改数据库名

切换数据库

选定哪一个数据库

1
2
3
use 数据库名;   		//注意: 在创建表之前一定要指定数据库. use 数据库名
# 练习: 使用web14_1
use web14_1;

查看正在使用的数据库

1
select database();

DDL操作表

创建表

我们第四章已经把数据库的CRUD讲解完了,下面我们就学习创建表

创建表语法

1
2
3
4
5
create table [if not exists] 表名(
字段名1 字段类型[(宽度)] [约束条件] [comment '字段说明'],
字段名2 字段类型[(宽度)] [约束条件] [comment '字段说明'],
字段名3 字段类型[(宽度)] [约束条件]
)[表的一些设置];

类型

原则是:够用就行,尽量使用取值范围小的,而不用大的,这样可以更多的节省存储空间。(实际开发可能没这么精细,这么简单这么用,现在存储也没那么拮据了)

数值类型:

类型 大小 范围(有符号) 范围(无符号) 用途 用途
TINYINT [UNSIGNED] [ZEROFILL] 1 byte (-128,127) (0,255) 微整数值 带符号的覆盖是-128~127.无符号0~255.默认是有符号,无符号要加UNSIGNED
BOOL,BOOLEAN 使用0或1表示真或假
SMALLINT [UNSIGNED] [ZEROFILL] 2 bytes (-32 768,32 767) (0,65 535) 小整数值 2的16次方
MEDIUMINT 3 bytes (-8 388 608,8 388 607) (0,16 777 215) 中整数值 2的24次方
INT或INTEGER [UNSIGNED] [ZEROFILL] 4 bytes (-2 147 483 648,2 147 483 647) (0,4 294 967 295) 大整数值 2的32次方
BIGINT [UNSIGNED] [ZEROFILL] 8 bytes (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) (0,18 446 744 073 709 551 615) 极大整数值 2的64次方
FLOAT(M,D) [UNSIGNED] [ZEROFILL] 4 bytes (-3.402 823 466 E+38,3.402 823 466 351 E+38) 0,(1.175 494 351 E-38,3.402 823 466 E+38) 单精度浮点数值 M指定显示长度,d指定小数位数。float(4,2) 表达的范围: -99.99~99.99
DOUBLE(M,D) [UNSIGNED] [ZEROFILL] 8 bytes (-1.797 693 134 862 315 7 E+308,1.797 693 134 862 315 7 E+308) 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) 双精度浮点数值 表示比float精度更大的小数
DECIMAL(M,D) 依赖于M和D的值 依赖于M和D的值 小数值 M指定显示长度,d指定小数位数,可以表示整数decimal(10)或小数decimal(10,2)

字符串类型:

类型 大小 用途
CHAR(size) 0-255 bytes 定长字符串,char(20), 最大能存放20个字符. ‘aaa’, 还是占20个字符的空间,一般使用varchar(n) 节省空间; 如果长度(eg:身份证)是固定的话 可以使用char(n) 性能高一点
VARCHAR(size) 0-65535 bytes 变长字符串,varchar(20), 最大能存放20个字符. ‘aaa’, 占3个字符的空间
TINYBLOB 0-255 bytes 不超过 255 个字符的二进制字符串
TINYTEXT 0-255 bytes 短文本字符串
BLOB 0-65 535 bytes 二进制形式的长文本数据
TEXT(clob) 0-65 535 bytes 长文本数据
MEDIUMBLOB 0-16 777 215 bytes 二进制形式的中等长度文本数据
MEDIUMTEXT 0-16 777 215 bytes 中等长度文本数据
LONGBLOB 0-4 294 967 295 bytes 二进制形式的极大文本数据
LONGTEXT(longclob) 0-4 294 967 295 bytes 极大文本数据
  • 一般在数据库里面很少存文件的内容, 一般存文件的路径
  • 一般不使用二进制存, 使用varchar(n)存文件的路径

日期类型:

类型 大小( 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:00/2038结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07 YYYYMMDD HHMMSS 混合日期和时间值,时间戳,可用于自动记录insert,update操作的时间

约束

表在设计的时候加入约束的目的就是为了保证表中的记录完整性和有效性,比如用户表有些列的值(手机号)不能为空,有些列的值(身份证号)不能重复。

  • 即规则,规矩 限制;
  • 作用:保证用户插入的数据保存到数据库中是符合规范的

约束种类:

  • 主键约束(primary key) PK: (非空+唯一): 一般用在表的id列上面. 一张表基本上都有id列的, id列作为唯一标识的,默认会创建唯一索引。只有当设置了auto_increment 才可以插入null,由数据库维护。否则不给值插入null会报错
  • 自增长约束(auto_increment): 必须是设置了primary key之后,才可以使用auto_increment,id int primary key auto_increment;,id不需要我们自己维护了, 插入数据的时候直接插入null, 自动的增长进行填充进去, 避免重复了.
  • 非空约束(not null): username varchar(40) not null, username这个字段不能为空,必须要有数据
  • 唯一性约束(unique): cardNo char(18) unique; ,cardNo字段不能出现重复的数据
  • 默认约束(default)
  • 零填充约束(zerofill)
  • 外键约束(foreign key) FK

表中的id列一般设置以下2种情况:

  1. 给id设置为int类型, 添加主键约束, 自增长约束
  2. 或者给id设置为字符串类型,添加主键约束, 不能设置自动增长
1
2
3
4
5
6
-- 创建一张学生表(含有id字段,姓名字段,性别字段. id为主键自动增长)
create table student(
id int primary key auto_increment,
name varchar(40),
sex int
);

主键约束

  • 定义字段的同时指定主键
1
2
3
4
5
6
7
8
9
10
11
create table 表名(
...
<字段名> <数据类型> primary key
...
)
create table emp1(
eid int primary key,
name VARCHAR(20),
deptId int,
salary double
);
  • 定义完字段之后指定主键
1
2
3
4
5
6
7
8
9
10
11
create table 表名(
...
[constraint <约束名>] primary key [字段名]
);
create table emp2(
eid INT,
name VARCHAR(20),
deptId INT,
salary double,
constraint pk1 primary key(id)
);
  • 设置多列主键(联合主键)

一张表只能有一个主键,联合主键也是一个主键

1
2
3
4
5
6
7
8
9
10
create table 表名(
...
primary key (字段1,字段2,…,字段n)
);
create table emp3(
name varchar(20),
deptId int,
salary double,
primary key(name,deptId)
);
  • 通过修改表结构添加主键

主键约束不仅可以在创建表的同时创建,也可以在修改表时添加。

1
2
3
4
5
6
7
8
9
10
11
12
create table 表名(
...
);
alter table <表名> add primary key(字段列表);
-- 添加单列主键
create table emp4(
eid int,
name varchar(20),
deptId int,
salary double,
);
alter table emp4 add primary key(eid);
  • 删除主键约束
1
2
3
4
5
6
7
alter table <数据表名> drop primary key;

-- 删除单列主键
alter table emp1 drop primary key;

-- 删除联合主键
alter table emp5 drop primary key;

自增长约束

主键一般是唯一标识,设置了我们一般不想管它,就想让数据库系统自动赋值。每增加一条记录,主键会自动以相同的步长进行增长。 ———— 自增长约束(auto_increment)

语法:字段名 数据类型 primary key auto_increment

  • 默认情况下,auto_increment的初始值是 1,每新增一条记录,字段值自动加 1。
  • 一个表中只能有一个字段使用 auto_increment约束,且该字段必须有唯一索引,以避免序号重复(即为主键或主键的一部分)。
  • auto_increment约束的字段必须具备 NOT NULL 属性。
  • auto_increment约束的字段只能是整数类型(TINYINT、SMALLINT、INT、BIGINT 等)。
  • auto_increment约束字段的最大值受该字段的数据类型约束,如果达到上限,auto_increment就会失效。

比如字段id设置auto_increment,那么会自增长,全字段插入指定为NULL就行了。

insert into 表1 values(NULL,’张三’);

或者部分字段插入,就不用给null了

insert into 表1 (name) values(‘张三’);

  • 指定自增字段初始值,比如自增长从100开始。
1
2
3
4
5
6
7
8
9
10
11
12
# 添加自增长约束
-- 方式1,创建表时指定
create table t_user2 (
id int primary key auto_increment,
name varchar(20)
)auto_increment=100;
-- 方式2,创建表之后指定
create table t_user3 (
id int primary key auto_increment,
name varchar(20)
);
alter table t_user2 auto_increment=100;

delete和truncate在删除后自增列的变化

  • delete数据之后自动增长从断点开始
  • truncate数据之后自动增长从默认起始值开始

非空约束

对于使用了非空约束的字段,如果用户在添加数据时没有指定值,数据库系统就会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 添加非空约束
方式1<字段名><数据类型> not null;
-- 创建表时指定
create table t_user6 (
id int ,
name varchar(20) not null,
address varchar(20) not null
);


方式2alter table 表名 modify 字段 类型 not null;
-- 创建表后指定
create table t_user7 (
id int ,
name varchar(20) , -- 指定非空约束
address varchar(20) -- 指定非空约束
);
alter table t_user7 modify name varchar(20) not null;
alter table t_user7 modify address varchar(20) not null;

# 删除为空约束
-- alter table 表名 modify 字段 类型
alter table t_user7 modify name varchar(20) ;
alter table t_user7 modify address varchar(20) ;

唯一约束

唯一约束(Unique Key)是指所有记录中字段的值不能重复出现。

例如,为 id 字段加上唯一性约束后,每条记录的 id 值都是唯一的,不能出现重复的情况。

在MySQL中 NULL和任何值都不相同,包括自己,所以唯一约束的列可以为NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 添加唯一约束
方式1<字段名> <数据类型> unique
-- 创建表时指定
create table t_user8 (
id int ,
name varchar(20) ,
phone_number varchar(20) unique -- 指定唯一约束
);

方式2alter table 表名 add constraint 约束名 unique(列);
create table t_user9 (
id int ,
name varchar(20) ,
phone_number varchar(20) -- 指定唯一约束
);
alter table t_user9 add constraint unique_ph unique(phone_number);

# 删除唯一约束
-- alter table <表名> drop index <唯一约束名>;
alter table t_user9 drop index unique_ph;

默认约束

MySQL 默认值约束用来指定某列的默认值

设置了默认值的列,插入的时候也可以不用插入,但是如果插入的时候给了字段值(包括给NULL值),那相当于手动指定了,默认值就不会生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 添加默认约束
方式1<字段名> <数据类型> default <默认值>;
create table t_user10 (
id int ,
name varchar(20) ,
address varchar(20) default '北京' -- 指定默认约束
);

方式2: alter table 表名 modify 列名 类型 default 默认值;
-- alter table 表名 modify 列名 类型 default 默认值;
create table t_user11 (
id int ,
name varchar(20) ,
address varchar(20)
);
alter table t_user11 modify address varchar(20) default '北京';

# 删除默认约束
-- alter table <表名> modify column <字段名> <类型> default null;
alter table t_user11 modify column address varchar(20) default null;

零填充约束

1、插入数据时,当该字段的值的长度小于定义的长度时,会在该值的前面补上相应的0

2、zerofill默认为int(10)

3、当使用zerofill 时,默认会自动加unsigned(无符号)属性,使用unsigned属性后,数值范围是原值的2倍,例如,有符号为-128~+127,无符号为0~256

1
2
3
4
5
6
7
# 添加零填充约束
create table t_user12 (
id int zerofill , -- 零填充约束
name varchar(20)
);
# 删除零填充约束
alter table t_user12 modify id int;

查看表

我们把表创建好了, 下面就来介绍查看表

查看所有的表

1
show tables;

image-20230927203152853

查看表的定义结构

  • 语法

    desc 表名;

  • 练习: 查看student表的定义结构

1
desc student;

image-20230927203144922

查看创建表的SQL

1
SHOW CREATE TABLE 表名;

image-20230927203139778

修改表

我们表创建好了, 如果要增加一列,要删除一列呢? 那下面就来讲解修改表

语法

  • 增加一个字段;alter table 表 add 字段 类型 约束;
  • 修改一个字段的类型约束; alter table 表 modify 字段 类型 约束 ;
  • 修改一个字段的名称,类型,约束;alter table 表 change 旧列 新列 类型 约束;
  • 删除一个字段; alter table 表名 drop 列名;
  • 修改表名 ;rename table 旧表名 to 新表名;

练习

  • 给学生表增加一个grade字段
1
alter table student add grade varchar(20) not null;

image-20230927203133918

  • 给学生表的sex字段改成字符串类型
1
alter table student modify sex varchar(10);

image-20230927203128934

  • 给学生表的grade字段修改成class字段
1
alter table student change grade class varchar(20);

将student表中的remark字段名改成intro,类型varchar(30)

image-20230927203123488

  • 把class字段删除
1
alter table student drop class;

删除student表中的字段intro

image-20230927203117504

  • 把学生表修改成老师表(了解)
1
rename table student to teacher

image-20230927203112589

  • 修改字符集
1
2
ALTER TABLE 表名 character set 字符集;
ALTER TABLE student2 character set gbk; # sutden2表的编码修改成gbk

image-20230927203107447

删除表

表创建好了, 我们还可以删除。 掌握表的删除

  • 1 直接删除表
1
DROP TABLE 表名;

image-20230927203101798

  • 2 判断表是否存在并删除表
1
DROP TABLE IF EXISTS 表名;

image-20230927203055622

DML操作表记录-增删改

  • 准备工作: 创建一张商品表(商品id,商品名称,商品价格,商品数量.)
1
2
3
4
5
6
create table product(
pid int primary key auto_increment, //只有设置了auto_increment id列才可以赋值为null
pname varchar(40),
price double,
num int
);

插入记录

注意

  • 插入特定的列:没有赋值的列,系统自动赋为null(前提是当前列没有设置not null 约束)
  • 列名与列值的类型、个数、顺序要一一对应。
  • 值不要超出列定义的长度。
  • 插入的日期和字符串,使用引号括起来。

方式一: 插入指定列, 如果没有把这个列进行列出来, 以null进行自动赋值了.

1
2
3
4
5
6
7
insert into 表(列1,列2..) values(值1,值2..);

eg: 只想插入pname, price
insert into t_product(pname, price) values('mac',18000);

# 一次多行
insert into t_product(pname, price) values('mac1',18000),('mac2',28000),('mac3',38000);

注意: 如果没有插入的列设置了非空约束, 会报错的

方式二: 插入所有的列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
insert intovalues(值,值....);           

eg:
insert into product values(null,'苹果电脑',18000.0,10);
insert into product values(null,'华为5G手机',30000,20);
insert into product values(null,'小米手机',1800,30);
insert into product values(null,'iPhonex',8000,10);
insert into product values(null,'苹果电脑',8000,100);
insert into product values(null,'iPhone7',6000,200);
insert into product values(null,'iPhone6s',4000,1000);
insert into product values(null,'iPhone6',3500,100);
insert into product values(null,'iPhone5s',3000,100);

# 还可以这样写,一次多行
insert into product values(null,'方便面',4.5,1000),(null,'咖啡',11,200),(null,'矿泉水',3,500);

命令行插入中文数据报错:

image-20230927203041187

  • 关闭服务, net stop MySql
  • 在数据库软件的安装目录下面, 修改配置文件 my.ini中客户端的编码为gbk

image-20230927203016987

  • 重新打开命令行,开启服务, net start MySql

更新记录

我们数据插入成功了, 还可以对已有的数据进行更新。

语法

1
updateset1=值, 列2=值 [where 条件]
  • 如果没有加where 更新整个的
  • 工作里面一般是加where,可能也会设置门禁不让全表操作

练习

  • 将所有商品的价格修改为5000元
  • 将商品名是Mac的价格修改为18000元
  • 将商品名是Mac的价格修改为17000,数量修改为5
  • 将商品名是方便面的商品的价格在原有基础上增加2元
1
2
3
4
update product set price = 5000;
UPDATE product set price = 18000 WHERE name = 'Mac';
UPDATE product set price = 17000,num = 5 WHERE name = 'Mac';
UPDATE product set price = price+2 WHERE name = '方便面';

删除记录

delete

语法

1
delete from 表 [where 条件]    注意: 删除数据用delete,不用truncate

练习

  • 删除表中名称为’Mac’的记录
  • 删除价格小于5001的商品记录
  • 删除表中的所有记录
1
2
3
delete from product where pname = 'Mac';
delete from product where price < 5001;
delete from product;

truncate

1
truncate table 表;

delete和truncate区别【面试题】

  • DELETE 删除表中的数据,表结构还在; 删除的记录可以找回
  • TRUNCATE 删除是把表直接DROP掉,然后再创建一个同样的新表(空)。删除的记录不可以找回

工作里面的删除

  • 物理删除: 真正的删除了, 数据不在, 使用delete就属于物理删除
  • 逻辑删除: 没有真正的删除, 数据还在. 搞一个标记, 其实逻辑删除是更新 eg: state字段 1 启用 0禁用

工作里面一般看业务,重要的数据使用逻辑删除用的多

蠕虫复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 蠕虫复制
-- 创建student2表,student2结构和student表结构一样
CREATE TABLE student2 LIKE student;

-- 将student表中的数据添加到student2表中
-- 蠕虫复制语法:INSERT INTO 表名 SELECT * FROM 表名
INSERT INTO student2 SELECT * FROM student;

-- 蠕虫复制一部分数据
-- 将student表的name ,sex,math 插入到 student2中
delete from student2;

INSERT INTO student2 (name,sex,math) SELECT name,sex,math FROM student;

# 方式2select into from
SELECT vale1, value2 into Table2 from Table1
-- 要求目标表Table2不存在,因为在插入时会自动创建表Table2,并将Table1中指定字段数据复制到Table2中。

DQL操作表记录-查询

单表查询

我们上面讲解了对数据的增删改, 下面就来重点讲解数据的简单查询.

基本查询语法

1
2
3
4
5
6
7
8
9
10
11
12
13
select 
[all|distinct]
<目标列的表达式1> [别名],
<目标列的表达式2> [别名]...
from <表名或视图名> [别名],<表名或视图名> [别名]...
[where<条件表达式>]
[group by <列名>
[having <条件表达式>]]
[order by <列名> [asc|desc]]
[limit <数字或者列表>];

简化版:
select [*][列名 ,列名][列名 as 别名 ...] [distinct 字段] from 表名 [where 条件]

简单查询

查询所有的列的记录

  • 语法
1
select * form 表
  • 查询商品表里面的所有的列
1
select * from product;

查询某张表特定列的记录

  • 语法
1
select 列名,列名,列名... from
  • 查询商品名字和价格
1
select pname, price from product;

去重查询

  • 语法
1
SELECT DISTINCT 字段名 FROM 表名;   //要数据一模一样才能去重
  • 去重查询商品的价格
1
select distinct price from product;

注意点: 去重针对某列, distinct前面不能先出现列名

别名查询

  • 语法
1
2
select 列名 as 别名 ,列名  from//列别名  as可以不写
select 别名.* fromas 别名 //表别名(多表查询, 明天会具体讲)
  • 查询商品名称和商品价格,商品价格通过别名‘价格’来显示
1
select pname , price as 价格 from product;

算术运算查询

算术运算符 说明
+ 加法运算
- 减法运算
***** 乘法运算
/ DIV 除法运算,返回商
% MOD 求余运算,返回余数
  • 把商品名,和商品价格+10查询出来
1
select pname ,price+10 from product;

注意:

  • 运算查询字段,字段之间是可以的
  • 字符串等类型可以做运算查询,但结果没有意义

条件查询

语法

1
2
select ... fromwhere 条件 
//取出表中的每条数据,满足条件的记录就返回,不满足条件的记录不返回

运算符

比较运算符 说明
= 等于
< <= 小于和小于等于
> >= 大于和大于等于
<=> 安全的等于,两个操作码均为NULL时,其所得值为1;而当一个操作码为NULL时,其所得值为0
<>或!= 不等于
IS NULL ISNULL 判断一个值是否为 NULL
IS NOT NULL 判断一个值是否不为 NULL
LEAST 当有两个或多个参数时,返回最小值,如果有个值为NULL,则结果为NULL
GREATEST 当有两个或多个参数时,返回最大值,如果有个值为NULL,则结果为NULL
BETWEEN AND 判断一个值是否落在两个值之间
IN 判断一个值是IN列表中的任意一个值,例:in(100,200)
NOT IN 判断一个值不是IN列表中的任意一个值
LIKE 通配符匹配, 模糊查询,like ‘张pattern’
REGEXP 正则表达式匹配
逻辑运算符 说明
NOT 或者 ! 逻辑非,不成立,例:where not(salary > 100);
AND 或者 && 逻辑与,多个条件同时成立
OR 或者 || 逻辑或,多个条件任一成立
XOR 逻辑异或,不同为真,相同为假
位运算符 说明
| 按位或
& 按位与
^ 按位异或
<< 按位左移
>> 按位右移
~ 按位取反,反转所有比特

位运算符是在二进制数上进行计算的运算符。位运算会先将操作数变成二进制数,进行位运算。然后再将计算结果从二进制数变回十进制数。

常用的操作

  1. between…and… 区间查询
1
eg: where price between  1000 and 3000  相当于 1000<=price<=3000 
  1. in(值,值..)
1
2
3
4
5
6
7
-- 查询id为1,3,5,7的
select * from t_product where id = 1
select * from t_product where id = 3
select * from t_product where id = 5
select * from t_product where id = 7

select * from t_product where id in(1,3,5,7)
  1. like 模糊查询,一般和_或者%一起使用
    • _ 占1位
    • % 占0或者n位
1
2
name like '张%'  --查询姓张的用户, 名字的字数没有限制
name like '张_' --查询姓张的用户 并且名字是两个的字的
  1. and 多条件同时满足
1
where 条件1 and 条件2 and 条件3
  1. or 任意条件满足
1
where 条件1 or 条件2 or 条件3

6.使用least求n个数最小值和greatest求n个数最大值

1
2
3
4
select least(10,5,20) as small_num; -- 5
select least(10,5,20,NULL) as small_num; -- NULL
select greatest(10,5,20) as small_num; -- 20
select greatest(10,5,20,NULL) as small_num; -- NULL

练习

  • 查询商品价格>3000的商品
  • 查询id=1的商品
  • 查询id<>1的商品
  • 查询价格在3000到6000之间的商品
  • 查询id在1,5,7,15范围内的商品
  • 查询商品名以iPho开头的商品(iPhone系列)
  • 查询商品价格大于3000并且数量大于20的商品 (条件 and 条件 and…)
  • 查询id=1或者价格小于3000的商品
1
2
3
4
5
6
7
8
select * from product where price > 3000;
select * from product where pid = 1;
select * from product where pid <> 1;
select * from product where price between 3000 and 6000;
select * from product where id in (1,5,7,15);
select * from product where pname like 'iPho%';
select * from product where price > 3000 and num > 20;
select * from product where pid = 1 or price < 3000;
  • 位运算(了解)
1
2
3
4
5
6
select 3&5; -- 位与  -- 1
select 3|5; -- 位或 -- 7
select 3^5; -- 位异或 -- 6
select 3>>1; -- 位左移 -- 1
select 3<<1; -- 位右移 -- 6
select ~3; -- 位取反 -- 18446744073709551612

排序查询

​ 有时候我们需要对查询出来的结果排序显示,那么就可以通过ORDER BY子句将查询出的结果进行排序。排序可以根据一个字段排,也可以根据多个字段排序,排序只是对查询的结果集排序,并不会影响表中数据的顺序。

应用场景

  • 商城里面 根据价格, 销量, 上架时间, 评论数…
  • 社交里面 根据距离排序

环境的准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建学生表(有sid,学生姓名,学生性别,学生年龄,分数列,其中sid为主键自动增长)
CREATE TABLE student(
sid INT PRIMARY KEY auto_increment,
sname VARCHAR(40),
sex VARCHAR(10),
age INT,
score DOUBLE
);

INSERT INTO student VALUES(null,'zs','男',18,98.5);
INSERT INTO student VALUES(null,'ls','女',18,96.5);
INSERT INTO student VALUES(null,'ww','男',15,50.5);
INSERT INTO student VALUES(null,'zl','女',20,98.5);
INSERT INTO student VALUES(null,'tq','男',18,60.5);
INSERT INTO student VALUES(null,'wb','男',38,98.5);
INSERT INTO student VALUES(null,'小丽','男',18,100);
INSERT INTO student VALUES(null,'小红','女',28,28);
INSERT INTO student VALUES(null,'小强','男',21,95);

单列排序

  1. 语法: 只按某一个字段进行排序,单列排序
1
SELECT 字段名 FROM 表名 [WHERE 条件] ORDER BY 字段名 [ASC|DESC];  //ASC: 升序,默认值; DESC: 降序
  1. 练习: 以分数降序查询所有的学生
1
2
3
4
SELECT * FROM student ORDER BY score DESC

-- 显示商品的价格(去重复),并排序(降序)
select distinct price from product order by price desc;

组合排序

  1. 语法: 同时对多个字段进行排序,如果第1个字段相等,则按第2个字段排序,依次类推
1
SELECT 字段名 FROM 表名 WHERE 字段=ORDER BY 字段名1 [ASC|DESC], 字段名2 [ASC|DESC];
  1. 练习: 以分数降序查询所有的学生, 如果分数一致,再以age降序
1
SELECT * FROM student ORDER BY score DESC, age DESC

聚合函数

​ 之前我们做的查询都是横向查询,它们都是根据条件一行一行的进行判断,而使用聚合函数查询是纵向查询,它是对一列的值进行计算,然后返回一个结果值聚合函数会忽略空值NULL

聚合函数 作用
max(列名) 求这一列的最大值
min(列名) 求这一列的最小值
avg(列名) 求这一列的平均值
count(列名) 统计这一列有多少条记录
sum(列名) 对这一列求总和
  1. 语法
1
SELECT 聚合函数(列名) FROM 表名 [where 条件];
  1. 练习
1
2
3
4
5
6
7
8
9
10
11
-- 求出学生表里面的最高分数
-- 求出学生表里面的最低分数
-- 求出学生表里面的分数的总和(忽略null值)
-- 求出学生表里面的平均分
-- 统计学生的总人数 (忽略null)
SELECT MAX(score) FROM student -- 最大值
SELECT MIN(score) FROM student -- 最小值
SELECT SUM(score) FROM student -- 求和
SELECT AVG(score) FROM student -- 平均值
SELECT COUNT(sid) FROM student -- 统计数量
SELECT COUNT(*) FROM student

注意: 聚合函数会忽略空值NULL

​ 我们发现对于NULL的记录不会统计,建议如果统计个数则不要使用有可能为null的列,但如果需要把NULL也统计进去呢?我们可以通过 IFNULL(列名,默认值) 函数来解决这个问题. 如果列不为空,返回这列的值。如果为NULL,则返回默认值。

分组查询

分组查询是指使用 GROUP BY语句对查询信息进行分组

GROUP BY怎么分组的? 将分组字段结果中相同内容作为一组,如按性别将学生分成两组

GROUP BY将分组字段结果中相同内容作为一组,并且返回每组的第一条数据,所以单独分组没什么用处。分组的目的就是为了统计,一般分组会跟聚合函数一起使用

分组查询如果不查询出分组字段的值,就无法得知结果属于那组

在分组里面, 如果select后面的列没有出现在group by后面 ,展示这个组的这个列的第一个数据

分组

  1. 语法
1
SELECT 字段1,字段2... FROM 表名  [where 条件] GROUP BY 列 [HAVING 条件];
  1. 练习:根据性别分组, 统计每一组学生的总人数
1
2
-- 根据性别分组, 统计每一组学生的总人数
SELECT sex, count(*) FROM student GROUP BY sex

分组后筛选 having

  • 练习根据性别分组, 统计每一组学生的总人数> 5的(分组后筛选)
1
SELECT sex, count(*) FROM student GROUP BY sex HAVING count(*) > 5

where和having的区别【面试】

子名 作用
where 子句 1) 对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,即先过滤再分组。2) where后面不可以使用聚合函数
having字句 1) having 子句的作用是筛选满足条件的组,即在分组之后过滤数据,即先分组再过滤。2) having后面可以使用聚合函数

分页查询

​ LIMIT是限制的意思,所以LIMIT的作用就是限制查询记录的条数. 经常用来做分页查询

应用场景

如果数据库里面的数据量特别大, 我们不建议一次查询出来. 为了提升性能和用户体验, 使用分页

  1. 语法
1
2
3
4
5
select ... from .... limit 起始行数,查询的记录条数.

limit a,b;
a:从哪里开始查询, 从0开始计数,如果省略,默认就是0; 【a=(当前页码-1)*b】
b: 返回的行数,一页查询的数量【固定的,自定义的】
  1. 练习
1
2
3
4
5
6
eg: 分页查询学生, 每一页查询4
b=4; a=(当前页码-1)*b;

第一页: a=0, b=4;
第二页: a=4, b=4;
第三页: a=8, b=4;

正则表达式🆕

MySQL通过REGEXP关键字支持正则表达式进行字符串匹配。

模式 描述
^ 匹配输入字符串的开始位置。
$ 匹配输入字符串的结束位置。
. 匹配除 “\n” 之外的任何单个字符。
[…] 字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。
[^…] 负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’。
p1|p2|p3 匹配 p1 或 p2 或 p3。例如,’z|food’ 能匹配 “z” 或 “food”。’(z|f)ood’ 则匹配 “zood” 或 “food”。
***** 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
-- ^ 在字符串开始处进行匹配
SELECT 'abc' REGEXP '^a';
开发例子:
select * from product where pname regexp '^海'; -- 商品以海开头


-- $ 在字符串末尾开始匹配
SELECT 'abc' REGEXP 'a$';
SELECT 'abc' REGEXP 'c$’;

-- . 匹配任意字符
SELECT 'abc' REGEXP '.b'; -- 1
SELECT 'abc' REGEXP '.c'; -- 1
SELECT 'abc' REGEXP 'a.'; -- 1

-- [...] 匹配括号内的任意单个字符
SELECT 'abc' REGEXP '[xyz]'; -- 0
SELECT 'abc' REGEXP '[xaz]'; -- 1

-- [^...] 注意^符合只有在[]内才是取反的意思,在别的地方都是表示开始处匹配
SELECT 'a' REGEXP '[^abc]'; -- 0
SELECT 'x' REGEXP '[^abc]'; -- 1
SELECT 'abc' REGEXP '[^a]'; -- 1

-- a* 匹配0个或多个a,包括空字符串。 可以作为占位符使用.有没有指定字符都可以匹配到数据

SELECT 'stab' REGEXP '.ta*b'; -- 1
SELECT 'stb' REGEXP '.ta*b'; -- 1
SELECT '' REGEXP 'a*'; -- 1

-- a+ 匹配1个或者多个a,但是不包括空字符
SELECT 'stab' REGEXP '.ta+b'; -- 1
SELECT 'stb' REGEXP '.ta+b'; -- 0

-- a? 匹配0个或者1个a
SELECT 'stb' REGEXP '.ta?b'; -- 1
SELECT 'stab' REGEXP '.ta?b'; -- 1
SELECT 'staab' REGEXP '.ta?b'; -- 0

-- a1|a2 匹配a1或者a2,
SELECT 'a' REGEXP 'a|b'; -- 1
SELECT 'b' REGEXP 'a|b'; -- 1
SELECT 'b' REGEXP '^(a|b)'; -- 1
SELECT 'a' REGEXP '^(a|b)'; -- 1
SELECT 'c' REGEXP '^(a|b)'; -- 0

-- a{m} 匹配m个a

SELECT 'auuuuc' REGEXP 'au{4}c'; -- 1
SELECT 'auuuuc' REGEXP 'au{3}c'; -- 0

-- a{m,n} 匹配m到n个a,包含m和n

SELECT 'auuuuc' REGEXP 'au{3,5}c'; -- 1
SELECT 'auuuuc' REGEXP 'au{4,5}c'; -- 1
SELECT 'auuuuc' REGEXP 'au{5,10}c'; -- 0

-- (abc) abc作为一个序列匹配,不用括号括起来都是用单个字符去匹配,如果要把多个字符作为一个整体去匹配就需要用到括号,所以括号适合上面的所有情况。
SELECT 'xababy' REGEXP 'x(abab)y'; -- 1
SELECT 'xababy' REGEXP 'x(ab)*y'; -- 1
SELECT 'xababy' REGEXP 'x(ab){1,2}y'; -- 1

🔖MySQL进阶

多表间的关系

为什么要有多表

单表的缺点

创建一个员工表包含如下列(id, name, age, dep_name, dep_location),id主键并自动增长,添加5条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(30),
age INT,
dep_name VARCHAR(30),
dep_location VARCHAR(30)
);

-- 添加数据
INSERT INTO emp (NAME, age, dep_name, dep_location) VALUES ('张三', 20, '研发部', '广州');
INSERT INTO emp (NAME, age, dep_name, dep_location) VALUES ('李四', 21, '研发部', '广州');
INSERT INTO emp (NAME, age, dep_name, dep_location) VALUES ('王五', 20, '研发部', '广州');

INSERT INTO emp (NAME, age, dep_name, dep_location) VALUES ('老王', 20, '销售部', '深圳');
INSERT INTO emp (NAME, age, dep_name, dep_location) VALUES ('大王', 22, '销售部', '深圳');
INSERT INTO emp (NAME, age, dep_name, dep_location) VALUES ('小王', 18, '销售部', '深圳');

缺点:表中出现了很多重复的数据(数据冗余),如果要修改研发部的地址需要修改3个地方。

image-20230927203242262

解决方案:将一张表分成2张表(员工表和部门表)

image-20230927203236292

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 创建部门表
CREATE TABLE department (
id INT PRIMARY KEY AUTO_INCREMENT,
dep_name VARCHAR(20),
dep_location VARCHAR(20)
);

-- 创建员工表
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20),
age INT,
dep_id INT
);

-- 添加2个部门
INSERT INTO department (dep_name, dep_location) VALUES ('研发部', '广州');
INSERT INTO department (dep_name, dep_location) VALUES('销售部', '深圳');

-- 添加员工,dep_id表示员工所在的部门
INSERT INTO employee (NAME, age, dep_id) VALUES
('张三', 20, 1),
('李四', 21, 1),
('王五', 20, 1),
('老王', 20, 2),
('大王', 22, 2),
('小王', 18, 2);

问题: 当我们在employee的dep_id里面输入不存在的部门,数据依然可以添加.但是并没有对应的部门,不能出现这种情况。employee的dep_id中的内容只能是department表中存在的id

image-20230927203228033

目标:需要约束dep_id字段的值, 只能是department表中已经存在id 解决方式:使用外键约束

有些情况下,使用一张表表示数据 数据不好维护, 存在数据冗余,比较乱的现象

使用多张表,需要对数据进行约束,不约束,添加的数据会不合法

外键约束

表和表之间存在一种关系,但是这个关系需要谁来维护和约束?

外键约束作用

  • 用来维护多表之间关系

外键: 一张从表中的某个字段引用主表中的主键 主表: 约束别人 副表/从表: 使用别人的数据,被别人约束

image-20230927203219684

外键的语法

添加外键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1. 新建表时增加外键:
[CONSTRAINT] [外键约束名称] FOREIGN KEY(外键字段名) REFERENCES 主表名(主键字段名)
关键字解释:
CONSTRAINT -- 约束关键字
FOREIGN KEY(外键字段名) –- 某个字段作为外键
REFERENCES -- 主表名(主键字段名) 表示参照主表中的某个字段

2. 已有表增加外键:
ALTER TABLE 从表 ADD [CONSTRAINT] [外键约束名称] FOREIGN KEY (外键字段名) REFERENCES 主表(主键字段名);
-- 删除员工表,从新创建,并添加外键
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20),
age INT,
dep_id INT,
-- 添加一个外键
-- 外键取名公司要求,一般fk结尾
CONSTRAINT emp_depid_ref_dep_id_fk FOREIGN KEY(dep_id) REFERENCES department(id)
);
-- 添加正确数据
INSERT INTO employee (NAME, age, dep_id) VALUES
('张三', 20, 1),
('李四', 21, 1),
('王五', 20, 1),
('老王', 20, 2),
('大王', 22, 2),
('小王', 18, 2);


-- 验证: 添加错误数据
INSERT INTO employee (NAME, age, dep_id) VALUES ('二王', 20, 5);// 报错

删除外键

1
2
3
4
5
alter table 表 drop foreign key 外键名称;
-- 删除employee员工表的外键
alter table employee drop foreign key emp_dep_fk1;
-- 往员工信息表中添加非法数据---部门id不存在
INSERT INTO employee (NAME, age, dep_id) VALUES ('老张', 18, 6);-- 成功

为已存在的表添加外键,注意:外键字段上不能有非法数据

1
2
3
alter table 表名 add constraint 外键名称 foreign key(外键字段名) reference 主表(主键名)
-- 往员工信息表中添加非法数据---部门id不存在
INSERT INTO employee (NAME, age, dep_id) VALUES ('老张', 18, 6);-- 失败

外键的级联

  • 要把部门表中的id值2,改成5,能不能直接修改呢?

    1
    UPDATE department SET id=5 WHERE id=2;

    不能直接修改:Cannot delete or update a parent row: a foreign key constraint fails 如果副表(员工表)中有引用的数据,不能直接修改主表(部门表)主键

  • 要删除部门id等于1的部门, 能不能直接删除呢?

    1
    DELETE FROM department WHERE id = 1;

    不能直接删除:Cannot delete or update a parent row: a foreign key constraint fails 如果副表(员工表)中有引用的数据,不能直接删除主表(部门表)数据

什么是级联操作: 在修改和删除主表的主键时,同时更新或删除副表的外键值,称为级联操作 ON UPDATE CASCADE – 级联更新,主键发生更新时,外键也会更新 ON DELETE CASCADE – 级联删除,主键发生删除时,外键也会删除

具体操作:

  • 删除employee表
  • 重新创建employee表,添加级联更新和级联删除
1
2
3
4
5
6
7
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(30),
age INT,
dep_id INT,
CONSTRAINT employee_dep_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON UPDATE CASCADE ON DELETE CASCADE
);
  • 再次添加数据到员工表和部门表
1
2
3
4
5
6
INSERT INTO employee (NAME, age, dep_id) VALUES ('张三', 20, 1);
INSERT INTO employee (NAME, age, dep_id) VALUES ('李四', 21, 1);
INSERT INTO employee (NAME, age, dep_id) VALUES ('王五', 20, 1);
INSERT INTO employee (NAME, age, dep_id) VALUES ('老王', 20, 2);
INSERT INTO employee (NAME, age, dep_id) VALUES ('大王', 22, 2);
INSERT INTO employee (NAME, age, dep_id) VALUES ('小王', 18, 2);
  • 把部门表中id等于1的部门改成id等于10
1
UPDATE department SET id=10 WHERE id=1;

image-20230927203305244

  • 删除部门号是2的部门
1
DELETE FROM department WHERE id=2;

image-20230927203259377

小结

  1. 外键约束的作用: 维护多表的关系, 保证引用数据的完整性
  2. 外键的语法
1
constraint 外键名称 foreign key(外键字段名) references 主表(列[主键]) [ON UPDATE CASCADE][ON DELETE  CASCADE]
  1. 外键注意事项
    • 外键的这个列的类型必须和参照主表主键列的类型一致
    • 参照列必须是主键

多表间关系

能够说出多表之间的关系及其建表原则

​ eg: 下订单(t_order)—>谁下(t_user), 买了什么(t_product)

​ 现实生活中,实体与实体之间肯定是有关系的,比如:老公和老婆,部门和员工,老师和学生等。那么我们在设计表的时候,就应该体现出表与表之间的这种关系!分成三种:

  1. 一对多
  2. 多对多
  3. 一对一

一对多(1:n)

例如:班级和学生,部门和员工,客户和订单

一的一方: 班级 部门 客户

多的一方:学生 员工 订单

一对多建表原则: 在从表(多方的一方)创建1一个字段,字段作为外键指向主表(一方)的主键

image-20230927202957346

多对多

多对多(m:n) 例如:老师和学生,学生和课程,用户和角色

一个老师可以有多个学生,一个学生也可以有多个老师 多对多的关系

一个学生可以选多门课程,一门课程也可以由多个学生选择 多对多的关系

一个用户可以有多个角色,一个角色也可以有多个用户 多对多的关系

多对多关系建表原则: 需要创建第三张表,中间表中至少两个字段,这两个字段分别作为外键指向各自一方的主键。

image-20230927194532422

一对一(通常单表)

一对一(1:1)

例如: 一个公司可以有一个注册地址,一个注册地址只能对一个公司。

例如:一个老公可以有一个老婆,一个老婆只能有一个老公

在实际的开发中应用不多.因为一对一可以创建成一张表。 两种建表原则:

  • 外键唯一:主表的主键和从表的外键(唯一),形成主外键关系,外键唯一UNIQUE
  • 外键是主键:主表的主键和从表的主键,形成主外键关系 image-20230927194541479

多表设计之多表分析及创建

需求:完成一个学校的选课系统,在选课系统中包含班级,学生和课程这些实体。

分析:

班级和学生: 一对多

学生和课程: 多对多

  1. 班级和学生之间是有关系存在:

    一个班级下包含多个学生,一个学生只能属于某一个班级(一对多的关系)。

  2. 学生和课程之间是有关系存在:

    一个学生可以选择多门课程,一门课程也可以被多个学生所选择(多对多的关系)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    -- 多表建表原则练习
    -- 班级和学生: 一对多
    -- 学生和课程: 多对多
    -- 创建班级表
    create table class(
    cid int primary key auto_increment,
    cname varchar(40)
    );
    -- 创建学生表
    create table student(
    sid int primary key auto_increment,
    sname varchar(40),
    c_id int,
    constraint stu_cls_fk1 foreign key(c_id) references class(cid)
    );

    -- 创建课程表
    create table course(
    co_id int primary key auto_increment,
    co_name varchar(40)
    );

    -- 创建中间表
    create table stu_co(
    sno int,
    cno int,
    constraint stu_co_fk1 foreign key(sno) references student(sid),
    constraint stu_co_fk2 foreign key(cno) references course(co_id)
    );

小结

  1. 1对多: 在多方创建一个字段作为外键 指向一方的主键
  2. 多对多: 创建一张中间表,这个表里面至少包含两个字段,都作为外键,分别指向各自一方的主键
  3. 1对1: 先当做1对多, 再在外键列添加唯一约束;一般开发中是创建一张表

连接查询

环境准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 创建部门表
CREATE TABLE dept (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20)
);

INSERT INTO dept (NAME) VALUES ('开发部'),('市场部'),('财务部');

-- 创建员工表
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
gender CHAR(1), -- 性别
salary DOUBLE, -- 工资
join_date DATE, -- 入职日期
dept_id INT
);

INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('孙悟空','男',7200,'2013-02-24',1);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('猪八戒','男',3600,'2010-12-02',2);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('唐僧','男',9000,'2008-08-08',2);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('白骨精','女',5000,'2015-10-07',3);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('蜘蛛精','女',4500,'2011-03-14',1);

交叉查询【了解】

我思故我在 ———— 笛卡尔

交叉查询其实是一种错误.数据大部分是无用数据,叫笛卡尔积.

假设集合A={a,b},集合B={0,1,2},则两个集合的笛卡尔积为{(a,0),(a,1),(a,2),(b,0),(b,1),(b,2)}。可以扩展到多个集合的情况。

交叉查询把若干张表(>=2)没有条件的连接在一起,进行展示

  1. 语法
1
2
3
4
5
6
select ... from1,表2 ;  

select a.字段,b.字段 from a,b ;
select a.*,b.* from a,b ;
--或者
select * from a,b;
  1. 练习: 使用交叉查询部门和员工
1
SELECT * FROM dept, emp;

image-20230927194549996

以上数据其实是左表的每条数据和右表的每条数据组合。左表有3条,右表有5条,最终组合后3*5=15条数据。

左表的每条数据和右表的每条数据组合,这种效果称为笛卡尔乘积

image-20230927194557129

内连接查询【重点】

​ 交叉查询产生这样的结果并不是我们想要的,那么怎么去除错误的,不想要的记录呢,当然是通过条件过滤。通常要查询的多个表之间都存在关联关系,那么就通过**关联关系(主外键关系)**去除笛卡尔积。

隐式内连接(SQL92标准)

隐式里面是没有inner关键字的

1
2
select [字段,字段,字段][*] from1,表2 where 连接条件 --(外键的值等于主键的值) 
select * from emp,dept where emp.dept_id = dept.id;

练习:查询员工的id,姓名,性别,薪资,加入日期,所属部门

1
2
3
select emp.id,emp.name,emp.gender,emp.salary,emp.join_date,dept.name from emp,dept where emp.dept_id = dept.id;
-- 取别名---开发中一般使用取别名的方式
select e.id,e.name,e.gender,e.salary,e.join_date,d.name from emp e,dept d where e.dept_id = d.id;

显式内连接(SQL99标准)

吼吼,99年的事情瞒不住啦~

显示里面是有inner关键字的

1
2
3
4
select [字段,字段,字段][*] from a [inner] join b on 连接条件 [ where 其它条件]
select * from emp inner join dept on emp.dept_id = dept.id
select * from emp inner join dept on emp.dept_id = dept.id where emp.id = 2
select * from emp join dept on emp.dept_id = dept.id where emp.id = 2

练习

查询所有部门下的员工信息,如果该部门下没有员工则不展示.

1
2
3
4
5
6
insert into dept values(null,'设计部');
insert into emp values(null,'吴承恩','男',10000,'2000-01-01',null);
-- 隐式内连接查询
select * from emp e,dept d where e.dept_id = d.id;
-- 显示内连接查询
select * from emp e inner join dept d on e.dept_id = d.id;

image-20230927194605532

小结

  1. 内连接的特点(查的是什么东西)

    内连接查询的是公共部分,满足连接条件(主外键关系)的部分

  2. 使用内连接的关键点

    • 使用主外键关系做为条件来去除无用信息. 抓住主外键的关系,用主外键作为连接条件 b表里面的外键 = a表里面的主键
    • 显示内连接里面的,on只能用主外键关联作为条件,如果还有其它条件,后面加where
  3. 语法

1
2
3
4
5
-- 隐式(不出现inner)
select * from a,b where a.主键=b.外键 and 其它条件

-- 显示(出现inner)
select * from a [inner] join b on a.主键=b.外键 where 其它条件

外连接查询【重点】

我们发现内连接查询出来的是公共部分. 如果要保证某张表的全部数据情况下进行连接查询. 那么就要使用外连接查询了. 外连接分为左外连接和右外连接

左外连接

以join左边的表为主表,展示主表的所有数据,根据条件查询连接右边表的数据,若满足条件则展示,若不满足则以null显示.

可以理解为:在内连接的基础上保证左边表的数据全部显示

image-20230927194616412

  1. 语法
1
select [字段][*] from a left [outer] join b on 条件
  1. 练习:查询所有部门下的员工
1
SSELECT * FROM dept LEFT OUTER JOIN emp ON emp.`dept_id`=dept.`id`;

右外连接

​ 以join右边的表为主表,展示右边表的所有数据,根据条件查询join左边表的数据,若满足则展示,若不满足则以null显示

可以理解为:在内连接的基础上保证右边表的数据全部显示

image-20230927194628900

  1. 语法
1
select 字段 from a right [outer] join b on 条件
  1. 练习:查询所有员工所对应的部门
1
SELECT * FROM dept RIGHT OUTER JOIN emp ON emp.dept_id=dept.id;

小结

  1. 语法
1
2
select * from a left [outer] join b on 连接条件  --左外连接
select * from a right [outer] join b on 连接条件 --右外连接
  1. 内连接和外连接的区别
    • 内连接: 查询的是公共部分,满足连接条件的部分
    • 左外连接: 以左边表为主表, 查询出左边表的所有的数据. 再通过连接条件匹配出右边表的数据, 如果满足连接条件, 展示右边表的数据; 如果不满足, 右边的数据通过null代替
    • 右外连接: 以右边表为主表, 查询出右边表的所有的数据. 再通过连接条件匹配出左边表的数据, 如果满足连接条件, 展示左边表的数据; 如果不满足, 左边的数据通过null代替
  2. 应用
1
2
3
4
5
6
7
1.用户1和订单m
查询所有的用户的订单信息 外连接
查询下单的用户的信息 内连接

2.用户1和账户m
查询所有的用户的账户信息 外连接
查询所有用户的开户信息 内连接

满外连接

注意:oracle里面有full join,可是在mysql对full join支持的不好。我们可以使用union来达到目的。

image-20230108160929768

1
2
3
4
5
6
7
select * from A full outer join B on 条件;

# mysql中用union达到目的
-- 使用union关键字实现左外连接和右外连接的并集
select * from dept3 left outer join emp3 on dept3.deptno = emp3.dept_id
union
select * from dept3 right outer join emp3 on dept3.deptno = emp3.dept_id;

自关联查询

MySQL有时在信息查询时需要进行对表自身进行关联查询,即一张表自己和自己关联,一张表当成多张表来用。注意自关联时表必须给表起别名。

1
2
3
select 字段列表 from1 a , 表1 b where 条件;
或者
select 字段列表 from1 a [left] join1 b on 条件;

操作例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 创建表,并建立自关联约束
create table t_sanguo(
eid int primary key ,
ename varchar(20),
manager_id int,
foreign key (manager_id) references t_sanguo (eid) -- 添加自关联约束
);
-- 添加数据
insert into t_sanguo values(1,'刘协',NULL);
insert into t_sanguo values(2,'刘备',1);
insert into t_sanguo values(3,'关羽',2);
insert into t_sanguo values(4,'张飞',2);
insert into t_sanguo values(5,'曹操',1);
insert into t_sanguo values(6,'许褚',5);
insert into t_sanguo values(7,'典韦',5);
insert into t_sanguo values(8,'孙权',1);
insert into t_sanguo values(9,'周瑜',8);
insert into t_sanguo values(10,'鲁肃',8);

-- 进行关联查询
-- 1.查询每个三国人物及他的上级信息,如: 关羽 刘备
select * from t_sanguo a, t_sanguo b where a.manager_id = b.eid;

子查询

我们刚刚讲解了内连接和外连接查询, 但是如果遇到很复杂的场景, 内连接和外连接查询可能查询不出来.我们就可以使用子查询了.

什么是子查询

直观一点: 一个查询语句里面至少包含2个select

  • 一个查询语句的结果作为另一个查询语句的条件
  • 有查询的嵌套,内部的查询称为子查询
  • 子查询要使用括号

尽管子查询的语法很灵活,没有固定的写法.但是它也有一些规律.

子查询结果的三种情况:

  1. 子查询的结果是一个值的时候 image-20230927194648300
  2. 子查询结果是单列多行的时候 image-20230927194654144
  3. 子查询的结果是多行多列 image-20230927194659770

子查询逻辑关键字

ALL关键字

  • ALL: 与子查询返回的所有值比较为true 则返回true
  • ALL可以与=、>、>=、<、<=、<>结合是来使用,分别表示等于、大于、大于等于、小于、小于等于、不等于其中的其中的所有数据。
  • ALL表示指定列中的值必须要大于子查询集的每一个值,即必须要大于子查询集的最大值;如果是小于号即小于子查询集的最小值。同理可以推出其它的比较运算符的情况。
1
2
3
4
5
6
7
8
9
selectfromwhere c > all(查询语句)
--等价于:
select ...from ... where c > result1 and c > result2 and c > result3

操作例子:
-- 查询年龄大于‘1003’部门所有年龄的员工信息
select * from emp3 where age > all(select age from emp3 where dept_id = '1003');
-- 查询不属于任何一个部门的员工信息
select * from emp3 where dept_id != all(select deptno from dept3);

ANY 和 SOME关键字

  • ANY:与子查询返回的任何值比较为true 则返回true
  • ANY可以与=、>、>=、<、<=、<>结合是来使用,分别表示等于、大于、大于等于、小于、小于等于、不等于其中的其中的任何一个数据。
  • 表示制定列中的值要大于子查询中的任意一个值,即必须要大于子查询集中的最小值。同理可以推出其它的比较运算符的情况。
  • SOME和ANY的作用一样,SOME可以理解为ANY的别名
1
2
3
4
5
6
7
selectfromwhere c > any(查询语句)
--等价于:
select ...from ... where c > result1 or c > result2 or c > result3

操作例子:
-- 查询年龄大于‘1003’部门任意一个员工年龄的员工信息
select * from emp3 where age > any(select age from emp3 where dept_id = '1003’);

IN关键字

  • IN关键字,用于判断某个记录的值,是否在指定的集合中
  • 在IN关键字前边加上not可以将条件反过来
1
2
3
4
5
6
7
selectfromwhere c in(查询语句)
--等价于:
select ...from ... where c = result1 or c = result2 or c = result3

# 操作:
-- 查询研发部和销售部的员工信息,包含员工号、员工名字
select eid,ename,t.name from emp3 where dept_id in (select deptno from dept3 where name = '研发部' or name = '销售部') ;

EXISTS关键字

  • 该子查询如果“有数据结果”(至少返回一行数据), 则该EXISTS() 的结果为“true”,外层查询执行
  • 该子查询如果“没有数据结果”(没有任何数据返回),则该EXISTS()的结果为“false”,外层查询不执行
  • EXISTS后面的子查询不返回任何实际数据,只返回真或假,当返回真时 where条件成立
  • 注意,EXISTS关键字,比IN关键字的运算效率高,因此,在实际开发中,特别是大数据量时,推荐使用EXISTS关键字
1
2
3
4
5
6
7
8
selectfromwhere exists(查询语句)

# 操作
-- 查询公司是否有大于60岁的员工,有则输出
select * from emp3 a where exists(select * from emp3 b where a.age > 60);

-- 查询有所属部门的员工信息
select * from emp3 a where exists(select * from dept3 b where a.dept_id = b.deptno);

子查询的结果是一个值的时候

子查询结果只要是单个值,肯定在WHERE后面作为条件 SELECT 查询字段 FROM 表 WHERE 字段[= > < <>](子查询);

查询工资最高的员工是谁?

  1. 查询最高工资是多少
1
SELECT MAX(salary) FROM emp;

image-20230927194714568

  1. 根据最高工资到员工表查询到对应的员工信息
1
SELECT * FROM emp WHERE salary=(SELECT MAX(salary) FROM emp);

image-20230927194725541

查询工资小于平均工资的员工有哪些?

  1. 查询平均工资是多少
1
SELECT AVG(salary) FROM emp;

image-20230927194737954

  1. 到员工表查询小于平均的员工信息
1
SELECT * FROM emp WHERE salary < (SELECT AVG(salary) FROM emp);

image-20230927194747846

子查询结果是单列多行的时候

子查询结果只要是单列,肯定在WHERE后面作为条件 子查询结果是单列多行,结果集类似于一个数组,父查询使用IN运算符 SELECT 查询字段 FROM 表 WHERE 字段 IN (子查询);

查询工资大于5000的员工,来自于哪些部门的名字

  1. 先查询大于5000的员工所在的部门id
1
SELECT dept_id FROM emp WHERE salary > 5000;

image-20230927194759702

  1. 再查询在这些部门id中部门的名字
1
SELECT dept.name FROM dept WHERE dept.id IN (SELECT dept_id FROM emp WHERE salary > 5000);

查询开发部与财务部所有的员工信息

  1. 先查询开发部与财务部的id
1
SELECT id FROM dept WHERE NAME IN('开发部','财务部');

image-20230927194806058

  1. 再查询在这些部门id中有哪些员工
1
SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE NAME IN('开发部','财务部'));

image-20230927194811976

子查询的结果是多行多列

子查询结果只要是多行多列,肯定在FROM后面作为 SELECT 查询字段 FROM (子查询) 表别名 WHERE 条件; 子查询作为表需要取别名,否则这张表没用名称无法访问表中的字段

查询出2011年以后入职的员工信息,包括部门名称

  1. 在员工表中查询2011-1-1以后入职的员工
1
SELECT * FROM emp WHERE join_date > '2011-1-1';

image-20230927194817612

  1. 查询所有的部门信息,与上面的虚拟表中的信息组合,找出所有部门id等于的dept_id
1
SELECT * FROM dept d, (SELECT * FROM emp WHERE join_date > '2011-1-1') e WHERE e.dept_id = d.id;

image-20230927194824106

小结

  1. 子查询的结果是单行单列(一个值情况), 一般放在where后面作为条件**, 通过=,>,<,<>**
1
select ... from ... where 列 [=><<>...] (子查询) 
  1. 子查询的结果是单列多行, 一般放在where后面作为条件**, 通过in**
1
select ... from ... wherein (子查询) 
  1. 子查询的结果是多行多列, 一般放在from后面作为虚拟表, 需要给虚拟表取别名
1
select ... from (子查询) as 别名   where 条件

事务

环境的准备

1
2
3
4
5
6
7
8
9
10
-- 账户表
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);

insert into account values (null,'zs',1000);
insert into account values (null,'ls',1000);
insert into account values (null,'ww',1000);

事务的概述

我们有这样的需求:保证一组操作全部成功或者失败,就要通过事务来实现,保证数据库数据的完整性。

事务指逻辑上的一组操作,组成这组操作的单元要么全部成功,要么全部失败。

事务用来管理 DDL、DML、DCL 操作,比如 insert,update,delete 语句,默认是自动提交的。

在MySQL中的事务(Transaction)是由存储引擎实现的,在MySQL中,只有InnoDB存储引擎才支持事务。

转账案例:

  • 操作: zs向李四转账100元
  • 组成单元: zs钱-100, ls钱+100
    • 操作成功: zs钱900,ls钱1100
    • 操作失败: zs钱1000,ls钱1000
    • 不可能发生: zs钱900,ls钱1000; zs钱1000,ls钱1100

MYSQL进行事务管理

自动事务(mysql默认)

一条sql语句就是一个事务

1
2
3
4
5
6
-- 场景: zs向ls转账100元
-- zs钱-100 ls钱+100
-- 自动事务管理: MySQL默认就是自动事务管理(自动开启事务,自动提交事务),一条sql语句就是一个事务
update account set money = money - 100 where name = 'zs';
-- 异常
update account set money = money + 100 where name = 'ls';

手动开启一个事务

方式一: 手动开启事务的方式 【掌握】

  • start transaction;开启事务
  • commit;提交
  • rollback;回滚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 没有异常
start transaction; -- 开启事务
update account set money = money - 100 where name = 'zs'; -- zs钱-100
-- 没有异常
update account set money = money + 100 where name = 'ls'; -- ls钱 +100
commit; -- 提交事务


-- 有异常
start transaction; -- 开启事务
update account set money = money - 100 where name = 'zs'; -- zs钱-100
-- 有异常
update account set money = money + 100 where name = 'ls'; -- ls钱 +100
rollback; -- 回滚事务

image-20230114071845294

方式二: 设置MYSQL中的自动提交的参数【了解】

查看MYSQL中事务是否自动提交

1
show variables like '%commit%';

设置自动提交的参数为OFF

1
2
3
select @@autocommit; -- 查看当前自动提交的状态
set autocommit=0 -- 0:OFF 禁止自动提交
-- set autocommit=1 -- 1:ON 开启自动提交

回滚点【了解】

什么是回滚点

​ 在某些成功的操作完成之后,后续的操作有可能成功有可能失败,但是不管成功还是失败,前面操作都已经成功,可以在当前成功的位置设置一个回滚点。可以供后续失败操作返回到该位置,而不是返回所有操作,这个点称之为回滚点。

回滚点的操作语句

image-20230927194836822

具体操作

  1. 将数据还原到1000
  2. 开启事务
  3. 让张三账号减3次钱
  4. 设置回滚点:savepoint three_times;
  5. 让张三账号减4次钱
  6. 回到回滚点:rollback to three_times;
  • 总结:设置回滚点可以让我们在失败的时候回到回滚点,而不是回到事务开启的时候。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
start transaction;
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
-- 以上sql语句没有问题
savepoint abc;
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
-- 出现异常,回滚到abc回滚点位置
rollback to abc;
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
update account set money = money - 100 where name = 'zs'; -- zs账户-100
commit;

应用场景

1
2
插入大量的数据的时候.  1亿条数据 需要插入很久.  
要求: 1亿条数据是一个整体,要么全部插入成功的 要么都不插入成功.

小结

  1. 手动开启事务 语法

    1
    2
    3
    start transaction;  -- 开启事务
    commit; -- 提交
    rollback; -- 回滚
  2. 注意

    • 建议手动开启事务, 用一次 就开启一次
    • 开启事务之后, 要么commit, 要么rollback
    • 一旦commit或者rollback, 当前的事务就结束了
    • 回滚到指定的回滚点, 但是这个时候事务没有结束的

事务特性和隔离级别

事务特性【面试题】

  • 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
1
2
3
4
eg: zs 1000; ls 1000; 
zs 给 ls转100
要么都发生zs 900; ls 1100;
要么都不发生zs 1000; ls 1000;
  • 一致性(Consistency)事务前后数据的完整性必须保持一致.
1
2
3
4
eg: zs 1000; ls 1000;  一共2000
zs 给 ls转100
要么都发生zs 900; ls 1100; 一共2000
要么都不发生zs 1000; ls 1000; 一共2000
  • 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
1
eg: zs 1000 给小红 转520, 张三 提交了
  • 隔离性(Isolation)事务的隔离性是指多个用户并发操作数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。 简单来说: 事务之间互不干扰

如果不考虑隔离性,会引发下面的问题

事务在操作时的理想状态: 所有的事务之间保持隔离,互不影响。因为并发操作,多个用户同时访问同一个数据。可能引发并发访问的问题

  • 读未提交(Read uncommitted) 一个事务可以读取另一个未提交事务的数据,最低级别,任何情况都无法保证,会造成脏读
  • 读已提交(Read committed) 一个事务要等另一个事务提交后才能读取数据,可避免脏读的发生,会造成不可重复读
  • 可重复读(Repeatable read) 就是在开始读取数据(事务开启)时,不再允许修改操作,可避免脏读、不可重复读的发生,但是会造成幻读
  • 串行(Serializable) 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

  

image-20230927194844316

事务隔离级别

可以通过设置事物隔离级别解决读的问题

事务四个隔离级别

级别 名字 隔离级别 脏读 不可重复读 幻读 数据库默认隔离级别
1 读未提交 read uncommitted 不合理,不用
2 读已提交 read committed Oracle
3 可重复读 repeatable read MySQL
4 串行化 serializable 效率低,不用

隔离级别越高,安全性越高,性能(效率)越差。

设置隔离级别

  • 设置事务隔离级别
1
2
3
set session transaction isolation level  隔离级别;
eg: 设置事务隔离级别为:read uncommitted,read committed,repeatable read,serializable
set session transaction isolation level read uncommitted;
  • 查询当前事务隔离级别
1
2
3
4
select @@tx_isolation;

-- 查看隔离级别
show variables like '%isolation%’;

实操-演示数据库安全性问题的发生

演示脏读

一个事物里面读到了另外一个事物没有提交的数据: read uncommitted

1.开启A,B窗口

2.分别查询A,B的隔离级别

1
select @@tx_isolation;

3.设置A窗口的隔离级别为read uncommitted(读未提交)

1
set session transaction isolation level read uncommitted;

4.A,B都开启事物

5.在B中zs向ls转账100,事务不提交

6.在A中查询账户

image-20230112131748346

解决脏读—演示不可重复读

不可重复读: 在一个事务里面,同一条语句,两次查询的结果不一致.

1.开启A,B窗口

2.分别查询A,B的隔离级别

1
select @@tx_isolation;

3.设置A窗口的隔离级别为Read committed(读已提交)

1
set session transaction isolation level Read committed;

4.A,B都开启事物

5.在B中张三向李四转账100,事物不提交

6.在A中查询账户(避免脏读发生)

7.B中提交事物

8.在A中查询账户(两次查询的结果不一致,不可重复读发生)

image-20230112132319535

解决不可重复读

1.开启A,B窗口

2.分别查询A,B的隔离级别

1
select @@tx_isolation

3.设置A窗口的隔离级别为Repeatable read

1
set session transaction isolation level Repeatable read;

4.A,B都开启事物

5.在B中张三向李四转账100,事物不提交

6.A中查询账户

7.B中提交

8.A中查询账户

9.A中结束事物,再重新查询

image-20230112132844617

演示隔离级别Serializable

1.开启A,B窗口

2.分别查询A,B的隔离级别

1
select @@tx_isolation

3.设置A窗口的隔离级别为Serializable

1
set session transaction isolation level Serializable;

4.A,B都开始事物

5.B中向account账户插入一条数据,不提交

6.A中查询

7.在B中结束事物

8.A中查询

image-20230112133535441

image-20230112133702052

实际上是对表加锁了,所以序列化效率低。

概述

相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。下表中罗列出了各存储引擎对锁的支持情况:

image-20230112230424210

image-20230112230450189

从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web 应用;

而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理(OLTP)系统。

MyISAM表锁

MyISAM 存储引擎只支持表锁

如何加表锁:

MyISAM

在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁

在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁

这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。

1
2
加读锁 : lock table table_name read; 
加写锁 : lock table table_name write;

表锁特点:

1) 对MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;

2) 对MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作;

简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁,则既会阻塞读,又会阻塞写。

此外,MyISAM 的读写锁调度是写优先,这也是MyISAM不适合做写为主的表的存储引擎的原因。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- MySQL的锁机制
drop database if exists mydb14_lock;
create database mydb14_lock ;

use mydb14_lock;

create table `tb_book` (
`id` int(11) auto_increment,
`name` varchar(50) default null,
`publish_time` date default null,
`status` char(1) default null,
primary key (`id`)
) engine=myisam default charset=utf8 ;

insert into tb_book (id, name, publish_time, status) values(null,'java编程思想','2088-08-01','1');
insert into tb_book (id, name, publish_time, status) values(null,'solr编程思想','2088-08-08','0');


create table `tb_user` (
`id` int(11) auto_increment,
`name` varchar(50) default null,
primary key (`id`)
) engine=myisam default charset=utf8 ;

insert into tb_user (id, name) values(null,'令狐冲');
insert into tb_user (id, name) values(null,'田伯光');

image-20230112231621652

image-20230112231902236

InnoDB行锁

行锁特点 :偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是 采用了行级锁。

InnoDB 实现了以下两种类型的行锁。

  • 共享锁(S):又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁(X):又称为写锁,简称X锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);

对于普通SELECT语句,InnoDB不会加任何锁;

1
2
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE 
排他锁(X) :SELECT * FROM table_name WHERE ... FOR UPDATE

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 行锁 
drop table if exists test_innodb_lock;
create table test_innodb_lock(
id int(11),
name varchar(16),
sex varchar(1)
)engine = innodb ;

insert into test_innodb_lock values(1,'100','1');
insert into test_innodb_lock values(3,'3','1');
insert into test_innodb_lock values(4,'400','0');
insert into test_innodb_lock values(5,'500','1');
insert into test_innodb_lock values(6,'600','0');
insert into test_innodb_lock values(7,'700','0');
insert into test_innodb_lock values(8,'800','1');
insert into test_innodb_lock values(9,'900','1');
insert into test_innodb_lock values(1,'200','0');

create index idx_test_innodb_lock_id on test_innodb_lock(id);
create index idx_test_innodb_lock_name on test_innodb_lock(name);

image-20230112233405246

日志

在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的方方面面,以帮助数据库管理员追踪数据库曾经发生过的各种事件。MySQL 也不例外。

错误日志

错误日志是 MySQL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志。

该日志是默认开启的 , 默认存放目录为 mysql 的数据目录, 默认的日志文件名为 hostname.err(hostname是主机名)。

查看日志位置指令 : show variables like 'log_error%';

二进制日志-binlog

二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该binlog实现的。

二进制日志,MySQl8.0默认已经开启,低版本的MySQL的需要通过配置文件开启,并配置MySQL日志的格式。

Windows系统:my.ini Linux系统:my.cnf

1
2
3
4
5
#配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如 : mysqlbin.000001,mysqlbin.000002
log_bin=mysqlbin

#配置二进制日志的格式
binlog_format=STATEMENT

STATEMENT 该日志格式在日志文件中记录的都是SQL语句(statement),每一条对数据进行修改的SQL都会记录在日志文件中,通过Mysql提供的mysqlbinlog工具,可以清晰的查看到每条语句的文本。主从复制的时候,从库(slave)会将日志解析为原文本,并在从库重新执行一次。

ROW

该日志格式在日志文件中记录的是每一行的数据变更,而不是记录SQL语句。比如,执行SQL语句 : update tb_book set status='1' , 如果是STATEMENT 日志格式,在日志中会记录一行SQL文件; 如果是ROW,由于是对全表进行更新,也就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更。

MIXED

混合了STATEMENT 和 ROW两种格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- 查看MySQL是否开启了binlog日志
show variables like 'log_bin';


-- 查看binlog日志的格式
show variables like 'binlog_format';

-- 查看所有日志
show binlog events;

-- 查看最新的日志
show master status;

-- 查询指定的binlog日志
show binlog events in 'binlog.000010';

# 执行以下命令测试binlog
select * from mydb1.emp2; -- 不会记录
select count(*) from mydb1.emp2; -- 不会记录
update mydb1.emp2 set salary = 8000; -- 会记录

-- 从指定的位置开始,查看指定的Binlog日志
show binlog events in 'binlog.000010' from 156;


-- 从指定的位置开始,查看指定的Binlog日志,限制查询的条数
show binlog events in 'binlog.000010' from 156 limit 2;
--从指定的位置开始,带有偏移,查看指定的Binlog日志,限制查询的条数
show binlog events in 'binlog.000010' from 666 limit 1, 2;

-- 清空所有的 binlog 日志文件
reset master

查询日志

查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句。

默认情况下, 查询日志是未开启的。如果需要开启查询日志,可以设置以下配置 :

1
2
3
4
5
#该选项用来开启查询日志 , 可选值 : 0 或者 10 代表关闭, 1 代表开启 
general_log=1

#设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.log
general_log_file=file_name

操作:

1
2
3
4
5
6
7
8
9
10
11
12
-- 查看MySQL是否开启了查询日志
show variables like 'general_log';

-- 开启查询日志
set global general_log=1;

select * from mydb1.emp2;
select * from mydb6_view.emp;

select count(*) from mydb1.emp2;
select count(*) from mydb6_view.emp;
update mydb1.emp2 set salary = 9000;

慢查询日志

慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于 min_examined_row_limit 的所有的SQL语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度可以到微秒。

1
2
3
4
5
6
7
8
9
# 该参数用来控制慢查询日志是否开启, 可取值: 101 代表开启, 0 代表关闭
slow_query_log=1

# 该参数用来指定慢查询日志的文件名
slow_query_log_file=slow_query.log

# 该选项用来配置查询的时间限制, 超过这个时间将认为值慢查询, 将需要进行日志记录, 默认10s

long_query_time=10

也可以用命令开启

1
2
3
4
5
-- 查看MySQL是否开启了慢查询日志
show variables like 'slow_query_log%';

-- 开启慢查询日志
set global slow_query_log=1;

数据的备份和还原

实操-数据的备份和还原

​ 在服务器进行数据传输、数据存储和数据交换,就有可能产生数据故障。比如发生意外停机或存储介质损坏。这时,如果没有采取数据备份和数据恢复手段与措施,就会导致数据的丢失,造成的损失是无法弥补与估量的。

命令行方式

  1. 备份格式
1
mysqldump -u用户名 -p密码 数据库 > 文件的路径
  1. 还原格式
1
SOURCE 导入文件的路径

注意:还原的时候需要先登录MySQL,并创建数据库和选中对应的数据库

使用navicat备份和还原

  1. 备份

image-20230927194903254

  1. 还原

image-20230927194910398

会使用navicat操作就行了, 工作里面一般是运维在处理, 运维处理的话一般使用定时任务自动备份.

数据库设计三大范式

数据库设计三大范式

​ 好的数据库设计对数据的存储性能和后期的程序开发,都会产生重要的影响。

​ 建立科学的,规范的数据库就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式。

1NF

概述

​ 数据库表的每一列都是不可分割的原子数据项,不能是集合、数组等非原子数据项。即表中的某个列有多个值时,必须拆分为不同的列。简而言之,第一范式每一列不可再拆分,称为原子性

应用

image-20230927194918374

总结

​ 如果不遵守第一范式,查询出数据还需要进一步处理(查询不方便)。遵守第一范式,需要什么字段的数据就查询什么数据(方便查询)

2NF

概述

在满足第一范式的前提下,表中的每一个字段都完全依赖于主键。所谓完全依赖是指不能存在仅依赖主键一部分的列。简而言之,第二范式就是在第一范式的基础上所有列完全依赖于主键列。当存在一个复合主键包含多个主键列的时候,才会发生不符合第二范式的情况。比如有一个主键有两个列,不能存在这样的属性,它只依赖于其中一个列,这就是不符合第二范式。

简而言之,第二范式需要满足:

  1. 一张表只描述一件事情
  2. 表中的每一个列都依赖于主键

应用

image-20230927203339409

总结

​ 如果不准守第二范式,数据冗余,相同数据无法区分。遵守第二范式减少数据冗余,通过主键区分相同数据。

3NF

概述

​ 在满足第二范式的前提下,表中的每一列都直接依赖于主键,而不是通过其它的列来间接依赖于主键。简而言之,第三范式就是所有列不依赖于其它非主键列,也就是在满足2NF的基础上,任何非主列不得传递依赖于主键。所谓传递依赖,指的是如果存在”A → B → C”的决定关系,则C传递依赖于A。因此,满足第三范式的数据库表应该不存在如下依赖关系:主键列 → 非主键列x → 非主键列y

应用

image-20230927194927594

总结

​ 如果不准守第三范式,可能会有相同数据无法区分,修改数据的时候多张表都需要修改(不方便修改)。遵守第三范式通过id可以区分相同数据,修改数据的时候只需要修改一张表(方便修改)。

小结

image-20230927194936246

🔖MySQL再次进阶

函数

在MySQL中,为了提高代码重用性和隐藏实现细节,MySQL提供了很多函数。函数可以理解为别人封装好的模板代码

聚合函数

在MySQL中,聚合函数主要由:count,sum,min,max,avg,这些聚合函数我们之前都学过,不再重复。这里我们学习另外一个函数:group_concat(),该函数用户实现行的合并

group_concat()

group_concat()函数首先根据group by指定的列进行分组,并且用分隔符分隔,将同一个分组中的值连接起来,返回一个字符串结果。

1
group_concat([distinct] 字段名 [order by 排序字段 asc/desc] [separator '分隔符'])

说明:

(1)使用distinct可以排除重复值; (2)如果需要对结果中的值进行排序,可以使用order by子句; (3)separator是一个字符串值,默认为逗号

准备数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create database mydb4;
use mydb4;

create table emp(
emp_id int primary key auto_increment comment '编号',
emp_name char(20) not null default '' comment '姓名',
salary decimal(10,2) not null default 0 comment '工资',
department char(20) not null default '' comment '部门'
);

insert into emp(emp_name,salary,department)
values('张晶晶',5000,'财务部'),('王飞飞',5800,'财务部'),('赵刚',6200,'财务部'),('刘小贝',5700,'人事部'),
('王大鹏',6700,'人事部'),('张小斐',5200,'人事部'),('刘云云',7500,'销售部'),('刘云鹏',7200,'销售部'),
('刘云鹏',7800,'销售部');

操作:

1
2
-- 将所有员工的名字合并成一行 
select group_concat(emp_name) from emp;

image-20230108163334628

1
2
-- 指定分隔符合并 
select department,group_concat(emp_name separator ';' ) from emp group by department;

image-20230108163419721

1
2
-- 指定排序方式和分隔符 
select department,group_concat(emp_name order by salary desc separator ';' ) from emp group by department;

image-20230108163503835

数学函数

函数名 描述 实例
ABS(x) 返回 x 的绝对值 返回 -1 的绝对值:SELECT ABS(-1) – 返回1
CEIL(x) 返回大于或等于 x 的最小整数 SELECT CEIL(1.5) – 返回2
FLOOR(x) 返回小于或等于 x 的最大整数 小于或等于 1.5 的整数:SELECT FLOOR(1.5) – 返回1
GREATEST(expr1, expr2, expr3, ...) 返回列表中的最大值 返回以下数字列表中的最大值:SELECT GREATEST(3, 12, 34, 8, 25); – 34
返回以下字符串列表中的最大值:SELECT GREATEST(“Google”, “Runoob”, “Apple”); – Runoob
LEAST(expr1, expr2, expr3, ...) 返回列表中的最小值 返回以下数字列表中的最小值:SELECT LEAST(3, 12, 34, 8, 25); – 3
返回以下字符串列表中的最小值:SELECT LEAST(“Google”, “Runoob”, “Apple”); – Apple
MAX(expression) 返回字段 expression 中的最大值 返回数据表 Products 中字段 Price 的最大值:
SELECT MAX(Price) AS LargestPrice FROM Products;
MIN(expression) 返回字段 expression 中的最小值 返回数据表 Products 中字段 Price 的最小值:
SELECT MIN(Price) AS MinPrice FROM Products;
MOD(x,y) 返回 x 除以 y 以后的余数 5 除于 2 的余数:SELECT MOD(5,2) – 1
PI() 返回圆周率(3.141593) SELECT PI() –3.141593
POW(x,y) 返回 x 的 y 次方 2 的 3 次方:SELECT POW(2,3) – 8
RAND() 返回 0 到 1 的随机数 SELECT RAND() –0.93099315644334
ROUND(x) 返回离 x 最近的整数(遵循四舍五入) SELECT ROUND(1.23456) –1
ROUND(x,y) 返回指定位数的小数(遵循四舍五入) SELECT ROUND(1.23456,3) –1.235
TRUNCATE(x,y) 返回数值 x 保留到小数点后 y 位的值(与 ROUND 最大的区别是不会进行四舍五入) SELECT TRUNCATE(1.23456,3) – 1.234

字符串函数

函数 描述 实例
CHAR_LENGTH(s) 返回字符串 s 的字符数,注意length(返回的是字节的长度,汉字会有区别,utf8一个汉字3个字节) 返回字符串 RUNOOB 的字符数:
SELECT CHAR_LENGTH(“RUNOOB”) AS LengthOfString;
CHARACTER_LENGTH(s) 返回字符串 s 的字符数,相当于char_length的简写 返回字符串 RUNOOB 的字符数:
SELECT CHARACTER_LENGTH(“RUNOOB”) AS LengthOfString;
CONCAT(s1,s2…sn) 字符串 s1,s2 等多个字符串合并为一个字符串 合并多个字符串:
SELECT CONCAT(“SQL “, “Runoob “, “Gooogle “, “Facebook”) AS ConcatenatedString;
CONCAT_WS(x, s1,s2…sn) 同 CONCAT(s1,s2,…) 函数,但是每个字符串之间要加上 x,x 可以是分隔符 合并多个字符串,并添加分隔符:
SELECT CONCAT_WS(“-“, “SQL”, “Tutorial”, “is”, “fun!”)AS ConcatenatedString;
FIELD(s,s1,s2…) 返回第一个字符串 s 在字符串列表(s1,s2…)中的位置 返回字符串 c 在列表值中的位置:
SELECT FIELD(“c”, “a”, “b”, “c”, “d”, “e”);
LTRIM(s) 去掉字符串 s 开始处的空格 去掉字符串 RUNOOB开始处的空格:
SELECT LTRIM(“ RUNOOB”) AS LeftTrimmedString;– RUNOOB
MID(s,n,len) 从字符串 s 的 n 位置截取长度为 len 的子字符串,同 SUBSTRING(s,n,len) 从字符串 RUNOOB 中的第 2 个位置截取 3个 字符:
SELECT MID(“RUNOOB”, 2, 3) AS ExtractString; – UNO
POSITION(s1 IN s) 从字符串 s 中获取 s1 的开始位置 返回字符串 abc 中 b 的位置:
SELECT POSITION(‘b’ in ‘abc’) – 2
REPLACE(s,s1,s2) 将字符串 s2 替代字符串 s 中的字符串 s1 将字符串 abc 中的字符 a 替换为字符 x:
SELECT REPLACE(‘abc’,’a’,’x’) –xbc
REVERSE(s) 将字符串s的顺序反过来 将字符串 abc 的顺序反过来:
SELECT REVERSE(‘abc’) – cba
RIGHT(s,n) 返回字符串 s 的后 n 个字符 返回字符串 runoob 的后两个字符:
SELECT RIGHT(‘runoob’,2) – ob
RTRIM(s) 去掉字符串 s 结尾处的空格 去掉字符串 RUNOOB 的末尾空格:
SELECT RTRIM(“RUNOOB “) AS RightTrimmedString; – RUNOOB
STRCMP(s1,s2) 比较字符串 s1 和 s2,如果 s1 与 s2 相等返回 0 ,如果 s1>s2 返回 1,如果 s1<s2 返回 -1 比较字符串:
SELECT STRCMP(“runoob”, “runoob”); – 0
SUBSTR(s, start, length) 从字符串 s 的 start 位置截取长度为 length 的子字符串,和substring一样 从字符串 RUNOOB 中的第 2 个位置截取 3个 字符:
SELECT SUBSTR(“RUNOOB”, 2, 3) AS ExtractString; – UNO
SUBSTRING(s, start, length) 从字符串 s 的 start 位置截取长度为 length 的子字符串 从字符串 RUNOOB 中的第 2 个位置截取 3个 字符:
SELECT SUBSTRING(“RUNOOB”, 2, 3) AS ExtractString; – UNO
TRIM(s) 去掉字符串 s 开始和结尾处的空格 去掉字符串 RUNOOB 的首尾空格:
SELECT TRIM(‘ RUNOOB ‘) AS TrimmedString;
UCASE(s) 将字符串转换为大写,和UPPER一样 将字符串 runoob 转换为大写:
SELECT UCASE(“runoob”); – RUNOOB
UPPER(s) 将字符串转换为大写 将字符串 runoob 转换为大写:
SELECT UPPER(“runoob”); – RUNOOB
LCASE(s) 将字符串 s 的所有字母变成小写字母,和LOWER一样 字符串 RUNOOB 转换为小写:
SELECT LCASE(‘RUNOOB’) – runoob
LOWER(s) 将字符串 s 的所有字母变成小写字母 字符串 RUNOOB 转换为小写:
SELECT LOWER(‘RUNOOB’) – runoob

日期函数

函数名 描述 实例
UNIX_TIMESTAMP() 返回从1970-01-01 00:00:00到当前毫秒值 select UNIX_TIMESTAMP() -> 1632729059
UNIX_TIMESTAMP(DATE_STRING) 将制定日期转为毫秒值时间戳 SELECT UNIX_TIMESTAMP(‘2011-12-07 13:01:03’);
FROM_UNIXTIME(BIGINT UNIXTIME[, STRING FORMAT]) 将毫秒值时间戳转为指定格式日期 SELECT FROM_UNIXTIME(1598079966,’%Y-%m-%d %H:%i:%s’); (1598079966,’%Y-%m-%d %H:%i:%s’); -> 2020-08-22 15-06-06
CURDATE() 返回当前日期 SELECT CURDATE();-> 2018-09-19
CURRENT_DATE() 返回当前日期 SELECT CURRENT_DATE();-> 2018-09-19
CURRENT_TIME 返回当前时间 SELECT CURRENT_TIME();-> 19:59:02
CURTIME() 返回当前时间 SELECT CURTIME();-> 19:59:02
CURRENT_TIMESTAMP() 返回当前日期和时间 SELECT CURRENT_TIMESTAMP()-> 2018-09-19 20:57:43
DATE() 从日期或日期时间表达式中提取日期值 SELECT DATE(“2017-06-15”); -> 2017-06-15
DATEDIFF(d1,d2) 计算日期 d1->d2 之间相隔的天数 SELECT DATEDIFF(‘2001-01-01’,’2001-02-02’)-> -32
TIMEDIFF(time1, time2) 计算时间差值 SELECT TIMEDIFF(“13:10:11”, “13:10:10”);-> 00:00:01
DATE_FORMAT(d,f) 按表达式 f的要求显示日期 d SELECT DATE_FORMAT(‘2011-11-11 11:11:11’,’%Y-%m-%d %r’)-> 2011-11-11 11:11:11 AM
STR_TO_DATE(string, format_mask) 将字符串转变为日期 SELECT STR_TO_DATE(“August 10 2017”, “%M %d %Y”);-> 2017-08-10
DATE_SUB(date,INTERVAL expr type) 函数从日期减去指定的时间间隔。 Orders 表中 OrderDate 字段减去 2 天:SELECT OrderId,DATE_SUB(OrderDate,INTERVAL 2 DAY) AS OrderPayDateFROM Orders
ADDDATE/DATE_ADD(d,INTERVAL expr type) 计算起始日期 d 加上一个时间段后的日期,type 值可以是:MICROSECOND
SECOND
MINUTE
HOUR
DAY
WEEK
MONTH
QUARTER
YEAR
DAY_MINUTE
DAY_HOUR
YEAR_MONTH
SELECT DATE_ADD(“2017-06-15”, INTERVAL 10 DAY);
-> 2017-06-25

SELECT DATE_ADD(“2017-06-15 09:34:21”, INTERVAL 15 MINUTE);
-> 2017-06-15 09:49:21

SELECT DATE_ADD(“2017-06-15 09:34:21”, INTERVAL -3 HOUR);
->2017-06-15 06:34:21

SELECT DATE_ADD(“2017-06-15 09:34:21”, INTERVAL -3 HOUR);
->2017-04-15
DATE_ADD(d,INTERVAL expr type) 计算起始日期 d 加上一个时间段后的日期,type 值可以是:SECOND_MICROSECOND
MINUTE_MICROSECOND
MINUTE_SECOND
HOUR_MICROSECOND
HOUR_SECOND
HOUR_MINUTE
DAY_MICROSECOND
DAY_SECOND
DAY_MINUTE
DAY_HOUR
YEAR_MONTH
SELECT DATE_ADD(“2017-06-15”, INTERVAL 10 DAY);
-> 2017-06-25

SELECT DATE_ADD(“2017-06-15 09:34:21”, INTERVAL 15 MINUTE);
-> 2017-06-15 09:49:21

SELECT DATE_ADD(“2017-06-15 09:34:21”, INTERVAL -3 HOUR);
->2017-06-15 06:34:21

SELECT DATE_ADD(“2017-06-15 09:34:21”, INTERVAL -3 HOUR);
->2017-04-15
EXTRACT(type FROM d) 从日期 d 中获取指定的值,type 指定返回的值。type可取值为:
MICROSECOND
SECOND
MINUTE
HOUR
SELECT EXTRACT(MINUTE FROM ‘2011-11-11 11:11:11’) -> 11
LAST_DAY(d) 返回给给定日期的那一月份的最后一天 SELECT LAST_DAY(“2017-06-20”);-> 2017-06-30
MAKEDATE(year, day-of-year) 基于给定参数年份 year 和所在年中的天数序号 day-of-year 返回一个日期 SELECT MAKEDATE(2017, 3);-> 2017-01-03
YEAR(d) 返回年份 SELECT YEAR(“2017-06-15”);-> 2017
MONTH(d) 返回日期d中的月份值,1 到 12 SELECT MONTH(‘2011-11-11 11:11:11’)->11
DAY(d) 返回日期值 d 的日期部分 SELECT DAY(“2017-06-15”); -> 15
HOUR(t) 返回 t 中的小时值 SELECT HOUR(‘1:2:3’)-> 1
MINUTE(t) 返回 t 中的分钟值 SELECT MINUTE(‘1:2:3’)-> 2
SECOND(t) 返回 t 中的秒钟值 SELECT SECOND(‘1:2:3’)-> 3
QUARTER(d) 返回日期d是第几季节,返回 1 到 4 SELECT QUARTER(‘2011-11-11 11:11:11’)-> 4
MONTHNAME(d) 返回日期当中的月份名称,如 November SELECT MONTHNAME(‘2011-11-11 11:11:11’)-> November
MONTH(d) 返回日期d中的月份值,1 到 12 SELECT MONTH(‘2011-11-11 11:11:11’)->11
DAYNAME(d) 返回日期 d 是星期几,如 Monday,Tuesday SELECT DAYNAME(‘2011-11-11 11:11:11’)->Friday
DAYOFMONTH(d) 计算日期 d 是本月的第几天 SELECT DAYOFMONTH(‘2011-11-11 11:11:11’)->11
DAYOFWEEK(d) 日期 d 今天是星期几,1 星期日,2 星期一,以此类推 SELECT DAYOFWEEK(‘2011-11-11 11:11:11’)->6
DAYOFYEAR(d) 计算日期 d 是本年的第几天 SELECT DAYOFYEAR(‘2011-11-11 11:11:11’)->315
WEEK(d) 计算日期 d 是本年的第几个星期,范围是 0 到 53 SELECT WEEK(‘2011-11-11 11:11:11’)-> 45
WEEKDAY(d) 日期 d 是星期几,0 表示星期一,1 表示星期二 SELECT WEEKDAY(“2017-06-15”);-> 3
WEEKOFYEAR(d) 计算日期 d 是本年的第几个星期,范围是 0 到 53 SELECT WEEKOFYEAR(‘2011-11-11 11:11:11’)-> 45
YEARWEEK(date, mode) 返回年份及第几周(0到53),mode 中 0 表示周天,1表示周一,以此类推 SELECT YEARWEEK(“2017-06-15”);-> 201724
NOW() 返回当前日期和时间 SELECT NOW()-> 2018-09-19 20:57:43

控制流函数

if逻辑判断语句

格式 解释 案例
IF(expr,v1,v2) 如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2。 SELECT IF(1 > 0,’正确’,’错误’) ->正确
IFNULL(v1,v2) 如果 v1 的值不为 NULL,则返回 v1,否则返回 v2。 SELECT IFNULL(null,’Hello Word’)->Hello Word
ISNULL(expression) 判断表达式是否为 NULL SELECT ISNULL(NULL);->1
NULLIF(expr1, expr2) 比较两个字符串,如果字符串 expr1 与 expr2 相等 返回 NULL,否则返回 expr1 SELECT NULLIF(25, 25);->

case when语句

1
2
3
4
5
6
7
CASE expression
WHEN condition1 THEN result1
WHEN condition2 THEN result2
...
WHEN conditionN THEN resultN
ELSE result
END

CASE 表示函数开始,END 表示函数结束。如果 condition1 成立,则返回 result1, 如果 condition2 成立,则返回 result2,当全部不成立则返回 result,而当有一个成立之后,后面的就不执行了

1
2
select case 100 when 50 then 'tom' when 100 then 'mary'else 'tim' end ;
select case when 1=2 then 'tom' when 2=2 then 'mary' else'tim' end ;

操作例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use mydb4; 
-- 创建订单表
create table orders(
oid int primary key, -- 订单id
price double, -- 订单价格
payType int -- 支付类型(1:微信支付 2:支付宝支付 3:银行卡支付 4:其他)
);

insert into orders values(1,1200,1);
insert into orders values(2,1000,2);
insert into orders values(3,200,3);
insert into orders values(4,3000,1);
insert into orders values(5,1500,2);

-- 方式1
select
* ,
case
when payType=1 then '微信支付'
when payType=2 then '支付宝支付'
when payType=3 then '银行卡支付'
else '其他支付方式'
end as payTypeStr
from orders;
-- 方式2
select
* ,
case payType
when 1 then '微信支付'
when 2 then '支付宝支付'
when 3 then '银行卡支付'
else '其他支付方式'
end as payTypeStr
from orders;

窗口函数🆕

介绍

MySQL 8.0 新增窗口函数,窗口函数又被称为开窗函数,与Oracle 窗口函数类似,属于MySQL的一大特点.

非聚合窗口函数是相对于聚函数来说的。聚合函数是对一组数据计算后返回单个值(即分组),非聚合函数一次只会处理一行数据。窗口聚合函数在行记录上计算某个字段的结果时,可将窗口范围内的数据输入到聚合函数中,并不改变行数。

类别

image-20230108172044523

image-20230108172117277

另外还有开窗聚合函数: SUM,AVG,MIN,MAX

语法结构

1
2
3
4
5
window_function ( expr ) OVER ( 
PARTITION BY ...
ORDER BY ...
frame_clause
)

其中,window_function 是窗口函数的名称;expr 是参数,有些函数不需要参数;OVER子句包含三个选项:

  • 分区(PARTITION BY):PARTITION BY选项用于将数据行拆分成多个分区(组),它的作用类似于GROUP BY分组。如果省略了 PARTITION BY,所有的数据作为一个组进行计算
  • 排序(ORDER BY):OVER 子句中的ORDER BY选项用于指定分区内的排序方式,与 ORDER BY 子句的作用类似
  • 窗口大小(frame_clause):frame_clause选项用于在当前分区内指定一个计算窗口,也就是一个与当前行相关的数据子集。

序号函数

可以用来实现分组排序,并添加序号。

1
2
3
4
row_number()|rank()|dense_rank() over ( 
partition by ...
order by ...
)

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use mydb4; 
create table employee(
dname varchar(20), -- 部门名
eid varchar(20),
ename varchar(20),
hiredate date, -- 入职日期
salary double -- 薪资
);

insert into employee values('研发部','1001','刘备','2021-11-01',3000);
insert into employee values('研发部','1002','关羽','2021-11-02',5000);
insert into employee values('研发部','1003','张飞','2021-11-03',7000);
insert into employee values('研发部','1004','赵云','2021-11-04',7000);
insert into employee values('研发部','1005','马超','2021-11-05',4000);
insert into employee values('研发部','1006','黄忠','2021-11-06',4000);

insert into employee values('销售部','1007','曹操','2021-11-01',2000);
insert into employee values('销售部','1008','许褚','2021-11-02',3000);
insert into employee values('销售部','1009','典韦','2021-11-03',5000);
insert into employee values('销售部','1010','张辽','2021-11-04',6000);
insert into employee values('销售部','1011','徐晃','2021-11-05',9000);
insert into employee values('销售部','1012','曹洪','2021-11-06',6000);

image-20230108173157504

ROW_NUMBER()

排序:1,2,3

1
2
3
4
5
6
7
-- 对每个部门的员工按照薪资排序,并给出排名
select
dname,
ename,
salary,
row_number() over(partition by dname order by salary desc) as rn
from employee;

image-20230108172604072

可以发现薪资一样,但是有第一第二名,是连续的按顺序的。

RANK()

排序:1,1,3

1
2
3
4
5
6
7
-- 对每个部门的员工按照薪资排序,并给出排名 rank
select
dname,
ename,
salary,
rank() over(partition by dname order by salary desc) as rn
from employee;

image-20230108172633646

可以发现如果薪资一样,排序序号不是连续的。

DENSE_RANK()

排序:1,1,2

1
2
3
4
5
6
7
-- 对每个部门的员工按照薪资排序,并给出排名 dense-rank
select
dname,
ename,
salary,
dense_rank() over(partition by dname order by salary desc) as rn
from employee;

image-20230108172657767

可以发现薪资一样,是并列第一,而且是连续的排名,这种应该更符合平常的思维。

1
2
3
4
5
6
7
8
9
10
11
12
13
--求出每个部门薪资排在前三名的员工- 分组求TOPN
select
*
from
(
select
dname,
ename,
salary,
dense_rank() over(partition by dname order by salary desc) as rn
from employee
)t
where t.rn <= 3

image-20230108173739427

取每组的前三名,也很有用

1
2
3
4
5
6
7
8
-- 对所有员工进行全局排序(不分组)
-- 不加partition by表示全局排序
select
dname,
ename,
salary,
dense_rank() over( order by salary desc) as rn
from employee;

image-20230108172841571

不加partition by是所有的拿来排序

开窗聚合函数

在窗口中每条记录动态地应用聚合函数(SUM()、AVG()、MAX()、MIN()、COUNT()),可以动态计算在指定的窗口内的各种聚合函数值。

1
2
3
4
5
6
7
8
-- 每个部门从入职到现在的工资成本
select
dname,
ename,
salary,
hiredate,
sum(salary) over(partition by dname order by hiredate) as 工资成本
from employee;

image-20230108174832322

1
2
3
4
5
6
7
-- 每个部门从入职到现在的工资成本,可以计算占比
select
dname,
ename,
salary,
sum(salary) over(partition by dname) as pv1 -- 如果没有order by排序语句 默认把分组内的所有数据进行sum操作
from employee;

image-20230108175009497

1
2
3
4
5
6
7
8
9
-- 每个部门从入职到现在的工资成本,可以控制范围
select
dname,
ename,
hiredate,
salary,
-- 相加的时候,从第一行加到当前行。-> 其实默认的行为就是这样的
sum(salary) over(partition by dname order by hiredate rows between unbounded preceding and current row) as pv1
from employee;

image-20230108175225116

1
2
3
4
5
6
7
8
9
-- 每个部门从入职到现在的工资成本
select
dname,
ename,
hiredate,
salary,
-- 相加的时候,从前3行开始加到当前行
sum(salary) over(partition by dname order by hiredate rows between 3 preceding and current row) as pv1
from employee;

image-20230108175552368

1
2
3
4
5
6
7
8
9
-- 每个部门从入职到现在的工资成本
select
dname,
ename,
hiredate,
salary,
-- 相加的时候,从前3行开始加到当前行往后1行
sum(salary) over(partition by dname order by hiredate rows between 3 preceding and 1 following) as pv1
from employee;

image-20230108175831325

1
2
3
4
5
6
7
8
9
-- 每个部门从入职到现在的工资成本
select
dname,
ename,
hiredate,
salary,
-- 相加的时候,从当前行开始加到最后行
sum(salary) over(partition by dname order by hiredate rows between current row and unbounded following) as pv1
from employee;

image-20230108180057487

以上sum同理可以求平均值avg,最大值max,最小值min

分布函数

CUME_DIST

用途:分组内小于、等于当前rank值的行数 / 分组内总行数

应用场景:查询小于等于当前薪资(salary)的比例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
select  
dname,
ename,
salary,
cume_dist() over(order by salary) as rn1, -- 没有partition语句 所有的数据位于一组
cume_dist() over(partition by dname order by salary) as rn2
from employee;
/*
说明:
rn1: 没有partition,所有数据均为1组,总行数为12,
第一行:小于等于3000的行数为3,因此,3/12=0.25
第二行:小于等于4000的行数为5,因此,5/12=0.4166666666666667
rn2: 按照部门分组,dname='研发部'的行数为6,
第一行:研发部小于等于3000的行数为1,因此,1/6=0.16666666666666666
*/

image-20230108174414929

PERCENT_RANK

用途:每行按照公式(rank-1) / (rows-1)进行计算。其中,rank为RANK()函数产生的序号,rows为当前窗口的记录总行数

应用场景:不常用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select 
dname,
ename,
salary,
rank() over(partition by dname order by salary desc ) as rn,
percent_rank() over(partition by dname order by salary desc ) as rn2
from employee;
/*
说明:
rn2:
第一行: (1 - 1) / (6 - 1) = 0
第二行: (1 - 1) / (6 - 1) = 0
第三行: (3 - 1) / (6 - 1) = 0.4
*/

image-20230108174632139

前后函数

LAG

用途:返回位于当前行的前n行(LAG(expr,n))或后n行(LEAD(expr,n))的expr的值

应用场景:查询前1名同学的成绩和当前同学成绩的差值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- lag的用法
select
dname,
ename,
hiredate,
salary,
lag(hiredate,1,'2000-01-01') over(partition by dname order by hiredate) as last_1_time,
lag(hiredate,2) over(partition by dname order by hiredate) as last_2_time
from employee;
/*
last_1_time: 指定了往上第1行的值,default为'2000-01-01'
第一行,往上1行为null,因此取默认值 '2000-01-01'
第二行,往上1行值为第一行值,2021-11-01
第三行,往上1行值为第二行值,2021-11-02
last_2_time: 指定了往上第2行的值,为指定默认值
第一行,往上2行为null
第二行,往上2行为null
第四行,往上2行为第二行值,2021-11-01
第七行,往上2行为第五行值,2021-11-02
*/

image-20230108180313096

把上1行的值给当前行

LEAD

1
2
3
4
5
6
7
8
9
-- lead的用法
select
dname,
ename,
hiredate,
salary,
lead(hiredate,1,'2000-01-01') over(partition by dname order by hiredate) as last_1_time,
lead(hiredate,2) over(partition by dname order by hiredate) as last_2_time
from employee;

image-20230108180333669

把下1行的值给当前行

头尾函数

FIRST_VALUE和LAST_VALUE

用途:返回第一个(FIRST_VALUE(expr))或最后一个(LAST_VALUE(expr))expr的值

应用场景:截止到当前,按照日期排序查询第1个入职和最后1个入职员工的薪资

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 注意,  如果不指定ORDER BY,则进行排序混乱,会出现错误的结果
select
dname,
ename,
hiredate,
salary,
first_value(salary) over(partition by dname order by hiredate) as first,
last_value(salary) over(partition by dname order by hiredate) as last
from employee;
/*
first1, 到目前为止第一个薪资是3000
last1,到目前为止最后一个薪资,其实就是本行
*/

image-20230108181018441

其他函数

NTH_VALUE(expr, n)

用途:返回窗口中第n个expr的值。expr可以是表达式,也可以是列名

应用场景:截止到当前薪资,显示每个员工的薪资中排名第2或者第3的薪资

1
2
3
4
5
6
7
8
9
-- 查询每个部门截止目前薪资排在第二和第三的员工信息
select
dname,
ename,
hiredate,
salary,
nth_value(salary,2) over(partition by dname order by hiredate) as second_score,
nth_value(salary,3) over(partition by dname order by hiredate) as third_score
from employee

image-20230108183908265

second_score第一行为NULL的原因: 截至到当前,排名第二的,还没有,只有3000一行,所以为NULL

NTILE(n)

用途:将分区中的有序数据分为n个等级,记录等级数

应用场景:将每个部门员工按照入职日期分成3组

1
2
3
4
5
6
7
8
-- 根据入职日期将每个部门的员工分成3组
select
dname,
ename,
hiredate,
salary,
ntile(3) over(partition by dname order by hiredate ) as rn
from employee;

image-20230108183953453

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 取出每个部门的第一组员工
select
*
from
(
SELECT
dname,
ename,
hiredate,
salary,
NTILE(3) OVER(PARTITION BY dname ORDER BY hiredate ) AS rn
FROM employee
)t
where t.rn = 1;

image-20230108184438049

自定义函数

MySQL存储函数(自定义函数),函数一般用于计算和返回一个值,可以将经常需要使用的计算或功能写成一个函数。存储函数和存储过程一样,都是在数据库中定义一些 SQL 语句的集合。

存储函数与存储过程的区别

1.存储函数有且只有一个返回值,而存储过程可以有多个返回值,也可以没有返回值。

2.存储函数只能有输入参数,而且不能带in, 而存储过程可以有多个in,out,inout参数。

3.存储过程中的语句功能更强大,存储过程可以实现很复杂的业务逻辑,而函数有很多限制,如不能在函数中使用insert,update,delete,create等语句;

4.存储函数只完成查询的工作,可接受输入参数并返回一个结果,也就是函数实现的功能针对性比较强。

5.存储过程可以调用存储函数。但函数不能调用存储过程。

6.存储过程一般是作为一个独立的部分来执行(call调用)。而函数可以作为查询语句的一个部分来调用.

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
create function func_name ([param_name type[,...]])
returns type
[characteristic ...]
begin
routine_body
end;

参数说明:
1)func_name :存储函数的名称。
2)param_name type:可选项,指定存储函数的参数。type参数用于指定存储函数的参数类型,该类型可以是MySQL数据库中所有支持的类型。
3RETURNS type:指定返回值的类型。
4)characteristic:可选项,指定存储函数的特性。
5)routine_body:SQL代码内容。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
create database mydb9_function;
-- 导入测试数据
use mydb9_function;
set global log_bin_trust_function_creators=TRUE; -- 信任子程序的创建者,自定义存储函数才可以创建
-- 如果想要永久信任,需要修改my.ini,然后重启MySQL服务。

-- 创建存储函数-没有输输入参数
drop function if exists myfunc1_emp;

delimiter $$
create function myfunc1_emp() returns int
begin
declare cnt int default 0;
select count(*) into cnt from emp;
return cnt;
end $$
delimiter ;
-- 调用存储函数
select myfunc1_emp();


-- 创建存储过程-有输入参数

drop function if exists myfunc2_emp;
delimiter $$
create function myfunc2_emp(in_empno int) returns varchar(50)
begin
declare out_name varchar(50);
select ename into out_name from emp where empno = in_empno;
return out_name;
end $$
delimiter ;


select myfunc2_emp(1008);

视图

介绍

视图(view)是一个虚拟表,非真实存在,其本质是根据SQL语句获取动态的数据集,并为其命名,用户使用时只需使用视图名称即可获取结果集,并可以将其当作表来使用。

数据库中只存放了视图的定义,而并没有存放视图中的数据。这些数据存放在原来的表中。

使用视图查询数据时,数据库系统会从原来的表中取出对应的数据。因此,视图中的数据是依赖于原来的表中的数据的。一旦表中的数据发生改变,显示在视图中的数据也会发生改变。

作用

简化代码,可以把重复使用的查询封装成视图重复使用,同时可以使复杂的查询易于理解和使用。

安全原因,如果一张表中有很多数据,很多信息不希望让所有人看到,此时可以使用视图视,如:社会保险基金表,可以用视图只显示姓名,地址,而不显示社会保险号和工资数等,可以对不同的用户,设定不同的视图。

创建视图

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create [or replace] [algorithm = {undefined | merge | temptable}]

view view_name [(column_list)]

as select_statement

[with [cascaded | local] check option]

参数说明:
1)algorithm:可选项,表示视图选择的算法。
2)view_name :表示要创建的视图名称。
3)column_list:可选项,指定视图中各个属性的名词,默认情况下与SELECT语句中的查询的属性相同。
4)select_statement:表示一个完整的查询语句,将查询记录导入视图中。
5)[with [cascaded | local] check option]:可选项,表示更新视图时要保证在该视图的权限范围之内。

数据准备

创建 数据库mydb6_view,然后在该数据库下执行sql脚本view_data.sql 导入数据

1
2
3
4
5
6
7
create database mydb6_view;
create or replace view view1_emp
as
select ename,job from emp;

-- 查看表和视图
show full tables;

修改视图

修改视图是指修改数据库中已存在的表的定义。当基本表的某些字段发生改变时,可以通过修改视图来保持视图和基本表之间一致。MySQL中通过CREATE OR REPLACE VIEW语句和ALTER VIEW语句来修改视图。

1
2
3
4
5
alter view 视图名 as select语句

alter view view1_emp
as
select a.deptno,a.dname,a.loc,b.ename,b.sal from dept a, emp b where a.deptno = b.deptno;

更新视图

某些视图是可更新的。也就是说,可以对视图做UPDATE、DELETE或INSERT操作,以更新基表(源表)的内容。对于可更新的视图,在视图中的行和基表中的行之间必须具有一对一的关系如果视图包含下述结构中的任何一种,那么它就是不可更新的

  • 聚合函数(SUM(), MIN(), MAX(), COUNT()等)
  • DISTINCT
  • GROUP BY
  • HAVING
  • UNION或UNION ALL
  • 位于选择列表中的子查询
  • JOIN
  • FROM子句中的不可更新视图
  • WHERE子句中的子查询,引用FROM子句中的表。
  • 仅引用文字值(在该情况下,没有要更新的基本表)

视图中虽然可以更新数据,但是有很多的限制。一般情况下,最好将视图作为查询数据的虚拟表,而不要通过视图更新数据。因为,使用视图更新数据时,如果没有全面考虑在视图中更新数据的限制,就可能会造成数据更新失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
--  ---------更新视图-------
create or replace view view1_emp
as
select ename,job from emp;

update view1_emp set ename = '周瑜' where ename = '鲁肃'; -- 可以修改
insert into view1_emp values('孙权','文员'); -- 不可以插入

-- ----------视图包含聚合函数不可更新--------------
create or replace view view2_emp
as
select count(*) cnt from emp;

insert into view2_emp values(100);
update view2_emp set cnt = 100;

-- ----------视图包含distinct不可更新---------
create or replace view view3_emp
as
select distinct job from emp;

insert into view3_emp values('财务');

-- ----------视图包含goup by 、having不可更新------------------
create or replace view view4_emp
as
select deptno ,count(*) cnt from emp group by deptno having cnt > 2;

insert into view4_emp values(30,100);

-- ----------------视图包含union或者union all不可更新----------------
create or replace view view5_emp
as
select empno,ename from emp where empno <= 1005
union
select empno,ename from emp where empno > 1005;

insert into view5_emp values(1015,'韦小宝');

-- -------------------视图包含子查询不可更新--------------------
create or replace view view6_emp
as
select empno,ename,sal from emp where sal = (select max(sal) from emp);

insert into view6_emp values(1015,'韦小宝',30000);

-- ----------------------视图包含join不可更新-----------------
create or replace view view7_emp
as
select dname,ename,sal from emp a join dept b on a.deptno = b.deptno;

insert into view7_emp(dname,ename,sal) values('行政部','韦小宝',30000);

-- --------------------视图包含常量文字值不可更新-------------------
create or replace view view8_emp
as
select '行政部' dname,'杨过' ename;

insert into view8_emp values('行政部','韦小宝');

重命名视图

1
2
-- rename table 视图名 to 新视图名; 
rename table view1_emp to my_view1

删除视图

删除视图时,只能删除视图的定义,不会删除数据。

1
2
-- drop view 视图名[,视图名…];
drop view if exists view_student;

视图练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 1:查询部门平均薪水最高的部门名称
select dname from dept a ,(select deptno,avg(sal) from emp group by deptno order by avg(sal) desc limit 1) b
where a.deptno = b.deptno;

-- 2:查询员工比所属领导薪资高的部门名、员工名、员工领导编号
select * from dept x,
(select a.ename aname ,a.sal asal,b.ename bname,b.sal bsal,a.deptno
from emp a, emp b
where a.mgr = b.empno and a.sal > b.sal) y
where x.deptno = y.deptno;

-- 3:查询工资等级为4级,2000年以后入职的工作地点为北京的员工编号、姓名和工资,并查询出薪资在前三名的员工信息
create view xxx
as
SELECT e.empno,e.ename,e.sal,e.hiredate
FROM emp e,dept d,salgrade s
WHERE (e.sal BETWEEN losal AND hisal) AND s.GRADE = 4
AND year(e.hiredate) > '2000'
AND d.loc = '北京';

select * from
(
select
*,
dense_rank() over(order by sal desc ) rn
from xxx
) t
where t.rn <=3;

存储过程

介绍

MySQL5.0开始支持存储过程,存储过程就是多条SQL的封装实现某个复杂逻辑的功能,类似java的方法,可重用

有输入输出,可以声明变量,有if、case、while等控制语句。

速度快,首次执行需要编译和优化,后续调用直接执行。

入门案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
语法:
delimiter 自定义结束符号
create procedure 储存名([ in ,out ,inout ] 参数名 数据类形...)
begin
sql语句
end 自定义的结束符合
delimiter ;

-- 1:创建数据库
create database mydb7_procedure;

-- 2:在该数据库下导入sql脚本:procedure_data.sql
delimiter $$
create procedure proc01()
begin
select empno,ename from emp;
end $$
delimiter ;

-- 3: 调用存储过程
call proc01();

变量

局部变量

用户自定义,在begin/end块中有效

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:声明变量 declare var_name type [default var_value]; 
举例: declare nickname varchar(32);

delimiter $$
create procedure proc02()
begin
declare var_name01 varchar(20) default ‘aaa’; -- 定义局部变量
set var_name01 = 'zhangsan'; -- 给局部变量赋值
select var_name01;
end $$
delimiter ;
-- 调用存储过程
call proc02();

还可以用select into赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
select col_name [...] into var_name[,...] 
from table_name wehre condition

其中:
col_name 参数表示查询的字段名称;
var_name 参数是变量的名称;
table_name 参数指表的名称;
condition 参数指查询条件。
注意:当将查询结果赋值给变量时,该查询语句的返回结果只能是单行单列。

delimiter $$
create procedure proc03()
begin
declare my_ename varchar(20) ;
select ename into my_ename from emp where empno=1001; -- 将ename赋值给my_ename
select my_ename;
end $$
delimiter ;
-- 调用存储过程
call proc03();

用户变量

用户自定义,当前会话(连接)有效。类比java的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
语法: 
@var_name
不需要提前声明,使用即声明


delimiter $$
create procedure proc04()
begin
set @var_name01 = 'ZS';
end $$
delimiter;
call proc04() ;
select @var_name01 ; --可以看到结果

系统变量

系统变量又分为全局变量与会话变量

全局变量在MYSQL启动的时候由服务器自动将它们初始化为默认值,这些默认值可以通过更改my.ini这个文件来更改。

会话变量在每次建立一个新的连接的时候,由MYSQL来初始化。MYSQL会将当前所有全局变量的值复制一份。来做为会话变量。

也就是说,如果在建立会话以后,没有手动更改过会话变量与全局变量的值,那所有这些变量的值都是一样的。

全局变量与会话变量的区别就在于,对全局变量的修改会影响到整个服务器,但是对会话变量的修改,只会影响到当前的会话(也就是当前的数据库连接)。

有些系统变量的值是可以利用语句来动态进行更改的,但是有些系统变量的值却是只读的,对于那些可以更改的系统变量,我们可以利用set语句进行更改。

  • 全局变量

由系统提供,在整个数据库有效

1
2
3
4
5
6
7
8
9
10
11
语法:
@@global.var_name


-- 查看全局变量
show global variables;
-- 查看某全局变量
select @@global.auto_increment_increment;
-- 修改全局变量的值
set global sort_buffer_size = 40000;
set @@global.sort_buffer_size = 40000;
  • 会话变量

由系统提供,当前会话(连接)有效

1
2
3
4
5
6
7
8
9
10
11
语法:
@@session.var_name

操作:
-- 查看会话变量
show session variables;
-- 查看某会话变量
select @@session.auto_increment_increment;
-- 修改会话变量的值
set session sort_buffer_size = 50000;
set @@session.sort_buffer_size = 50000 ;

参数传递

in

入参,相当于Java给方法传的形参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 【传单个】封装有参数的存储过程,传入员工编号,查找员工信息
delimiter $$
create procedure dec_param01(in param_empno varchar(20))
begin
select * from emp where empno = param_empno;
end $$

delimiter ;
call dec_param01('1001'); -> 调用存过,传入参数,param_empno接收

-- 【传多个】封装有参数的存储过程,可以通过传入部门名和薪资,查询指定部门,并且薪资大于指定值的员工信息
delimiter $$
create procedure dec_param0x(in dname varchar(50),in sal decimal(7,2),)
begin
select * from dept a, emp b where b.sal > sal and a.dname = dname;
end $$

delimiter ;
call dec_param0x('学工部',20000);

out

出参,相当于返回值,返回给调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- ---------传出参数:out---------------------------------
use mysql7_procedure;
-- 【返回单个】封装有参数的存储过程,传入员工编号,返回员工名字
delimiter $$
create procedure proc08(in empno int ,out out_ename varchar(50) )
begin
select ename into out_ename from emp where emp.empno = empno;
end $$

delimiter ;

call proc08(1001, @o_ename); -> 这里出参也要参变量接收
select @o_ename; -> 拿到参数的值

-- 【返回多个】封装有参数的存储过程,传入员工编号,返回员工名字和薪资
delimiter $$
create procedure proc09(in empno int ,out out_ename varchar(50) ,out out_sal decimal(7,2))
begin
select ename,sal into out_ename,out_sal from emp where emp.empno = empno;
end $$

delimiter ;

call proc09(1001, @o_dname,@o_sal);
select @o_dname;
select @o_sal;

inout

参数传进去,经过修改,会再传出来,拿到修改后的值。 值被加工了。

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 传入员工名,拼接部门号,传入薪资,求出年薪
delimiter $$
create procedure proc10(inout inout_ename varchar(50),inout inout_sal int)
begin
select concat(deptno,"_",inout_ename) into inout_ename from emp where ename = inout_ename;
set inout_sal = inout_sal * 12;
end $$
delimiter ;
set @inout_ename = '关羽';
set @inout_sal = 3000;
call proc10(@inout_ename, @inout_sal) ;
select @inout_ename ;
select @inout_sal ;

流程控制

分支语句-if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-- 语法
if search_condition_1 then statement_list_1
[elseif search_condition_2 then statement_list_2] ...
[else statement_list_n]
end if

-- 操作
-- 输入学生的成绩,来判断成绩的级别:
/*
score < 60 :不及格
score >= 60 , score <80 :及格
score >= 80 , score < 90 :良好
score >= 90 , score <= 100 :优秀
score > 100 :成绩错误
*/
delimiter $$
create procedure proc_12_if(in score int)
begin
if score < 60
then
select '不及格';
elseif score < 80
then
select '及格' ;
elseif score >= 80 and score < 90
then
select '良好';
elseif score >= 90 and score <= 100
then
select '优秀';
else
select '成绩错误';
end if;
end $$
delimiter ;
call proc_12_if(120)

-- 输入员工的名字,判断工资的情况。
delimiter $$
create procedure proc12_if(in in_ename varchar(50))
begin
declare result varchar(20);
declare var_sal decimal(7,2);
select sal into var_sal from emp where ename = in_ename;
if var_sal < 10000
then set result = '试用薪资';
elseif var_sal < 30000
then set result = '转正薪资';
else
set result = '元老薪资';
end if;
select result;
end$$
delimiter ;
call proc12_if('庞统');

分支语句-case

CASE是另一个条件判断的语句,类似于编程语言中的switch语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-- 语法一(类比java的switch):
case case_value
when when_value then statement_list
[when when_value then statement_list] ...
[else statement_list]
end case
-- 语法二:
case
when search_condition then statement_list
[when search_condition then statement_list] ...
[else statement_list]
end case

-- 操作
-- 语法一
delimiter $$
create procedure proc14_case(in pay_type int)
begin
case pay_type
when 1 then select '微信支付' ;
when 2 then select '支付宝支付' ;
when 3 then select '银行卡支付';
else select '其他方式支付';
end case ;
end $$
delimiter ;

call proc14_case(2);
call proc14_case(4);


-- 语法二
delimiter $$
create procedure proc_15_case(in score int)
begin
case
when score < 60
then
select '不及格';
when score < 80
then
select '及格' ;
when score >= 80 and score < 90
then
select '良好';
when score >= 90 and score <= 100
then
select '优秀';
else
select '成绩错误';
end case;
end $$
delimiter ;

call proc_15_case(88);

循环语句-while

leave 类似于 break,跳出,结束当前所在的循环

iterate类似于 continue,继续,结束本次循环,继续下一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
【标签:】while 循环条件 do
循环体;
end while【 标签】;

-- 创建测试表
create table user (
uid int primary key,
username varchar ( 50 ),
password varchar ( 50 )
);

-- -------存储过程-while
delimiter $$
create procedure proc16_while1(in insertcount int)
begin
declare i int default 1;
label:while i<=insertcount do
insert into user(uid,username,`password`) values(i,concat('user-',i),'123456');
set i=i+1;
end while label;
end $$
delimiter ;

call proc16_while(10);

-- -------存储过程-while + leave
truncate table user;
delimiter $$
create procedure proc16_while2(in insertcount int)
begin
declare i int default 1;
label:while i<=insertcount do
insert into user(uid,username,`password`) values(i,concat('user-',i),'123456');
if i=5 then leave label;
end if;
set i=i+1;
end while label;
end $$
delimiter ;

call proc16_while2(10);

-- -------存储过程-while+iterate
truncate table user;
delimiter $$
create procedure proc16_while3(in insertcount int)
begin
declare i int default 1;
label:while i<=insertcount do
set i=i+1;
if i=5 then iterate label;
end if;
insert into user(uid,username,`password`) values(i,concat('user-',i),'123456');
end while label;
end $$
delimiter ;
call proc16_while3(10);

循环语句-repeat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[标签:]repeat 
循环体;
until 条件表达式
end repeat [标签];

-- -------存储过程-循环控制-repeat
use mysql7_procedure;
truncate table user;


delimiter $$
create procedure proc18_repeat(in insertCount int)
begin
declare i int default 1;
label:repeat
insert into user(uid, username, password) values(i,concat('user-',i),'123456');
set i = i + 1;
until i > insertCount -- 这里是判断退出条件,不能加逗号
end repeat label;
select '循环结束';
end $$
delimiter ;

call proc18_repeat(100);

循环语句-loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[标签:] loop
循环体;
if 条件表达式 then
leave [标签];
end if;
end loop;

-- -------存储过程-循环控制-loop + leave
truncate table user;

delimiter $$
create procedure proc19_loop(in insertCount int)
begin
declare i int default 1;
label:loop
insert into user(uid, username, password) values(i,concat('user-',i),'123456');
set i = i + 1;
if i > 5 -- 这里做判断
then
leave label; -- 通过leave跳出循环
end if;
end loop label;
select '循环结束';
end $$
delimiter ;

call proc19_loop(10);

游标cursor

游标(cursor)是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用光标对结果集进行循环的处理(拿到结果集的每一行)。光标的使用包括光标的声明、OPEN、FETCH 和 CLOSE.

1
2
3
4
5
6
7
8
-- 声明语法
declare cursor_name cursor for select_statement
-- 打开语法
open cursor_name
-- 取值语法
fetch cursor_name into var_name [, var_name] ...
-- 关闭语法
close cursor_name

操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use mysql7_procedure;
delimiter $$
create procedure proc20_cursor(in in_dname varchar(50))
begin
-- 定义局部变量
declare var_empno varchar(50);
declare var_ename varchar(50);
declare var_sal decimal(7,2);

-- 1.声明游标
declare my_cursor cursor for
select empno , ename, sal
from dept a ,emp b
where a.deptno = b.deptno and a.dname = in_dname;

-- 2.打开游标
open my_cursor;
-- 3.通过游标获取每一行数据
label:loop
fetch my_cursor into var_empno, var_ename, var_sal;
select var_empno, var_ename, var_sal;
end loop label;

-- 4.关闭游标
close my_cursor;
end

-- 调用存储过程
call proc20_cursor('销售部');

最后往下fetch,报错no data,退出存过。所以我们要进一步做异常处理。

异常处理句柄handler

MySql存储过程也提供了对异常处理的功能:通过定义HANDLER来完成异常声明的实现.

官方文档:https://dev.mysql.com/doc/refman/5.7/en/declare-handler.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DECLARE handler_action HANDLER
FOR condition_value [, condition_value] ...
statement -- 异常触发了采取什么样的措施

handler_action: { -- 处理的行为
CONTINUE -- 向下走
| EXIT -- 退出
| UNDO -- 不做事情
}

condition_value: {
mysql_error_code -- 错误码
| condition_name -- 条件名字:如下3种
| SQLWARNING -- SQL警告
| NOT FOUND -- 数据未发现
| SQLEXCEPTION -- SQL异常

在语法中,变量声明、游标声明、handler声明是必须按照先后顺序书写的,否则创建存储过程出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
use mysql7_procedure;
drop procedure if exists proc21_cursor_handler;
-- 需求:输入一个部门名,查询该部门员工的编号、名字、薪资 ,将查询的结果集添加游标
delimiter $$
create procedure proc20_cursor(in in_dname varchar(50))
begin
-- 定义局部变量
declare var_empno int;
declare var_ename varchar(50);
declare var_sal decimal(7,2);

declare flag int default 1; -- 设置flag为1,表示正常的情况

-- 声明游标
declare my_cursor cursor for
select empno,ename,sal
from dept a, emp b
where a.deptno = b.deptno and a.dname = in_dname;

-- 定义句柄,当数据未发现时将标记位设置为0
declare continue handler for NOT FOUND set flag = 0; -- 设置flag为0,表示异常的情况
-- 打开游标
open my_cursor;
-- 通过游标获取值
label:loop
fetch my_cursor into var_empno, var_ename,var_sal;
-- 判断标志位Flag
if flag = 1 then
select var_empno, var_ename,var_sal;
else
leave label; -- 如果flag为0 说明异常,退出循环
end if;
end loop label;

-- 关闭游标
close my_cursor;
end $$;

delimiter ;
call proc21_cursor_handler('销售部');

存过练习

创建下个月的每天对应的表user_2021_11_01、user_2021_11_02、…

需求描述: 我们需要用某个表记录很多数据,比如记录某某用户的搜索、购买行为(注意,此处是假设用数据库保存),当每天记录较多时,如果把所有数据都记录到一张表中太庞大,需要分表,我们的要求是,每天一张表,存当天的统计数据,就要求提前生产这些表——每月月底创建下一个月每天的表!

image-20230111130612332

需要的知识点:

1
2
3
4
5
6
7
8
9
10
PREPARE stmt_name FROM preparable_stmt
EXECUTE stmt_name [USING @var_name [, @var_name] ...]
{DEALLOCATE | DROP} PREPARE stmt_name
-- 知识点 时间的处理
-- EXTRACT(unit FROM date)截取时间的指定位置值
-- DATE_ADD(date,INTERVAL expr unit) 日期运算
-- LAST_DAY(date) 获取日期的最后一天
-- YEAR(date) 返回日期中的年
-- MONTH(date) 返回日期的月
-- DAYOFMONTH(date) 返回日

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
-- 思路:循环构建表名 user_2021_11_01 到 user_2020_11_30;并执行create语句。
use mysql7_procedure;
drop procedure if exists proc22_demo;
delimiter $$
create procedure proc22_demo()
begin
declare next_year int; -- 下一个月的年份
declare next_month int; -- 下一个月的月份
declare next_month_day int; -- 下一个月的日期

declare next_month_str char(2); -- 下一个月的字符串
declare next_month_day_str char(2); -- 下一个月的日字符串

-- 处理每天的表名
declare table_name_str char(10);

declare t_index int default 1;
-- declare create_table_sql varchar(200);

-- 获取下个月的年份
set next_year = year(date_add(now(),INTERVAL 1 month));
-- 获取下个月是几月
set next_month = month(date_add(now(),INTERVAL 1 month));
-- 下个月最后一天是几号
set next_month_day = dayofmonth(LAST_DAY(date_add(now(),INTERVAL 1 month)));

-- 月份 小于 10 补0
if next_month < 10
then set next_month_str = concat('0',next_month);
else
set next_month_str = concat('',next_month);
end if;


while t_index <= next_month_day do

-- 对日字符串补0
if (t_index < 10)
then set next_month_day_str = concat('0',t_index);
else
set next_month_day_str = concat('',t_index);
end if;

-- 2021_11_01
set table_name_str = concat(next_year,'_',next_month_str,'_',next_month_day_str);
-- 拼接create sql语句
set @create_table_sql = concat(
'create table user_',
table_name_str,
'(`uid` INT ,`ename` varchar(50) ,`information` varchar(50)) COLLATE=\'utf8_general_ci\' ENGINE=InnoDB');
-- FROM后面不能使用局部变量!
prepare create_table_stmt FROM @create_table_sql; -- SQL预处理
execute create_table_stmt; -- 执行SQL
DEALLOCATE prepare create_table_stmt; -- 释放预编译的SQL

set t_index = t_index + 1;

end while;
end $$
delimiter ;

call proc22_demo();

触发器

介绍

触发器,就是一种特殊的存储过程。触发器和存储过程一样是一个能够完成特定功能、存储在数据库服务器上的SQL片段,但是触发器无需调用,当对数据库表中的数据执行DML操作时自动触发这个SQL片段的执行,无需手动条用。

在MySQL中,只有执行insert,delete,update操作时才能触发触发器的执行。(修改A表->B表自动记录数据)

触发器的这种特性可以协助应用在数据库端确保数据的完整性 , 日志记录 , 数据校验等操作 。

使用别名 OLDNEW 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还只支持行级触发,不支持语句级触发。

image-20230111131759881

特性

1、什么条件会触发:I、D、U

2、什么时候触发:在增删改前(B)或者后(A)

3、触发频率:针对每一行执行

4、触发器定义在表上,附着在表上

语法

1、创建只有一个执行语句的触发器

1
2
3
create trigger 触发器名 before|after 触发事件
on 表名 for each row
执行语句;

2、创建有多个执行语句的触发器

1
2
3
4
5
create trigger 触发器名 before|after  触发事件 
on 表名 for each row
begin
执行语句列表
end;

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-- 数据准备
create database if not exists mydb10_trigger;
use mydb10_trigger;

-- 用户表
create table user(
uid int primary key ,
username varchar(50) not null,
password varchar(50) not null
);
-- 用户信息操作日志表
create table user_logs(
id int primary key auto_increment,
time timestamp,
log_text varchar(255)
);

-- 如果触发器存在,则先删除
drop trigger if exists trigger_test1;

-- 创建触发器trigger_test1
create trigger trigger_test1
after insert on user -- 触发时机:当添加user表数据时触发
for each row
insert into user_logs values(NULL,now(), '有新用户注册');

-- 添加数据,触发器自动执行并添加日志代码
insert into user values(1,'张三','123456');


-- 如果触发器trigger_test2存在,则先删除
drop trigger if exists trigger_test2;

-- 创建触发器trigger_test2
delimiter $$
create trigger trigger_test2
after update on user -- 触发时机:当修改user表数据时触发
for each row -- 每一行
begin
insert into user_logs values(NULL,now(), '用户修改发生了修改');
end $$

delimiter ;

-- 添加数据,触发器自动执行并添加日志代码
update user set password = '888888' where uid = 1;

NEW与OLD

MySQL 中定义了 NEW 和 OLD,用来表示触发器的所在表中,触发了触发器的那一行数据,来引用触发器中发生变化的记录内容,具体地:

触发器类型 触发器类型NEW OLD的使用**
INSERT 型触发器 NEW 表示将要或者已经新增的数据
UPDATE 型触发器 OLD 表示修改之前的数据 , NEW 表示将要或已经修改后的数据
DELETE 型触发器 OLD 表示将要或者已经删除的数据

使用方法:NEW.columnName(columnName为相应数据表某一列名)

1
2
3
4
5
6
create trigger trigger_test3 after insert
on user for each row
insert into user_logs values(NULL,now(),concat('有新用户添加,信息为:',NEW.uid,NEW.username,NEW.password));

-- 测试
insert into user values(4,'赵六','123456');

查看删除触发器

1
2
3
4
5
6
# 查看触发器
show triggers;

# 删除触发器
-- drop trigger [if exists] trigger_name
drop trigger if exists trigger_test1;
  1. MYSQL中触发器中不能对本表进行 insert ,update ,delete 操作,以免递归循环触发
  2. 尽量少使用触发器,假设触发器触发每次执行1s,insert table 500条数据,那么就需要触发500次触发器,光是触发器执行的时间就花费了500s,而insert 500条数据一共是1s,那么这个insert的效率就非常低了。
  3. 触发器是针对每一行的;对增删改非常频繁的表上切记不要使用触发器,因为它会非常消耗资源。

本质是一张表修改影响另一张表,不用触发器的话,可以用java后台代码实现。

🔖MySQL表设计

层级数据表结构设计

概述

层级结构也称树形结构,是数据元素之间存在着的一种 一对多 的数据结构,可应用在从属关系、并列关系。生活中有很多这样的例子。如:

我们在京东或淘宝购物提交订单时选择的收货人地址,都是先选所在的省,再选市,然后再到县或区。一个省下有多个市,一个市下有多个县或区,它们的这种结构就跟树的分叉一样,主干可以有很多分叉,每个分支也可以拥有很多分叉。我们可以把树的每一个分叉都看成一个节点,每一个节点可能有它的子节集合,也可能有它的父节点。

面对不同的项目业务需求,复杂且多层级关系,如何设计出一种比较好的数据库表结构来存储数据,以便快速的查询或插入与更新数据呢?

解决方案

对于层级结构的数据,我们有以下四种解决方案。接下来分别演示这4个方案及分析它们的优缺点

  1. Adjacency List (邻接表)是一种链式存储结构,这种表结构中用一字段来表示上级ID
  2. Path Enumeration 路径枚举,存储数据时,把路径信息也存到一个列中
  3. Nested Sets 嵌套集合, 利用集合区间的特性,存储数据时还要存储它的左右边界值,查询效率快
  4. Closure Table 闭包表, 最广泛的一种,适应多种场景,存储数据时,把所有的路径信息也保存下来。缺点是存储数据量较大

案例和实现

针对公司中存在的等级制度,即老板管理各部门经理,经理管理其部门下的组长,组长管理其下的组员等。

Adjacency List

表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
CREATE TABLE emp( 
id NUMBER(10) primary key,
name varchar2(100),
pid number(10)
);
insert into emp (id,name,pid) values(1,'小强1',0);
insert into emp (id,name,pid) values(2,'小强11',1);
insert into emp (id,name,pid) values(3,'小强111',2);
insert into emp (id,name,pid) values(4,'小强1111',3);
insert into emp (id,name,pid) values(5,'小强1112',3);
insert into emp (id,name,pid) values(6,'小强112',2);
insert into emp (id,name,pid) values(7,'小强1121',6);
insert into emp (id,name,pid) values(8,'小强1122',6);
insert into emp (id,name,pid) values(9,'小强12',1);
insert into emp (id,name,pid) values(10,'小强121',9);
insert into emp (id,name,pid) values(11,'小强1211',10);
insert into emp (id,name,pid) values(12,'小强1212',10);
insert into emp (id,name,pid) values(13,'小强122',9);
insert into emp (id,name,pid) values(14,'小强1221',13);
insert into emp (id,name,pid) values(15,'小强1222',13);
COMMIT;

‐‐结构如下
‐‐部门经理 小强1
‐‐‐‐产品经理 小强11
‐‐‐‐‐‐企业应用产品经理 小强111
‐‐‐‐‐‐‐‐普通产品经理 小强1111
‐‐‐‐‐‐‐‐普通产品经理 小强1112
‐‐‐‐‐‐个人应用产品经理 小强112
‐‐‐‐‐‐‐‐普通产品经理 小强1121
‐‐‐‐‐‐‐‐普通产品经理 小强1122
‐‐‐‐技术经理 小强12
‐‐‐‐‐‐企业应用技术经理 小强121
‐‐‐‐‐‐‐‐普通技术人员 小强1211
‐‐‐‐‐‐‐‐普通技术人员 小强1212
‐‐‐‐‐‐个人应用产品经理 小强122
‐‐‐‐‐‐‐‐普通技术人员 小强1221
‐‐‐‐‐‐‐‐普通技术人员 小强1222

查询

  1. 查询小强11的直接下属有哪些人?小强11的编号为2。
1
2
3
4
5
6
7
‐‐这个比较简单 
select * from emp where pid=2

‐‐结果
ID Name PID
3 小强111 2
6 小强112 2
  1. 查询小强11的下属有哪些人?小强11的编号为2。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
‐‐1. 首先得查询出直接下属 
select * from emp where pid=2

/*
结果
ID Name PID
3 小强111 2
6 小强112 2
*/

‐‐2. 小强111和小强112他们有没有下属呢?如果有,也要把它们查询出来
select * from emp where pid=3
union
select * from emp where pid=6

/*
结果
ID Name PID
4 小强1111 3
5 小强1112 3
7 小强1121 6
8 小强1122 6
*/

如果想要用一条语句查询出来,则要考虑递归来实现了,oracle中就有递归的实现
  1. Oracle中的归查询

查询小强11及其所有的下属,小强11的编号为2

1
2
3
select * from emp 
start with id=2
connect by prior id=pid

image-20220701104903735

查询小强111及所有上司,小强111的编号为3

1
2
3
select * from emp 
start with id=3
connect by prior pid=id

image-20220701104924601

  1. 如果再来一个跨级查询,则无法实现

维护

  1. 插入数据
1
insert into emp (id,name,pid) values(16,'小强XXX',5);
  1. 修改数据

移动某个节点到指定的节点下。比如 小强1122 升职了,这时我们只要一条语句就可以搞定

1
2
update emp set pid=2 where id=8; 
commit;
  1. 删除数据
1
2
3
4
5
6
7
8
9
‐‐ 删除的数据本身及其的包含子节点 
delete from emp where (pid=2 and id=8) or pid=8 ;

‐‐ 只有删除本节点,其子节点不能删除
‐‐‐‐ 更新它的子节点
update emp set pid=3 where pid=8;

‐‐‐‐ 再来删除这个节点
delete from emp where id=8

Path Enumeration

只存储当前节点信息,及节点所在的路径

表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE emp2( 
id NUMBER(10) primary key,
name varchar2(100),
path varchar2(100)
);
insert into emp2 (id,name,path) values(1,'小强1',0);
insert into emp2 (id,name,path) values(2,'小强11','1/2');
insert into emp2 (id,name,path) values(3,'小强111','1/2/3');
insert into emp2 (id,name,path) values(4,'小强1111','1/2/3/4');
insert into emp2 (id,name,path) values(5,'小强1112','1/2/3/5');
insert into emp2 (id,name,path) values(6,'小强112','1/2/6');
insert into emp2 (id,name,path) values(7,'小强1121','1/2/6/7');
insert into emp2 (id,name,path) values(8,'小强1122','1/2/6/8');
insert into emp2 (id,name,path) values(9,'小强12','1/9');
insert into emp2 (id,name,path) values(10,'小强121','1/9/10');
insert into emp2 (id,name,path) values(11,'小强1211','1/9/10/11');
insert into emp2 (id,name,path) values(12,'小强1212','1/9/10/12');
insert into emp2 (id,name,path) values(13,'小强122','1/9/13');
insert into emp2 (id,name,path) values(14,'小强1221','1/9/13/14');
insert into emp2 (id,name,path) values(15,'小强1222','1/9/13/15');
COMMIT;

查询

查询小强11的下属有哪些人。

1
select * from emp2 where path like (select path from emp2 where name='小强11') || '%'; 

image-20220701105213173

维护

  1. 插入数据,我们给小强11再加入一个子节点
1
2
3
insert into emp2 (id,name) values(16,'小强XX'); 
update emp2 set path=(select path || '/' || 16 from emp2 where name='小强11') where id=16;
commit;
  1. 修改数据

移动某个节点到指定的节点下。比如 小强1122 升了1级

1
2
3
4
5
6
7
‐‐ 首先找小强1122的路径 
select path from emp2 where name='小强1122' ‐‐ 1/2/6/8

‐‐ 更新小强1122的路径,这只是我们凭肉眼来处理,如果要使用sql语句处理则比较麻烦
update emp2 set path='1/2/8' where name='小强1122';

‐‐以上的例子是小强1122是路径中最后的节点,如果它还有其它子节点的话,那么小强1122下所有的子节点的path都要更新
  1. 删除数据
1
2
3
4
5
6
‐‐ 删除时,如果该节点下有其它子节点(不删除),那么要先更新子节点的path路径值。 
‐‐ 如小强112 path='1/2/6'
update emp2 set path=replace(path,'6/','3/') where path like '1/2/6/%';

‐‐ 删除节点本身
delete from emp2 where name='小强112';

Nested Sets

首先我们通过下图来理解一下什么是【嵌套集合】

image-20220701105335339

结构如下

1
2
3
4
5
6
7
8
9
‐‐强哥1 
‐‐‐‐强哥3
‐‐‐‐‐‐强哥5
‐‐‐‐‐‐强哥6
‐‐‐‐强哥4
‐‐强哥2
‐‐‐‐强哥7
‐‐‐‐强哥8
‐‐‐‐强哥9

表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE emp3( 
id NUMBER(10) primary key,
name varchar2(100),
nleft number(10),
nright number(10),
nodeCount number(10)
);

insert into emp3 (id,name,nleft,nright,nodeCount) values(1,'强哥1',1,30,1);
insert into emp3 (id,name,nleft,nright,nodeCount) values(2,'强哥2',36,50,1);
insert into emp3 (id,name,nleft,nright,nodeCount) values(3,'强哥3',5,20,2);
insert into emp3 (id,name,nleft,nright,nodeCount) values(4,'强哥4',22,28,2);
insert into emp3 (id,name,nleft,nright,nodeCount) values(5,'强哥5',8,12,3);
insert into emp3 (id,name,nleft,nright,nodeCount) values(6,'强哥6',14,18,3);
insert into emp3 (id,name,nleft,nright,nodeCount) values(7,'强哥7',38,42,2);
insert into emp3 (id,name,nleft,nright,nodeCount) values(8,'强哥8',43,45,2);
insert into emp3 (id,name,nleft,nright,nodeCount) values(9,'强哥9',46,48,2);
COMMIT;

查询

  1. 查询强哥1的所有下属
1
2
select e2.* from emp3 e1, emp3 e2 
where e1.name='强哥1' and e2.nleft > e1.nleft and e2.nleft<e1.nright;
  1. 查询强哥5的所有上级
1
2
select e2.* from emp3 e1, emp3 e2
where e1.name='强哥5' and e1.nleft>e2.nleft and e1.nleft<e2.nright;
  1. 向下越级查询,查询强哥1的所有下下级员工
1
2
select e2.* from emp3 e1, emp3 e2
where e1.name='强哥1' and e2.nleft > e1.nleft and e2.nleft<e1.nright and e2.NODECOUNT=e1.nodecount+2;
  1. 向上越级查询,查询强哥5的上二级领导
1
2
select e2.* from emp3 e1, emp3 e2 
where e1.name='强哥5' and e1.nleft>e2.nleft and e1.nleft<e2.nright and e2.nodecount=e1.nodecount‐2;

维护

嵌套集合的这种结构维护(插入新的节点,或移动节点)起来非常复杂,如果只是加入一个末端节点(叶子节点),则需要扩展相关的nleft的值。如果不是末端节点,则整个结构都得重新计算。以下演示加入末端节点

1
2
3
4
5
6
7
‐‐扩展要插入的节点的右边值 
update emp3 set nleft=(case when nleft >=21 then nleft+2 else nleft end), nright=nright+2
where nright>=21

‐‐ 再插入数据
insert into emp3 (id,name,nleft,nright,nodeCount) values(10,'强哥10',21,22,2);
commit;

Closure Table

这是一种简单又方便的存储层级结构的方案。每添加一个节点时都存储它的所有路径信息,存储数据量较大

表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
create table emp4( 
id number(10) primary key,
name varchar2(30)
);

create table emprelationship(
pid number(10),‐‐上级id
nodecount number(10),‐‐ 跟上级id之间的节点数
cid number(10)‐‐ 当前节点id
);
insert into emp4(id,name) values(1,'小强01');
insert into emp4(id,name) values(2,'小强02');
insert into emp4(id,name) values(3,'小强03');
insert into emp4(id,name) values(4,'小强04');
insert into emp4(id,name) values(5,'小强05');
insert into emp4(id,name) values(6,'小强06');
insert into emp4(id,name) values(7,'小强07');
insert into emp4(id,name) values(8,'小强08');
insert into emp4(id,name) values(9,'小强09');
insert into emp4(id,name) values(10,'小强10');
insert into emp4(id,name) values(11,'小强11');
insert into emp4(id,name) values(12,'小强12');
insert into emp4(id,name) values(13,'小强13');
insert into emp4(id,name) values(14,'小强14');
insert into emp4(id,name) values(15,'小强15');
insert into emp4(id,name) values(16,'小强16');
insert into emp4(id,name) values(17,'小强17');
insert into emp4(id,name) values(18,'小强18');
insert into emp4(id,name) values(19,'小强19');
insert into emp4(id,name) values(20,'小强20');

INSERT INTO emprelationship VALUES (1, 0, 1);
INSERT INTO emprelationship VALUES (1, 1, 2);
INSERT INTO emprelationship VALUES (2, 0, 2);
INSERT INTO emprelationship VALUES (1, 1, 3);
INSERT INTO emprelationship VALUES (3, 0, 3);
INSERT INTO emprelationship VALUES (1, 2, 4);
INSERT INTO emprelationship VALUES (2, 1, 4);
INSERT INTO emprelationship VALUES (4, 0, 4);
INSERT INTO emprelationship VALUES (1, 2, 5);
INSERT INTO emprelationship VALUES (2, 1, 5);
INSERT INTO emprelationship VALUES (5, 0, 5);
INSERT INTO emprelationship VALUES (1, 2, 6);
INSERT INTO emprelationship VALUES (3, 1, 6);
INSERT INTO emprelationship VALUES (6, 0, 6);
INSERT INTO emprelationship VALUES (1, 2, 7);
INSERT INTO emprelationship VALUES (3, 1, 7);
INSERT INTO emprelationship VALUES (7, 0, 7);
INSERT INTO emprelationship VALUES (1, 3, 8);
INSERT INTO emprelationship VALUES (2, 2, 8);
INSERT INTO emprelationship VALUES (4, 1, 8);
INSERT INTO emprelationship VALUES (8, 0, 8);
INSERT INTO emprelationship VALUES (1, 3, 9);
INSERT INTO emprelationship VALUES (2, 2, 9);
INSERT INTO emprelationship VALUES (4, 1, 9);
INSERT INTO emprelationship VALUES (9, 0, 9);
INSERT INTO emprelationship VALUES (1, 2, 10);
INSERT INTO emprelationship VALUES (3, 1, 10);
INSERT INTO emprelationship VALUES (10, 0, 10);
INSERT INTO emprelationship VALUES (1, 4, 11);
INSERT INTO emprelationship VALUES (2, 3, 11);
INSERT INTO emprelationship VALUES (4, 2, 11);
INSERT INTO emprelationship VALUES (8, 1, 11);
INSERT INTO emprelationship VALUES (11, 0, 11);
INSERT INTO emprelationship VALUES (1, 2, 12);
INSERT INTO emprelationship VALUES (2, 1, 12);
INSERT INTO emprelationship VALUES (12, 0, 12);
INSERT INTO emprelationship VALUES (1, 5, 13);
INSERT INTO emprelationship VALUES (2, 4, 13);
INSERT INTO emprelationship VALUES (4, 3, 13);
INSERT INTO emprelationship VALUES (8, 2, 13);
INSERT INTO emprelationship VALUES (11, 1, 13);
INSERT INTO emprelationship VALUES (13, 0, 13);
commit;

结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
‐‐小强01 
‐‐‐‐|小强02
‐‐‐‐|‐‐|小强04
‐‐‐‐|‐‐|‐‐|小强08
‐‐‐‐|‐‐|‐‐|‐‐|小强11
‐‐‐‐|‐‐|‐‐|‐‐|‐‐|小强13
‐‐‐‐|‐‐|‐‐|小强09
‐‐‐‐|‐‐|小强12
‐‐‐‐|小强03
‐‐‐‐|‐‐|小强05
‐‐‐‐|‐‐|小强06
‐‐‐‐|‐‐|小强07
‐‐‐‐|‐‐|小强10

查询

  1. 查询小强1的所有下属
1
2
3
4
5
select e.* 
from emp4 e
inner join emprelationship er on e.id=er.cID
inner join emp4 m on er.pid=m.id
where m.name='小强01'
  1. 越级查询小强1的所有二级下属
1
2
3
4
5
select e.* 
from emp4 e
inner join emprelationship er on e.id=er.cID
inner join emp4 m on er.pid=m.id
where m.name='小强01' and er.nodecount=2
  1. 查询小强13所有上级领导
1
2
3
4
5
select m.* 
from emp4 e
inner join emprelationship er on e.id=er.cID
inner join emp4 m on er.pid=m.id
where e.name='小强13'
  1. 越级查询小强13上二级导
1
2
3
4
5
select m.* 
from emp4 e
inner join emprelationship er on e.id=er.cID
inner join emp4 m on er.pid=m.id
where e.name='小强13' and er.nodecount=2

维护

  1. 插入数据,添加一个新员工 小强21,该员工与 小强13 同等级,同一个领导
1
2
3
4
5
6
insert into emp4 (id,name) values(21, '小强21'); 
insert into emprelationship (pid,nodecount,cid)

select pid,nodecount+1,21 from emprelationship where cid=11 ‐‐ 上级领导
union
select 21,0,21 from dual;‐‐加上自己本身
  1. 移动节点

把小强8 (编号为8) 移动到 小强3(编号为3) 下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
‐‐ 删除所有跟小强8有关的节点及这些节点的上级节点关系 
delete from emprelationship where cid in (
select cid from emprelationship where pid=8
) and pid in (select pid from emprelationship where cid=8 and pid!=cid)

‐‐ 插入新的关系
insert into emprelationship(pid,cid)
SELECT p.pid,c.cid
FROM emprelationship p,
emprelationship c
WHERE p.cid = 3
AND c.pid = 8;

‐‐ 更新节点数
update emprelationship e1 set nodecount=(
select max(e2.nodecount)+1 from emprelationship e2 where e2.cid=e1.cid
) where e1.pid=3 and e1.nodecount is null;
update emprelationship e1 set nodecount=(
select max(e2.nodecount)+1 from emprelationship e2 where e2.cid=e1.cid
) where e1.pid=3 and e1.nodecount is null
commit;
  1. 删除数据

删除的数据是叶子节点

1
2
3
4
5
‐‐ 先删除关系 
delete from emprelationship where cid=21;

‐‐ 再删除数据本身
delete from emp4 where id=21;

删除的数据包含有子节点

1
2
3
delete from emprelationship where cid in ( 
select cid from emprelationship where pid=11
)

总结

介绍完了这4个方案,下面我们来总结一下每一种方案的优缺点:

image-20220701110423408

  • Adjacency List: 层次结构无深度限制,可以随意添加,修改与删除。但查询只能查询自己本身及下一级结构的数据,无法越级查询。这种结构比较常用
  • Path Enumeration: 存储路径的方式在进行多级查询的时候很方便,而在查询直接上下级的时候稍微复杂一点。缺点,那就是path的大小决定于列的类型长度,理论上是不能无限层级的扩展,且产生冗余数据。
  • Nested Sets: 在左右列上添加索引后查询速度最快,可越级查询,但缺点是插入与修改的时候比较复杂。适合一旦创建,只做查询用,不做维护的数据。
  • Closure Table: 查询很方便,可以随意越级查询,维护起来也比较容易,一目了然,用途最广。存储数据时需要用到2张表,如果层次结构较多,由于每个节点都要存储它的所有路径,因此存储的开销比较大

最后,到底该用哪一种还得看业务需求再做决定

🔖MySQL扩容

数据库平滑扩容

学习目标

目标1:理解传统扩容实现方案 目标2:理解平滑扩容双写方案 目标3:掌握数据库2N扩容方案 目标4:实现数据库双主同步 目标5:掌握ShardingJDBC路由以及动态扩容技术 目标6:掌握KeepAlived+MariaDB数据库高可用方案

扩容方案剖析

数据库容量不足,数据库数据量特别大,DAO层响应时间久等都需要扩容

扩容问题

在项目初期,我们部署了三个数据库A、B、C,此时数据库的规模可以满足我们的业务需求。为了将数据做到平均分配,我们在Service服务层使用uid%3进行取模分片,从而将数据平均分配到三个数据库中。

如图所示:

image-20230927200526184

后期随着用户量的增加,用户产生的数据信息被源源不断的添加到数据库中,最终达到数据库的最佳存储容量。如果此时继续向数据库中新增数据,会导致数据库的CRUD等基本操作变慢,进而影响整个服务的响应速度。

这时,我们需要增加新的节点,对数据库进行水平扩容,那么加入新的数据库D后,数据库的规模由原来的3个变为4个。

如图所示:

image-20230927200521869

此时由于分片规则发生了变化(uid%3 变为uid%4),导致大部分的数据,无法命中原有的数据,需要重新进行分配,要做大量的数据迁移处理。

比如之前uid如果是uid=3取模3%3=0, 是分配在A库上,新加入D库后, uid=3取模3%4=3,分配在D库上

image-20230927200517924

新增一个节点, 大概会有90%的数据需要迁移, 这样会面临大量的数据压力,并且对服务造成极大的不稳定性。

提问:如果数据量持续增大,分2个库性能扛不住了,该怎么办呢?

回答:继续水平拆分,拆成更多的库,降低单库数据量,增加库主库实例(机器)数量,提高性能。

最终问题抛出:分成x个库后,随着数据量的增加,要增加到y个库,数据库扩容的过程中,能否平滑,持续对外提供服务,保证服务的可用性,是本文要讨论的问题。

停机方案

image-20230927200512510

  1. 发布公告 为了进行数据的重新拆分,在停止服务之前,我们需要提前通知用户,比如:我们的服务会在yyyy-MM-dd进行升级,给您带来的不便敬请谅解。
  2. 停止服务 关闭Service
  3. 新建y个库,做好高可用
  4. 离线数据迁移(拆分,重新分配数据)【耗时】 将旧库中的数据按照Service层的算法(%x升级为%y)(开发一个数据库迁移工具),将数据拆分,重新分配数据
  5. 数据校验 开发定制一个程序对旧库和新库中的数据进行校验,比对
  6. 更改配置 修改Service层的配置算法,也就是将原来的uid%3变为uid%4
  7. 恢复服务 重启Service服务
  8. 回滚预案 针对上述的每个步骤都要有数据回滚预案,一旦某个环节(如:数据迁移,恢复服务等)执行失败,立刻进行回滚,重新再来

停止服务之后, 能够保证迁移工作的正常进行, 但是服务停止,伤害用户体验, 并造成了时间压力,必须在指定的时间内完成迁移。

停机方案用户体验差,停机时间太长,不符合高可用的理念

技术同学压力大,所有工作要在规定时间内做完,根据经验,压力越大约容易出错(这一点很致命)

如果有问题第一时间没检查出来,启动了服务,运行一段时间后再发现有问题,难以回滚,需要回档,可能会丢失一部分数据

停写方案

image-20230927200507950

  1. 支持读写分离 数据库支持读写分离,在扩容之前,每个数据库都提供了读写功能,数据重新分配的过程中,将每个数据库设置为只读状态,关闭写的功能
  2. 升级公告 为了进行数据的重新拆分,在停写之前,我们需要提前通知用户,比如:我们的服务会在yyyyMM-dd进行升级,给您带来的不便敬请谅解。
  3. 中断写操作,隔离写数据源(或拦截返回统一提示) 在Service层对所有的写请求进行拦截,统一返回提示信息,如:服务正在升级中,只对外提供读服务、
  4. 数据同步处理

​ 将旧库中的数据按照Service层的算法,将数据重新分配,迁移(复制数据)

  1. 数据校验 开发定制一个程序对旧库中的数据进行备份,使用备份的数据和重新分配后的数据进行校验,比对
  2. 更改配置 通过配置中心,修改Service层的配置算法,也就是将原来的uid%3变为uid%4,这个过程不需要重启服务
  3. 恢复写操作 设置数据库恢复读写功能,去除Service层的拦截提示
  4. 数据清理 使用delete语句对冗余数据进行删除
  5. 回滚预案 针对上述的每个步骤都要有数据回滚预案,一旦某个环节(如:数据迁移等)执行失败,立刻进行回滚,重新再来

缺点:在数据的复制过程需要消耗大量的时间,停写时间太长,数据需要先复制,再清理冗余数据

优点,不需要停机,缺点是不能写入还是影响了用户体验。而且停写复制也是很耗时的。

日志方案

核心是通过日志进行数据库的同步迁移, 主要操作步骤如下:

  1. 数据迁移之前, 业务应用访问旧的数据库节点。

image-20230927200503397

  1. 日志记录 在升级之前, 记录“对旧数据库上的数据修改”的日志(这里修改包括增、删、改),这个日志不需要记录详细的数据信息,主要记录: (1)修改的库; (2)修改的表; (3)修改的唯一主键; (4)修改操作类型。

image-20230927200457031

日志记录不用关注新增了哪些信息,修改的数据格式,只需要记录以上数据信息,这样日志格式是固定的, 这样能保证方案的通用性。 服务升级日志记录功能风险较小:

写和修改接口是少数, 改动点少;

升级只是增加了一些日志,采用异步方式实现, 对业务功能没有太多影响。

  1. 数据迁移: 研发定制数据迁移工具, 作用是把旧库中的数据迁移至新库中。

image-20230927200449372

整个过程仍然采用旧库进行对外服务。 数据同步工具实现复杂度不高。 只对旧库进行读取操作, 如果同步出现问题, 都可以对新库进行回滚操作。 可以限速或分批迁移执行, 不会有时间压力。(不然极端情况,一直有增删改操作,同步工具会不断同步数据)

数据迁移完成之后, 并不能切换至新库提供服务。 因为旧库依然对线上提供服务, 库中的数据随时会发生变化, 但这些变化的数据并没有同步到新库中, 旧库和新库数据不一致, 所以不能直接进行切换, 需要将数据同步完整。

  1. 日志增量迁移

image-20230927200444676

研发一个日志迁移工具,把上面迁移数据过程中的差异数据追平,处理步骤:

  • 读取log日志,获取具体是哪个库、表和主键发生了变化修改;
  • 把旧库中的主键记录读取出来
  • 根据主键ID,把新库中的记录替换掉

这样可以最大程度的保障数据的一致性。风险分析:

  • 整个过程, 仍然是旧库对线上提供服务;
  • 日志迁移工具实现的复杂度较低;
  • 任何时间发现问题, 可以重新再来,有充分的容错空间;
  • 可以限速重放处理日志, 处理过程不会因为对线上影响造成时间压力。

但是, 日志增量同步完成之后, 还不能切换到新的数据库。 因为日志增量同步过程中,旧库中可能有数据发生变化, 导致数据不一致,所以需要进一步读取日志, 追平数据记录; 日志增量同步过程随时可能会产生新的数据, 新库与旧库的数据追平也会是一个无限逼近的过程。

  1. 数据校验 准备好数据校验工具,将旧库和新库中的数据进行比对,直到数据完全一致。

image-20230927200440168

  1. 切换新库 数据比对完成之后, 将流量转移切换至新库, 至此新库提供服务, 完成迁移。

image-20230927200435892

但是在极限情况下, 即便通过上面的数据校验处理, 也有可能出现99.99%数据一致, 不能保障完全一致,这个时候可以在旧库做一个readonly只读功能, 或者将流量屏蔽降级,等待日志增量同步工具完全追平后, 再进行新库的切换。

至此,完成日志方案的迁移扩容处理, 整个过程能够持续对线上提供服务, 只会短暂的影响服务的可用性。

这种方案的弊端,是操作繁琐,需要适配多个同步处理工具,成本较高, 需要制定个性化业务的同步处理, 不具备普遍性,耗费的时间周期也较长

要开发很多定制化的同步工具,不通用。开发成本周期高。繁琐复杂

双写方案(中小型数据)

image-20230927200432102

双写方案可通过canal或mq做实现。

  1. 增加新库,按照现有节点, 增加对应的数量。
  2. 数据迁移:避免增量影响, 先断开主从,再导入(耗时较长), 同步完成并做校验
  3. 增量同步:开启Canal同步服务, 监听从节点数据库, 再开启主从同步,从节点收到数据后会通过 Canal服务, 传递至新的DB节点。
  4. 切换新库:通过Nginx,切换访问流量至新的服务。
  5. 修复切换异常数据:在切换过程中, 如果出现,Canal未同步,但已切换至新库的请求(比如下单,修改了资金, 但还未同步 ), 可以通过定制程序, 读取检测异常日志,做自动修复或人工处理。 针对此种情况, 最好是在凌晨用户量小的时候, 或专门停止外网访问,进行切换,减少异常数据的产生。
  6. 数据校验:为保障数据的完全一致, 有必要对数据的数量完整性做校验。

平滑2N方案(大数据量)

  1. 线上数据库,为了保障其高可用,一般每台主库会配置一台从库,主库负责读写,从库负责读取。 下图所示,A,B是主库,A0和B0是从库。

image-20230927200426462

  1. 当需要扩容的时候,我们把A0和B0升级为新的主库节点,如此由2个分库变为4个分库。同时在上 层的分片配置,做好映射,规则如下:

把uid%4=0和uid%4=2的数据分别分配到A和A0主库中

把uid%4=1和uid%4=3的数据分配到B和B0主库中

image-20230927200421854

  1. 因为A和A0库的数据相同,B和B0数据相同,此时无需做数据迁移。只需调整变更一下分片配置即可,通过配置中心更新,不需要重启。

image-20230927200417598

由于之前uid%2的数据是分配在2个库里面,扩容之后需要分布到4个库中,但由于旧数据仍存在(uid%4=0的节点,还有一半uid%4=2的数据),所以需要对冗余数据做一次清理。 这个清理,并不会影响线上数据的一致性,可以随时随地进行。

  1. 处理完成之后,为保证数据的高可用,以及将来下一步的扩容需求。 可以为现有的主库再次分配一个从库。

image-20230927200412308

平滑2N主要是存在数据冗余的问题

平滑2N扩容方案实践

实现应用服务级别的动态扩容

扩容前部署架构

image-20230927200406865

keepalived作用:1、健康监测 2、故障转移(vrrp,虚拟路由冗余协议)

MariaDB服务安装

MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可。开发这个分支的原因之一是:甲骨文公司收购了MySQL后,有将MySQL闭源的潜在风险,因此社区采用分支的方式来避开这个风险。

  1. 切换阿里云镜像服务(YUM安装过慢可以切换)
1
2
3
4
5
6
7
8
9
10
11
12
[root@linux30 ~]# yum -y install wget 
## 备份CentOS-Base.repo

[root@linux30 ~]# mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak

[root@linux30 ~]# wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

[root@linux30 ~]# wget -P /etc/yum.repos.d/ http://mirrors.aliyun.com/repo/epel-7.repo

[root@linux30 ~]# yum clean all

[root@linux30 ~]# yum makecache
  1. 配置YUM源
1
[root@linux30 ~]# vi /etc/yum.repos.d/mariadb-10.2.repo

增加以下内容:

1
2
3
4
5
[mariadb] 
name = MariaDB
baseurl = https://mirrors.ustc.edu.cn/mariadb/yum/10.2/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
  1. 执行安装
1
[root@linux30 ~]# yum -y install mariadb mariadb-server MariaDB-client MariaDB-common
  1. 如果之前已经安装, 需要先删除(如果之前没有安装, 可以忽略此步骤)
  • 停止Mariadb服务
1
2
3
4
[root@localhost yum.repos.d]# ps -ef | grep mysql 
root 1954 1 0 Oct04 ? 00:05:43 /usr/sbin/mysqld --wsrep-new-cluster --user=root
root 89521 81403 0 07:40 pts/0 00:00:00 grep --color=auto mysql
[root@localhost yum.repos.d]# kill 1954
  • 卸载Mariadb服务
1
yum -y remove Maria*
  • 删除数据与配置:
1
2
3
rm -rf /var/lib/mysql/* 
rm -rf /etc/my.cnf.d/
rm -rf /etc/my.cnf
  1. 启动MariaDB后,执行安全配置向导命令,可根据安全配置向导提高数据库的安全性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
[root@linux30 ~]# systemctl start mariadb 

## 执行安全配置向导命令

[root@linux30 ~]# mysql_secure_installation

NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY!
In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
you haven't set the root password yet, the password will be blank,
so you should just press enter here.

# 首次安装,直接跳过

Enter current password for root (enter for none):
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using
password: YES)
Enter current password for root (enter for none):
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using
password: YES)
Enter current password for root (enter for none):
OK, successfully used password, moving on...
Setting the root password ensures that nobody can log into the MariaDB
root user without the proper authorisation.

# 设置root用户密码

Set root password? [Y/n] y
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
... Success!

By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them. This is intended only for testing, and to make the installation
go a bit smoother. You should remove them before moving into a
production environment.

# 删除匿名用户

Remove anonymous users? [Y/n] y
... Success!

Normally, root should only be allowed to connect from 'localhost'. This
ensures that someone cannot guess at the root password from the network.

# 允许root用户远程登录

Disallow root login remotely? [Y/n] n
... skipping.
By default, MariaDB comes with a database named 'test' that anyone can
access. This is also intended only for testing, and should be removed
before moving into a production environment.

# 删除test数据库

Remove test database and access to it? [Y/n] y
- Dropping test database...
... Success!
- Removing privileges on test database...
... Success!
Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

# 刷新权限

Reload privilege tables now? [Y/n] y
... Success!

Cleaning up...
All done! If you've completed all of the above steps, your MariaDB
installation should now be secure.
Thanks for using MariaDB!

image-20230927200359746

  1. 开启用户远程连接权限

将连接用户root开启远程连接权限;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 登录
mysql -uroot -proot

## 切换到mysql数据库
MariaDB [(none)]> use mysql;

## 删除所有用户
MariaDB [mysql]> delete from user;

## 配置root用户使用密码123456从任何主机都可以连接到mysql服务器
MariaDB [mysql]> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;

## 刷新权限
MariaDB [mysql]> FLUSH PRIVILEGES;

可以远程连接访问了

MariaDB双主同步

  1. 在linux30增加配置:

在/etc/my.cnf中添加以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@linux30 ~]# vi /etc/my.cnf 

[mysqld]
server-id = 1
log-bin=mysql-bin
relay-log = mysql-relay-bin
## 忽略mysql、information_schema库下对表的操作
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=information_schema.%
## 默认的情况下mysql是关闭的; relay-log内容要不要写入到bin-log,开启了30-31,31可以同步到它下面的slave
log-slave-updates=on
## 复制过程中,有任何错误,直接跳过
slave-skip-errors=all
## 主键自增基数, 从1开始
auto-increment-offset=1
## 主键自增偏移量,每次为2
auto-increment-increment=2
## binlog的格式:STATEMENT,ROW,MIXED
binlog_format=mixed
## 自动过期清理binlog,默认0天,即不自动清理
expire_logs_days=10

注意, linux30自增为奇数位:

auto-increment-offset=1 主键自增基数, 从1开始。

auto-increment-increment=2 主键自增偏移量,每次为2。

  1. 在linux31增加配置:

修改/etc/my.cnf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@linux31 ~]# vi /etc/my.cnf 

[mysqld]
server-id = 2
log-bin=mysql-bin
relay-log = mysql-relay-bin
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=information_schema.%
log-slave-updates=on
slave-skip-errors=all
auto-increment-offset=2
auto-increment-increment=2
binlog_format=mixed
expire_logs_days=10

linux31自增为偶数位:

auto-increment-offset=2 主键自增基数, 从2开始。

auto-increment-increment=2 主键自增偏移量,每次为2。

配置修改完成后, 重启数据库。

  1. 同步授权配置

在linux30创建replica用于主从同步的用户:

1
2
3
4
5
6
7
[root@linux30 ~]# mysql -uroot -p123456 

## 授权replica用户 replication slave, replication client 权限
MariaDB [(none)]> grant replication slave, replication client on *.* to 'replica'@'%' identified by 'replica';

## 刷新权限
MariaDB [(none)]> flush privileges;

查询日志文件与偏移量,开启同步时需使用:

1
2
3
4
5
6
7
8
9
10
11
MariaDB [(none)]> show master status; 

+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 663 | | |
+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)

MariaDB [(none)]>

同样, 在linux31创建replica用于主从同步的用户:

1
2
3
4
5
6
[root@linux31 ~]# mysql -uroot -p123456 
## 授权
MariaDB [(none)]> grant replication slave, replication client on *.* to 'replica'@'%' identified by 'replica';

## 刷新权限
MariaDB [(none)]> flush privileges;

查询日志文件与偏移量:

1
2
3
4
5
6
7
8
9
MariaDB [(none)]> show master status; 

+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 663 | | |
+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)
  1. 配置主从同步信息

在linux30中执行:

1
2
3
4
MariaDB [(none)]> change master to master_host='192.168.10.31',master_user='replica', master_password='replica', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=328, master_connect_retry=30; 

Query OK, 0 rows affected (0.01 sec)
MariaDB [(none)]>

在linux31中执行:

1
2
3
4
5
6
7
8
# 如果报错要停止slave
stop slave
reset slave

MariaDB [(none)]> change master to master_host='192.168.10.30',master_user='replica', master_password='replica', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=328, master_connect_retry=30;

Query OK, 0 rows affected (0.01 sec)
MariaDB [(none)]>
  1. 开启双主同步

在linux30和linux31中分别执行:

1
2
3
4
MariaDB [(none)]> start slave;   # stop/reset slave 停止主从关系

Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]>

在linux30查询同步信息:

MariaDB [(none)]> show slave status\G;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
*************************** 1. row *************************** 
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.10.31
Master_User: replica
Master_Port: 3306
Connect_Retry: 30
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 663
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 555
Relay_Master_Log_File: mysql-bin.000001

Slave_IO_Running: Yes
Slave_SQL_Running: Yes

...

在linux31查询同步信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MariaDB [(none)]> show slave status\G; 

*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.10.30
Master_User: replica
Master_Port: 3306
Connect_Retry: 30
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 663
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 555
Relay_Master_Log_File: mysql-bin.000001

Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...

Slave_IO_Running和Slave_SQL_Running 都是Yes,说明双主同步配置成功

KeepAlived安装与高可用配置

  1. 在linux30与linux31两台节点安装keepalived:
1
[root@linux30 ~]# yum -y install keepalived
  1. 关闭防火墙
1
2
3
[root@linux30 ~]# systemctl stop firewalld 
[root@linux30 ~]# systemctl disable firewalld
[root@linux30 ~]# systemctl status firewalld
  1. 设置主机名称:

linux30节点:

1
[root@linux30 ~]# hostnamectl set-hostname linux30

linux31节点:

1
[root@linux31 ~]# hostnamectl set-hostname linux31
  1. linux30节点配置

/etc/keepalived/keepalived.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[root@linux30 ~]# vi /etc/keepalived/keepalived.conf 

global_defs {
router_id linux30 # 机器标识,和主机名保持一致,运行keepalived服务器的 一个标识
}
vrrp_instance VI_1 { #vrrp实例定义
state BACKUP #lvs的状态模式,MASTER代表主, BACKUP代表备份节点
interface ens33 #绑定对外访问的网卡,vrrp实例绑定的网卡
virtual_router_id 111 #虚拟路由标示,同一个vrrp实例采用唯一标示
priority 100 #优先级,100代表最大优先级, 数字越大优先级越高
advert_int 1 #master与backup节点同步检查的时间间隔,单位是秒
authentication { #设置验证信息
auth_type PASS #有PASS和AH两种
auth_pass 6666 #验证密码,BACKUP密码须相同
}
virtual_ipaddress { #KeepAlived虚拟的IP地址
192.168.10.150
}
}

virtual_server 192.168.10.150 3306 { #配置虚拟服务器IP与访问端口
delay_loop 6 #健康检查时间
lb_algo rr #负载均衡调度算法, rr代表轮询
lb_kind DR #负载均衡转发规则 DR/NAT/
persistence_timeout 0 #会话保持时间,这里要做测试, 所以设为0, 实际可根 据session有效时间配置
protocol TCP #转发协议类型,支持TCP和UDP
real_server 192.168.10.30 3306 { #配置服务器节点VIP1
notify_down /opt/mariaDB/mariadb.sh #当服务挂掉时, 会执行此脚本,结束
keepalived进程
weight 1 #设置权重,越大权重越高
TCP_CHECK { #状态监测设置
connect_timeout 10 #超时配置, 单位秒
retry 3 #重试次数
delay_before_retry 3 #重试间隔
connect_port 3306 #连接端口, 和上面保持一致
}
}
}

image-20230927200351745

创建关闭脚本mariadb.sh

1
2
3
4
5
[root@linux30 mariaDB]# vi /opt/mariaDB/mariadb.sh 

## 添加脚本内容

pkill keepalived

加入执行权限:

1
[root@linux30 mariaDB]# chmod a+x mariadb.sh
  1. linux31节点配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[root@linux31 ~]# vi /etc/keepalived/keepalived.conf 

global_defs {
router_id linux31 # 机器标识,和主机名保持一致,运行keepalived服务器的 一个标识
}
vrrp_instance VI_1 { #vrrp实例定义
state BACKUP #lvs的状态模式,MASTER代表主, BACKUP代表备份节点
interface ens33 #绑定对外访问的网卡
virtual_router_id 111 #虚拟路由标示,同一个vrrp实例采用唯一标示
priority 98 #优先级,100代表最大优先级, 数字越大优先级越高
advert_int 1 #master与backup节点同步检查的时间间隔,单位是秒
authentication { #设置验证信息
auth_type PASS #有PASS和AH两种
auth_pass 6666 #验证密码,BACKUP密码须相同
}

virtual_ipaddress { #KeepAlived虚拟的IP地址
192.168.10.150
}

}

virtual_server 192.168.10.150 3306 { #配置虚拟服务器IP与访问端口
delay_loop 6 #健康检查时间
lb_algo rr #负载均衡调度算法, rr代表轮询, 可以关闭
lb_kind DR #负载均衡转发规则, 可以关闭
persistence_timeout 0 #会话保持时间,这里要做测试, 所以设为0, 实际可根 据session有效时间配置
protocol TCP #转发协议类型,支持TCP和UDP
real_server 192.168.10.31 3306 { #配置服务器节点linux31
notify_down /opt/mariaDB/mariadb.sh #当服务挂掉时, 会执行此脚本,结束
keepalived进程
weight 1 #设置权重,越大权重越高
TCP_CHECK { #r状态监测设置
connect_timeout 10 #超时配置, 单位秒
retry 3 #重试次数
delay_before_retry 3 #重试间隔
connect_port 3306 #连接端口, 和上面保持一致
}
}
}

和linux30的差异项:

image-20230927200346487

1
2
3
4
5
router_id linux31 # 机器标识,和主机名保持一致 

priority 98 #优先级,100代表最大优先级, 数字越大优先级越高

real_server 192.168.10.31 3306 #配置服务器节点linux31

注意, 两台节点都设为BACKUP

1
2
3
virtual_router_id 111 #同一个vrrp实例采用唯一标示 

state BACKUP

如果不想重启后, 争夺备用节点的VIP, 可以设置此项

1
nopreempt #不主动抢占资源

注意:这个配置只能设置在backup主机上,而且这个主机优先级要比另外一台高

  1. 验证高可用

停止主节点MariaDB服务, 验证是否自动切换。

1
2
3
4
5
6
7
[root@linux30 mariaDB]# systemctl start keepalived 

[root@linux31 mariaDB]# systemctl start keepalived

[root@linux30 mariaDB]# systemctl stop mariadb

[root@linux30 mariaDB]# systemctl start mariadb

搭建应用服务工程

ShardingJDBC的介绍

是ShardingSphere 下的一个产品

定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种ORM 框架。

  • 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
  • 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库
ShardingJDBC初始化流程

1)配置ShardingRuleConfiguration对象

2)配置表分片规则TableRuleConfiguration对象,设置分库、分表策略

3)通过Factory对象将Rule对象与DataSource对象装配

4)ShardingJDBC使用DataSource对象进行分库

image-20230927200341024

ShardingJDBC集成配置
1)maven依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties> 
<sharding.jdbc.version>4.0.0</sharding.jdbc.version>
</properties>
<!-- sharding-jdbc 依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>${sharding.jdbc.version}</version>
</dependency>

<!-- sharding-jdbc 服务编排依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-orchestration</artifactId>
<version>${sharding.jdbc.version}</version>
</dependency>
2)规则配置application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
server:
port: 10692
spring:
application:
name: smooth-database
# 数据源配置, 采用Druid
datasource:
tradesystem: # 数据源1的名称(自定义的)
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://192.168.10.150:3306/smooth?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
druid:
# 连接池的配置信息
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
#connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000

tradesystem2: # 数据源2的名称(自定义的)
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://192.168.10.31:3306/smooth?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
druid:
# 连接池的配置信息
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
#connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000

# Jpa功能配置
jpa:
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false
hibernate:
ddl-auto: none
naming:
# 实际命名, 无转换
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
3)创建DataSource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.itcast.database.smooth.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.itcast.database.smooth.service.DynamicShardingService;
import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.TableRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.strategy.StandardShardingStrategyConfiguration;
import org.apache.shardingsphere.orchestration.config.OrchestrationConfiguration;
import org.apache.shardingsphere.orchestration.reg.api.RegistryCenterConfiguration;
import org.apache.shardingsphere.shardingjdbc.orchestration.api.OrchestrationShardingDataSourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;

import javax.sql.DataSource;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
@Order(1)
public class DruidSystemDataSourceConfiguration {

public static final String DYNAMIC_SHARDING = "Dynamic-Sharding-JDBC";



/**
* 交易数据源
* @return
*/
@Bean(name = "tradeDruidDataSource")
@ConfigurationProperties(prefix = "spring.datasource.tradesystem")
public DruidDataSource tradeDruidDataSource(){
return new DruidDataSource();
}

/**
* 交易数据源2号
* @return
*/
@Bean(name = "tradeDruidDataSource2")
@ConfigurationProperties(prefix = "spring.datasource.tradesystem2")
public DruidDataSource tradeDruidDataSource2(){
return new DruidDataSource();
}

/**
* 交易数据源Sharding JDBC配置
* @return
*/
@Bean(name = "tradeSystemDataSource")
@Primary
@DependsOn("tradeDruidDataSource") // 依赖到数据源1
public DataSource tradeSystemDataSource(@Autowired DruidDataSource tradeDruidDataSource) throws Exception{

ShardingRuleConfiguration shardJdbcConfig = new ShardingRuleConfiguration();
shardJdbcConfig.getTableRuleConfigs().add(orderRuleConfig());
shardJdbcConfig.setDefaultDataSourceName(DatasourceEnum.DATASOURCE_1.getValue());

Properties props = new Properties();
//打印sql语句,生产环境关闭减少日志量
props.setProperty("sql.show",Boolean.TRUE.toString());

Map<String,DataSource> dataSourceMap = new LinkedHashMap<>() ;
dataSourceMap.put(DatasourceEnum.DATASOURCE_1.getValue(),tradeDruidDataSource) ;
// 多个分库配置
// dataSourceMap.put(DatasourceEnum.DATASOURCE_2.getValue(),tradeDruidDataSource2) ;
OrchestrationConfiguration orchestrationConfig = new OrchestrationConfiguration(
DYNAMIC_SHARDING, new RegistryCenterConfiguration("localRegisterCenter"),
false);
return OrchestrationShardingDataSourceFactory.createDataSource(dataSourceMap, shardJdbcConfig, props,
orchestrationConfig);

}

/**
* 订单分片规则(分库分表规则)
*/
private TableRuleConfiguration orderRuleConfig(){
//订单表, 多个分片示例: "DB_${1..3}.t_order_${1..3}" ds_0.t_trade_order
DynamicShardingService.SHARDING_RULE_DATASOURCE = DatasourceEnum.DATASOURCE_1.getValue();
String actualDataNodes = DatasourceEnum.DATASOURCE_1.getValue() + "." + DatasourceEnum.TABLE_ORDER.getValue() ;
TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration(DatasourceEnum.TABLE_ORDER.getValue(), actualDataNodes);
//设置分表策略
tableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("accountNo", new ShardingDataSourceRule()));
tableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("accountNo",new ShardingTableRule()));
// 记录订单表的分片规则, 便于后续编排管理
DynamicShardingService.SHARDING_RULE_TABLE_ORDER = actualDataNodes;
return tableRuleConfig;
}


}
4)用到的枚举类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.itcast.database.smooth.config;


public enum DatasourceEnum {

TABLE_ORDER("t_trade_order", "订单表"),
DATASOURCE_PREFIX("ds_", "数据源前缀"),
DATASOURCE_1("ds_0", "第一个数据源"),
DATASOURCE_2("ds_1", "扩展第二个数据源"),
;

private final String value;
private final String desc;

DatasourceEnum(final String value, final String desc) {
this.value = value;
this.desc = desc;
}

public String getValue() {
return this.value;
}

/**
* 根据value 获取枚举
*/
public static DatasourceEnum getByValue(Integer value) {
for (DatasourceEnum carSpaceTypeEnum : values()) {
if (carSpaceTypeEnum.getValue() .equals(value) ) {
//获取指定的枚举
return carSpaceTypeEnum;
}
}
return null;
}
}
6) 动态分片服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* 动态分片服务实现
*/
@Component
@Log4j2
public class DynamicShardingService {

/**
* 记录订单表的分片规则
*/
public static String SHARDING_RULE_TABLE_ORDER = "";

/**
* 记录数据源的分库规则
*/
public static String SHARDING_RULE_DATASOURCE = "";

/**
* 动态修改分片规则
* @param mod
*/
public void dynamicSharding(int mod) {

ShardingDataSourceRule.MOD = mod;
String newRule = DatasourceEnum.DATASOURCE_PREFIX.getValue() + "${0.." + (mod - 1) + "}";
if(mod == 1) {
// 只有一个数据源, 不需要动态表达式
newRule = DatasourceEnum.DATASOURCE_1.getValue();
//获取Sharding map数据源
OrchestrationShardingDataSource dataSource = SpringContextUtil.getBean("tradeSystemDataSource", OrchestrationShardingDataSource.class);
Map<String, DataSource> dataSourceMap = dataSource.getDataSource().getDataSourceMap();
DataSource dataSource1 = dataSourceMap.get(DatasourceEnum.DATASOURCE_1.getValue());
dataSourceMap.clear();
dataSourceMap.put(DatasourceEnum.DATASOURCE_1.getValue(), dataSource1);

}else {

// 动态数据源配置实现扩容
Properties properties = loadPropertiesFile("datasource1.properties");
try {
log.info("load datasource config url: " + properties.get("url"));
DruidDataSource druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(600);
druidDataSource.setLogAbandoned(true);
// 设置数据源错误重连时间
druidDataSource.setTimeBetweenConnectErrorMillis(60000);
druidDataSource.init();
OrchestrationShardingDataSource dataSource = SpringContextUtil.getBean("tradeSystemDataSource", OrchestrationShardingDataSource.class);
Map<String, DataSource> dataSourceMap = dataSource.getDataSource().getDataSourceMap();
dataSourceMap.put(DatasourceEnum.DATASOURCE_2.getValue(), druidDataSource);

Map<String, DataSourceConfiguration> dataSourceConfigMap = new HashMap<String, DataSourceConfiguration>();
for(String key : dataSourceMap.keySet()) {
dataSourceConfigMap.put(key, DataSourceConfiguration.getDataSourceConfiguration(dataSourceMap.get(key)));
}
String result = SHARDING_RULE_TABLE_ORDER.replace(SHARDING_RULE_DATASOURCE, newRule);
replaceActualDataNodes(result);
SHARDING_RULE_DATASOURCE = newRule;

dataSource.renew(new DataSourceChangedEvent(
"/" + DruidSystemDataSourceConfiguration.DYNAMIC_SHARDING + "/config/schema/logic_db/datasource",
dataSourceConfigMap));
return;


} catch (Exception e) {
log.error(e.getMessage(), e);
}


}
String result = SHARDING_RULE_TABLE_ORDER.replace(SHARDING_RULE_DATASOURCE, newRule);
replaceActualDataNodes(result);
SHARDING_RULE_DATASOURCE = newRule;

}


/**
* 加载数据源配置文件
* @param configName
* @return
*/
private Properties loadPropertiesFile(String configName) {
Properties prop = new Properties();
try {
InputStream ins = DynamicShardingService.class.getClassLoader().getResourceAsStream(configName);
prop.load(ins);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return prop;
}

/**
* 替换sharding里的分片规则
*/
public void replaceActualDataNodes(String newRule){
// 获取已有的配置
String rules = LocalRegistryCenter.values
.get("/" + DruidSystemDataSourceConfiguration.DYNAMIC_SHARDING + "/config/schema/logic_db/rule");
// 修改规则
String rule = rules.replace(SHARDING_RULE_TABLE_ORDER, newRule);
LocalRegistryCenter.listeners.get("/" + DruidSystemDataSourceConfiguration.DYNAMIC_SHARDING + "/config/schema")
.onChange(new DataChangedEvent(
"/" + DruidSystemDataSourceConfiguration.DYNAMIC_SHARDING + "/config/schema/logic_db/rule",
rule, DataChangedEvent.ChangedType.UPDATED));
LocalRegistryCenter.values.put("/" + DruidSystemDataSourceConfiguration.DYNAMIC_SHARDING + "/config/schema/logic_db/rule",rule);
SHARDING_RULE_TABLE_ORDER = newRule;

}
}
分库规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.itcast.database.smooth.config;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

/**
* 分库配置规则
*/
public class ShardingDataSourceRule implements PreciseShardingAlgorithm<Long> {

/**
* 分片规则, 取模运算
*/
public static int MOD = 1;

/**
* 根据订单ID做分库处理
* @param names
* @param value
* @return
*/
@Override
public String doSharding(Collection<String> names, PreciseShardingValue<Long> preciseShardingValue) {
Long accountNo = preciseShardingValue.getValue();
String dataSource = DatasourceEnum.DATASOURCE_PREFIX.getValue() + accountNo % MOD;
return dataSource;
}
}
分表规则

这里只做了分库,没有分表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itcast.database.smooth.config;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

/**
* 表分片规则
*/
public class ShardingTableRule implements PreciseShardingAlgorithm<Long> {


@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
// 不做分表处理, 直接返回表名
return preciseShardingValue.getLogicTableName();
}
}
第二个数据源访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
   /**
* 动态扩容调整
* @param mod
* @param response
* @return
*/
@GetMapping("/resize")
public String resize(int mod, HttpServletResponse response) {
try {
dynamicShardingService.dynamicSharding(mod);
response.getWriter().println("mod: " + mod);
}catch(Exception e) {
e.printStackTrace();
}
return null;
}


/**
* 动态修改分片规则
* @param mod
*/
public void dynamicSharding(int mod) {

ShardingDataSourceRule.MOD = mod;
String newRule = DatasourceEnum.DATASOURCE_PREFIX.getValue() + "${0.." + (mod - 1) + "}";
if(mod == 1) {
// 只有一个数据源, 不需要动态表达式
newRule = DatasourceEnum.DATASOURCE_1.getValue();
//获取Sharding map数据源
OrchestrationShardingDataSource dataSource = SpringContextUtil.getBean("tradeSystemDataSource", OrchestrationShardingDataSource.class);
Map<String, DataSource> dataSourceMap = dataSource.getDataSource().getDataSourceMap();
DataSource dataSource1 = dataSourceMap.get(DatasourceEnum.DATASOURCE_1.getValue());
dataSourceMap.clear();
dataSourceMap.put(DatasourceEnum.DATASOURCE_1.getValue(), dataSource1);

}else { // 加载第二个数据源

// 动态数据源配置实现扩容
Properties properties = loadPropertiesFile("datasource1.properties");
try {
log.info("load datasource config url: " + properties.get("url"));
DruidDataSource druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(600);
druidDataSource.setLogAbandoned(true);
// 设置数据源错误重连时间
druidDataSource.setTimeBetweenConnectErrorMillis(60000);
druidDataSource.init();
OrchestrationShardingDataSource dataSource = SpringContextUtil.getBean("tradeSystemDataSource", OrchestrationShardingDataSource.class);
Map<String, DataSource> dataSourceMap = dataSource.getDataSource().getDataSourceMap();
dataSourceMap.put(DatasourceEnum.DATASOURCE_2.getValue(), druidDataSource);

Map<String, DataSourceConfiguration> dataSourceConfigMap = new HashMap<String, DataSourceConfiguration>();
for(String key : dataSourceMap.keySet()) {
dataSourceConfigMap.put(key, DataSourceConfiguration.getDataSourceConfiguration(dataSourceMap.get(key)));
}
String result = SHARDING_RULE_TABLE_ORDER.replace(SHARDING_RULE_DATASOURCE, newRule);
replaceActualDataNodes(result);
SHARDING_RULE_DATASOURCE = newRule;

dataSource.renew(new DataSourceChangedEvent(
"/" + DruidSystemDataSourceConfiguration.DYNAMIC_SHARDING + "/config/schema/logic_db/datasource",
dataSourceConfigMap));
return;


} catch (Exception e) {
log.error(e.getMessage(), e);
}


}
String result = SHARDING_RULE_TABLE_ORDER.replace(SHARDING_RULE_DATASOURCE, newRule);
replaceActualDataNodes(result);
SHARDING_RULE_DATASOURCE = newRule;

}
datasource1.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://192.168.10.151:3306/smooth?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
initialSize=5
minIdle=5
maxActive=20
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 1
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
验证应用服务高可用
  • 4.1 新增一条数据
  • 4.2 停止linux30数据库,新增一条数据
  • 4.3 重启linux30数据库和keepalived,新增一条数据

image-20230927200327943

image-20230927200322467

注意事项

Sharding JDBC, Mycat, Drds 等产品都是分布式数据库中间件, 相比直接的数据源操作, 会存在一些限制, Sharding JDBC在使用时, 要注意以下问题:

  • 有限支持子查询
  • 不支持HAVING
  • 不支持OR,UNION 和 UNION ALL
  • 不支持特殊INSERT
  • 每条INSERT语句只能插入一条数据,不支持VALUES后有多行数据的语句
  • 不支持DISTINCT聚合
  • 不支持dual虚拟表查询
  • 不支持SELECT LAST_INSERT_ID(), 不支持自增序列
  • 不支持CASE WHEN

实现数据库的秒级平滑2N扩容

扩容部署架构:

image-20230927200317203

新增数据库VIP

  1. 在linux31节点, 增加VIP

修改/etc/keepalived/keepalived.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@linux31 mariaDB]# vi /etc/keepalived/keepalived.conf 

global_defs {
router_id linux31
}
vrrp_instance VI_1 { #vrrp实例定义
state BACKUP #lvs的状态模式,MASTER代表主, BACKUP代表备份节点
interface ens33 #绑定对外访问的网卡
virtual_router_id 112 #虚拟路由标示,同一个vrrp实例采用唯一标示
priority 100 #优先级,100代表最大优先级, 数字越大优先级越高
advert_int 1 #master与backup节点同步检查的时间间隔,单位是秒
authentication { #设置验证信息
auth_type PASS #有PASS和AH两种
auth_pass 6666 #验证密码,BACKUP密码须相同
}

virtual_ipaddress { #KeepAlived虚拟的IP地址
192.168.10.151
}
}
virtual_server 192.168.10.151 3306 { #配置虚拟服务器IP与访问端口
delay_loop 6 #健康检查时间
persistence_timeout 0 #会话保持时间,这里要做测试, 所以设为0, 实际可根 据session有效时间配置
protocol TCP #转发协议类型,支持TCP和UDP
real_server 192.168.10.31 3306 { #配置服务器节点linux31
notify_down /opt/mariaDB/mariadb.sh
weight 1 #设置权重,越大权重越高
TCP_CHECK { #r状态监测设置
connect_timeout 10 #超时配置, 单位秒
retry 3 #重试次数
delay_before_retry 3 #重试间隔
connect_port 3306 #连接端口, 和上面保持一致
}
}
}

注意配置项:

1
2
virtual_router_id 112 #虚拟路由标示,同一个vrrp实例采用唯一标示 
priority 100 #优先级,100代表最大优先级, 数字越大优先级越高

应用服务增加动态数据源

  1. 修改应用服务配置, 增加新的数据源, 指向新设置的VIP: 192.168.10.151
  2. 通过应用服务接口, 动态扩容调整

image-20230927200308440

解除原双主同步

mysql -uroot -p123456

  1. 进入linux30:
1
MariaDB [(none)]> stop slave;
  1. 进入linux31:
1
MariaDB [(none)]> stop slave;
  1. 通过应用服务接口验证数据是否解除同步

安装MariaDB扩容服务器

  1. 新建两台虚拟机, 分别为linux32和linux33。 2. 在linux32和linux33两台节点上安装MariaDB服务 参考2.1.1 MariaDB服务安装
  2. 配置linux32与linux30,实现新的双主同步
  3. linux32节点, 修改/etc/my.cnf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@linux32 ~]# vi /etc/my.cnf 

[mysqld]
server-id = 3
log-bin=mysql-bin
relay-log = mysql-relay-bin
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=information_schema.%
log-slave-updates=on
slave-skip-errors=all
auto-increment-offset=2
auto-increment-increment=2
binlog_format=mixed
expire_logs_days=10
  1. 重启linux32数据库
1
[root@linux32 ~]# service mariadb restart
  1. 创建replica用于主从同步的用户:
1
2
3
4
5
6
7
[root@linux32 ~]# mysql -uroot -p123456 

MariaDB [(none)]> grant replication slave, replication client on *.* to

'replica'@'%' identified by 'replica';

MariaDB [(none)]> flush privileges;
  1. 在linux30节点,进行数据全量备份:
1
2
3
4
5
6
7
[root@linux30 mariaDB]# mysqldump -uroot -p123456 --routines --single_transaction --master-data=2 --databases smooth > linux30.sql 

[root@linux30 mariaDB]# ll
总用量 8
-rw-r--r--. 1 root root 2873 1月 5 11:22 linux30.sql
-rwxr-xr-x. 1 root root 17 15 09:52 mariadb.sh
[root@linux30 mariaDB]#
  1. 查看并记录master status信息
1
2
3
4
5
6
7
8
9
MariaDB [(none)]> show master status; 

+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000003 | 1462 | | |
+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)
  1. 将备份的linux30.sql通过scp命令拷贝至linux32节点。
1
[root@linux30 mariaDB]# scp linux30.sql root@192.168.10.32:/opt/mariaDB
  1. 将数据还原至linux32节点:
1
[root@linux32 mariaDB]# mysql -uroot -p123456 < /opt/mariaDB/linux30.sql
  1. 配置主从同步信息

根据上面的master status信息, 在linux32中执行:

1
2
3
MariaDB [(none)]> change master to master_host='192.168.10.30',master_user='replica', master_password='replica', master_port=3306, master_log_file='mysql-bin.000004', master_log_pos=3133, master_connect_retry=30; 

Query OK, 0 rows affected (0.00 sec)
  1. 开启主从同步:
1
2
3
MariaDB [(none)]> start slave; 

Query OK, 0 rows affected (0.00 sec)

如果出现问题, 复原主从同步信息:

1
2
3
MariaDB [(none)]> reset slave; 

Query OK, 0 rows affected (0.01 sec)
  1. 检查同步状态信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MariaDB [(none)]> show slave status \G; 

*************************** 1. row ***************************

Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.10.30
Master_User: replica
Master_Port: 3306
Connect_Retry: 30
Master_Log_File: mysql-bin.000003
Read_Master_Log_Pos: 1462
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 555
Relay_Master_Log_File: mysql-bin.000003

Slave_IO_Running: Yes
Slave_SQL_Running: Yes
  1. 配置linux30与linux32节点的同步

查看linux32的日志信息:

1
2
3
4
5
6
7
8
9
MariaDB [(none)]> show master status; 

+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 2358 | | |
+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)

在linux30节点, 配置同步信息:

1
2
3
4
5
6
7
8
9
10
11
MariaDB [(none)]> reset slave; 

Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> change master to master_host='192.168.10.32',master_user='replica', master_password='replica', master_port=3306, master_log_file='mysql-bin.000003', master_log_pos=358, master_connect_retry=30;

Query OK, 0 rows affected (0.02 sec)

MariaDB [(none)]> start slave;

Query OK, 0 rows affected (0.00 sec)
  1. 配置linux33与linux31的双主同步

1.linux33节点, 修改/etc/my.cnf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@linux33 ~]# vi /etc/my.cnf 

[mysqld]
server-id = 4
log-bin=mysql-bin
relay-log = mysql-relay-bin
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=information_schema.%
log-slave-updates=on
slave-skip-errors=all
auto-increment-offset=2
auto-increment-increment=2
binlog_format=mixed
expire_logs_days=10
  1. 重启linux33数据库
1
[root@linux33 ~]# service mariadb restart
  1. 创建replica用于主从同步的用户:
1
2
3
4
5
6
7
8
9
[root@linux33 ~]# mysql -uroot -p123456 

MariaDB [(none)]> grant replication slave, replication client on *.* to 'replica'@'%' identified by 'replica';

Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> flush privileges;

Query OK, 0 rows affected (0.00 sec)
  1. 在linux31节点,进行数据全量备份:
1
2
3
4
5
6
7
[root@linux31 mariaDB]# mysqldump -uroot -p123456 --routines --single_transaction --master-data=2 --databases smooth > linux31.sql 

[root@linux31 mariaDB]# ll
总用量 8
-rw-r--r--. 1 root root 2873 1月 5 11:35 linux31.sql
-rwxr-xr-x. 1 root root 17 15 09:56 mariadb.sh
[root@linux31 mariaDB]#
  1. 查看并记录master status信息
1
2
3
4
5
6
7
8
9
10
11
[root@linux31 mariaDB]# mysql -uroot -p123456 

MariaDB [(none)]> show master status;

+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 4470 | | |
+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)
  1. 将备份的linux31.sql通过scp命令拷贝至linux33节点。
1
[root@linux31 mariaDB]# scp linux31.sql root@192.168.10.33:/opt/mariaDB
  1. 将数据还原至linux33节点:
1
[root@linux33 mariaDB]# mysql -uroot -p123456 < /opt/mariaDB/linux31.sql
  1. 配置主从同步信息

根据上面的master status信息, 在linux33中执行

1
2
3
4
5
[root@linux33 mariaDB]# mysql -uroot -p123456 

MariaDB [(none)]> change master to master_host='192.168.10.31',master_user='replica', master_password='replica', master_port=3306, master_log_file='mysql-bin.000002', master_log_pos=3133, master_connect_retry=30;

Query OK, 0 rows affected (0.00 sec)
  1. 开启主从同步:
1
2
3
MariaDB [(none)]> start slave; 

Query OK, 0 rows affected (0.00 sec)

注意, 如果出现问题, 复原主从同步信息:

1
2
3
MariaDB [(none)]> reset slave; 

Query OK, 0 rows affected (0.01 sec)
  1. 检查同步状态信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MariaDB [(none)]> show slave status \G; 

*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.10.31
Master_User: replica
Master_Port: 3306
Connect_Retry: 30
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 4470
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 555
Relay_Master_Log_File: mysql-bin.000001

Slave_IO_Running: Yes
Slave_SQL_Running: Yes
  1. 配置linux31与linux33节点的同步

查看linux33的日志信息:

1
2
3
4
5
6
7
8
9
MariaDB [(none)]> show master status; 

+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 2358 | | |
+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)

在linux31节点, 配置同步信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@linux31 mariaDB]# mysql -uroot -p123456 

MariaDB [(none)]> reset slave;

Query OK, 0 rows affected (0.01 sec)

MariaDB [(none)]> change master to master_host='192.168.10.33',master_user='replica', master_password='replica', master_port=3306, master_log_file='mysql-bin.000003', master_log_pos=2268, master_connect_retry=30;

Query OK, 0 rows affected (0.01 sec)

MariaDB [(none)]> start slave;

Query OK, 0 rows affected (0.00 sec)

增加KeepAlived服务实现高可用

  1. 确保新增的linux32和linux33节点安装Keepalived服务。
  2. 修改linux32节点配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[root@linux32 mariaDB]# vi /etc/keepalived/keepalived.conf 

global_defs {
router_id linux32 # 机器标识,一般设为hostname,故障发生时,邮件通知会使用到。
}

vrrp_instance VI_1 { #vrrp实例定义
state BACKUP #lvs的状态模式,MASTER代表主, BACKUP代表备份节点
interface ens33 #绑定对外访问的网卡
virtual_router_id 111 #虚拟路由标示,同一个vrrp实例采用唯一标示
priority 98 #优先级,100代表最大优先级, 数字越大优先级越高
advert_int 1 #master与backup节点同步检查的时间间隔,单位是秒
authentication { #设置验证信息
auth_type PASS #有PASS和AH两种
auth_pass 6666 #验证密码,BACKUP密码须相同
}
virtual_ipaddress { #KeepAlived虚拟的IP地址
192.168.10.150
}
}

virtual_server 192.168.10.150 3306 { #配置虚拟服务器IP与访问端口
delay_loop 6 #健康检查时间
persistence_timeout 0 #会话保持时间,这里要做测试, 所以设为0, 实际可根 据session有效时间配置
protocol TCP #转发协议类型,支持TCP和UDP
real_server 192.168.10.32 3306 { #配置服务器节点linux32
notify_down /opt/mariaDB/mariadb.sh
weight 1 #设置权重,越大权重越高
TCP_CHECK { #r状态监测设置
connect_timeout 10 #超时配置, 单位秒
retry 3 #重试次数
delay_before_retry 3 #重试间隔
connect_port 3306 #连接端口, 和上面保持一致
}
}
}

注意里面IP配置正确, 修改完成后重启服务。

1
[root@linux32 mariaDB]# systemctl restart keepalived

创建关闭脚本mariadb.sh

1
2
3
4
5
[root@linux32 mariaDB]# vi /opt/mariaDB/mariadb.sh 

# 添加如下内容

pkill keepalived

加入执行权限:

1
chmod a+x mariadb.sh
  1. 修改linux33节点配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@linux33 mariaDB]# vi /etc/keepalived/keepalived.conf 

global_defs {
router_id linux33 # 机器标识,一般设为hostname,故障发生时,邮件通知会使 用到。
}
vrrp_instance VI_1 { #vrrp实例定义
state BACKUP #lvs的状态模式,MASTER代表主, BACKUP代表备份节点
interface ens33 #绑定对外访问的网卡
virtual_router_id 112 #虚拟路由标示,同一个vrrp实例采用唯一标示
priority 98 #优先级,100代表最大优先级, 数字越大优先级越高
advert_int 1 #master与backup节点同步检查的时间间隔,单位是秒
authentication { #设置验证信息
auth_type PASS #有PASS和AH两种
auth_pass 6666 #验证密码,BACKUP密码须相同
}
virtual_ipaddress { #KeepAlived虚拟的IP地址
192.168.10.151
}
}

virtual_server 192.168.10.151 3306 { #配置虚拟服务器IP与访问端口
delay_loop 6 #健康检查时间
persistence_timeout 0 #会话保持时间,这里要做测试, 所以设为0, 实际可根 据session有效时间配置
protocol TCP #转发协议类型,支持TCP和UDP
real_server 192.168.10.33 3306{ #配置服务器节点linux33
notify_down /opt/mariaDB/mariadb.sh
weight 1 #设置权重,越大权重越高
TCP_CHECK { #r状态监测设置
connect_timeout 10 #超时配置, 单位秒
retry 3 #重试次数
delay_before_retry 3 #重试间隔
connect_port 3306 #连接端口, 和上面保持一致
}
}
}

创建关闭脚本mariadb.sh

1
2
3
4
5
[root@linux33 mariaDB]# vi /opt/mariaDB/mariadb.sh 

# 添加如下内容

pkill keepalived

给所有的用户组加入执行权限

1
chmod a+x mariadb.sh 
  1. 修改完后重启Keepalived服务。
1
[root@linux33 mariaDB]# systemctl restart keepalived

清理数据并验证

  1. 通过应用服务动态扩容接口做调整和验证
  2. 在linux30节点清理数据 根据取模规则, 保留accountNo为偶数的数据
1
delete from smooth.t_trade_order where accountNo % 2 != 0;
  1. 在linux31节点清理数据 根据取模规则, 保留accountNo为奇数的数据
1
delete from smooth.t_trade_order where accountNo % 2 != 1;

🔖MySQL调优

Web优化

背景

老板说系统反应慢,要求提高速度,怎么办?

客户反馈系统响应太慢,体验差,怎么办?

问题:

性能优化时常见的事,概念广,在web应用中,不管前后端,或是单点/分布式系统。都涉及性能调优。

Web访问流程性能优化

image-20231007201022159

性能优化脑图

image-20231007201056630

原则

  • 适度优化,切忌过度优化
  • 先优化最大瓶颈,事半功倍
  • 依据数据而不是凭空猜测
  • 性能优化是持久战,道高一尺魔高一丈
  • 深入理解业务

数据库性能优化法则

木桶理论

木桶理论和性能优化有何关系 性能优化应该首先要找到系统的短板在哪里,优先对短板进行优化处理。 性能优化中短板举例 大多数情况性能最慢的设备会是瓶颈点:

  • 如下载时网络速度可能会是瓶颈点
  • 本地复制文件时硬盘可能会是瓶颈点

数据库IO各层性能分析

数据库本质: 数据库本质上是查找磁盘物理文件的过程,而这个过程必然涉及到IO操作。

数据库IO短板分析

因此,为了快速找到SQL的性能瓶颈点,我们有必要了解下数据库相关IO的性能情况,也就是计算机系统的硬件基本性能指标,下图展示的当前主流计算机性能指标数据。

image-20231007201935817

相关指标:

  • 延时(响应时间):表示硬件的突发处理能力;
  • 带宽(吞吐量):代表硬件持续处理能力。

性能从高到低排序:

  • CPU > Cache(L1-L2-L3) > 内存 > SSD硬盘 > 网络 > 普通硬盘

优化从倒序去优化,符合木桶理论,也就是短板最可能出现在磁盘。也就是漏斗法则中的先优化磁盘访问,效果最明显。

数据库优化漏斗法则

image-20231007202759310

这个优化法则归纳为5个层次:

  1. 减少数据访问(减少磁盘访问)
  2. 返回更少数据(减少网络传输或磁盘访问)
  3. 减少交互次数(减少网络传输)
  4. 减少服务器CPU开销(减少CPU及内存开销)
  5. 利用更多资源(增加资源)

说明

  • 由于每一层优化法则都是解决其对应硬件的性能问题,所以带来的性能提升比例也不一样。
  • 针对低速设备问题的可优化手段更多,优化成本也更低。
  • 任何一个SQL的性能优化都应该按这个规则由上到下来诊断问题并提出解决方案,而不应该首先想到的是增加资源解决问题。

优化效果

image-20231007203502256

案例引入

案例演示:在表里插入1000条数据

执行时间变化:235.945s—-> 1.685s

性能变化:同样都是插入1000条数据,为什么性能变化接近200倍?

MySQL架构设计

引言

查询语句:

1
select * from user_info where id = 1;

返回结果为:o

1
2
3
4
5
+----+----------+----------+--------+------+---------------------+---------------------+
| id | username | password | openid | role | create_time | update_time |
+----+----------+----------+--------+------+---------------------+---------------------+
| 1 | 张三 | 123 | 1 | 1 | 2022-01-01 00:29:08 | 2022-01-01 00:29:08 |
+----+----------+----------+--------+------+---------------------+---------------------+

问题:

  • 思考:一条SQL查询语句是如何执行的?

Server层

MySQL 架构可以分为 Server层Engine层两部分:

image-20230927201438246

​ MySQL 的逻辑架构图

连接器(Connector)

Mysql作为服务器,一个客户端的Sql连接过来就需要分配一个线程进行处理,这个线程会专门负责监听请求并读取数据。这部分的线程和连接管理都是有一个连接器,专门负责跟客户端建立连接、权限认证、维持和管理连接。

思考:

(1)一个客户端只会和MySQL服务器建立一个连接吗?

(2)只能有一个客户端MySQL服务器建立连接吗?

image-20230927201433714

答:

多个系统都可以和MySQL服务器建立连接,每个系统建立的连接肯定不止一个。

所以,为了解决TCP无限创建与TCP频繁创建销毁带来的资源耗尽、性能下降问题。

MySQL服务器里有专门的TCP连接池限制接数,采用长连接模式复用TCP连接,来解决上述问题。

TCP连接收到请求后,必须要分配给一个线程去执行,所以还会有个线程池,去走后面的流程。

连接器负责跟客户端建立连接、获取权限、维持和管理连接。

连接命令一般是这么写的:

1
mysql -h$ip -P$port -u$user -p

在完成 经典TCP 握手后,连接器会基于用户名和密码来验证身份。

  • 验证不通过:”Access denied for user”错误
  • 验证通过:连接器会到权限表里面查出拥有的权限,之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限
1
show processlist  -- 查看连接状态

image-20230927201429184

图中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。

查询缓存(Query Cache)

经过了连接管理,现在MySQL服务器已经获取到SQL字符串。

执行逻辑就会来到第二步:查询缓存

image-20230927201424221

查询语句,MySQL服务器会使用select SQL字符串作为key,去缓存中获取:

  • 缓存命中,直接返回结果
  • 缓存未命中:执行后面的阶段,执行完成后,执行结果会被存入查询缓存中

缓存中数据:key:(查询的语句) value:(查询的结果)

注意:但是大多数情况下建议不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利

  • 查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空
  • 5.x版本可以按需使用”的方式。可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定例:
1
mysql> select SQL_CACHE * from T where ID=10
  • MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了

分析器(Analyzer)

缓存如果未命中,就要开始真正执行语句了

首先,MySQL 需要知道要做什么,因此需要对 SQL 语句做解析

image-20230927201419807

  • 词法分析

首先,会进行词法分析。 将一个完整的SQL语句,拆分成语句类型(select? insert? update? …)、表名、列名等等。

  • 语法分析

其次,会进行语法分析。 根据语法规则,判断输入的这个 SQL 语句是否满足 MySQL 语法。 如果错误,会报出下面的错误:

1
2
3
mysql> elect * from t where ID=1;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1

这时,我们只要修正 use near 后面的语句即可。

优化器(optimizer)

通过了分析器,说明SQL字符串符合语法规范,现在MySQL服务器要执行SQL语句了。

MySQL服务器要怎么执行呢?

那么就需要产出执行计划,交给MySQL服务器执行,所以来到了优化器阶段。

image-20230927201414357

优化器不仅仅只是生成执行计划这么简单,这个过程它会帮你优化SQL语句。

外连接转换为内连接、表达式简化、子查询转为连接、连接顺序、索引选择等一堆东西,优化的结果就是执行计划。

例:执行下面这样的语句,这个语句是执行两个表的 join:

1
mysql> select * from t1 join t2 using(ID)  where t1.c=10 and t2.d=20;
  • 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
  • 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。

这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。

截止到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划。

执行器(Actuator)

MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。

image-20230927201408570

开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示

1
2
3
mysql> select * from T where ID=10;

ERROR 1142 (42000): SELECT command denied to user 'zimu'@'localhost' for table 'T'

如果有权限,就会根据表的 Engine 选择来调用对应的引擎接口。

例:

user_info 表的存储引擎是 InnoDB

1
select * from user_info where name = "zimu";
  • 如果 name 列没有声明任何索引,执行步骤如下:
  1. 调用 innoDB 引擎接口获取表的第一行,判断 name 是否等于 zimu。如果不是,跳过。如果是,将结果保存。
  2. 调用 innoDB 引擎接口获取表的下一行,重复相同逻辑,一直到表的最后一行。
  3. 将所有满足条件的结果集返回给客户端。
  • 如果 name 列有索引,执行步骤如下:
  1. 调用 innoDB 引擎接口获取索引树(B+树),基于索引树快速查到 name 等于 zimu 的所有主键id。
  2. 将所有满足条件的组件 id,回主表查详细信息。(这个操作称为“回表”)
  3. 将所有满足条件的结果集返回给客户端。

Engine层

什么是存储引擎?

引擎(Engine),我们都知道是机器发动机的核心所在,数据库存储引擎便是数据库的底层软件组织。

数据库使用数据存储引擎实现存储、处理和保护数据的核心服务

不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySql的核心就是插件式存储引擎。

mysql支持哪些存储引擎?

我们可以使用MySQL命令行查看:

1
SHOW ENGINES ;

image-20230927201402742

可以发现,MySQL目前支持多种数据库存储引擎,默认引擎为InnoDB,且是唯一支持事务的存储引擎。

常见的存储引擎对比

image-20230927201355849

InnoDB引擎

概述:InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,InnoDB是默认的MySQL引擎。

主要特性:

  • 为MySQL提供了具有提交、回滚和崩溃恢复能力的事物安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在 SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合
  • InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上
  • InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键

  使用 InnoDB存储引擎 MySQL将在数据目录下创建一个名为 ibdata1的10MB大小的自动扩展数据文件,以及两个名为 ib_logfile0和 ib_logfile1的5MB大小的日志文件。

MyISAM存储引擎

概述:MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事务

主要特性:

  • 被大文件系统和操作系统支持
  • 当把删除和更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,若下一个块被删除,就扩展到下一块自动完成
  • 每个MyISAM表最大索引数是64,这可以通过重新编译来改变。每个索引最大的列数是16
  • 最大的键长度是1000字节,这也可以通过编译来改变,对于键长度超过250字节的情况,一个超过1024字节的键将被用上
  • BLOB和TEXT列可以被索引
  • NULL被允许在索引的列中,这个值占每个键的0~1个字节
  • 所有数字键值以高字节优先被存储以允许一个更高的索引压缩
  • 每个MyISAM类型的表都有一个AUTOINCREMENT的内部列,当INSERT和UPDATE操作的时候该列被更新,同时AUTOINCREMENT列将被刷新。所以说,MyISAM类型表的AUTOINCREMENT列更新比InnoDB类型的AUTOINCREMENT更快
  • 可以把数据文件和索引文件放在不同目录
  • 每个字符列可以有不同的字符集
  • 有VARCHAR的表可以固定或动态记录长度
  • VARCHAR和CHAR列可以多达64KB

  使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名时.MYI(MYIndex)。

MEMORY存储引擎

概述:MEMORY存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问

主要特性:

  • MEMORY表的每个表可以有多达32个索引,每个索引16列,以及500字节的最大键长度
  • MEMORY存储引擎执行HASH和BTREE缩影
  • 可以在一个MEMORY表中有非唯一键值
  • MEMORY表使用一个固定的记录长度格式
  • MEMORY不支持BLOB或TEXT列
  • MEMORY支持AUTO_INCREMENT列和对可包含NULL值的列的索引
  • MEMORY表在所由客户端之间共享(就像其他任何非TEMPORARY表)
  • MEMORY表内存被存储在内存中,内存是MEMORY表和服务器在查询处理时的空闲中,创建的内部表共享
  • 当不再需要MEMORY表的内容时,要释放被MEMORY表使用的内存,应该执行 DELETE FROM或 TRUNCATE TABLE,或者删除整个表(使用DROP TABLE)

其他存储引擎(了解)

  • Archive :非常适合存储大量的独立的,作为历史记录的数据。因为它们不经常被读取。Archive 拥有高效的插入速度,但其对查询的支持相对较差
  • Federated :将不同的 MySQL 服务器联合起来,逻辑上组成一个完整的数据库。非常适合分布式应用

存储引擎的选择

不同的数据处理选择适合的存储引擎是使用MySQL的一大优势。

image-20230927201344504

  • InnoDB: 支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
  • MyISAM: 插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。
  • MEMORY: 所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。

  注意:同一个数据库也可以使用多种存储引擎的表。如果一个表要求比较高的事务处理,可以选择InnoDB。这个数据库中可以将查询要求比较高的表选择MyISAM存储。如果该数据库需要一个用于查询的临时表,可以选择MEMORY存储引擎。

存储引擎常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 查询当前数据库支持的存储引擎:
show engines;

-- 查看当前的默认存储引擎:
show variables like%storage_engine%’;

-- 查看某个表用了什么引擎(在显示结果里参数engine后面的就表示该表当前用的存储引擎):
show create table student;

-- 创建新表时指定存储引擎:
create table(...) engine=MyISAM;

-- 修改数据库引擎
alter table student engine = INNODB;
alter table student engine = MyISAM;

-- 修改MySQL默认存储引擎方法
1. 关闭mysql服务
2. 找到mysql安装目录下的my.ini文件:
3. 找到default-storage-engine=INNODB 改为目标引擎,
如:default-storage-engine=MYISAM
4. 启动mysql服务

MySQL索引原理&优化

什么是索引?

引言

  • 官方上面说索引是帮助MySQL高效获取数据数据结构,通俗点的说,数据库索引好比是一本书的目录,可以直接根据页码找到对应的内容,目的就是为了加快数据库的查询速度
  • 索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
  • 一种能帮助mysql提高了查询效率的数据结构:索引数据结构

索引原理

索引的存储原理可以概括为一句话:以空间换时间

一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。

数据库在未添加索引进行查询的时候默认是进行全文搜索,也就是说有多少数据就进行多少次查询,然后找到相应的数据就把它们放到结果集中,直到全文扫描完毕。

索引优缺点

优点:

  • 大大提高数据查询速度。
  • 可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
  • 通过索引列对数据进行排序,降低数据的排序成本降低了CPU的消耗。
  • 被索引的列会自动进行排序,包括【单例索引】和【组合索引】,只是组合索引的排序需要复杂一些。
  • 如果按照索引列的顺序进行排序,对order 不用语句来说,效率就会提高很多,显著减少查询时分组和排序的时间。

缺点:

  • 索引会占据磁盘空间。
  • 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改查操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。
  • 维护索引需要消耗数据库资源。

综合索引的优缺点:

数据库表中不是索引越多越好,而是仅为那些常用的搜索字段建立索引效果最佳!

创建索引原则

  • 更新频繁的列不应设置索引
  • 数据量小的表不要使用索引(毕竟总共2页的文档,还要目录吗?)
  • 重复数据多的字段不应设为索引(比如性别,只有男和女,一般来说:重复的数据超过15%就不该建索引)
  • 首先应该考虑对where 和 order by 涉及的列上建立索引

索引功能分类

1014094

单列索引

单列索引:单列索引是最基本的索引,它没有任何限制,一个索引只包含单个列,但一个表中可以有多个单列索引。

普通索引

  • 普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一点。
1
2
3
4
5
6
7
8
9
10
#  (1) 直接创建索引
CREATE INDEX index_name ON table_name(`col_name`);
# (2) 修改表结构的方式添加索引
ALTER TABLE `table_name` ADD INDEX index_name(`col_name`);
# (3) 创建表的时候同时创建索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`id`),
INDEX index_name (`col_name`) -- 给col_name列创建索引
)

创建索引并不一定要有数据的,可以在建表的时候就指定索引列,后续增加数据的时候会将该列按特定方式存储到索引里。

唯一索引

唯一索引:唯一索引和普通索引类似,主要的区别在于,唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)

如果在已经有数据的表上添加唯一性索引的话:

  • 如果添加索引的列的值存在两个或者两个以上的空值,则不能创建唯一性索引会失败。(一般在创建表的时候,要对自动设置唯一性索引,需要在字段上加上 not null
  • 如果添加索引的列的值存在两个或者两个以上的null值,还是可以创建唯一性索引,只是后面创建的数据不能再插入null值 ,并且严格意义上此列并不是唯一的,因为存在多个null值。
  • 对于多个字段创建唯一索引规定列值的组合必须唯一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
“空值” 和”NULL”的概念: 
1:空值是不占用空间的 .
2: MySQL中的NULL其实是占用空间的.

长度验证:注意空值的之间是没有空格的。

> select length(''),length(null),length(' ');
+------------+--------------+-------------+
| length('') | length(null) | length(' ') |
+------------+--------------+-------------+
| 0 | NULL | 1 |
+------------+--------------+-------------+
-- (1)创建唯一索引
# 创建单个索引
CREATE UNIQUE INDEX index_name ON table_name(`col_name`);
# 创建多个索引
CREATE UNIQUE INDEX index_name on table_name(`col_name`,...);

-- (2)修改表结构
# 单个
ALTER TABLE table_name ADD UNIQUE index index_name(`col_name`);
# 多个
ALTER TABLE table_name ADD UNIQUE index index_name(`col_name`,...);

-- (3)创建表的时候直接指定索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`id`),
UNIQUE index_name_unique(`col_name`)
)

主键索引

一般表都有主键,创建表设定为主键后,数据库自动建立索引,我们什么都不用做。InnoDB为聚簇索引,主键索引列值具有唯一性且不能为空(Null),是一种特殊的唯一索引。

1
2
3
4
5
6
7
# (1) 创建表添加主键索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`col_name`),
)
# (2) 添加主键索引
ALTER TABLE `table_name` ADD PRIMARY KEY (`col_name`);

组合索引(复合索引)

  • 复合索引:复合索引是在多个字段上创建的索引。复合索引遵守“最左前缀”原则即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。
  • 例如同时使用身份证和手机号建立索引,同样的可以建立为普通索引或者是唯一索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# (1)创建一个复合索引
create index index_name on table_name(`col_name1`,`col_name2`,...); -- 普通索引
create unique index index_phone_name on student(phone_num,name); -- 唯一索引

最左原则演示:
select * from student where name = '张三'; -- 行
select * from student where phone_num = '15100046637'; -- 用到了最左边一个,可以
select * from student where phone_num = '15100046637' and name = '张三'; -- 都用到,可以
select * from student where name = '张三' and phone_num = '15100046637'; -- mysql有优化,可以
/*
三条sql只有 2 、 3、4能使用的到索引 index_phone_name,因为条件里面必须包含索引前面的字段,才能够进行匹配。
而3和4相比where条件的顺序不一样,为什么4可以用到索引呢?是因为mysql本身就有一层sql优化,他会根据sql来识别出来该用哪个索引,我们可以理解为3和4在mysql眼中是等价的。
*/


# (2)修改表结构的方式添加索引
alter table table_name add index index_name(`col_name1`,`col_name2`,...);

全文索引

  • Full Text类型索引(FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB,而之前的版本只支持 MyISAM 表)。
  • 全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较,目前只有char、varchar,text 列上可以创建全文索引。它更像是一个搜索引擎,基于相似度的查询,而不是简单的where语句的参数匹配。
  • like + % 就可以实现模糊匹配了,为什么还要全文索引?like + % 在文本比较少时是合适的,但是对于大量的文本数据检索,是不可想象的。全文索引在大量的数据面前,能比 like + % 快 N 倍,速度不是一个数量级,但是全文索引可能存在精度问题。

MySQL 中的全文索引,有两个变量,最小搜索长度和最大搜索长度,对于长度小于最小搜索长度和大于最大搜索长度的词语,都不会被索引。通俗点就是说,想对一个词语使用全文索引搜索,那么这个词语的长度必须在以上两个变量的区间内。这两个的默认值可以使用以下命令查看: show variables like '%ft%';

image-20230112064817788

参数名称 默认值 最小值 最大值 作用
ft_min_word_len 4 1 3600 MyISAM 引擎表全文索引包含的最小词长度
ft_query_expansion_limit 20 0 1000 MyISAM引擎表使用 with query expansion 进行全文搜索的最大匹配数
innodb_ft_min_token_size 3 0 16 InnoDB 引擎表全文索引包含的最小词长度
innodb_ft_max_token_size 84 10 84 InnoDB 引擎表全文索引包含的最大词长度
1
2
3
4
5
6
7
8
9
10
11
12
-- (1)创建表的适合添加全文索引
CREATE TABLE `table_name` (
[...] ,
PRIMARY KEY (`id`),
FULLTEXT (`col_name`) -- 创建全文检索,不推荐
)

-- (2)修改表结构添加全文索引
ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(`col_name`)

-- (3)直接创建索引
CREATE FULLTEXT INDEX index_fulltext_content ON table_name(`col_name`)

注意:

  • 默认 MySQL 不支持中文全文检索!
  • MySQL 全文搜索只是一个临时方案,对于全文搜索场景,更专业的做法是使用全文搜索引擎,例如 ElasticSearch 或 Solr。

使用全文索引

使用全文索引和常用的模糊匹配使用 like + % 不同,全文索引有自己的语法格式,使用 match 和 against 关键字.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
match (col1,col2,...)  against(expr [search_modifier])

# 准备数据
-- 创建表的时候添加全文索引
create table t_article (
id int primary key auto_increment ,
title varchar(255) ,
content varchar(1000) ,
writing_date date -- ,
-- fulltext (content) -- 创建全文检索
);
insert into t_article values(null,"Yesterday Once More","When I was young I listen to the radio",'2021-10-01');
insert into t_article values(null,"Right Here Waiting","Oceans apart, day after day,and I slowly go insane",'2021-10-02');
insert into t_article values(null,"My Heart Will Go On","every night in my dreams,i see you, i feel you",'2021-10-03');
insert into t_article values(null,"Everything I Do","eLook into my eyes,You will see what you mean to me",'2021-10-04');
insert into t_article values(null,"Called To Say I Love You","say love you no new year's day, to celebrate",'2021-10-05');
insert into t_article values(null,"Nothing's Gonna Change My Love For You","if i had to live my life without you near me",'2021-10-06');
insert into t_article values(null,"Everybody","We're gonna bring the flavor show U how.",'2021-10-07');
# 创建表,添加数据之后,创建全文索引(推荐)
create fulltext index index_content on t_article(content);

# 测试
select * from t_article where match(content) against('yo'); -- 没有结果 单词数需要大于等于3
select * from t_article where match(content) against('you'); -- 有结果

空间索引(了解)

  • MySQL在5.7之后的版本支持了空间索引,而且支持OpenGIS几何数据模型
  • 空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。
  • MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。
  • 创建空间索引的列,必须将其声明为NOT NULL。

image-20230112065619370

类型 含义 说明
Geometry 空间数据 任何一种空间类型
Point 坐标值
LineString 线 有一系列点连接而成
Polygon 多边形 由多条线组成
1
2
3
4
5
6
create table shop_info (
id int primary key auto_increment comment 'id',
shop_name varchar(64) not null comment '门店名称',
geom_point geometry not null comment '经纬度', -- 必须为not null
spatial key geom_index(geom_point) -- 创建空间索引
);

索引的查询和删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#查看表索引
show indexes from `表名`;

show keys from `表名`;

-- select * from mysql.`innodb_index_stats` a where a.`database_name` = '数据库名' and a.table_name like '%表名%’;
select * from mysql.`innodb_index_stats` a where a.`database_name` = 'mydb5' and a.table_name like '%student%';

#查看数据库所有索引
-- select * from mysql.`innodb_index_stats` a where a.`database_name` = '数据库名’;
select * from mysql.`innodb_index_stats` a where a.`database_name` = 'mydb5';

#删除表索引
alter table `表名` drop index 索引名;
drop index 索引名 on 表名

索引数据结构

MySQL索引使用的数据结构主要有BTree索引hash索引

对于hash索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景建议选择BTree索引。

Hash表

Hash表,在Java中的HashMap,TreeMap就是Hash表结构,以键值对的形式存储数据。我们使用hash表存储表数据结构,Key可以存储索引列,Value可以存储行记录或者行磁盘地址。Hash表在等值查询时效率很高,时间复杂度为O(1);

但是不支持范围快速查找(散列表中的数据是无序的),范围查找时只能通过扫描全表的方式,筛选出符合条件的数据。

显然这种方式,不适合我们经常需要查找和范围查找的数据库索引使用。

image-20230112055826944

相当于原来扫描全表数据,现在编程扫描一张索引字段值的表【缩减查找范围】。找到那行。然后根据公式找到这个记录对应的实际数据

ps:hash可能存在hash冲突,冲突了往下找比对id就行。即使是这样也比全表扫描快。

二叉树

image-20230927201324496

上面这个图就是我们常说的二叉树:每个节点最多有两个分叉节点,左子树和右子树数据按顺序左小右大。

二叉树的特点:就是为了保证每次查找都可以进行折半查找,从而减少IO次数。 但是二叉树不是一直保持二叉平衡,因为二叉树很考验根节点的取值,因为很容易在某个节点下不分叉了,这样的话二叉树就不平衡了,也就没有了所谓的能进行折半查找了,如下图:

image-20230927201319728

显然这种不稳定的情况,我们在选择存储数据结构的时候就会尽量避免这种的情况发生。

平衡二叉树

平衡二叉树采用的是二分法思维,平衡二叉查找树除了具备二叉树的特点,最主要的特征是树的左右两个子树的层级最多差1。在插入删除数据时通过左旋/右旋操作保持二叉树的平衡,不会出现左子树很高、右子树很矮的情况。

使用平衡二叉查找树查询的性能接近与二分查找,时间复杂度为O(log2n),查询id=6,只需要两次IO。

image-20230927201312731

就上述平衡二叉树的特点来看,其实是我们理想的状态下,然而其实内部还是存在一些问题:

  • 时间复杂度和树的高度有关。树有多高就需要检索多少次,每个节点的读取,都对应一次磁盘的IO操作。树的高度就等于每次查询数据时磁盘IO操作的次数。磁盘每次寻道的时间为10ms,在数据量大时,查询性能会很差。(1百万的数据量,log2n约等于20次磁盘IO读写,时间消耗约等于:20*10=0.2S)。
  • 平衡二叉树不支持范围查询快速查找,范围查询需要从根节点多次遍历,查询效率不高。很容易出现回旋的情况。

image-20230112071836956

B树:改造二叉树

加快了等值查找,但是范围查找速度不能加快

演示B树网站

MySQL的数据是存储在磁盘文件中的,查询处理数据时,需要先把磁盘中的数据加载到内存中,磁盘IO操作非常耗时,所以我们优化的重点就是尽量减少磁盘的IO操作。访问二叉树的每个节点都会发生一次IO,如果想要减少磁盘IO操作,就需要尽量降低树的高度。

那如何降低树的高度呢?

假如key为bigint=8字节,每个节点有两个指针,每个指针为4个字节,一个节点占用的空间为(8+4*2=16)。

因为在MySQL的InnoDB引擎的一次IO操作会读取一页的数据量(默认一页大小为16K),而二叉树一次IO操作的有效数据量只有16字节,空间利用率极低。为了最大化的利用一次IO操作空间,一个解决方法就是在一个节点处存储多个元素,在每个节点尽可能多的存储数据。每个节点可以存储1000个索引16k/16=1000),这样就将二叉树改造成了多叉树,通过增加树的分叉树,将树的体型从高瘦变成了矮胖。构建1百万条数据,树的高度需要2层就可以1000*1000=1百万),也就是说只需要两次磁盘IO操作就可以查询到数据,磁盘IO操作次数变少了,查询数据的效率整体也就提高了。

这种数据结构我们称之为B树,B树是一种多叉平衡查找树,如下图主要特点:

  • B树的节点中存储这多个元素,每个内节点有多个分叉。
  • 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点中都存储数据。
  • 父节点当中的元素不会出现在子节点中。
  • 所有的叶子节点都位于同一层,叶子节点具有相同的深度,叶子节点之间没有指针连接。

image-20230927201303467

举个简单的例子,在B树中查询数据的情况:

假如我们要查询key等于10对应的数据data,根据上图我们可知在磁盘中的查询路径是:磁盘块1->磁盘块2->磁盘块6

  • 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,10<15,走左子树,到磁盘中寻址到磁盘块2。
  • 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,10>7,走右子树,到磁盘中寻址到磁盘块6。
  • 第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,10=10,找到key=10的位置,取出对应的数据data,如果data存储的是行记录,直接取出数据,查询结束;如果data存储的是行磁盘地址,还需要根据磁盘地址到对应的磁盘中取出数据,查询结束。

相比较二叉平衡查找树,在整个查找过程中,虽然数据的比较次数并没有明显减少,但是对于磁盘IO的次数会大大减少,同时,由于我们是在内存中进行的数据比较,所以比较数据所消耗的时间可以忽略不计。B树的高度一般2至3层就能满足大部分的应用场景,所以使用B树构建索引可以很好的提升查询的效率。

过程如图:

image-20230927201258730

看到上面的情况,觉得B树已经很理想了,但是其中还是存在可以优化的地方:

  • B树不支持范围查询的快速查找,例如:仍然根据上图,我们想要查询10到35之间的数据,查找到10之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
  • 如果data存储的是行记录,行的大小随着列数的增加,所占空间会变大,这时一页中可存储的数据量就会减少,树相应就会变高,磁盘IO次数就会随之增加,有待优化。

B+树:改造B树

在B树基础上,等值查找速度不变的情况下,还加快了范围查找。而且data都放在叶子节点。

B+树,作为B树的升级版,MySQL在B树的基础上继续进行改造,使用B+树构建索引。B+树和B树最主要的区别在于非叶子节点是否存储数据的问题。

  • B树:叶子节点和非叶子节点都会存储数据。
  • B+树:只有叶子节点才会存储数据,非叶子节点只存储键值key;叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表

B+树的大致数据结构:

image-20230927201254473

B+树的最底层叶子节点包含了所有的索引项。从图上可以看到,B+树在查找数据的时候,由于数据都存放在最底层的叶子节点上,所以每次查找都需要检索到叶子节点才能查询到数据。所以在需要查询数据的情况下每次的磁盘的IO跟树高有直接的关系,但是从另一方面来说,由于数据都被放到了叶子节点,所以放索引的磁盘块锁存放的索引数量是会跟这增加的,所以相对于B树来说,B+树的树高理论上情况下是比B树要矮的。也存在索引覆盖查询的情况,在索引中数据满足了当前查询语句所需要的全部数据,此时只需要找到索引即可立刻返回,不需要检索到最底层的叶子节点。

举例:等值查询

假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6。

  • 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,9<15,走左路,到磁盘寻址磁盘块2。
  • 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<9<12,到磁盘中寻址定位到磁盘块6。
  • 第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,在第三个索引中找到9,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。(这里需要区分的是在InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址。

过程如图:

image-20230927201249416

举例:范围查询

假如我们想要查找9和26之间的数据,查找路径为:磁盘块1->磁盘块2->磁盘块6->磁盘块7

  • 前三次磁盘IO:首先查找到键值为9对应的数据(定位到磁盘块6),然后缓存大结果集中。这一步和前面等值查询流程一样,发生了三次磁盘IO。
  • 继续查询,查找到节点15之后,底层的所有叶子节点是一个有序列表,我们从磁盘块6中的键值9开始向后遍历筛选出所有符合条件的数据。
  • 第四次磁盘IO:根据磁盘块6的后继指针到磁盘中寻址定位到磁盘块7,将磁盘块7加载到内存中,在内存中从头遍历比较,9<25<26,9<26<=26,将数据data缓存到结果集中。
  • 逐渐具备唯一性(后面不会再有<=26的数据),不需要再向后查找,查询结束,将结果集返回给用户。

image-20230927201245168

可以看到B+树可以保证等值和范围查询的快速查找,MySQL的索引就采用了B+树的数据结构。

MySQL的索引实现

介绍完了索引数据结构,那肯定是要带入到Mysql里面看看真实的使用场景的,所以这里分析Mysql的两种存储引擎的索引实现:MyISAM索引InnoDB索引

InnoDB索引

主键索引(聚簇索引)

每个InnoDB表都有一个聚簇索引 ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。

InnoDB创建索引的具体规则如下:

  • 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引。
  • 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引。
  • 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增。

除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB,辅助索引中的叶子节点存储的数据是该行的主键值。 在检索时,InnoDB使用此主键值在聚簇索引中搜索行记录。

这里以user_innodb为例,user_innodb的id列为主键,age列为普通索引。

1
2
3
4
5
6
7
8
CREATE TABLE `user_innodb`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_age` (`age`) USING BTREE
) ENGINE = InnoDB;

image-20230927201123377

  • InnoDB的数据和索引存储在t_user_innodb.ibd文件中,InnoDB的数据组织方式,是聚簇索引。
  • 主键索引的叶子节点会存储数据行,辅助索引的叶子节点只会存储主键值。

image-20230927201227500

等值查询数据:

1
select * from user_innodb where id = 28;
  1. 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
  2. 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
  3. 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于28的索引项,直接可以获取整行数据。将改记录返回给客户端。(1次磁盘IO)

磁盘IO数量:3次。

image-20230927201221427

辅助索引

除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。

以表user_innodb的age列为例,age索引的索引结果如下图。

image-20230927201216623

  • 辅助索引的底层叶子节点是按照(age,id)的顺序排序,先按照age列从小到大排序,age相同时按照id列从小到大排序。
  • 使用辅助索引需要检索两遍索引:首先检索辅助索引获得主键,然后根据主键到主键索引中检索获得数据记录。

辅助索引等值查询的情况:

1
select * from t_user_innodb where age=19;

image-20230927201210241

根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为回表查询。

磁盘IO数:辅助索引3次+获取记录回表3次

组合索引

  • 以表abc_innodb为例,id列为主键索引,创建一个联合索引idx_abc(a,b,c)
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `abc_innodb`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` varchar(10) DEFAULT NULL,
`d` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_abc` (`a`, `b`, `c`)
) ENGINE = InnoDB;

组合索引的数据结构:

image-20230927201203710

组合索引的查询过程:

1
select * from abc_innodb where a = 13 and b = 16 and c = 4;

image-20230927201158809

最左匹配原则

最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。

在组合索引树中,最底层的叶子节点按照第一列a列从左到右递增排序,但是b列和c列是无序的,b列只有在a列值相等的情况下小范围内有序递增;而c列只能在a和b两列值相等的情况下小范围内有序递增。

就像上面的查询,B+ 树会先比较a列来确定下一步应该检索的方向,往左还是往右。如果a列相同再比较b列,但是如果查询条件中没有a列,B+树就不知道第一步应该从那个节点开始查起。

可以说创建的idx_(a,b,c)索引,相当于创建了(a)、(a,b)、(a,b,c)三个索引。

组合索引的最左前缀匹配原则:

1
使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)等就会停止匹配。

覆盖索引

覆盖索引并不是一种索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到相应的主键值,想要获取最终的数据记录,还需要根据主键通过主键索引再去检索,最终获取到符合条件的数据记录。

在上面的abc_innodb表中的组合索引查询时,如果我们查询的结果只需要a、b、c这三个字段,那我们使用这个idx_index(a,b,c)组合索引查询到叶子节点时就可以直接返回了,而不需要再次回表查询,这种情况就是覆盖索引。

未使用索引覆盖的情况:

image-20230927201139185

索引覆盖的情况

image-20230927201145571

MyIsam索引

以一个简单的user表为例。user表存在两个索引,id列为主键索引,age列为普通索引

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_age` (`age`) USING BTREE
) ENGINE = MyISAM
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;

image-20230927201051981

MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。

主键ID列索引:

image-20230927201057032

表user的索引存储在索引文件user.MYI中,数据文件存储在数据文件 user.MYD中。

简单分析下查询时的磁盘IO情况:

根据主键等值查询数据

1
select * from user where id = 28
  • 第一次磁盘IO:先在主键索引树中从根节点开始检索,将根节点加载到内存中,比较28<75,所以走左子树。
  • 第二次磁盘IO:将左子树节点加载到内存中,比较16<28<47,向下检索。
  • 第三次磁盘IO:检索到叶子节点,将节点加载到内存中遍历,从16<28,18<28,28=28,查找到键值等于28的索引项。
  • 第四次磁盘IO:从索引项中获取磁盘地址,然后到数据文件user.MYD中获取对应整行记录。
  • 将记录返回给客户端。

磁盘IO次数:3次索引检索+记录数据检索。

image-20230927201102429

根据主键范围查询数据:

1
select * from user where id between 28 and 47;
    1. 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
    1. 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
    1. 检索到叶节点,将节点加载到内存中遍历比较16<28,18<28,28=28<47。查找到值等于28的索引项。
    1. 根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO)
    1. 我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。
    1. 向后遍历底层叶子链表,将下一个节点加载到内存中,遍历比较,28<47=47,根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO)
    1. 最后得到两条符合筛选条件,将查询结果集返给客户端。

磁盘IO次数:4次索引检索+记录数据检索。

image-20230927201107322

辅助索引

在MyISAM存储引擎中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点中data阈存储的都是行记录的磁盘地址。 主键列索引的键值是唯一的,而辅助索引的键值是可以重复的。

查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树种检索数据。

回表(回旋)和联合索引的应用

回表查询

在InnoDB的存储引擎中,使用辅助索引查询的时候,因为辅助索引叶子节点保存的数据不是当前数据记录,而是当前数据记录的主键索引。如果需要获取当前记录完整的数据,就必须要再次根据主键从主键索引中继续检索查询,这个过程我们称之为回表查询。

由此可见,在数据量比较大的时候,回表必然会消耗很多的时间影响性能,所以我们要尽量避免回表的发生。

如何避免回表

  • 使用索引覆盖

举例:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` int(11) DEFAULT NULL,
`sex` char(3) DEFAULT NULL,
`address` varchar(10) DEFAULT NULL,
`hobby` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `i_name` (`name`)
) ENGINE = InnoDB;

如果有一个场景:

1
select id,name,sex from user where name = 'zhangsan';

这个语句在业务上频繁使用到,而user表中的其他字段使用频率远低于这几个字段,在这个情况下,如果我们在建立name字段的索引时,不是使用单一索引,而是使用联合索引(name,sex),这样的话再执行这个查询语句,根据这个辅助索引(name,sex)查询到的结果就包括了我们所需要的查询结果的所有字段的完整数据,这样就不需要再次回表查询去检索sex字段的数据了。

  • 以上就是一个典型的使用覆盖索引的优化策略减少了回表查询的情况。

联合索引的使用

联合索引:

1
在建立索引的时候,尽量在多个单列索引上判断下是否可以使用联合索引。联合索引的使用不仅可以节省空间,还可以更容易的使用到索引覆盖。

节省空间:

1
试想一下,索引的字段越多,是不是更容易满足查询需要返回的数据呢。比如联合索引(a_b_c),是不是等于有了索引:a,a_b,a_b_c三个索引,这样是不是节省了空间,当然节省的空间并不是三倍于(a,a_b,a_b_c)三个索引,因为索引树的数据没变,但是索引data字段的数据确实真实的节省了。

联合索引的创建原则:

1
2
在创建联合索引的时候因该把频繁使用的列、区分度高的列放在前面,频繁使用代表索引利用率高,区分度高代表筛选粒度大,这些都是在索引创建的需要考虑到的优化场景,也可以在常需要作为查询返回的字段上增加到联合索引中。
如果在联合索引上增加一个字段而使用到了覆盖索引,那建议这种情况下使用联合索引。

联合索引的使用:

  • 考虑当前是否已经存在多个可以合并的单列索引,如果有,那么将当前多个单列索引创建为一个联合索引。
  • 当前索引存在频繁使用作为返回字段的列,这个时候就可以考虑当前列是否可以加入到当前已经存在索引上,使其查询语句可以使用到覆盖索引。

索引速度验证

索引的最大特点是提高查询速度,接下来我们来验证一下。

创建数据库test_shop,导入测试数据test_shop.sql

image-20230112074438973

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use test_shop;

-- 创建临时表(客户端关闭,临时表就会删除掉)
create temporary table tmp_goods_cat
as
select t3.catid as cat_id_l3, -- 3级分类id
t3.catname as cat_name_l3, -- 3级分类名称
t2.catid as cat_id_l2, -- 2级分类id
t2.catname as cat_name_l2, -- 2级分类名称
t1.catid as cat_id_l1, -- 1级分类id
t1.catname as cat_name_l1 -- 1级分类名称
from test_shop.test_goods_cats t3,
test_shop.test_goods_cats t2,
test_shop.test_goods_cats t1
where t3.parentid = t2.catid
and t2.parentid = t1.catid
and t3.cat_level = 3;

-- 统计分析不同一级商品分类对应的总金额、总笔数
select
'2019-09-05',
t1.cat_name_l1 as goods_cat_l1,
sum(t3.payprice * t3.goodsnum) as total_money,
count(distinct t3.orderid) as total_cnt
from
tmp_goods_cat t1
left join test_goods t2
on t1.cat_id_l3 = t2.goodscatid
left join test_order_goods t3
on t2.goodsid = t3.goodsid
where
substring(t3.createtime, 1, 10) = '2019-09-05'
group by
t1.cat_name_l1;

image-20230112080054388

1
2
3
4
5
6
7
8
9
10
-- 创建索引
考虑把关联条件创建索引,
cat_id_l3 是唯一的,创建唯一索引
goodscatid 存在重复情况,所以创建普通索引
test_goods表的goodsid 是唯一的,所以创建唯一索引
test_order_goods表的goodsid 存在重复情况,所以创建普通索引
create unique index idx_goods_cat3 on tmp_goods_cat(cat_id_l3);
create index idx_test_goodscatid on test_goods(goodscatid);-- 这个要建吗?考虑重复占比问题!
create unique index idx_test_goods on test_goods(goodsid);
create index idx_test__order_goods on test_order_goods(goodsid);

image-20230112080038462

可以看到添加索引之后,查询速度明显提高了很多。

索引优化原则&失效情况

索引很棒,我们不能创建了,因为使用不当让索引失效了

简单来说联合索引A_B_C,生效的只有

  • A -> 生效
  • AB -> 生效
  • ABC -> 生效
  • BCA,BAC,CBA,CAB -> 生效(优化器优化顺序)
  • AC -> 只有A生效(不能跳跃索引)
  • B -> 失效
  • BC -> 失效
  • C -> 失效
  • CB -> 失效

准备数据

  • 创建表 插入数据
1
2
3
4
5
6
7
8
9
CREATE TABLE users(
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(20) NOT NULL COMMENT '姓名',
user_age INT NOT NULL DEFAULT 0 COMMENT '年龄',
user_level VARCHAR(20) NOT NULL COMMENT '用户等级',
reg_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间'
);
INSERT INTO users(user_name,user_age,user_level,reg_time)
VALUES('tom',17,'A',NOW()),('jack',18,'B',NOW()),('lucy',18,'C',NOW());
  • 创建联合索引
1
ALTER TABLE users ADD INDEX idx_nal (user_name,user_age,user_level) USING BTREE;

全值匹配

按索引字段顺序匹配使用。

该情况下,索引生效,执行效率高

1
2
3
4
5
6
7
8
9
10
EXPLAIN SELECT * FROM users WHERE user_name = 'tom';

EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17

EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17
AND user_level = 'A';

-- 索引的顺序可以调整,MySQL优化器会优化,索引还是生效的
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17
AND user_level = 'A';

按顺序使用联合索引时, type类型都是 ref ,使用到了索引 效率比较高

image-20230927202448864

最佳左前缀法则

如果创建的是联合索引,就要遵循 最佳左前缀法则: 使用索引时,where后面的条件需要从索引的最左前列开始并且不跳过索引中的列使用。

  • 场景1: 按照索引字段顺序使用,三个字段都使用了索引,没有问题。
1
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';

image-20230927202443278

  • 场景2: 直接跳过user_name使用索引字段,索引无效,未使用到索引。
1
EXPLAIN SELECT * FROM users WHERE user_age = 17 AND user_level = 'A';

image-20230927202436669

  • 场景3: 不按照创建联合索引的顺序,使用索引
1
EXPLAIN SELECT * FROM users WHERE user_age = 17 AND user_name = 'tom' AND user_level = 'A';

image-20230927202428594

where后面查询条件顺序是 user_age、user_level、user_name与我们建的索引顺序user_name、user_age、user_level不一致,为什么还是使用了索引,这是因为MySql底层优化器给咱们做了优化。

但是,最好还是要按照顺序 使用索引。

最佳左前缀底层原理

​ MySQL创建联合索引的规则是: 首先会对联合索引最左边的字段进行排序 ( 例子中是 user_name ), 在第一个字段的基础之上 再对第二个字段进行排序 ( 例子中是 user_age )

​ 所以: 最佳左前缀原则其实是个B+树的结构有关系, 最左字段肯定是有序的, 第二个字段则是无序的(联合索引的排序方式是: 先按照第一个字段进行排序,如果第一个字段相等再根据第二个字段排序). 所以如果直接使用第二个字段 user_age 通常是使用不到索引的.

image-20230927202420879

不要在索引列上做任何计算

不要在索引列上做任何操作,比如计算、使用函数、自动或手动进行类型转换,会导致索引失效,从而使查询转向全表扫描。

  • 插入数据
1
INSERT INTO users(user_name,user_age,user_level,reg_time) VALUES('11223344',22,'D',NOW());
  • 场景1: 使用系统函数 left()函数
1
EXPLAIN SELECT * FROM users WHERE LEFT(user_name, 6) = '112233';

where条件使用计算后的索引字段 user_name,没有使用索引,索引失效。

image-20230927202413693

  • 场景2: 字符串不加单引号 (隐式类型转换)
1
EXPLAIN SELECT * FROM users WHERE user_name = 11223344;

image-20230927202409016

注: Extra = Using where 表示Mysql将对storage engine提取的结果进行过滤,过滤条件字段无索引;

( 需要回表去查询所需的数据 )

范围之后全失效

存储引擎不能使用索引中范围条件右边的列

  • 场景1: 条件单独使用user_name时, type=ref, key_len=82
1
2
-- 条件只有一个 user_name
EXPLAIN SELECT * FROM users WHERE user_name = 'tom';

image-20230927202401693

  • 场景2: 条件增加一个 user_age ( 使用常量等值) ,type= ref , key_len = 86
1
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17;

image-20230927202356692

  • 场景3: 使用全值匹配, type = ref , key_len = 168 , 索引都利用上了.
1
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';

image-20230927202350140

  • 场景4: 使用范围条件时, avg > 17 , type = range , key_len = 86 , 与场景3 比较,可以发现 user_level 索引没有用上.
1
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age > 17 AND user_level = 'A';

image-20230927202344675

尽量使用覆盖索引

尽量使用覆盖索引(查询列和索引列尽量一致,通俗说就是对A、B列创建了索引,然后查询中也使用A、B列),减少select *的使用。

简单说就是select * 会除了从索引树找,还会回表从磁盘找。如果查询列在索引内,就直接从索引树找,效率就高。

  • 场景1: 全值匹配查询, 使用 select *
1
EXPLAIN SELECT * FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';

image-20230927202339956

  • 场景2: 全值匹配查询, 使用 select 字段名1 ,字段名2
1
EXPLAIN SELECT user_name , user_age , user_level FROM users WHERE user_name = 'tom' AND user_age = 17 AND user_level = 'A';

使用覆盖索引(查询列与条件列对应),可看到Extra从Null变成了Using index,提高检索效率。

image-20230927202335299

注: Using index 表示 使用到了索引 , 并且所取的数据完全在索引中就能拿到,

(使用覆盖索引的时候就会出现)

ps: 如果除了覆盖字段,还可以其他字段,也不会使用Using Index

使用不等于(!=或<>)会使索引失效

使用 != 会使type=ALL,key=Null,导致全表扫描,并且索引失效。

  • 使用 !=
1
EXPLAIN SELECT * FROM users WHERE user_name != 'tom';

image-20230927202329064

is null 或 is not null也无法使用索引

在使用is null的时候,索引完全失效,使用is not null的时候,type=ALL全表扫描,key=Null索引失效。

  • 场景1: 使用 is null
1
EXPLAIN SELECT * FROM users WHERE user_name IS NULL;

image-20230927202323091

  • 场景2: 使用 not null
1
EXPLAIN SELECT * FROM users WHERE user_name IS NOT NULL;

image-20230927202316755

like通配符以%开头会使索引失效

like查询为范围查询,%出现在左边,则索引失效。%出现在右边索引未失效。口诀:like百分加右边。

  • 场景1
1
EXPLAIN SELECT * FROM users WHERE user_name LIKE '%tom%';

image-20230927202307952

  • 场景2
1
EXPLAIN SELECT * FROM users WHERE user_name LIKE '%tom';

image-20230927202302792

  • 场景3
1
EXPLAIN SELECT * FROM users WHERE user_name LIKE 'tom%';

image-20230927202256445

注: Using index condition 表示 查找使用了索引,但是需要;’;查询数据

解决%出现在左边索引失效的方法:使用覆盖索引。

Case1:

1
EXPLAIN SELECT user_name FROM users WHERE user_name LIKE '%jack%';

image-20230927202249950

  • 对比场景1可以知道, 通过使用覆盖索引 type = index,并且使用了 Using index,从全表扫描变成了全索引扫描.

注: Useing where; Using index; 查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据

Case2:

1
EXPLAIN SELECT id FROM users WHERE user_name LIKE '%jack%';

image-20230927202244653

  • 这里出现 type=index因为主键自动创建唯一索引。

Case3:

1
2
3
EXPLAIN SELECT user_name,user_age FROM users WHERE user_name LIKE '%jack%';
EXPLAIN SELECT user_name,user_age,user_level FROM users WHERE user_name LIKE '%jack%';
EXPLAIN SELECT id,user_name,user_age,user_level FROM users WHERE user_name LIKE '%jack%';

image-20230927202239480

  • 上面三组, explain执行的结果都相同,表明都使用了索引.

Case4:

1
2
EXPLAIN SELECT id,user_name,user_age,user_level,reg_time FROM users WHERE user_name 
LIKE '%jack%';

image-20230927202235161

  • 分析:由于只在(user_name,user_age,user_level)上创建索引, 当包含reg_time时,导致结果集偏大(reg_time未建索引)【锅大,锅盖小,不能匹配】,所以type=ALL。

  • like 失效的原理

image-20230927202156720

  1. %号在右: 由于B+树的索引顺序,是按照首字母的大小进行排序,%号在右的匹配又是匹配首字母。所以可以在B+树上进行有序的查找,查找首字母符合要求的数据。所以有些时候可以用到索引.
  2. %号在左: 是匹配字符串尾部的数据,我们上面说了排序规则,尾部的字母是没有顺序的,所以不能按照索引顺序查询,就用不到索引.
  3. 两个%%号: 这个是查询任意位置的字母满足条件即可,只有首字母是进行索引排序的,其他位置的字母都是相对无序的,所以查找任意位置的字母是用不上索引的.

字符串不加单引号导致索引失效

varchar类型的字段,在查询的时候不加单引号导致索引失效,转向全表扫描。

  • 场景1
1
2
SELECT * FROM users WHERE user_name = '123';
SELECT * FROM users WHERE user_name = 123;

上述两条sql语句都能查询出相同的数据。

image-20230927202204214

  • 场景2:

image-20230927202210165

image-20230927202216398

通过explain执行结果可以看出,字符串(name)不加单引号在查询的时候,导致索引失效(type=ref变成了type=ALL,并且key=Null),并全表扫描。

少用or,用or连接会使索引失效

在使用or连接的时候 type=ALL,key=Null,索引失效,并全表扫描。

image-20230927202221808

性能瓶颈定位MySQL慢查询

在应用的的开发过程中,由于初期数据量小,开发人员写 SQL 语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多 SQL 语句开始逐渐显露出性能问题,对生产的影响也越来越大,此时这些有问题的 SQL 语句就成为整个系统性能的瓶颈,因此我们必须要对它们进行优化.

MySQL的优化方式有很多,大致可以分为:

  • 从设计上优化
  • 从查询上优化
  • 从索引上优化
  • 从存储上优化

性能优化的思路

  • 首先需要使用慢查询功能,去获取所有查询时间比较长的SQL语句
  • 其次使用explain命令去查询由问题的SQL的执行计划
  • 最后可以使用show profile[s] 查看由问题的SQL的性能使用情况
  • 优化SQL语句

查看SQL执行频率

MySQL 客户端连接成功后,通过 show [session|global] status 命令可以查看服务器状态信息。通过查看状态信息可以查看对当前数据库的主要操作类型。

1
2
3
4
5
--下面的命令显示了当前 session 中所有统计参数的值
show session status like 'Com_______'; -- 查看当前会话统计结果
show global status like 'Com_______'; -- 查看自数据库上次启动至今统计结果

show status like 'Innodb_rows_%'; -- 查看针对Innodb引擎的统计结果

image-20230112235808325

定位低效率执行SQL

可以通过以下两种方式定位执行效率较低的 SQL 语句。

  • show processlist:该命令查看当前MySQL在进行的线程,包括线程的状态、是否锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化。
  • 慢查询日志 : 通过慢查询日志定位那些执行效率较低的 SQL 语句。

定位:show processlist

1
show processlist; 

image-20230113000149067

1) id列,用户登录mysql时,系统分配的”connection_id”,可以使用函数connection_id()查看

2) user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句

3) host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户

4) db列,显示这个进程目前连接的是哪个数据库

5) command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等

6) time列,显示这个状态持续的时间,单位是秒

7) state列,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成

8) info列,显示这个sql语句,是判断问题语句的一个重要依据

定位:MySQL慢查询日志

定位慢的效率低的SQL

数据库查询快慢是影响项目性能的一大因素,对于数据库,我们除了要优化SQL,更重要的是得先找到需要优化的SQL语句

  MySQL数据库有一个“慢查询日志”功能,用来记录查询时间超过某个设定值的SQL,这将极大程度帮助我们快速定位到问题所在,以便对症下药

慢查询日志用来记录在 MySQL 中执行时间超过指定时间的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率低,以便进行优化。

慢查询参数

  1. 执行下面的语句
1
SHOW VARIABLES LIKE "%slow_query%" ;

image-20230927194316942

  • slow_query_log:是否开启慢查询,on为开启,off为关闭;
  • log-slow-queries:慢查询日志文件路径
1
SHOW VARIABLES LIKE "%long_query_time%" ;

image-20230927194347404

  • long_query_time : 阈值,超过多少秒的查询就写入日志
1
show variables like 'log_queries_not_using_indexes';

image-20230927194252717

  • **系统变量 log-queries-not-using-indexes**:未使用索引的查询也被记录到慢查询日志中(可选项)。如果调优的话,建议开启这个选项。

开启慢查询日志(临时)

在MySQL执行SQL语句设置,但是如果重启MySQL的话会失效。

1
2
set global slow_query_log=on;
set global long_query_time=1;

开启慢查询日志(永久)

修改:/etc/my.cnf,添加以下内容,然后重启MySQL服务

1
2
3
4
5
[mysqld]
lower_case_table_names=1
slow_query_log=ON
slow_query_log_file=D:\dev\mysql-8.0.22-winx64\data\DESKTOP-LEC7QQM-slow.log
long_query_time=1

数据库操作超过100毫秒认为是慢查询,可根据需要进行设定,如果过多,可逐步设定,比如先行设定为2秒,逐渐降低来确认瓶颈所在

慢查询测试

1
select SLEEP(3);

image-20230927194242209

格式说明:

  • 第一行,SQL查询执行的具体时间
  • 第二行,执行SQL查询的连接信息,用户和连接IP
  • 第三行,记录了一些我们比较有用的信息,
    • Query_timme,这条SQL执行的时间,越长则越慢
    • Lock_time,在MySQL服务器阶段(不是在存储引擎阶段)等待表锁时间
    • Rows_sent,查询返回的行数
    • Rows_examined,查询检查的行数,越长就越浪费时间
  • 第四行,设置时间戳,没有实际意义,只是和第一行对应执行时间。
  • 第五行,执行的SQL语句记录信息

MySQL性能分析 EXPLAIN

分析慢的效率低的SQL的性能瓶颈

概述

explain(执行计划),使用explain关键字可以模拟优化器执行sql查询语句,从而知道MySQL是如何处理sql语句。

explain主要用于分析查询语句或表结构的性能瓶颈。

通过explain命令可以得到:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

EXPLAIN字段介绍

explain使用:explain+sql语句,通过执行explain可以获得sql语句执行的相关信息。

1
explain select * from course;

image-20230927194236799

expain出来的信息有10列,分别是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra

数据准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 创建数据库
CREATE DATABASE test_explain CHARACTER SET 'utf8';

-- 创建表
CREATE TABLE L1(id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(100) );
CREATE TABLE L2(id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(100) );
CREATE TABLE L3(id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(100) );
CREATE TABLE L4(id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(100) );

-- 每张表插入3条数据
INSERT INTO L1(title) VALUES('zuoer001'),('zuoer002'),('zuoer003');
INSERT INTO L2(title) VALUES('zuoer004'),('zuoer005'),('zuoer006');
INSERT INTO L3(title) VALUES('zuoer007'),('zuoer008'),('zuoer009');
INSERT INTO L4(title) VALUES('zuoer010'),('zuoer011'),('zuoer012');

id字段

select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序

  • id相同,执行顺序由上至下(从L1到L2到L3)
1
EXPLAIN SELECT * FROM  L1,L2,L3 WHERE L1.id=L2.id AND L2.id = L3.id;

image-20230927194231299

  • id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行(从L2到L1到L3)
1
2
EXPLAIN SELECT * FROM L2 WHERE id = (
SELECT id FROM L1 WHERE id = (SELECT L3.id FROM L3 WHERE L3.title = 'zuoer009'));

image-20230927194226079

嵌套的先执行子查询,然后再执行外面的

  • id 有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行

select_type 与 table字段

select_type 查询类型,主要用于区别普通查询,联合查询,子查询等的复杂查询

table 显示这一步所访问数据库中表名称有时不是真实的表名字,可能是简称,

  • simple : 简单的select查询,查询中不包含子查询或者UNION
1
EXPLAIN SELECT * FROM L1;

image-20230927194218482

  • primary : 查询中若包含任何复杂的子部分,最外层查询被标记。也就是子查询的最外层查询(主查询)。
1
EXPLAIN SELECT * FROM L2 WHERE id = (SELECT id FROM L1 WHERE id = (SELECT L3.id FROM L3 WHERE L3.title = 'zuoer009'));

image-20230927194212058

  • subquery : 在select或where列表中包含了子查询
1
EXPLAIN SELECT * FROM L2 WHERE L2.id = (SELECT id FROM L3 WHERE L3.title = 'zuoer009' )

image-20230927194205265

  • derived : 在from列表中包含的子查询被标记为derived(衍生),MySQL会递归执行这些子查询, 把结果放到临时表中
1
2
EXPLAIN
SELECT * FROM (SELECT * FROM L3 limit 2) t;

image-20230113124228911

  • union : 如果第二个select出现在UNION之后,则被标记为UNION,如果union包含在from子句的子查询中,外层select被标记为derived
  • union result : UNION 的结果
1
2
3
4
EXPLAIN 
SELECT * FROM L2
UNION
SELECT * FROM L3

image-20230927194154923

type字段

type显示的是连接类型,是较为重要的一个指标。下面给出各种连接类型,按照从最佳类型到最坏类型进行排序:

1
2
3
4
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

-- 简化
system > const > eq_ref > ref > range > index > ALL
  • NULL: MySQL不访问任何表,索引,直接返回结果
1
EXPLAIN SELECT now()

image-20230113124629159

  • system : 表仅有一行 (等于系统表)。这是const连接类型的一个特例,很少出现。少量数据,往往不需要进行磁盘IO(如果是5.7及以上版本系统表就不是system了,而是all,即使只有1条记录)
1
EXPLAIN SELECT * from mysql.tables_priv

image-20230113124836289

  • const : 表示通过索引 一次就找到了, const用于比较 primary key 或者 unique 索引. 因为只匹配一行数据,所以如果将主键 放在 where条件中, MySQL就能将该查询转换为一个常量
1
EXPLAIN SELECT * FROM L1 WHERE L1.id = 1

image-20230927194143740

  • eq_ref : 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配. 常见与主键或唯一索引扫描.比如左表有主键,且左表的每一行和右表的每一行刚好匹配。
1
EXPLAIN SELECT * FROM L1 ,L2 WHERE L1.id = L2.id ;

image-20230927194138724

  • ref : 非唯一性索引扫描, 返回匹配某个单独值的所有行, 本质上也是一种索引访问, 它返回所有匹配某个单独值的行, 这是比较常见连接类型.比如左表是普通索引,和右表匹配时可能会匹配到多行。

未加索引之前

1
EXPLAIN SELECT * FROM L1 ,L2 WHERE L1.title = L2.title ;

image-20230927194132406

加索引之后

1
2
CREATE INDEX idx_title ON L2(title); -- 如果加的是unique index,type会是const
EXPLAIN SELECT * FROM L1 ,L2 WHERE L1.title = L2.title ;

image-20230927194126345

  • range : 只检索给定范围的行,使用一个索引来选择行。
1
2
EXPLAIN SELECT * FROM L1 WHERE L1.id > 10;
EXPLAIN SELECT * FROM L1 WHERE L1.id IN (1,2);

image-20230927194119632

key显示使用了哪个索引. where 子句后面 使用 between 、< 、> 、in 等查询, 这种范围查询要比全表扫描好

  • index : 出现index 是 SQL 使用了索引, 但是没有通过索引进行过滤,一般是使用了索引进行排序分组,索引列全部扫描。
1
EXPLAIN SELECT * FROM L1 ORDER BY id;

image-20230927194112049

  • ALL : 对于每个来自于先前的表的行组合,进行完整的表扫描
1
EXPLAIN SELECT * FROM L1;  

image-20230927194106747

一般来说,需要保证查询至少达到 range级别,最好能到ref

possible_keys 与 key字段

  • possible_keys
    • 显示可能应用到这张表上的索引, 一个或者多个. 查询涉及到的字段上若存在索引, 则该索引将被列出, 但不一定被查询实际使用.
  • key
    • 实际使用的索引,若为null,则没有使用到索引。(两种可能,1.没建立索引, 2.建立索引,但索引失效)。查询中若使用了覆盖索引,则该索引仅出现在key列表中。
    • 覆盖索引:一个索引包含(或覆盖)所有需要查询的字段的值,通过查询索引就可以获取到字段值
  1. 理论上没有使用索引,但实际上使用了
1
EXPLAIN SELECT L1.id FROM L1;

image-20230927194045766

  1. 理论和实际上都没有使用索引
1
EXPLAIN SELECT * FROM L1 WHERE title = 'zuoer001';

image-20230927194040725

  1. 理论和实际上都使用了索引
1
EXPLAIN SELECT * FROM L2 WHERE title = 'zuoer002';

image-20230927194017416

key_len字段

表示索引中使用的字节数, 可以通过该列计算查询中使用索引的长度.

key_len 字段能够帮你检查是否充分利用了索引 ken_len 越长, 说明索引使用的越充分

  • 创建表
1
2
3
4
5
6
CREATE TABLE L5(
a INT PRIMARY KEY,
b INT NOT NULL,
c INT DEFAULT NULL,
d CHAR(10) NOT NULL
);
  • 使用explain 进行测试
1
EXPLAIN SELECT * FROM L5 WHERE a > 1 AND b = 1;

索引中只包含了1列,所以,key_len是4。

image-20230927194000562

  • 为b字段添加索引
1
2
3
ALTER TABLE L5 ADD INDEX idx_b(b);
-- 执行SQL,这次将b字段也作为条件
EXPLAIN SELECT * FROM L5 WHERE a > 1 AND b = 1;

再次测试

  • 为c、d字段添加联合索引,然后进行测试
1
2
ALTER TABLE L5 ADD INDEX idx_c_b(c,d);
explain select * from L5 where c = 1 and d = '';

image-20230927193943958

c字段是int类型 4个字节, d字段是 char(10)代表的是10个字符相当30个字节

数据库的字符集是utf8 一个字符3个字节,d字段是 char(10)代表的是10个字符相当30个字节,多出的一个字节用来表示是联合索引

下面这个例子中,虽然使用了联合索引,但是可以根据ken_len的长度推测出该联合索引只使用了一部分,没有充分利用索引,还有优化空间.

1
explain select * from L5 where c = 1 ; 

image-20230927193925704

ref 字段

  • 显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值
    • L1.id=’1’; 1是常量 , ref = const
1
EXPLAIN SELECT * FROM L1 WHERE  L1.id='1';

image-20230927193913210

  • L2表被关联查询的时候,使用了主键索引, 而值使用的是驱动表(执行计划中靠前的表是驱动表)L1表的ID, 所以 ref = test_explain.L1.id
1
EXPLAIN SELECT * FROM L1 LEFT JOIN L2 ON  L1.id = L2.id WHERE L1.title = 'zuoer001';

image-20230927193843801

rows 字段

  • 表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数;越少越好
  1. 使用like 查询,会产生全表扫描, L2中有3条记录,就需要读取3条记录进行查找
1
EXPLAIN SELECT * FROM L1,L2 WHERE L1.id = L2.id AND L2.title LIKE '%zuoer001%'; 

image-20230113130850674

  1. 如果使用等值查询, 则可以直接找到要查询的记录,返回即可,所以只需要读取一条
1
EXPLAIN SELECT * FROM L1,L2 WHERE L1.id = L2.id AND L2.title = 'zuoer003'; 

image-20220606172719911

总结: 当我们需要优化一个SQL语句的时候,我们需要知道该SQL的执行计划,比如是全表扫描,还是索引扫描; 使用explain关键字可以模拟优化器执行sql语句,从而知道mysql是如何处理sql语句的,方便我们开发人员有针对性的对SQL进行优化.

  • 表的读取顺序。(对应id)
  • 数据读取操作的操作类型。(对应select_type)
  • 哪些索引可以使用。(对应possible_keys)
  • 哪些索引被实际使用。(对应key)
  • 每张表有多少行被优化器查询。(对应rows)
  • 评估sql的质量与效率 (对应type)

filtered 字段

  • 它指返回结果的行占需要读到的行(rows列的值)的百分比

extra 字段

Extra 是 EXPLAIN 输出中另外一个很重要的列,该列显示MySQL在查询过程中的一些详细信息

  • 准备数据
1
2
3
4
5
6
7
8
9
10
CREATE TABLE users (
uid INT PRIMARY KEY AUTO_INCREMENT,
uname VARCHAR(20),
age INT(11)
);
INSERT INTO users VALUES(NULL, 'lisa',10);
INSERT INTO users VALUES(NULL, 'lisa',10);
INSERT INTO users VALUES(NULL, 'rose',11);
INSERT INTO users VALUES(NULL, 'jack', 12);
INSERT INTO users VALUES(NULL, 'sam', 13);
  • Using filesort
1
EXPLAIN SELECT * FROM users ORDER BY age;

image-20230927195115121

执行结果Extra为Using filesort,这说明,得到所需结果集,需要对所有记录进行文件排序。这类SQL语句性能极差,需要进行优化。

典型的,在一个没有建立索引的列上进行了order by,就会触发filesort,常见的优化方案是,在order by的列上添加索引,避免每次查询都全量排序。

filtered 它指返回结果的行占需要读到的行(rows列的值)的百分比

  • Using temporary
1
EXPLAIN SELECT COUNT(*),uname FROM users WHERE uid > 2  GROUP BY uname;

image-20230927195106757

执行结果Extra为Using temporary,这说明需要建立临时表 (temporary table) 来暂存中间结果。 常见与 group byorder by,这类SQL语句性能较低,往往也需要进行优化。

  • Using where

    意味着全表扫描或者在查找使用索引的情况下,但是还有查询条件不在索引字段当中.

1
EXPLAIN SELECT * FROM users WHERE age=10;

image-20230927195059457

此语句的执行结果Extra为Using where,表示使用了where条件过滤数据

需要注意的是:

  1. 返回所有记录的SQL,不使用where条件过滤数据,大概率不符合预期,对于这类SQL往往需要进行优化;
  2. 使用了where条件的SQL,并不代表不需要优化,往往需要配合explain结果中的type(连接类型)来综合判断。例如本例查询的 age 未设置索引,所以返回的type为ALL,仍有优化空间,可以建立索引优化查询。
  • Using index

    表示直接访问索引就能够获取到所需要的数据(覆盖索引) , 不需要通过索引回表.

1
2
3
-- 为uname创建索引
alter table users add index idx_uname(uname);
EXPLAIN SELECT uid,uname FROM users WHERE uname='lisa';

image-20230927195048914

此句执行结果为Extra为Using index,说明sql所需要返回的所有列数据均在一棵索引树上,而无需访问实际的行记录。效率不错。

  • Using join buffer

    使用了连接缓存, 会显示join连接查询时,MySQL选择的查询算法

1
EXPLAIN SELECT * FROM users u1 LEFT JOIN (SELECT * FROM users WHERE sex = '0') u2 ON u1.uname = u2.uname;

image-20230927195040749

执行结果Extra为Using join buffer (Block Nested Loop) 说明,需要进行嵌套循环计算, 这里每个表都有五条记录,内外表查询的type都为ALL。

问题在于 两个关联表join 使用 uname,关联字段均未建立索引,就会出现这种情况。

常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算

  • Using index condition

    查找使用了索引 (但是只使用了一部分,一般是指联合索引),但是需要回表查询数

1
explain select * from L5 where c > 10 and d = ''; 

Extra主要指标的含义(有时会同时出现)

  • using index :使用覆盖索引的时候就会出现
  • using where:在查找使用索引的情况下,需要回表去查询所需的数据
  • using index condition:查找使用了索引,但是需要回表查询数据
  • using index & using where:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据

show profile分析SQL

Mysql从5.0.37版本开始增加了对 show profiles 和 show profile 语句的支持。show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。

通过 have_profiling 参数,能够看到当前MySQL是否支持profile:

1
2
select @@have_profiling; 
set profiling=1; -- 开启profiling 开关;

image-20230113132121554

通过profile,我们能够更清楚地了解SQL执行的过程。首先,我们可以执行一系列的操作

1
2
3
4
5
6
show databases;
use mydb13_optimize;
show tables;

select * from user where id < 2;
select count(*) from user;

执行完上述命令之后,再执行show profiles 指令, 来查看SQL语句执行的耗时:

1
show profiles;

image-20230113132246748

通过show profile for query query_id 语句可以查看到该SQL执行过程中每个线程的状态和消耗的时间:

1
show profile for query 8;

image-20230113132432084

在获取到最消耗时间的线程状态后,MySQL支持进一步选择all、cpu、block io 、context switch、page faults等明细类型类查看MySQL在使用什么资源上耗费了过高的时间。例如,选择查看CPU的耗费时间 :

1
show profile cpu for query 133;  

image-20230113132654718

image-20230113132727603

trace分析优化器执行计划

MySQL5.6提供了对SQL的跟踪trace, 通过trace文件能够进一步了解为什么优化器选择A计划, 而不是选择B计划

image-20230113132845147

打开trace , 设置格式为 JSON,并设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。

1
2
SET optimizer_trace="enabled=on",end_markers_in_json=on; 
set optimizer_trace_max_mem_size=1000000;

执行SQL语句 :

1
select * from user where uid < 2;

最后, 检查information_schema.optimizer_trace就可以知道MySQL是如何执行SQL的 :

1
select * from information_schema.optimizer_trace\G;

image-20230113133011664

表中的数据导索引失效

如果MySQL评估使用索引比全表更慢,则不使用索引。

这种情况是由数据本身的特点来决定的

1
2
3
4
5
6
7
8
9
10
11
explain select * from tb_seller where address = '北京市'; -- 没有使用索引  90%都是北京市,不好定位
explain select * from tb_seller where address = '西安市'; -- 没有使用索引 西安市只有一个,好定位

create index index_address on tb_seller(nickname);

explain select * from tb_seller where nickname is NULL; -- 索引有效 NULL比较少, IS NULL 很容易找到,好定位
explain select * from tb_seller where nickname is not NULL; -- 无效 不为UNLL比较多,不好定位

-- 普通索引,in走索引,not in 不走索引。主键索引in 和 not in 都走索引。
explain select * from tb_seller where nickname in('阿里小店','百度小店'); -- 使用索引
explain select * from tb_seller where nickname not in('阿里小店','百度小店'); -- 不使用索引

多个单列索引和复合索引

如果创建多个单列索引,不如使用复合索引。

image-20230114081824270

SQL索引优化

索引是数据库优化最常用也是最重要的手段之一, 通过索引通常可以帮助用户解决大多数的MySQL的性能优化问题。

大批量插入数据

当使用load 命令导入数据的时候,适当的设置可以提高导入的效率。对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率:

主键顺序插入

因为InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。如果InnoDB表没有主键,那么系统会自动默认创建一个内部列作为主键,所以如果可以给表创建一个主键,将可以利用这点,来提高导入数据的效率。

image-20230114082259613

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 1、首先,检查一个全局系统变量 'local_infile' 的状态, 如果得到如下显示 Value=OFF,则说明从本地加载文件是不可用的
show global variables like 'local_infile';

-- 2、修改local_infile值为on,开启local_infile
set global local_infile=1;

-- 3、加载数据
/*
脚本文件介绍 :
sql1.log ----> 主键有序 100W 22s
sql2.log ----> 主键无序 100W 88s
*/
load data local infile 'D:\\sql_data\\sql1.log' into table tb_user fields terminated by ',' lines terminated by '\n';

加载表的时候(shell同步表),尽量保证表的数据是有序的。这样可以提高执行效率。(如果本身数据有序,构建索引时间大大降低)

关闭唯一性校验

在导入数据前执行 SET UNIQUE_CHECKS=0,关闭唯一性校验,在导入结束后执行SET UNIQUE_CHECKS=1,恢复唯一性校验,可以提高导入的效率。

1
2
3
4
5
6
7
-- 关闭唯一性校验
SET UNIQUE_CHECKS=0;

truncate table tb_user;
load data local infile 'D:\\sql_data\\sql1.log' into table tb_user fields terminated by ',' lines terminated by '\n';

SET UNIQUE_CHECKS=1;

优化insert语句

当进行数据的insert操作的时候,可以考虑采用以下几种优化方案:

如果需要同时对一张表插入很多行数据时,应该尽量使用多个值表的insert语句,这种方式将大大的缩减客户端与数据库之间的连接、关闭等消耗。使得效率比分开执行的单个insert语句快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 优化1
-- 原始方式为:
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Cat');
insert into tb_test values(3,'Jerry');

-- 优化后的方案为 : (优)(减少了连接)
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');


# 优化2
-- 在事务中进行数据插入。(优)(减少了事务频繁开启提交)
begin;
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Cat');
insert into tb_test values(3,'Jerry');
commit;


# 优化3
-- 数据有序插入
insert into tb_test values(4,'Tim');
insert into tb_test values(1,'Tom');
insert into tb_test values(3,'Jerry');
insert into tb_test values(5,'Rose');
insert into tb_test values(2,'Cat');

-- 优化后:(优)(减少了构建索引的时间)
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Cat');
insert into tb_test values(3,'Jerry');
insert into tb_test values(4,'Tim');
insert into tb_test values(5,'Rose');

优化group by

于GROUP BY 实际上也同样会进行排序操作,而且与ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。

如果查询包含 group by 但是用户想要避免排序结果的消耗, 则可以执行order by null 禁止排序。如下 :

1
2
3
4
5
6
7
drop index idx_emp_age_salary on emp; 

explain select age,count(*) from emp group by age;

explain select age,count(*) from emp group by age order by null;

create index idx_emp_age_salary on emp(age,salary);

优化子查询

使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询是可以被更高效的连接(JOIN)替代。

1
explain select * from user where uid in (select uid from user_role ); 

image-20230114085542825

修改为join查询

1
explain select * from user u , user_role ur where u.uid = ur.uid;

image-20230114085623991

1
system>const>eq_ref>ref>range>index>ALL

连接(Join)查询之所以更有效率一些 ,是因为MySQL不需要在内存中创建临时表(子查询会创建临时表)来完成这个逻辑上需要两个步骤的查询工作。

优化limit查询

一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是 limit 900000,10 ,此时需要MySQL排序前900010 记录,仅仅返回900000 - 900010 的记录,其他记录丢弃,查询排序的代价非常大 。

优化方式1:在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。

image-20230114090300683

优化方式2:该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。

image-20230114090427926

优化JOIN

JOIN算法原理

1) JOIN回顾

JOIN 是 MySQL 用来进行联表操作的,用来匹配两个表的数据,筛选并合并出符合我们要求的结果集。

JOIN 操作有多种方式,取决于最终数据的合并效果。常用连接方式的有以下几种:

image-20230927200053981

2) 驱动表的定义

什么是驱动表 ?

  • 多表关联查询时,第一个被处理的表就是驱动表,使用驱动表去关联其他表.
  • 驱动表的确定非常的关键,会直接影响多表关联的顺序,也决定后续关联查询的性能

驱动表的选择要遵循一个规则:

  • 在对最终的结果集没有影响的前提下,优先选择结果集最小的那张表作为驱动表(可以从双层for循环的角度理解,小的在外边)

3) 三种JOIN算法

1.Simple Nested-Loop Join( 简单的嵌套循环连接,SNL)

  • 简单来说嵌套循环连接算法就是一个双层for 循环 ,通过循环外层表的行数据,逐个与内层表的所有行数据进行比较来获取结果.
  • 这种算法是最简单的方案,性能也一般。对内循环没优化。
  • 例如有这样一条SQL:
1
2
3
-- 连接用户表与订单表 连接条件是 u.id = o.user_id
select * from user t1 left join order t2 on t1.id = t2.user_id;
-- user表为驱动表,order表为被驱动表
  • 转换成代码执行时的思路是这样的:
1
2
3
4
5
6
7
for(user表行 uRow : user表){
for(Order表的行 oRow : order表){
if(uRow.id = oRow.user_id){
return uRow;
}
}
}
  • 匹配过程如下图

    image-20230927200048932

  • SNL 的特点

    • 简单粗暴容易理解,就是通过双层循环比较数据来获得结果
    • 查询效率会非常慢,假设 A 表有 N 行,B 表有 M 行。SNL 的开销如下:
      • A 表扫描 1 次。
      • B 表扫描 M 次。
      • 一共有 N 个内循环,每个内循环要 M 次,一共有内循环 N * M

2) Index Nested-Loop Join( 索引嵌套循环连接,INL )

  • Index Nested-Loop Join 其优化的思路: 主要是为了减少内层表数据的匹配次数 , 最大的区别在于,用来进行 join 的字段已经在被驱动表中建立了索引。

  • 从原来的 匹配次数 = 外层表行数 * 内层表行数 , 变成了 匹配次数 = 外层表的行数 * 内层表索引的高度 ,极大的提升了 join的性能。

  • order 表的 user_id 为索引的时候执行过程会如下图:

    image-20230927200044848

    注意:使用Index Nested-Loop Join 算法的前提是匹配的字段必须建立了索引。

3) Block Nested-Loop Join( 块嵌套循环连接,BNL)

  • 如果 join 的字段有索引,MySQL 会使用 INL 算法。如果没有的话,MySQL 会如何处理?

  • 因为不存在索引了,所以被驱动表需要进行扫描。这里 MySQL 并不会简单粗暴的应用 SNL 算法,而是加入了 buffer 缓冲区,降低了内循环的个数,也就是被驱动表的扫描次数。

    image-20230927200040806

    • 在外层循环扫描 user表中的所有记录。扫描的时候,会把需要进行 join 用到的列都缓存到 buffer 中。buffer 中的数据有一个特点,里面的记录不需要一条一条地取出来和 order 表进行比较,而是整个 buffer 和 order表进行批量比较。
    • 如果我们把 buffer 的空间开得很大,可以容纳下 user 表的所有记录,那么 order 表也只需要访问一次。
    • MySQL 默认 buffer 大小 256K,如果有 n 个 join 操作,会生成 n-1 个 join buffer。
1
2
3
4
5
6
7
8
mysql> show variables like '%join_buffer%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| join_buffer_size | 262144 |
+------------------+--------+
mysql> set session join_buffer_size=262144;
Query OK, 0 rows affected (0.00 sec)

4) 总结

  1. 永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量)

  2. 为匹配的条件增加索引(减少内层表的循环匹配次数)

  3. 增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)

  4. 减少不必要的字段查询(字段越少,join buffer 所缓存的数据就越多)

in和exists函数

上面我们说了 小表驱动大表,就是小的数据集驱动大的数据集, 主要是为了减少数据库的连接次数,根据具体情况的不同,又出现了两个函数 existsin 函数

创建部门表与员工表,并插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- 部门表
CREATE TABLE department (
id INT(11) PRIMARY KEY,
deptName VARCHAR(30) ,
address VARCHAR(40)
) ;

-- 部门表测试数据
INSERT INTO `department` VALUES (1, '研发部', '1层');
INSERT INTO `department` VALUES (2, '人事部', '3层');
INSERT INTO `department` VALUES (3, '市场部', '4层');
INSERT INTO `department` VALUES (5, '财务部', '2层');

-- 员工表
CREATE TABLE employee (
id INT(11) PRIMARY KEY,
NAME VARCHAR(20) ,
dep_id INT(11) ,
age INT(11) ,
salary DECIMAL(10, 2)
);

-- 员工表测试数据
INSERT INTO `employee` VALUES (1, '鲁班', 1, 15, 1000.00);
INSERT INTO `employee` VALUES (2, '后裔', 1, 22, 2000.00)
INSERT INTO `employee` VALUES (4, '阿凯', 2, 20, 3000.00);
INSERT INTO `employee` VALUES (5, '露娜', 2, 30, 3500.00);
INSERT INTO `employee` VALUES (6, '李白', 3, 25, 5000.00);
INSERT INTO `employee` VALUES (7, '韩信', 3, 50, 5000.00);
INSERT INTO `employee` VALUES (8, '蔡文姬', 3, 35, 4000.00);
INSERT INTO `employee` VALUES (3, '孙尚香', 4, 20, 2500.00);

1) in 函数

  • 假设: department表的数据小于 employee表数据, 将所有部门下的员工都查出来,应该使用 in 函数
1
2
-- 编写SQL,使in 函数
SELECT * FROM employee e WHERE e.dep_id IN (SELECT id FROM department);
  • in函数的执行原理
    1. in 语句, 只执行一次, 将 department 表中的所有id字段查询出来并且缓存.
    2. 检查 department 表中的id与 employee 表中的 dep_id 是否相等, 如果相等 添加到结果集, 直到遍历完department 所有的记录.

image-20230927200035373

1
2
3
4
5
6
7
8
9
10
-- 先循环: select id from department; 相当于得到了小表的数据
for(i = 0; i < $dept.length; i++){ -- 小表
-- 后循环: select * from employee where e.dep_id = d.id;
for(j = 0 ; j < $emp.legth; j++){ -- 大表
if($dept[i].id == $emp[j].dep_id){
$result[i] = $emp[j]
break;
}
}
}
  • 结论: 如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用 in

2) exists 函数

  • 假设: department表的数据大于 employee表数据, 将所有部门下的的员工都查出来,应该使用 exists 函数.
1
2
explain SELECT * FROM employee e WHERE EXISTS 
(SELECT id FROM department d WHERE d.id = e.dep_id);
  • exists 特点

    exists 子句返回的是一个 布尔值,如果有返回数据,则返回值是true,反之是false

    如果结果为 true , 外层的查询语句会进行匹配,否则 外层查询语句将不进行查询或者查不出任何记录。

    image-20230927200030845

  • exists 函数的执行原理

1
2
3
4
5
6
7
8
-- 先循环: SELECT * FROM employee e;
-- 再判断: SELECT id FROM department d WHERE d.id = e.dep_id
for(j = 0; j < $emp.length; j++){ -- 小表
-- 遍历循环外表,检查外表中的记录有没有和内表的的数据一致的, 匹配得上就放入结果集。
if(exists(emp[i].dep_id)){ -- 大表
$result[i] = $emp[i];
}
}

3) in 和 exists 的区别

  • 如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用 in

  • 如果主查询得出的结果集记录较少,子查询中的表较大且又有索引时应该用 exists

  • 一句话: in后面跟的是小表,exists后面跟的是大表。

优化order by

MySQL中的两种排序方式

  • 索引排序: 通过有序索引顺序扫描直接返回有序数据
  • 额外排序: 对返回的数据进行文件排序
  • ORDER BY优化的核心原则: 尽量减少额外的排序,通过索引直接返回有序数据。

索引排序

因为索引的结构是B+树,索引中的数据是按照一定顺序进行排列的,所以在排序查询中如果能利用索引,就能避免额外的排序操作。EXPLAIN分析查询时,Extra显示为Using index。

image-20230927200025875

比如查询条件是 where age = 21 order by name,那么查询过程就是会找到满足 age = 21 的记录,而符合这条的所有记录一定是按照 name 排序的,所以也不需要额外进行排序.

额外排序

所有不是通过索引直接返回排序结果的操作都是Filesort排序,也就是说进行了额外的排序操作。EXPLAIN分析查询时,Extra显示为Using filesort

下面的2个参数优化,目的是优先在内存中排序

1) 按执行位置划分

  • Sort_Buffer MySQL 为每个线程各维护了一块内存区域 sort_buffer ,用于进行排序。sort_buffer 的大小可以通过 sort_buffer_size 来设置。
1
2
3
4
5
6
7
8
9
10
11
12
mysql> show variables like '%sort_buffer_size%';
+-------------------------+---------+
| Variable_name | Value |
+-------------------------+---------+
| sort_buffer_size | 262144 |
+-------------------------+---------+
mysql> select 262144 / 1024;
+---------------+
| 262144 / 1024 |
+---------------+
| 256.0000 |
+---------------+

注: sort_Buffer_Size 并不是越大越好,由于是connection级的参数,过大的设置+高并发可能会耗尽系统内存资源。

  • Sort_Buffer + 临时文件

    如果加载的记录字段总长度(可能是全字段也可能是 rowid排序的字段)小于 sort_buffer_size 便使用 sort_buffer 排序;如果超过则使用 sort_buffer + 临时文件进行排序。

    临时文件种类:

    临时表种类由参数 tmp_table_size 与临时表大小决定,如果内存临时表大小超过 tmp_table_size ,那么就会转成磁盘临时表。因为磁盘临时表在磁盘上,所以使用内存临时表的效率是大于磁盘临时表的。

2) 按执行方式划分

执行方式是由 max_length_for_sort_data 参数与用于排序的单条记录字段长度决定的,如果用于排序的单条记录字段长度 <= max_length_for_sort_data ,就使用全字段排序;反之则使用 rowid 排序。

1
2
3
4
5
6
mysql> show variables like 'max_length_for_sort_data';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| max_length_for_sort_data | 1024 |
+--------------------------+-------+

2.1) 全字段排序

全字段排序就是将查询的所有字段全部加载进来进行排序。

优点:查询快,执行过程简单 缺点:需要的空间大。

1
select name,age,add from user where addr = '北京' order by name limit 1000; -- addr有索引

image-20230927200018633

上面查询语句的执行流程:

  1. 初始化 sort_buffer,确定放入 name、age、addr 这3个字段。

  2. 从索引 addr 中找到第一个满足 addr=’北京’ 的主键ID(ID_x)。

  3. 到主键索引中找到 ID_x,取出整行,取 name、addr、age 3个字段的值,存入 sort_buffer。

  4. 从索引 addr 取下一个记录的主键ID。

  5. 重复3、4,直到 addr 值不满足条件。

  6. 对 sort_buffer 中的数据按照 name 做快速排序。

  7. 把排序结果中的前1000行返回给客户端。

2.2) rowid排序

rowid 排序相对于全字段排序,不会把所有字段都放入sort_buffer。所以在sort buffer中进行排序之后还得回表查询。

缺点:会产生更多次数的回表查询,查询可能会慢一些。

优点:所需的空间更小

1
select name,age,add from user where addr = '北京' order by name limit 1000; -- addr有索引

假设 name、age、addr3个字段定义的总长度为36,而 max_length_for_sort_data = 16,就是单行的长度超了,MySQL认为单行太大,需要换一个算法。 放入 sort_buffer 的字段就会只有要排序的字段 name,和主键 id,那么排序的结果中就少了 addr 和 age,就需要回表了。

image-20230927200013337

上面查询语句的执行流程:

  1. 初始化 sort_buffer,确定放入2个字段,name 和 id。
  2. 从索引 addr 中找到第一个满足addr=’北京’的主键ID(ID_x)。
  3. 到主键索引中取出整行,把 name、id 这2个字段放入 sort_buffer。
  4. 从索引 addr 取下一个记录的主键ID。
  5. 重复3、4,直到addr值不满足条件。
  6. 对 sort_buffer 中的数据按照 name 做快速排序。
  7. 取排序结果中的前1000行,并按照 id 的值到原表中取出 name、age、addr 3个字段的值返回给客户端。

总结

  • 如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer中, 这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
  • MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。 对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

排序优化

添加索引

  • employee 表 创建索引
1
2
3
4
-- 联合索引
ALTER TABLE employee ADD INDEX idx_name_age(NAME,age);
-- 为薪资字段添加索引
ALTER TABLE employee ADD INDEX idx_salary(salary);
  • 查看 employee 表的索引情况
1
SHOW INDEX FROM employee; 

image-20230927200007710

场景1: 只查询用于排序的 索引字段, 可以利用索引进行排序,最左原则

  • 查询 name, age 两个字段, 并使用 nameage 行排序
1
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name,e.age;

image-20230927200003007

场景2: 排序字段在多个索引中,无法使用索引排序

  • 查询 name , salary 字段, 并使用 namesalary 排序
1
EXPLAIN SELECT e.name, e.salary FROM employee e ORDER BY e.name,e.salary;

image-20230927195958033

场景3: 只查询用于排序的索引字段和主键, 可以利用索引进行排序

  • 查询 id , name , 使用 name 排序
1
EXPLAIN SELECT e.id, e.name FROM employee e ORDER BY e.name;

image-20230927195952314

场景4: 查询主键之外的没有添加索引的字段,不会利用索引排序

  • 查询 dep_id ,使用 name 进行排序
1
EXPLAIN SELECT e.dep_id FROM employee e ORDER BY e.name;

image-20230927195946850

场景5: 排序字段顺序与索引列顺序不一致,无法利用索引排序

  • 使用联合索引时, ORDER BY子句也要求, 排序字段顺序和联合索引列顺序匹配。
1
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.age,e.name;

image-20230927195939182

场景6: where 条件是 范围查询时, 会使order by 索引 失效

  • 比如 添加一个条件 : age > 18 ,然后再根据 age 排序.
1
EXPLAIN SELECT e.name, e.age FROM employee e WHERE e.age > 10 ORDER BY e.age;

image-20230927195929513

  • 注意: ORDERBY子句不要求必须索引中第一列,没有仍然可以利用索引排序。但是有个前提条件,只有在等值过滤时才可以,范围查询时不
1
EXPLAIN SELECT e.name, e.age FROM employee e WHERE e.age = 18 ORDER BY e.age;

image-20230927195924237

场景7: 升降序不一致,无法利用索引排序

  • ORDER BY排序字段要么全部正序排序,要么全部倒序排序,否则无法利用索引排序。
1
2
3
4
-- 升序
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name , e.age ;
-- 降序
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name DESC, e.age DESC;

image-20230927195918357

  • name字段升序,age字段降序,索引失效
1
EXPLAIN SELECT e.name, e.age FROM employee e ORDER BY e.name, e.age DESC;

image-20230927195909230

索引单表优化案例

建表

  • 创建表 插入数据
  • 下面是一张用户通讯表的表结构信息,这张表来源于真实企业的实际项目中,有接近500万条数据.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE user_contacts (
id INT(11) NOT NULL AUTO_INCREMENT,
user_id INT(11) DEFAULT NULL COMMENT '用户标识',
mobile VARCHAR(50) DEFAULT NULL COMMENT '手机号',
NAME VARCHAR(20) DEFAULT NULL COMMENT '姓名',
verson INT(11) NOT NULL DEFAULT '0' COMMENT '版本',
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建者',
create_date DATETIME NOT NULL COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新者',
update_date DATETIME NOT NULL COMMENT '更新时间',
remarks VARCHAR(255) DEFAULT NULL COMMENT '备注信息',
del_flag CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (id)
);
-- 数据:课后资料 sql脚本中(测试前需删除表全部索引)

需求一:

  • 查询所有名字中包含李的用户姓名和手机号,并根据user_id字段排序
1
SELECT NAME, mobile FROM  user_contacts WHERE NAME LIKE '李%' ORDER BY user_id;
  • 通过explain命令 查看SQL查询优化信息
1
EXPLAIN SELECT NAME, mobile FROM  user_contacts WHERE NAME LIKE '%李%' ORDER BY user_id;

image-20230927200249808

  • 结论:很显然type是ALL,即最坏情况。Extra里还出现Using filesort(文件内排序,未使用到索引),也是最坏情况,所以优化是必须的。

优化

  1. 首先添加联合索引, 该联合索引包含所有要查询的字段,使其成为覆盖索引,一并解决like模糊查询时索引失效问题
1
2
-- 添加联合索引
ALTER TABLE user_contacts ADD INDEX idx_nmu(NAME,mobile,user_id);
  1. 进行分析
1
EXPLAIN SELECT NAME, mobile FROM  user_contacts WHERE NAME LIKE '%李%' ORDER BY user_id;
  1. 结果: type的类型提升到了index, 但是 Using filesort 还有.

image-20230927200244732

分析结果显示: type连接类型提升到了index级别,通过索引就获取到了全部数据,但是Extra字段中还是存在 Using filesort.

  1. 继续优化: 根根据最佳左前缀法则,之后最左侧列是有序的, 在创建联合索引时,正确的顺序应该是: user_id,NAME,mobile,这样order by user_id才能用到索引
1
2
3
4
-- 删除索引
DROP INDEX idx_nmu ON user_contacts
-- 添加重新排序后的索引
ALTER TABLE user_contacts ADD INDEX idx_unm(user_id,NAME,mobile);
  1. 执行查询,发现type=index , Using filesort没有了.
1
EXPLAIN SELECT NAME, mobile FROM  user_contacts WHERE NAME LIKE '%李%' ORDER BY user_id;

image-20230927200239175

需求二:

  • 统计手机号是135、136、186、187开头的用户数量.
1
2
EXPLAIN  SELECT COUNT(*) FROM user_contacts 
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';
  • 通过explain命令 查看SQL查询优化信息

image-20230927200233923

type=index : 用到了索引,但是进行了索引全表扫描

key=idx_unm: 使用到了联合索引,但是效果并不是很好

Extra=Using where; Using index: 查询的列被索引覆盖了,但是无法通过该索引直接获取数据.

综合上面的执行计划给出的信息,需要进行优化.

优化

  1. 经过上面的分析,发现联合索引没有发挥作用,所以尝试对 mobile字段单独建立索引
1
ALTER TABLE user_contacts ADD INDEX idx_m(mobile);
  1. 再次执行,得到下面的分析结果
1
2
EXPLAIN  SELECT COUNT(*) FROM user_contacts 
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';

image-20230927200227725

type=range: 使用了索引进行范围查询,常见于使用>,>=,<,<=,BETWEEN,IN() 或者 like 等运算符的查询中。

key=idx_m: mysql选择了我们为mobile字段创建的索引,进行数据检索

rows=1575026: 为获取所需数据而进行扫描的行数,比之前减少了近三分之一

count(*) 和 count(1)和count(列名)区别

进行统计操作时,count中的统计条件可以三种选择:

1
2
3
4
5
6
7
8
EXPLAIN  SELECT COUNT(*) FROM user_contacts 
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';

EXPLAIN SELECT COUNT(id) FROM user_contacts
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';

EXPLAIN SELECT COUNT(1) FROM user_contacts
WHERE mobile LIKE '135%' OR mobile LIKE '136%' OR mobile LIKE '186%' OR mobile LIKE '187%';

效率:

1
2
3
4
5
6
7
8
9
执行效果:
count(*) 包括了所有的列,在统计时 不会忽略列值为null的数据.
count(1) 用1表示代码行,在统计时,不会忽略列值为null的数据.
count(列名)在统计时,会忽略列值为空的数据,就是说某个字段的值为null时不统计.
执行效率:
列名为主键, count(列名)会比count(1)快
列名为不是主键, count(1)会比count(列名)快
如果表没有主键,count(1)会比count(*)快
如果表只有一个字段,则count(*) 最优.

需求三:

  • 查询2017-2-16日,新增的用户联系人信息. 查询字段: name , mobile
1
EXPLAIN SELECT NAME,mobile FROM user_contacts  WHERE DATE_FORMAT(create_date,'%Y-%m-%d')='2017-02-16';

image-20230927200220196

优化

  • explain分析的结果显示 type=ALL : 进行了全表扫描,需要进行优化,为create_date字段添加索引.
1
2
3
ALTER TABLE user_contacts ADD INDEX idx_cd(create_date);

EXPLAIN SELECT NAME,mobile FROM user_contacts WHERE DATE_FORMAT(create_date,'%Y-%m-%d')='2017-02-16';

image-20230927200214514

添加索引后,发现并没有使用到索引 key=null

分析原因: create_date字段是datetime类型 ,转换为日期再匹配,需要查询出所有行进行过滤, 所以导致索引失效.

继续优化:

  • 改为使用 between ... and ... ,使索引生效

    1
    2
    EXPLAIN SELECT NAME,mobile FROM user_contacts  WHERE create_date 
    BETWEEN '2017-02-16 00:00:00' AND '2017-02-16 23:59:59';

    image-20230927200208344

    type=range : 使用了索引进行范围查询

    Extra=Using index condition; Using MRR :Using index condition 表示使用了部分索引, MRR表示InnoDB存储引擎 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能.

需求四:

  • 获取用户通讯录表第10万条数据开始后的100条数据.

    1
    2
    3
    4
    5
    6
    EXPLAIN SELECT * FROM user_contacts uc LIMIT 100000,100;

    -- 查询记录量越来越大,所花费的时间也会越来越多
    EXPLAIN SELECT * FROM user_contacts uc LIMIT 1000000,1000;
    EXPLAIN SELECT * FROM user_contacts uc LIMIT 2000000,10000;
    EXPLAIN SELECT * FROM user_contacts uc LIMIT 3000000,100000;

    image-20230927200203118

  • LIMIT 子句可以被用于指定 SELECT 语句返回的记录数。需注意以下几点:

    • 第一个参数指定第一个返回记录行的偏移量,注意从0开始()
    • 第二个参数指定返回记录行的最大数目
    • 如果只给定一个参数:它表示返回最大的记录行数目
    • 初始记录行的偏移量是 0(而不是 1)

优化

  • 优化1: 通过索引进行分页

    直接进行limit操作 会产生全表扫描,速度很慢. Limit限制的是从结果集的M位置处取出N条输出,其余抛弃.

    假设ID是连续递增的,我们根据查询的页数和查询的记录数可以算出查询的id的范围,然后配合 limit使用

1
EXPLAIN SELECT * FROM user_contacts WHERE id  >= 100001 LIMIT 100;

image-20230927200158776

type类型提升到了 range级别

  • 优化2: 使用子查询优化
1
2
3
4
5
-- 首先定位偏移位置的id
SELECT id FROM user_contacts LIMIT 100000,1;
-- 根据获取到的id值向后查询.
EXPLAIN SELECT * FROM user_contacts WHERE id >=
(SELECT id FROM user_contacts LIMIT 100000,1) LIMIT 100;

image-20230927200154199

索引多表优化案例

  • 用户手机认证表
    • 该表约有11万数据,保存的是通过手机认证后的用户数据
    • 关联字段: user_id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `mob_autht` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标识',
`user_id` int(11) NOT NULL COMMENT '用户标识',
`mobile` varchar(11) NOT NULL COMMENT '手机号码',
`seevc_pwd` varchar(12) NOT NULL COMMENT '服务密码',
`autht_indc` varchar(1) NOT NULL DEFAULT '0' COMMENT '认证标志',
`verson` int(11) NOT NULL DEFAULT '0' COMMENT '版本',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_date` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_date` datetime NOT NULL COMMENT '更新时间',
`remarks` varchar(255) DEFAULT NULL COMMENT '备注信息',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`)
) ;
  • 紧急联系人表
    • 该表约有22万数据,注册成功后,用户添加的紧急联系人信息.
    • 关联字段: user_id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `ugncy_cntct_psn` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标识',
`psn_info_id` int(11) DEFAULT NULL COMMENT '个人信息标识',
`user_id` int(11) NOT NULL COMMENT '向钱用户标识',
`cntct_psn_name` varchar(10) NOT NULL COMMENT '联系人姓名',
`cntct_psn_mob` varchar(11) NOT NULL COMMENT '联系手机号',
`and_self_rltn_cde` char(2) NOT NULL COMMENT '与本人关系代码 字典表关联',
`verson` int(11) NOT NULL DEFAULT '0' COMMENT '版本',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_date` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_date` datetime NOT NULL COMMENT '更新时间',
`remarks` varchar(255) DEFAULT NULL COMMENT '备注信息',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
PRIMARY KEY (`id`)
) ;
  • 借款申请表
    • 该表约有11万数据,保存的是每次用户申请借款时 填写的信息.
    • 关联字段: user_id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
CREATE TABLE `loan_apply` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '借款申请标识',
`loan_nbr` VARCHAR(50) NOT NULL COMMENT '借款编号',
`user_id` INT(11) NOT NULL COMMENT '用户标识',
`idnt_info_id` INT(11) DEFAULT NULL COMMENT '身份信息标识',
`psn_info_id` INT(11) DEFAULT NULL COMMENT '个人信息标识',
`mob_autht_id` INT(11) DEFAULT NULL COMMENT '手机认证标识',
`bnk_card_id` INT(11) DEFAULT NULL COMMENT '银行卡标识',
`apply_limit` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '申请额度',
`apply_tlmt` INT(3) NOT NULL COMMENT '申请期限',
`apply_time` DATETIME NOT NULL COMMENT '申请时间',
`audit_limit` DECIMAL(16,2) NOT NULL COMMENT '审核额度',
`audit_tlmt` INT(3) NOT NULL COMMENT '审核期限',
`audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
`cfrm_limit` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '确认额度',
`cfrm_tlmt` INT(3) NOT NULL COMMENT '确认期限',
`cfrm_time` DATETIME DEFAULT NULL COMMENT '确认时间',
`loan_sts_cde` CHAR(1) NOT NULL COMMENT '借款状态:0 未提交 1 提交申请(初始) 2 已校验 3 通过审核4 未通过审核 5开始放款 6放弃借款 7 放款成功 ',
`audit_mod_cde` CHAR(1) NOT NULL COMMENT '审核模式:1 人工 2 智能',
`day_rate` DECIMAL(16,8) NOT NULL DEFAULT '0.00000000' COMMENT '日利率',
`seevc_fee_day_rate` DECIMAL(16,8) NOT NULL DEFAULT '0.00000000' COMMENT '服务费日利率',
`normal_paybk_tot_day_rate` DECIMAL(16,8) NOT NULL DEFAULT '0.00000000' COMMENT '正常还款总日利率',
`ovrdu_fee_day_rate` DECIMAL(16,8) DEFAULT NULL COMMENT '逾期违约金日利率',
`day_intr_amt` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '日利率金额',
`seevc_fee_day_intr_amt` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '服务日利率金额',
`normal_paybk_tot_intr_amt` DECIMAL(16,2) NOT NULL DEFAULT '0.00' COMMENT '综合日利率金额',
`cnl_resn_time` DATETIME DEFAULT NULL COMMENT '放弃时间',
`cnl_resn_cde` CHAR(8) DEFAULT NULL COMMENT '放弃原因:关联字典代码',
`cnl_resn_othr` VARCHAR(255) DEFAULT NULL COMMENT '放弃的其他原因',
`verson` INT(11) NOT NULL DEFAULT '0' COMMENT '版本',
`create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
`create_date` DATETIME NOT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
`update_date` DATETIME NOT NULL COMMENT '更新时间',
`remarks` VARCHAR(255) DEFAULT NULL COMMENT '备注信息',
`loan_dst_cde` CHAR(1) NOT NULL DEFAULT '0' COMMENT '0,未分配; 1,已分配',
`del_flag` CHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
`last_loan_apply_id` INT(11) DEFAULT NULL COMMENT '上次借款申请标识',
PRIMARY KEY (`id`),
UNIQUE KEY `ind_loan_nbr` (`loan_nbr`) USING BTREE,
) ;

需求一:

  • 查询所有认证用户的手机号以及认证用户的紧急联系人的姓名与手机号信息

    1
    2
    3
    4
    5
    6
    explain select 
    ma.mobile '认证用户手机号',
    ucp.cntct_psn_name '紧急联系人姓名',
    ucp.cntct_psn_mob '紧急联系人手机号'
    from mob_autht ma left join ugncy_cntct_psn ucp
    on ma.user_id = ucp.user_id;

    image-20230927200148088

    • type 类型都是ALL, 使用了全表扫描

优化

  • 优化: 为mob_autht 表的 user_id字段 添加索引

    1
    alter table mob_autht add index idx_user_id(user_id);

    image-20230927200142159

    • 根据小结果及驱动大结果集的原则, mob_autht 是驱动表,驱动表即使建立索引也不会生效.

    • 一般情况下: 左外连接左表是驱动表,右外连接右表就是驱动表.

    • explain分析结果的第一行的表,就是驱动表

  • 继续优化: 为ugncy_cntct_psn表的 user_id字段 添加索引

    1
    ALTER TABLE ugncy_cntct_psn ADD INDEX idx_userid(user_id); 

    image-20230927200136652

    • mob_autht 的type类型为ALL, ugncy_cntct_psn的type类型是ref

需求二:

  • 获取所有智能审核的用户手机号和申请额度、申请时间、审核额度

    1
    2
    3
    4
    5
    6
    7
    EXPLAIN SELECT 
    ma.mobile '用户认证手机号',
    la.apply_limit '申请额度',
    la.apply_time '申请时间',
    la.audit_limit '审核额度'
    FROM mob_autht ma inner JOIN loan_apply la ON ma.id = la.mob_autht_id
    WHERE la.audit_mod_cde = '2';

    image-20230927200131347

优化

  • 查询 loan_apply表,使用的条件字段为 audit_mod_cde ,因为该字段没有添加索引,导致 type=ALL 发生全表扫描,
  • audit_mod_cde 字段添加索引,来提高查询效率.
1
ALTER TABLE loan_apply ADD INDEX idx_amc(audit_mod_cde); 

image-20230927200126422

添加索引后type的类型确实提升了,但是需要注意的扫描的行还是很高,并且 Extra字段的值为 Using where 表示: 通过索引访问时,需要再回表访问所需的数据.

注意: 如果执行计划中显示走了索引,但是rows值很高,extra显示为using where,那么执行效果就不会很好。因为索引访问的成本主要在回表上.

  • 继续优化:

    • audit_mod_cde 字段的含义是审核模式,只有两个值: 1 人工 2 智能 ,所以在根据该字段进行查询时,会有大量的相同数据.

    • 比如: 统计一下 audit_mod_cde = '2' 的数据总条数,查询结果是9万多条,该表的总数接近11万条,查询出的数据行超过了表的总记录数的30%, 这时就不建议添加索引 ( 比如有1000万的数据,就算平均分后结果集也有500万条,结果集还是太大,查询效率依然不高 ).

      1
      2
      SELECT COUNT(*) FROM loan_apply; -- 109181条
      SELECT COUNT(*) FROM loan_apply la WHERE la.audit_mod_cde = '2' ; -- 91630条

总结: 唯一性太差的字段不需要创建索引,即便用于where条件.

  • 继续优化:
    • 如果一定要根据状态字段进行查询,我们可以根据业务需求 添加一个日期条件,比如获取某一时间段的数据,然后再区分状态字段.
1
2
3
4
5
6
7
8
9
-- 获取2017年 1月1号~1月5号的数据
EXPLAIN SELECT
ma.mobile '用户认证手机号',
la.apply_time '申请时间',
la.apply_limit '申请额度',
la.audit_limit '审核额度'
FROM loan_apply la INNER JOIN mob_autht ma ON la.mob_autht_id = ma.id
WHERE apply_time BETWEEN '2017-01-01 00:00:00'
AND '2017-01-05 23:59:59' AND la.audit_mod_cde = '2';

image-20230927200117847

extra = Using index condition; : 只有一部分索引生效

MRR 算法: 通过范围扫描将数据存入 read_rnd_buffer_size ,然后对其按照 Primary Key(RowID)排序,最后使用排序好的数据进行顺序回表,因为 InnoDB 中叶子节点数据是按照 Primary Key(RowID)进行排列的,这样就转换随机IO为顺序IO了,从而减小磁盘的随机访问.

🔖MySQL常用

mysql 8 连接方式

1
2
3
4
5
6
7
url = jdbc:mysql://localhost:3306/thrcloud_db01?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false


如果是xml中,要转义: jdbc:mysql://localhost:3306/thrcloud_db01?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false

driver:com.mysql.cj.jdbc.Driver
url:jdbc:mysql:///test-mybatis?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true

随之引发的问题:时间插入少一天(date类型)/少8小时(dateTime类型)

1
2
3
4
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 添加serverTimezone=CTT,避免日期少一天
url: jdbc:mysql://xxxxx:3306/xxxx?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=CTT