MySql数据库系统基础
MySql数据库系统基础
课程目标
- 如何使用MySQL数据库(如何安装和基本语法,工具的使用)
- 如何设计数据库(一种策略、思想,与经验有关)
- SQL编程
我们要学四种语言:1.标签语言:HTML & CSS(执行在浏览器端)2.逻辑语言: Python,Java,Go,PHP...(执行在服务器端)3.MySQL编程语言(执行在MySQL服务器端)4.JS,TS逻辑语言(执行在浏览器端)在今天的AI时代,实际上学习数据库是比以前容易了很多。
今天目标(2026-07-01):
1.安装Typora打开markdown文档笔记2.什么是数据库3.在phpstudy(小皮中)安装和启动mysql8(重点)4.mysql8的命令行(重点)5.mysql是如何在osi模型中传递数据的原理剖析6.Innodb和MyISAM的存储引擎7.修改存储引擎(重点)8.datagrip的安装和绿化(重点)9.datagrip链接mysql(重点)10.datagrip的图形化操作(重点)11.SQL的经典分类和扩展分类(重点)12.DDL数据库操作语句(重点)13.通义灵码配置mysql插件14.通过vibe coding生成DDL语句1.数据库简介
简单来说,数据库就是存储数据的‘仓库(地方、方式、位置)’,比如我们平时使用的记事本、档案袋和生活便签等都是数据库,包括我们现在使用的word也是一种数据库!
很显然,少量的数据很容易管理,但是当数据量越来越大的时候,就需要引入专门管理数据的工具,我们就叫做数据库管理系统!
数据库系统 = 数据库管理系统 + 数据库 + 数据库管理员
DataBase System(DBS) = DataBase Management System + DataBase(DB) + DBA
对于mysql数据库系统来说,它是如下模式:
DBS = DBMS + DB + DBA (mysqld) (库表文件) (人) ↑ 服务端 ↑ 磁盘数据 ↑ root/运维数据库:对大量的信息进行管理的高效的解决方案,按照数据结构来组织、存储和管理数据的“仓库”;
通常一个中小型web项目(网站)会使用一个数据库来存储其所有的动态数据!
数据库发展历程:层状型,网状型,目前主流的数据库是关系型的!
1.1.关系型数据库
1. 分类
大型:
Oracle:甲骨文
中型:
SQL Server:微软
MySQL:目前也是甲骨文公司的(最开始是瑞典的MySQL AB公司,08年被Sun公司收购了,09年Sun公司又被甲骨文收购了)
小型:
access(ASP + access,ASP.net+SQL Server),VF
Sqllite数据库
在web应用中,使用的最多的就是MySQL数据库,原因如下:
1, 开源、免费
2, 功能足够强大,足以应付web应用开发(最高支持千万级别的并发访问)
除了关系型数据库以外,还有nosql和向量数据库
2. 关系型的定义
所谓的关系型数据库,就是基于关系模型的数据库,一个关系模型其实就是一张二维表!
而一张二维表往往对应着现实世界中的一个实体集!
什么是实体和实体集?
实体是人类观念世界中描述客观事物的一种概念,可以是具体的事物,比如一本书,一个人,一个手机,一条街等,也可以是抽象的事物,比如一种感受,一个电子订单!
同一类实体的所有实例就构成了一个实体集,实体集就是实体的集合,每一个实体都是该实体集的一个实例,实体与实体集之间的关系有点类似于数学上的元素与集合的关系!
实体集反映到数据库中,就是一张一张的二维表!
比如,我们现在的有2种实体集:
学生实体集,教师实体集,也就分别对应着数据库中的2张数据表:学生表,教师表!

在现实世界中,实体与实体之间肯定是有关系的,所以在数据库中,表与表之间也肯定是有关系的,所以叫做“关系型”数据库!

思考:假如为一个酒店管理系统设计一个数据库,需要设计哪些表?
1.2 总结
1. 什么是数据库?
- 通俗说:存数据的地方(如记事本、Word)。
- 专业说:按结构组织存储数据的“仓库”。
- 数据库系统 (DBS) = 管理软件 (DBMS) + 数据 (DB) + 管理员 (DBA)
2. 主流:关系型数据库
核心特点:用“二维表”存数据(像Excel)。
常见分类:
- 大型:Oracle
- 中型:SQL Server、MySQL(Web开发最常用,开源免费,性能好)
- 小型:Access
为什么叫“关系型”?
- 现实世界有实体(如:学生、老师)。
- 数据库里用表(如:学生表、教师表)来存储。
- 表与表之间是有联系的,所以叫“关系型”。
2.SQL语言
2.1 概念
SQL:Structured Query language,结构化查询语言! 是一种基于关系模型数据库的操作语言,也是一种数据库编程语言! SQL最初由IBM公司在70年代开发出来的,在80年代的时候被国际标准化组织ISO定义为关系型数据库的标准语言!
2.2 SQL语言的分类
思考一下:
如果我们现在准备往一个数据库里面存放一些数据,需要有哪些准备工作?
1, 先创建一个新的数据库
2, 再创建一张数据表
3, 定义这张表的结构(有哪些字段,字段是什么数据类型,有没有主键索引等)
所以,数据库的第一种操作语言就是DDL!
根据对数据库不同的操作对象或操作层次,SQL又可以分成不同的操作语言.
1.经典分类(4种,重点)
DDL DDL:Data Definition Language,数据定义语言用于创建或删除数据库、数据表、字段的SQL语句,包含以下几种指令:
| SQL关键字 | 描述 |
|---|---|
| create | 创建数据库和数据表等 |
| drop | 删除数据库和数据表等 |
| alter | 修改数据库和表等对象的结构 |
| show | 展示数据库引擎,建库建表语句等 |
DML DML:Data Manipulation Language,数据操作语言 主要就是对表中的记录进行增删改查的操作!
其中“查询”部分,又叫做DQL(Data Query Language)!
| SQL关键字 | 描述 |
|---|---|
| SELECT | 查询表中的数据 |
| INSERT | 向表中插入新数据 |
| UPDATE | 变更表中的数据 |
| DELETE | 删除表中的数据 |
DCL DCL:Data Control Language,数据控制语言用于对控制数据库的操作权限的,包括用户权限以及数据操作权限。
| SQL关键字 | 描述 |
|---|---|
| COMMIT | 确认对数据库中的数据进行的变更 |
| ROLLBACK | 取消对数据库中的数据进行的变更 |
| GRANT | 赋予用户操作权限 |
| REMOVE | 取消用户的操作权限 |
2.扩展分类(6种,了解)
DDL:数据定义语言,进行数据库、表的管理等,如create、drop DQL:数据查询语言,用于对数据进行查询,如select DML:数据操作语言,对数据进行增加、修改、删除,如insert、udpate、delete TPL:事务处理语言,对事务进行处理,包括begin transaction、commit、rollback DCL:数据控制语言,进行授权与权限回收,如grant、commit,rollback,remove CCL:指针控制语言,通过控制指针完成表的操作,如declare cursor
2.3 总结
SQL是操作数据库的标准语言
4个单词搞定分类:
| 简称 | 干什么的? | 记这俩词就够了 |
|---|---|---|
| DDL | 建库建表 | CREATE``DROP |
| DML | 增删改数据 | INSERT``UPDATE``DELETE |
| DQL | 查数据(最常用) | SELECT |
| DCL | 分配权限 | GRANT |
3.安装mysql
3.1 mysql版本
| 年份 | 版本 | 标志性事件 |
|---|---|---|
| 1995 | 1.0 | 诞生 |
| 2005 | 5.1 | 支持视图、存储过程 |
| 2010 | 5.5 | InnoDB 成默认引擎 |
| 2015 | 5.7 | 原生 JSON(曾经的”生产主力”) |
| 2018 | 8.0 | 🔥架构级重构,当前教学/生产首选 |
| 2024 | 8.4 LTS / 9.0 | 双轨制:LTS 长期支持,Innovation 尝鲜 |
为什么我们会选择mysql8呢?原因如下:
窗口函数 + CTE
不用再写嵌套子查询算排名了,ROW_NUMBER()、WITH递归一键搞定
默认 utf8mb4
建库不用再手动 CHARSET=utf8mb4,emoji 直接存
原子 DDL
以前 DROP TABLE a,b;中途崩了可能 a 删了 b 还在;8.0 要么全成要么全回滚
隐藏索引 / 降序索引
索引可以先 INVISIBLE 试水再删,降序排序真能用上索引了
角色(Role)
权限从”挨个给用户授权”升级成”角色模板”,DBA 狂喜
自增 ID 持久化
5.7 重启可能主键回退,8.0 写 redo 里,根治
干掉 .frm 文件
元数据全进 InnoDB 事务表,information_schema查询快 N 倍
3.2 使用phpstudy安装mysql8
…现场演示…
3.3 启动mysql8
在phpstudy中,启动mysql8是非常简单,如下图所示:

启动成功后,如下图所示:

3.4 mysql的配置文件
当mysql启动的时候,它会自动加载mysql的配置文件,我们可以通过这个方式找到该配置文件:

4.mysql8的命令行
4.1.打开mysql8命令行
先找到你安装phpstudy的地方,例如我的安装目录是:D:\tools\phpstudy_pro

点击进入Extensions目录,找到Mysql8的安装目录

在找到该目录下的bin目录:

然后输入cmd命令,回车

就能打开mysql的命令行

注意: 务必先启动mysql8,否则无法后面的操作
注意:phpstudy还是最好不要加mysql8的环境变量
4.2 打开mysql控制台
在phpstudy中,安装了mysql8后,默认的用户名和密码都是root,我们可以通过如下命令打开控制台:
# 方式一mysql -uroot -proot [数据库名]# 方式二mysql -u <用户名> -p [数据库名]
回车就可以进入控制台:

4.3 mysql的架构
MySQL基于C/S模型的,安装之后里面有两个部分:
服务器软件(mysqld)
客户端软件 (mysql)
要想正常的使用MySQL服务器,首先要完成两个步骤:
1, 开启MySQL服务器(就是通过mysqld启动的)
2, 通过客户端连接服务器(mysql,python/java/go/php…,datagrip)
4.4 网络连接3要素
- IP(Internet Protocol) IP 地址用于唯一标识网络中的每一台设备。它充当设备的“地址”,使得数据能够在网络中准确地找到目标设备。
IPV4: 格式: x.x.x.x x的取值范围(第一位x取值1-223,从第二位开始0-255) IP可以分为公网/外网ip和私网/内网ip
公网/外网IP: IP 是全球唯一的,可以在任何地方直接访问到该地址 私网/内网ip: 局域网内使用的地址,不能直接通过互联网访问,目的是为了内部网络的设备进行通信
IPV6: 由8个16位的十六进制数组成,每组数字之间用冒号分隔。 如:2001:0db8:85a3:0000:0000:8a2e:0370:7334
目前市场上,主要还是IPV4为主,IPV6为辅助.需要注意的是,IP需要确保在对应网络范围内唯一- 端口(Port) 端口用于区分同一台设备上不同的应用程序或服务。在计算机网络中,一个 IP 地址可以对应多个服务,每个服务通过不同的端口进行通信。端口号是通信中识别应用程序的方式。
- 端口的范围:
- 0-1023:这些端口称为 知名端口,通常被系统或服务使用(例如,HTTP 使用端口 80,HTTPS 使用端口 443,FTP使用端口 21)。
- 1024-49151:这些端口称为 注册端口,通常用于用户和应用程序之间的通信(mysql的端口默认是3306)。
- 49152-65535:这些端口是 动态或私有端口,用于临时连接或客户端通信。
- 协议(Protocol) 协议定义了数据在网络中传输时的规则和格式。常见的协议有 TCP/IP、Socket、UDP、HTTP、FTP 等,它们规定了数据如何被分割、传输、接收和重组。
在计算机当中127.0.0.1这个是一个默认的本地IP地址,也可以用localhost来表示。
Mysql是采用了Tcp/IP协议,它自定义了应用层.
MySQL 使用的是基于 TCP 的自定义应用层协议
✅ 本地连接可以用 Unix Domain Socket(常被误叫“Socket 协议”)

5.命令行基本操作
5.1 show命令
-- 展示所有的数据库有哪些show databases;
-- 展示引擎有哪些show engines;
在开发中我们一般使用InnoDB比较多,你简单理解就是这个引擎的功能更完整
-- 展示数据库创建的语句show create database demo;
# 额外补充# 还有一种显示表定义的语句,只不过不是DDL,而是结构化输出,是给人看的desc <表名>describe <表名>
-- 展示当前数据库使用默认编码字符集SHOW VARIABLES LIKE 'character_set%';SHOW VARIABLES LIKE '%char%';
这种命令你有个印象就差不多,在AI时代的今天,你是可以vibe coding的(比如:通义灵码)或者你问豆包/元宝也可以。
5.2 \G显示方式

5.3 exit命令
退出mysql控制台(ctrl+c快捷键也可以退出,不过windows可能不太支持,mac和linux是没问题的)。

6.存储引擎
6.1 修改存储引擎
我们可以通过修改配置文件的让mysql8的引擎默认为Innodb


注意:改完记得保存

在命令行中,输入show engines

6.2 Innodb和MyISAM引擎
6.2.1 简单理解Innodb和MyISAM引擎
白话理解innodb: 它是一个功能齐全的存储引擎且安全性高. 但查询性能稍差.
白话理解MyISAM: 它是一个功能简单的存储引擎,无安全能力. 但查询性能很强.
选用存储引擎示例:
登录:
1. 输入手机号+密码.1. 检验手机号是否存在(查询).1. 判断密码是否正确(查询)结论: 选用MyISAM,原因是需要高速查询.
支付: 张三给李四转账100
- 判断张三是否有100元(查询)
- 在张三账户-100,在李四账户+100(修改,写,需要事务处理(保持原子操作).)
- 返回李四的余额和张三的余额(查询)
结论: 选用Innodb: 第二步需要事务处理(原子操作),若出现问题,银行直接倒闭.
6.2.2 面试八股文(Leet Code)
-
引擎的作用域
所有的存储引擎是作用在表上,而不是数据库上.
create database nice;use nice;create table users(id int comment '用户编号',name varchar(20) comment '姓名');# 在这里,可以发现 ENGINE=InnoDBshow create table users;create table accounts(id int comment '用户编号',name varchar(20) comment '姓名') engine = myisam;# 在这里,可以发现 ENGINE=MyISAMshow create table accounts; -
innodb和MyISAM的文件结构
mysql8中
Innodb是以单个文件形式存储的,它会把数据与结构存放在一起,扩展名为.ibd.
MyISAM是以多个文件形式存储的.
- .sdi: 数据结构文件,在mysql8中结构被mysql使用json格式优化了.
- .myi: 索引文件.
- .myd: 数据文件.
在mysql5中
Innodb是以两个文件形式存储的,会将数据结构独立出来.
- .ibd: 数据与索引文件
- .frm: 数据结构文件
MyISAM是以多个文件形式存储的.
- .frm: 数据结构文件.
- .myi: 索引文件.
- .myd: 数据文件.
-
MyISAM在mysql8之前的功能(了解)
MyISAM是具有数据压缩功能和数据结构修复功能,但在mysql8中,功能被弃用.
在mysql5中修复MyISAM表的命令
- 在mysql5的bin目录下打开命令行
Terminal window myisamchk <表.myi文件路径>在mysql5中压缩MyISAM表的命令
- 在mysql5的bin目录下打开命令行
Terminal window myisampack <表.myi文件路径>对于历史数据有用
注意: 表在被修复和压缩过后,将会变为只读.
6.3 Arhive引擎
mysql8推荐用于取代MyISAM的方案
archive引擎同时具有安全写,高性能写入,压缩的功能.
不支持删除,修改.
6.4 并发
行锁:
MyISAM不支持行锁
innodb支持行锁功能
7.DataGrip和通义灵码
7.1 安装和绿化
请参考如下资料和观看视频进行实操:

7.2 使用DataGrip链接Mysql
首先复制mysql-connector-java-8.0.25.jar到DataGrip的安装目录下,建议创建driver目录进行存放

这个目录不一定要交driver,但必须是英文或者数字,不能包括任何空格和中文字符串和特殊符号







7.4 通义灵码配置和连接Mysql


最新的2026版本的DataGrip是可以集成通义灵码的,如果有兴趣的同学可以去闲鱼找找
但DataGrip是不是最新版的其实无所谓,所以这里就使用通义灵码的IDEA辅助就可以了。
在Python阶段,可以在Pycharm中安装最新版本。
7.3 MySQL注释符
单行注释:
# 注释内容
— 注释内容,这里的—与注释内容之间必须有一个空格!
多行注释
/* 注释内容 */
8.DDL之数据库操作
8.1 创建数据库语法规则
CREATE <DATABASE | SCHEMA> [IF NOT EXISTS] db_name [CHARACTER SET <charset_name>] [COLLATE <collation_name>];
DATABASE和SCHEMA完全等价,可互换使用。
需求场景:创建名为ecmall的数据库,创建名为easycms的数据库
create database ecmall;create schema easycms;推荐写法
create database if not exists itcast;完整写法(了解,不常用):
CREATE DATABASE IF NOT EXISTS test_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;8.2 use语句
格式:
use 数据库名字;示例:
use demo; # 使用或者切换数据库为demo有时候我们会在登录控制台的时候就使用某一个数据库了:
mysql -uroot -proot echop
加上数据库不存在,那么mysql客户端会报错 ERROR 1049 (42000): Unknown database ‘aaaaabbbbcccc’
8.4 查看当前数据库
格式:
select database();
8.5 删除数据库
格式:
drop database [if exists] 数据库名称drop schema [if exists] 数据库名称示例:
drop schema ecmall;drop database itcast;删除数据库尽量不要使用if exists ,最好让数据库有报错的可能性
8.6 修改数据库(了解)
ALTER DATABASE test_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这个学不学都无所谓
注意: 请不要忘记前面学过的show databases;命令
mysql中的数据库存储位置可以在my.ini中配置
# 若缺省,则使用运行目录下的./data文件夹datadir=D:/phpstudy_pro/Extensions/MySQL8.0.12/data/每个数据库在data文件夹中都有一个单独的同名文件夹
今天目标(2026-07-02):
0.回顾昨天1.Mysql中的数据类型2.DDL数据表操作3.DML增删改查操作4.软删除9.Mysql中的数据类型
数据库里面的数据在保存时也要通过指定数据的类型来告诉数据库管理系统,这些数据有什么用途,所以也会有对应的数据类型。数据类型是为了节约内存,提高计算速度,尽量使用存储空间少的类型。常用数据类型有数值类型、字符串类型、时间日期类型、枚举类型。mysql的单表数据可以支持最多千万级(2000W是比较合适的)
9.1 数值类型
MySQL中的数值类型提供了整型、浮点型、定点数,与python类似。
对于小数的表示,MYSQL分为两种方式:浮点数(float)和定点数(Decimal)。浮点数包括float(单精度)和double(双精度), 而定点数只有decimal一种,在mysql中底层以字符串的形式存放,比浮点数更精确,适合用来表示货币等精度要求高的数据。
| 分类 | 数据类型 | 存储大小 | 有符号范围(signed) | 无符号范围(unsigned) | 使用场景 |
|---|---|---|---|---|---|
| 整型 | tinyint(m) | 1个字节 | (-128,127) | (0,255) | 年龄,分类的编号 |
| 整型 | smallint(m) | 2个字节 | (-32 768,32 767) | (0,65 535) | 商品分类编号,员工编号, |
| 整型 | mediumint(m) | 3个字节 | (-8388608~8388607) | (0,16 777 215) | 小的数据表的主键id |
| 整型 | int(m) | 4个字节 | (-2147483648~2147483647) | (0,4 294 967 295) | 一般数据表的主键id |
| 整型 | bigint(m) | 8个字节 | (-9 233 372 036 854 775 808,9 223 372 036 854 775 807) | (0,18 446 744 073 709 551 615) | 超大表的主键id |
| 浮点型 | float(m,d) | 8位精度,4个字节 | 单精度,近似值的小数 m总个数,d小数位 (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) | 0,(1.175 494 351 E-38,3.402 823 466 E+38) | 数值类型的时间戳,带小数的经纬度 |
| 浮点型 | double(m,d) | 16位精度,8个字节 | 双精度,近似值的小数 m总个数,d小数位 (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 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) | |
| 定点数 | decimal(m,d) | 精确值,精确值的小数 m总个数,d小数位 依赖于m和d的值 | 依赖于m和d的值 | 货币,积分 | |
| 二进制位 | bit(m) | 1个字节 | 依赖于m的值 | 依赖于m的值 | 签到二进制记录,布隆过滤器。 |
示例:
格式: 字段名 数据类型 [unsigned]
age tinyint # 有符号ate tinyint unsigned # 无符号9.2 字符串类型
MySQL中针对文本内容的存储类型提供了字符串与文本两种格式,其中按存储方式不同,又分为固定长度(定长)与可变长度(变长)两种,按存储格式不同,又分为普通字符格式与二进制格式两种。
SQL 语句中(单|双)引号都能表示字符串或文本。
| 数据类型(n指定存储的长度上限) | 大小 | 描述 | 应用场景 |
|---|---|---|---|
| char(n) | 0-255字符 | 定长字符串 | 姓名,验证码 |
| varchar(n) | 0-65535字符 | 变长字符串 | 账号,密码,文章标题,商品标题 |
| binary(n) | 0-255字符 | 定长二进制字符串 | |
| varbinary(n) | 0-65535字符 | 变长二进制字符串 | |
| tinytext | 0-255字符 | 可变长度文本 | |
| text | 0-65 535字符 | 可变长度文本 | 文章内容, |
| mediumtext | 0-16 777 215字符 | 可变长度文本 | |
| longtext | 0-4 294 967 295字符 | 可变长度文本 | |
| tinyblob | 0-255字符 | 可变二进制文本 | |
| blob | 0-65 535字符 | 可变二进制文本 | 小图标、二进制的认证信息 |
| mediumblob | 0-16 777 215字符 | 可变二进制文本 | |
| longblob | 0-4 294 967 295字符 | 可变二进制文本 | |
| json | 0-4 294 967 295字符 | 可变二进制json文本 (也叫bson: binary json) | 主要实现一些NoSQL数据的存储 |
这里有两个经典的面试八股文
1.char与varchar的区别:
存入 'abc':CHAR(10)→ 存 'abc '(补 7 个空格)char若超过指定的长度,直接截断,不报错
VARCHAR(10)→ 存 'abc'(只占 3 个字符 + 长度标记)2.varchar字符串和text文本的区别
varchar可指定n,text不能指定text类型不能有默认值,注意json也不能有默认值。(但是在8.0.13版本后,支持常量默认值default 'textDefaultValue')varchar可直接创建索引,text创建索引要指定前多少个字符。varchar查询速度快于text。索引:index,主要为了加快查询数据的数据的一种技术,类似书籍的目录。9.3 日期类型
表示时间值的日期和时间类型为 DATETIME、DATE、TIMESTAMP、TIME 和 YEAR。
每个时间类型有一个有效值范围和一个”零”值,当指定不合法的 MySQL 不能表示的值时使用”零”值。
| 数据类型 | 取值范围 | 日期格式 | 零值 | 使用场景 |
|---|---|---|---|---|
| year | 1901~2155 | YYYY | 0000 | 电影年份,图书年份 |
| date | 1000-01-01~9999-12-31 | YYYY-MM-DD | 0000-00-00 | 生日 |
| time | -838:59<59>59>~838:59<59>59> | HH:MM | 00:00<00>00> | 餐牌时间,会议时间 |
| datetime | 1000-01-01 00:00<00>00>~9999-12-31 23:59<59>59> | YYYY-MM-DD HH:MM | 0000-00-00 00:00<00>00> | 添加时间,更新时间,删除时间,登陆时间 |
| timestamp | 1970-01-01 00:00<01>01>~2038-01-19 03:14<07>07> | YYYY-MM-DD HH:MM | 0000-00-00 00:00<00>00> | 添加时间,更新时间,删除时间,登陆时间 |
9.4 枚举与集合
enum中文名称叫枚举类型,它的值范围需要在创建表时通过枚举方式显示。ENUM只允许从值集合中选取单个值,而不能一次取多个值。SET和ENUM非常相似,也是一个字符串对象,里面可以包含0-64个成员。根据成员的不同,存储上也有所不同。set类型可以允许值集合中任意选择1或多个元素进行组合。对超出范围的内容将不允许注入,而对重复的值将进行自动去重。
| 类型 | 大小 (字节) | 用途 |
|---|---|---|
| enum | 对1-255个成员的枚举需要1个字节存储; 对于255-65535个成员,需要2个字节存储; 最多允许65535个成员。 | 单选:选择性别,现居地城市 |
| set | 1-8个成员的集合,占1个字节 9-16个成员的集合,占2个字节 17-24个成员的集合,占3个字节 25-32个成员的集合,占4个字节 33-64个成员的集合,占8个字节 | 多选:兴趣、爱好、标签 |
枚举是单选的
集合是多选的
集合的特殊用途
name, hobbies
张三 1,2
李四 1,3
王五 1,2,3
将所有值有1的人查找出来
select * from users where find_in_set('1',hobbies)>0;
# find_in_set('<值>',<字段名>) # 返回结果: 位置索引(下标从1开始).9.5 自增列
use nice;
create table human( num int primary key auto_increment comment '自增长列,因为它是自动增长的所以不需要手动维护', name varchar(20) not null);
insert into human values (null,'张三'), (null,'李四'), (null,'王五')10.DDL之数据表操作
10.1 CREATE TABLE(创建表)
语法规则:
CREATE TABLE [IF NOT EXISTS] 表名 ( 列定义1, 列定义2, ... [表级约束]) [ENGINE=InnoDB] [DEFAULT CHARSET=utf8mb4] [COMMENT='表注释'] [auto_increment = <值>];列定义规则:
列名 数据类型 [NOT NULL | NULL] [DEFAULT 默认值][AUTO_INCREMENT][UNIQUE [KEY]][PRIMARY KEY][COMMENT '列注释']示例:
# 设计一张会员表10.2 复制表结构
语法规则:
create table <表名> like <源表名>;示例:
# 创建store_members表.# 除了爱好字段以外,其它字段都与ai_members相同.# 1. 复制表结构create table store_members like ai_members;10.3 ALTER TABLE(修改表)
一旦涉及到修改表,你当初设计表的时候肯定多少存在一些不太合理的地方。
语法规则:
ALTER TABLE <表名> 操作类型;示例:
# 2. 删除爱好字段alter table store_members drop column hobbies;# 3. 添加字段alter table store_members add column login_time datetime default now() comment '登录时间', add column email varchar(36) default 'yourname@kulve.tech' comment '邮箱地址' after mobile;# 4. 修改字段名alter table store_members change column gender sex enum ('男','女') default '男' comment '性别', # 修改字段名 gender -> sex modify column email varchar(32) default 'nickname@kulve.tech' comment 'kulve.tech邮箱地址'; # 修改列属性
# 修改自增长列的值alter table <表名> auto_increment = 10;10.4 清空表
语法规则:
delete from 表名; # 删除所有记录,保留表状态truncate table 表名; # 截断表,不保留表状态delete与truncate的区别
两个命令都会把数据清空,但是delete会删除数据,保留表的历史状态,而truncate会清空数据并清楚表的历史状态。
11. DML数据库表操作
DML 的核心操作无非“增删改”,而在应用开发中,这些操作大多已由 ORM 框架封装完成。至于查询(SELECT,隶属 DQL),在表关系简单的场景下,ORM 同样能胜任;但对于需要深入挖掘数据或性能优化的复杂查询,手写原生 SQL 仍是必修课。虽然 SELECT 语句的复杂度往往取决于业务场景与个人经验,但随着 AI 辅助编程工具的普及,即便是普通开发者,如今也能借助 AI 轻松驾驭高阶的查询逻辑。
11.1 insert语句(增)
insert语句就是对表进行记录添加操作.
语法
# 单行插入与多行插入insert into <表名>[(<字段列表>)] values (<值列表>)[,(<值列表>)][,(...)];insert into <表名>[(<字段列表>)] values (<值列表>)[,(<值列表>)][,(...)];
# 复制表数据# 当然,可以只复制某几列,或者给列指定一个计算列或者只复制某些行,只需要修改select就可以了.# 若列名不同,则需要指定字段列表.insert into <表名>[(<字段列表>)] select * from <表名> [where <条件>]示例:
# 单行插入insert into ai_members(mobile, password, gender, hobbies)values ('13800001111', 'password', '女', 'cat');
# set集合会自动去重insert into ai_members(mobile, password, hobbies)values ('13800001112', 'password', 'foot,cat,foot');
insert into ai_membersvalues (null, '13833334444', '78956456', '男', 'music,foot', now());
# 多行插入,使用","隔开.insert into pet (pet_id, name, species, breed, intake_date, status)values (1, '小白', '猫', '中华田园猫', '2025-02-01', '健康'), (2, '旺财', '狗', '中华田园犬', '2025-02-05', '需治疗'), (3, '咖啡', '猫', '英国短毛猫', '2025-02-10', '健康'), (4, '雪球', '狗', '萨摩耶', '2025-02-12', '健康');
# 复制表数据(当然,可以带条件)create table pet_copy like pet;
insert into pet_copyselect *from pet;11.2 select语句(查)
select语句就是对表进行查的操作.
示例:
# 查询所有字段select *from ai_members;在select中使用where子句
# 一般查询select id, mobilefrom ai_memberswhere id = 5;11.3 update语句(改)
update语句就是对表进行记录修改的操作.一般都需要配合where子句进行
语法
update <表名> set <字段名> = <值> [where <条件>];11.4 delete语句(删)
update语句就是对表进行记录删除的操作(是物理删除,直接在磁盘中把记录删除了,一般是无法恢复的),在实际开发中,有90%以上的需求是软删除
示例
delete from <表名> [where <条件>]11.5 软删除操作
在基础课程中,我可以先跳过这个实操,后面可以配合Python的一些ORM做这个操作。
这里大家可以了解一下原理:
通过在表中添加支持null的字段delete_at来支持软删除.
当为null时,代表数据没有被删除,当该字段被赋予值时,代表已经被删除了.
当删除时,使用update 更新delete_at的值来代表软删除.
查询时,使用is null查询未删除的数据,使用is not null查询已删除的数据.
恢复数据时,只需要使用update将delete_at设置为null即可.
create table ai15_Users ( id int unsigned not null primary key comment '主键,非增长列', name varchar(30) not null comment '姓名', mobile char(11) not null comment '手机号码', # default null是可以省略不写,不写就是default null deleteAt datetime default null comment '软删除标识')comment '软删除表';
-- 插入数据insert into ai15_Users(id,name,mobile)values(1,'武二郎','12345678901');insert into ai15_Users(id,name,mobile)values(2,'武大郎','12345678902');insert into ai15_Users(id,name,mobile)values(3,'潘金莲','12345678903');
-- 如果在软删除中,我们要删除数据,不是调用delete删除-- 而是调用update去更新deleteAt字段为当前时间update ai15_Users set deleteAt=NOW() where id=2;
-- 如果我们要查询当前没有被删除的数据,我们通过IS NULL来查询select * from ai15_Users where deleteAt Is NULL ;
-- 如果我们要被删除的数据,我们通过IS NOT NULL来查询select * from ai15_Users where deleteAt Is NOT NULL ;
-- 如果我们要恢复武大郎是未被删除的,我们只需把deleteAt重新修改为Null就可以了update ai15_Users set deleteAt=NULL where id=2;11.6 Mysql数据库的导出(备份)和导入(恢复)
这个技术是运维的工作.
# 导出mysqldump -u root -p <数据库名> > <文件名># 导入mysql -u root -p <数据库名> < <文件名>12.字段约束
也叫完整性约束条件,主要是为了防止不符合规范的数据进入数据库,在用户对数据进行插入、修改、删除等操作时,DBMS自动按照一定的约束条件对数据进行监测,使不符合规范的数据不能进入数据库,以确保数据库中存储的数据正确、有效、相容。
| 约束类型 | SQL关键字 | 语法 | 描述 |
|---|---|---|---|
| 填充 | zerofill | 字段名 整型 zerofill | 为数据表中的整型字段设置数值左边补0 |
| 无符号 | unsigned | 字段名 数据类型 unsigned | 为数据表中的数值类型字段设置数值指定不能小于0,可以让字段值的取值范围,在正数范围内增加1倍。 |
| 默认值 | default | 字段名 数据类型 default 默认值 | 为数据表中的字段指定默认值。但blob、text与json类型不支持default。 |
| 非空 | not null | 字段名 数据类型 not null | 非空字段指字段的值不能为NULL。 |
| 唯一索引 | unique | 列级约束 字段名 数据类型 unique 表级约束 unique (字段名 1,字段名 2…) | 用于保证数据表中字段的不同行的值唯一性,即表中字段的值不能重复出现在多行。 列级约束定义在一个列上,只对该列起约束作用; 表级约束是独立于列的定义,可以应用在一个表的多个列上。 |
| 主键索引 | primary key | 列级约束 字段名 数据类型 primary key 表级约束 primary key(字段名 1,字段名2…) | 一个表中只能有一个主键。可以指定单个字段为单列主键,也可以指定多个字段为联合主键。 |
| 自动增长 | auto_increment | 字段名 数据类型 auto_increment | 一个表中只能有一个自动增长的字段,该字段类型是整数类型,一般用于设置主键。 自动增长值从1开始自增,每次加1。 |
| 索引 | index / key | index / key (字段名) | 给对应的字段的值设置添加索引(目的让当前字段的值在被删除,修改,查询时,加快执行速度) |
| 外键索引 | foreign key | constraint 外键名 foreign key 字段名 [,字段名2,…] references <主表名> 主键列1 [,主键列2,…] | 用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性。 |
12.1 默认值约束
default 的应用场景: 年龄默认值,性别默认值
示例:
age tinyint unsigned default 18 # 年龄gender enum('男','女','保密') dfault '保密' #性别12.2 非空约束
not null的应用场景:账号,手机号码
示例:
username varchar(16) not null # 用户名phone char(11) not null # 手机idcard varchar(22) not null # 大陆身份证号码12.3 唯一性约束(索引)
unique的应用场景: 手机号码,身份证号码
当表的相关列中已经有重复值时,无法对这些列创建唯一索引.
示例:
# 创建方式一:alter table <表名> add unique index <索引名> (id_card);
# 创建方式二: 注意这个是有on关键字的create unique index <索引名> on <表名> (<单个字段>);
# 额外: 联合索引,两个索引使用同一个名字create unique index <索引名> on <表名> (<字段名列表>);当为某一字段创建了唯一性约束,同时也为这个字段标记为索引字段
12.4 主键约束(索引)
primary key的应用场景:id,username. 主键也是唯一的,同样起到了唯一性的作用。同时它具有not null的作用
示例:
id int unsigned primary key auto_increment12.5 unique和主键有什么不同
口诀: PRIMARY KEY 一张表只能有一个且不允许 NULL,而 UNIQUE 可以有多个并允许 NULL(多数数据库仅允许一个 NULL)。
primary key: 主键,每张表只允许一个,不允许null,键不允许重复.
unique: 唯一索引,每张表可以有多个,允许null,键不允许重复,允许多个null.(原因是mysql8中,null 是不可比较的,所以允许多个为空,但如果是mysql5,则不允许重复.)
13. 索引和Explain关键字
13.1 索引的好处和缺点
好处:
加快查询速度(尤其是 WHERE、JOIN、ORDER BY、GROUP BY)
提高排序和去重效率
约束数据唯一性(如唯一索引)
缺点:
降低增删改性能(索引也要同步维护)
占用磁盘空间
过多索引会让优化器更难选、甚至选错
13.2 使用Explain查看索引的情况
# 如果有and,or,in,between,like等运算符,就是证明where子句具有逻辑# 如果逻辑不当,可能索引会失效.# 如果逻辑合理,但逻辑非常复杂,也有可能会导致索引失效.# mysql8是拥有查询缓存的.# 用上索引最好的结果是const,次结果是range,最差的结果是all.explainselect * from user where id = 1 and username = 'sj';13.3
14.索引的创建和删除
在 MySQL 中,创建索引主要有以下几种常用方式,简单说明如下:
14.1 建表时直接创建
drop table if exists user;create table user( id int unsigned primary key, username varchar(20), password varchar(20), email varchar(55), # 创建普通索引 # index <索引名> (<字段名>) index idx_username (username), # 创建唯一索引 # unique index <索引名> (<字段名>) unique index uidx_email (email)
);show indexes from user;mobile varchar(11) unique not null comment ‘手机号码’ 这种方式创建的索引名称是mysql创建的
什么时候该使用索引?
- 主键必须为索引.
- 如果一个字段在查询中经常被where使用,那么就考虑设计为索引字段.
14.2 已存在的表添加索引(最常用)
-- 普通索引create index <索引名称> ON <表名>(<字段名>);
-- 唯一索引create unique index <索引名称> ON <表名>(<字段名>);CREATE UNIQUE INDEX uk_email ON user(email);14.3 使用 ALTER TABLE 创建索引
# alter table <表名> add [unique] index <索引名>(<字段名>)ALTER TABLE user ADD INDEX idx_name (name);ALTER TABLE user ADD UNIQUE INDEX uk_email (email);14.4 查看/删除索引
-- 查看SHOW INDEX FROM user;show index from <表名>;# 或show indexes from <表名>;
-- 删除索引,风险很高,很容易破坏数据结构,产生大碎片# 删除索引方式一:drop index <索引名> on <表名>DROP INDEX idx_name ON user;-- 删除索引方式二:alter table <表名> drop index <索引名>ALTER TABLE user DROP INDEX idx_name;15.DQL单表查询(Select)
15.1 DQL 单表查询核心语法
select [distinct] <[*]|<字段列表>|<表达式>>from <表名> [where <条件>] [order by <字段> [asc|desc]] [limit [<偏移>,] <行数>];
# 关于limit关键字的解释# limit 1: 从第0行开始,返回1条记录,即返回第1条记录. 省略第一个参数,默认为0.# limit 1,1: 从第1行开始,返回1条记录,即返回第2条记录# limit 3,3: 从第3行开始,返回3条记录,即返回第4-6条记录✅ MySQL 8 仍沿用传统的标准结构,执行顺序:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
15.2 数据准备
CREATE TABLE students( id INT unsigned primary key auto_increment comment '主键,pk', name varchar(20) not null comment '姓名', gender char(1) not null comment '性别', score DECIMAL(5,2) not null comment '分数');
INSERT INTO students VALUES(1,'张三','男',88),(2,'李四','女',95),(3,'王五','男',88),(4,'赵六','女',72),(5,'张无忌','男',80),(6,'张三丰','男',76),(7,'黎明','男',83),(8,'王明辉','男',97);15.3 快速上手
案例1:查询全部 / 指定列
select *from students;
select name as cnname, scorefrom students案例2:带条件查询
-- 成绩 ≥ 80 的学生select name as cnname, scorefrom studentswhere score >=80;| 类型 | 操作符 | 含义 | 示例 |
|---|---|---|---|
| 比较运算 | = | 等于 | score = 88 |
!=/ <> | 不等于 | gender <> '女' | |
>``>=``<``<= | 大小比较 | score > 90 | |
| 区间判断 | BETWEEN … AND … | 闭区间 | score BETWEEN 80 AND 100 |
| 集合判断 | IN (…) | 在集合中 | score IN (88,95) |
| 空值判断 | IS NULL | 为空 | score IS NULL |
IS NOT NULL | 不为空 | score IS NOT NULL | |
| 模糊匹配 | LIKE | 模糊查询 | name LIKE '张%' |
| 逻辑运算 | AND | 并且 | score>=80 AND gender='男' |
OR | 或者 | score<60 OR score>90 | |
NOT | 取反 | NOT score BETWEEN 60 AND 80 |
案例3:AND / OR / BETWEEN / IN
-- 男生且成绩80~90# between是包括两个边界的,两个边界都会被返回.select * from studentswhere gender='男' and score between 80 and 90;
-- 成绩是88或95SELECT * FROM studentsWHERE score IN (88,95);
-- 查id是1,10,13的同学select * from student where id=1 or id=10 or id=13;
-- 用in来确定范围 in(1,10,13)-- in在mysql8之前用上索引的几率不高-- in不要大范围查询select * from student where id in(1,10,13);
-- in一般用在批量软删除比较多,这个几率高于select语句使用in-- 批量软删除update student set deleteAt=NOW() where id in(1,11,13);-- 查出批量软删除的用户select * from student where deleteAt IS NOT NULL;
--- in还可以字符,尤其是时间字符select * from students where name='张三' or name='张三丰';select * from students where name in('张三','张三丰');案例4: Like模糊查询
从mysql5.6开始才支持中文检索的.如果版本低于mysql5.6那么我们需要安装一个叫Sphinx
like这个search工具只适合做小型应用检索。现实当中Elasticseach,OpenSearch是工业级的,MeiliSearch是轻量级的
| 通配符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
% | 任意长度的字符(0~多个) | '张%' | 张三、张无忌、张三丰 |
_ | 任意一个字符(必须有1个) | '_明%' | 李明、王明辉 |
'张__' | 张三丰(三个字) |
-- 查询姓“张”的学生SELECT * FROM studentsWHERE name LIKE '张%';
-- 查询名字中包含“明”的学生SELECT * FROM studentsWHERE name LIKE '%明%';
-- 查询名字以“丰”结尾的学生SELECT * FROM studentsWHERE name LIKE '%丰';
-- 查询姓名为两个字的学生SELECT * FROM studentsWHERE name LIKE '__';
-- 查询姓“张”且名字为三个字的学生SELECT * FROM studentsWHERE name LIKE '张__';案例5:去重,排序
一般不会使用去重,因为去重量会导致索引失效,去重一般在程序中的set集合中去除.
-- 去除重复的成绩select distinct score from students;
-- asc表示从小到大(升序),desc表示从大到小(降序)-- 按照分数的升序进行排序select * from students order by score asc;-- 如果省略asc或者desc,那么默认就是ascselect * from students order by score;-- 按照分数的降序进行排序select * from students order by score desc;
-- 按名字排列,如果是中文,其实utf8编码select * from students order by name asc;
-- age的升序来排,如果年龄是一样的,就按id的降序来排select * from student order by age asc,id desc;
-- mysql8是做优化的,如果比mysql5.6低,order by id desc是用不上索引的select * from student order by id desc;案例6: 限制行数
表记录下标从0开始.
select *from <表名>limit [<起始行下标>,] <行数>
# 关于limit关键字的解释# limit 1: 从第0行开始,返回1条记录,即返回第1条记录. 省略第一个参数,默认为0.# limit 1,1: 从第1行开始,返回1条记录,即返回第2条记录# limit 3,3: 从第3行开始,返回3条记录,即返回第4-6条记录面试八股文: SQL注入攻击
原理: 从SQL层面,注入一个恒成立的条件,进面绕过字符串比对.
-- 赋值逻辑的功能select 1=1 as a;select ''='' as b;
-- 创建一张表是会员登录表login_members,字段有id,username,passwordcreate table login_members( id int unsigned primary key auto_increment, username varchar(16) unique comment '登录名', password char(32) comment '密码md5-32的,5e64fe04bfd8363b6c74ea86f5c867f1');
-- 插入数据insert into login_members(username,password)values ('zhangsan','5e64fe04bfd8363b6c74ea86f5c867f1');
insert into login_members(username,password)values ('lisi','5e64fe04bfd8363b6c74ea86f5c867f1');
replace into login_members(username,password)values ('pengjin','5e64fe04bfd8363b6c74ea86f5c867f1');
select * from login_members;
-- 如果我们让lisi登录成功,输入正确的用户名和密码select * from login_members where username='lisi' and password='5e64fe04bfd8363b6c74ea86f5c867f1';
-- 注入攻击(原理)select * from login_members where 1=1 or ''='';
-- 网页中'' or ''='' 或者 ' or 1=1-- 可以同postman 或者 apifox去抓包输入的-- 要控制它最好是预处理,或者使用优秀知名的orm框架select * from login_members where username='' or ''='' and password='' or 1=1;
-- 在sql的层面操作,那么把select的and# 输入用户的时候查出用户名和密码select username,password from login_members where username='' or ''='';# 密码判断'' or ''=''# 做密码的逻辑判断是在java/python中做的,对于这个取出的用户,使用or条件进行比对是不正确的.# '' or ''='' == '5e64fe04bfd8363b6c74ea86f5c867f1'15.4 常用聚合函数
| 函数 | 作用 | 是否忽略 NULL |
|---|---|---|
COUNT() | 统计行数 / 非空值个数 | ✅(除 COUNT(*)) |
SUM() | 求和 | ✅ |
AVG() | 求平均值 | ✅ |
MAX() | 求最大值 | ✅ |
MIN() | 求最小值 | ✅ |
案例: 平均分、最高分、人数
-- 平均分、最高分、人数SELECT AVG(score) as 平均分, MAX(score) as 最高分, COUNT(*) as 人数FROM students;扩展:count(1)是什么意思
如果没有where 子句count的性能很高,因为是获取的optimized中的内容.
有where子句时,有可能会使用不到索引.
# 获取表中行计数select count(1) from <表名>;
-- count()是一个函数-- * 是一个参数,count(*)统计表有多条记录-- count并不是一定要用*来做占位的select count(*) as rsCount from students;select count('a') rstotal from students;案例:求总分和平均分,并且小数保留2位
# sum求总分select sum(score) as 总分 from students;# round一般会配合sum,avg使用,保留多少位小数select round(avg(score),2) as 平均分 from students;15.5 GROUP BY 分组查询
思考: 为什么需要分组(Group By) ?
前面学的聚合函数是 “整张表算一次”
实际业务中,往往需要:
“先分组,再分别统计”
例如:
按性别统计人数
按科目统计最高
语法规则:
SELECT 分组列, 聚合函数()FROM 表名[WHERE 条件]GROUP BY 分组列[ORDER BY 列];案例 1:统计男女人数
-- 如果没有分组,select语句统计男女各占多少,那么就需要2条select语句select count(1) as 总数男 from students where gender='男';select count(1) as 总数女 from students where gender='女';
-- 分组:你要对什么类型的数据进行分组,你就需要把group by 作用在谁的身上-- 分组语句一般是跟聚合函数一起使用-- 如果有分组/窗口,那么聚合函数就可能是多行的数据select gender as 性别,count(1) as 总数 from studentsgroup by gender;案例 2:按性别统计平均成绩
select gender as 性别,round(avg(score),2) as 平均分 from studentsgroup by gender;案例 3:只统计成绩 ≥ 80 学生的平均分,再按性别分组
-- 步骤1: 只统计成绩 ≥ 80 学生的平均分select round(avg(score),2) as 平均分 from students where score>=80;
-- 步骤2: 只统计成绩 ≥ 80 男学生的平均分select round(avg(score),2) as 平均分 from studentswhere score>=80 and gender='男'; #85.28-- 步骤3: 只统计成绩 ≥ 80 女学生的平均分select round(avg(score),2) as 平均分 from studentswhere score>=80 and gender='女'; #95.80
-- 只统计成绩 ≥ 80 学生的平均分,再按性别分组select gender 性别,round(avg(score),2) 平均分 from studentswhere score>=80 group by gender;WHERE 作用于 分组前
15.6 HAVING 分组后筛选
where用于筛选记录,having用于筛选分组.
SELECT 分组列, 聚合函数()FROM 表名GROUP BY 分组列HAVING 聚合条件;基础案例:只显示平均成绩 ≥ 85 的性别组
select gender, count(1) as 人数, avg(score) as 平均分from studentsgroup by genderhaving avg(score)>=85;优化代码后:
综合案例
SELECT gender AS 性别, COUNT(*) AS 人数, AVG(score) AS 平均成绩, MAX(score) AS 最高分, MIN(score) AS 最低分FROM studentsWHERE score IS NOT NULLGROUP BY genderHAVING AVG(score) >= 80ORDER BY 平均成绩 DESC;15.7 速记方法和思考题
GROUP BY:按某一列或多列“分组”聚合函数:对每一组分别计算
WHERE:分组前过滤行
HAVING:分组后过滤组SELECT 中只能是:分组列 + 聚合函数
🔹 想分组,GROUP BY
🔹 想统计,聚合函数
🔹 想筛行,用 WHERE
🔹 想筛组,用 HAVING
思考: group by 某个字段,在个字段必须出现在select的字段列表中,对吗?
语法中不是必须的,语义是是必须的.
16.常用内置函数
数值函数
| 函数 | 说明 | 示例 |
|---|---|---|
ROUND(x,n) | 四舍五入 | ROUND(3.1415,2)→ 3.14 |
TRUNCATE(x,n) | 截断 | TRUNCATE(3.149,1)→ 3.1 |
CEIL(x)/ CEILING(x) | 向上取整 | CEIL(3.01)→ 4 |
FLOOR(x) | 向下取整 | FLOOR(3.99)→ 3 |
ABS(x) | 绝对值 | ABS(-10)→ 10 |
MOD(a,b) | 取余 | MOD(10,3)→ 1 |
RAND() | 随机数 | RAND()→ 0~1 |
字符串函数
| 函数 | 说明 | 示例 |
|---|---|---|
CONCAT(s1,s2…) | 拼接字符串 | CONCAT('张','三') |
CONCAT_WS(sep,s1,s2…) | 带分隔符拼接 | CONCAT_WS('-','2026','01') |
UPPER(s)/ UCASE(s) | 转大写 | UPPER('abc') |
LOWER(s)/ LCASE(s) | 转小写 | LOWER('ABC') |
LENGTH(s) | 字节长度 | LENGTH('张三') |
CHAR_LENGTH(s) | 字符长度 | CHAR_LENGTH('张三') |
SUBSTRING(s,pos,len) | 截取 | SUBSTRING('abcdef',2,3) |
REPLACE(s,old,new) | 替换 | REPLACE('abc','a','A') |
TRIM(s) | 去两端空格 | TRIM(' abc ') |
LEFT(s,n)/ RIGHT(s,n) | 左右截取 | LEFT('abcd',2) |
这里单独抽出一个GROUP_CONCAT()让大家关注一下,其作用如下:
将同一组中的多个值,拼接成一个字符串返回,常用于:“一对多”结果的单行展示
我个人是非常喜欢用这个函数的。
日期时间函数
| 函数 | 说明 | 示例 |
|---|---|---|
NOW() | 当前日期+时间 | 2026-06-28 10:30:00 |
CURDATE() | 当前日期 | 2026-06-28 |
CURTIME() | 当前时间 | 10:30:00 |
YEAR(d) | 年 | YEAR(NOW()) |
MONTH(d) | 月 | MONTH(NOW()) |
DAY(d) | 日 | DAY(NOW()) |
DATE(d) | 取日期部分 | DATE(NOW()) |
DATEDIFF(d1,d2) | 相差天数 | DATEDIFF('2026-07-01','2026-06-28') |
DATE_FORMAT(d,fmt) | 格式化 | DATE_FORMAT(NOW(),'%Y年%m月%d日') |
其他函数
| 函数 | 说明 |
|---|---|
VERSION() | MySQL 版本 |
DATABASE() | 当前数据库 |
USER() | 当前用户 |
LAST_INSERT_ID() | 最后插入ID |
重点1:GROUP_CONCAT函数
数据准备:
CREATE TABLE stu_course( stu_name VARCHAR(10), course VARCHAR(10));
INSERT INTO stu_course VALUES('张三','语文'),('张三','数学'),('李四','英语'),('李四','物理');需求场景:一条记录显示每个学生的姓名和对应的课程
| 学生姓名 | 课程列表 |
|---|---|
| 张三 | 语文,数学 |
| 李四 | 英语,物理 |
| 王五 | 体育,音乐,艺术 |
select stu_name,GROUP_CONCAT(course) as 选课列表 from stu_course group by stu_name;重点2: find_in_set
功能强大,但性能一般,适合做内部系统场景
用于在集合中查询一个值
find_in_set始终无法利用索引.
语法规则:
find_in_set(关键字,字段名)数据准备:
CREATE TABLE `bjxz_sickers` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '患者id', `name` varchar(11) COLLATE utf8_unicode_ci NOT NULL COMMENT '患者名称', `gender` enum('男','女') COLLATE utf8_unicode_ci DEFAULT '男' COMMENT '性别', `drug` set('奥卡西平','普拉克索','左旋多巴','地芬尼多') COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '用药情况', `reg_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`)) comment '患者表';
INSERT INTO `bjxz_sickers` (`name`, `gender`, `drug`) VALUES-- 包含'地芬尼多'的数据(5条)('张伟', '男', '奥卡西平,地芬尼多'),('李娜', '女', '普拉克索,左旋多巴,地芬尼多'),('王强', '男', '奥卡西平,普拉克索,地芬尼多'),('赵敏', '女', '左旋多巴,地芬尼多'),('陈浩', '男', '奥卡西平,地芬尼多'),
-- 不包含'地芬尼多'的数据(15条)('刘洋', '男', '奥卡西平'),('孙婷', '女', '普拉克索,左旋多巴'),('周磊', '男', '左旋多巴,奥卡西平'),('吴芳', '女', '奥卡西平,普拉克索'),('郑凯', '男', '普拉克索'),('黄丽', '女', '左旋多巴'),('林峰', '男', '奥卡西平,左旋多巴,普拉克索'),('何雪', '女', '普拉克索'),('马超', '男', '左旋多巴,奥卡西平'),('罗琳', '女', '奥卡西平,普拉克索'),('谢鹏', '男', '左旋多巴'),('韩梅', '女', '普拉克索,左旋多巴'),('唐昊', '男', '奥卡西平'),('许静', '女', '左旋多巴,奥卡西平,普拉克索'),('邓飞', '男', '普拉克索');
# 其实如果是like和find_in_set这个索引是没有用的alter table bjxz_sickers add index `ik_durg`(`drug`);# 其实这样也是是可以查出来的select * from bjxz_sickers where drug like '%地芬尼多%';# 这样就可读性就比较高select * from bjxz_sickers where find_in_set('地芬尼多',drug);这个函数的真实场景:广州市第八人民医院神经内科与北京修正药业进行合作(是一个药物研发,关于耳水平衡的),北京修正要求医院提供一份使用”xxx药物”的病人清单给他们做专访。而我也是人生第一次接触到了find_in_set这个函数,当时因为用不上这个索引,通不过测试。
另外这里还有一个很有意思的约定,你会发觉很多医院的项目的药物字段都是drug这个单词.
最后,你可能会问,医院的药物那么它的集合是真的这样写得死死的吗,当然不是。
不过合作方的数据库表一般是新增的子模块功能,还真的是这样写得死死的,因为这张表当时就是我自己建立的,合作方它能进入医院的药物我遇到的最多也就16个,而医院的药物数据是会动态加一张合作方数据表的。
上述这些需求,如果你换着社区医院,那是绝对够用的。
json_contains与member of
对于json类型的字段,使用json_contains与member of可以有效利用json字段的多值索引.
# json_contains函数语法json_contains(<json列名>,<查找目标>[,<路径>])# 要注意,json_contains的语法较为复杂,有以下几点# - 如果查找目标是字符串,以"kulve"为例,那么,在写查找目标时,需要使用另外一种引号包裹,即 '"kulve"'.# - 如果查找目标是数字,那么需要写成'1'# - 如果查找目标是多个值,那么需要写成 '["kulve1", "kulve2"]'# path: 用于指定要搜索的json路径# return: 1(true) / 0(false)
# member of函数语法<查找目标> member of(<json列名 | json函数返回的结果>)数据准备:
CREATE TABLE `bjxz_sickers_json` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '患者id', `name` varchar(11) COLLATE utf8_unicode_ci NOT NULL COMMENT '患者名称', `gender` enum('男','女') COLLATE utf8_unicode_ci DEFAULT '男' COMMENT '性别', `drugs` json COMMENT '用药情况', PRIMARY KEY (`id`)) comment '患者表,json形式的';
INSERT INTO `bjxz_sickers_json` (`name`, `gender`, `drugs`) VALUES-- ✅ 包含'地芬尼多'的数据(5条)('张伟', '男', '["奥卡西平","地芬尼多"]'),('李娜', '女', '["普拉克索","左旋多巴","地芬尼多"]'),('王强', '男', '["奥卡西平","普拉克索","地芬尼多"]'),('赵敏', '女', '["左旋多巴","地芬尼多"]'),('陈浩', '男', '["奥卡西平","地芬尼多"]'),
-- ❌ 不包含'地芬尼多'的数据(15条)('刘洋', '男', '["奥卡西平"]'),('孙婷', '女', '["普拉克索","左旋多巴"]'),('周磊', '男', '["左旋多巴","奥卡西平"]'),('吴芳', '女', '["奥卡西平","普拉克索"]'),('郑凯', '男', '["普拉克索"]'),('黄丽', '女', '["左旋多巴"]'),('林峰', '男', '["奥卡西平","左旋多巴","普拉克索"]'),('何雪', '女', '["普拉克索"]'),('马超', '男', '["左旋多巴","奥卡西平"]'),('罗琳', '女', '["奥卡西平","普拉克索"]'),('谢鹏', '男', '["左旋多巴"]'),('韩梅', '女', '["普拉克索","左旋多巴"]'),('唐昊', '男', '["奥卡西平"]'),('许静', '女', '["左旋多巴","奥卡西平","普拉克索"]'),('邓飞', '男', '["普拉克索"]');建立索引(只支持在Mysql8.0.17+):如下语句叫做建立多值索引,char(20)是表示json元素只支持20个字符
在多值索引里面,varchar和text是都不能建立索引
# 创建json多值索引.alter table bjxz_sickers_json add index idx_drugs ((cast(drugs as char(20) array)));
# 语法alter table <表名> add index <索引名> (( cast(<列名> as 类型 array)))# 解释:# (()): 这是mysql8.0.13引入的"函数索引"的强制语法格式.# cast(... as ...): 将一种格式的数据转换为指定类型# char(20): 定义了被拆分出来的元素的存储类型与长度. 这里,因为药名是字符串,所以使用char(20). 若数组中存储的为数字,那么需要使用unsigned或double等.# array: 这是mysql8.0.17引入的"多值索引"的专属关键字,用于告诉索引器或者说cast函数 将这个数组中的每一个元素拆分出来,作为独立的索引值存入索引表.# 在这里,类型有严格的限制,只能使用以下几种# 数值类: unsigned,signed,decimal,double,float# 时间类: date,datetime,time,year# 字符类: char(n),不可使用varchar.
# Extra Part:# 多值索引原理:# 现在有这么一行数据 id = 1, drugs = ["阿莫西林", "头孢"]# 然后呢,当执行上述修改表的语句后,索引器会生成两条记录指向主键id = 1.# 阿莫西林 -> id: 1# 头孢 -> id: 1# 这就是多值索引,数组中有多少个元素,就会拆成几条索引项,但都指向了同一行.SELECT * FROM bjxz_sickers_json WHERE JSON_CONTAINS(drugs, '"地芬尼多"'); # 注意必须是"地芬尼多"
-- 不过我喜欢写成这样子,不过Member of好像要8.0.17+才能支持SELECT *FROM bjxz_sickers_jsonWHERE '地芬尼多' MEMBER OF (drugs);
-- 把用了"地芬尼多"或者"左旋多巴"的患者找出来,这样有可能用不上索引的SELECT * FROM bjxz_sickers_json WHERE JSON_CONTAINS(drugs, '"地芬尼多"') or JSON_CONTAINS(drugs, '"左旋多巴"') ;
-- 你可以把上面的语句优化成下面这样子,就可以用上索引了SELECT *FROM bjxz_sickers_jsonWHERE '地芬尼多' or '左旋多巴' MEMBER OF (drugs);17.流程控制函数
数据准备:
CREATE TABLE students( id INT unsigned primary key auto_increment comment '主键,pk', name varchar(20) not null comment '姓名', gender char(1) not null comment '性别', score DECIMAL(5,2) comment '分数');
INSERT INTO students VALUES(1,'张三','男',88),(2,'李四','女',95),(3,'王五','男',88),(4,'赵六','女',null),(5,'张无忌','男',null),(6,'张三丰','男',76),(7,'黎明','男',83),(8,'王明辉','男',97);| 函数 | 说明 |
|---|---|
IF(expr,v1,v2) | 二选一 |
IFNULL(v1,v2) | 空值处理 |
CASE WHEN … THEN … END | 多分支 |
17.1 IF函数
当给定的条件被满足时,将返回then块,否则返回else块.
select if(<条件>,<then 块>,<else 块>) as <列名>from <表名>示例:
select name, if(gender='男','先生','小姐') as sexfrom bjxz_sickers_json;17.2 IFNULL函数
当列返回null时,将会使用给定的值进行替换.
select ifnull(<字段名>,<值>)from <表名>示例:
select stu_name, # course varchar(20) default '这家伙很懒,啥也没有选' ifnull(course,'这家伙很懒,啥也没有选') as lessonfrom stu_course17.3 CASE分支
select case when <条件1> then <then 块1> when <条件2> then <then 块2> ... else <else块> end as <列名>from <表名>示例:
select name as 姓名, ifnull(score,0) as score, case when score>=95 then '优秀' when score>=80 and score<95 then '良好' when score>=60 then '及格' else '不及格' end as 等级from students;18.limit语句和分页
page -> 表示当前页# 路径参数http://kulve.tech/news/:page# 示例: 页为3http://kulve.tech/news/3
# 查询参数http://kulve.tech/news?page=<页码># 示例: 页为3http://kulve.tech/news?page=3# 若page_size分页大小为2,那么
# 如果当前记录总数是6条数据(record_count)-> record_count/page_size,那么被分为了三页# 如果当前记录总数是5条数据(record_count)-> ceil( record_count/page_size ),那么还是被分为了三页# 一共分为几页: ceil(记录总数/page_size). # ceil是向上取整,如 ceil(3.5) = 4
# 分页公式,查询指定页的内容(SQL where): limit (page-1)*page_sizge, page_sizge;19.范式,表关系和外键Foreign Key
19.1 数据库三范式概念
第一范式:
数据不允许再拆分,如EXCEL中的单元格拆分
第二范式:
表中添加ID字段,要有主键
记录之间没有依赖性
第三范式:
- 将依赖传递消除,意思就是将一部分数据迁移到其它表中,并用外键进行关联.
这里的范就是规范的意思,范式就是我们设计表的基本规范,Normal Format。
范式的作用就是通过合理的数据储存,从而使得数据的冗余度最小化以及运行效率的最大化!
范式是分层的!!
所谓的分层,就是根据不同的需要标准,一层一层的严格递进,一层比一层严格!理论上来说一共有6层!
比如:第一范式、第二范式……
但是,后面的范式实在是太严格了,很难达到,所以,在数据库中,只引入了前三层!
一般来说,我们认为满足了第三层范式的数据库就是合理的优秀的数据库!
第一范式 1NF
第一范式是最容易满足的,就是要求把各个数据设计成一个一个单独的信息,不能再进行拆分!
也就是说,字段里面的数据都可以直接被外部所调用,而不是提出出来之后还要进行分割!

上面的数据表就不满足第一范式!
解决方案:对上面的姓名和性别进行拆分即可!

很显而易见的事情是:就算你不知道范式,实际上你是不是也用上了?
第二范式2NF
第二范式就是在满足第一范式的基础之上,满足以下两个条件:
1.增加唯一性的标识。这个简单的理解就是加上主键。
2.记录之间不要具有依赖性(这是标准的),主要是因为这样会产生数据冗余
思考<下图的例子符合第2范式吗>下图的例子符合第2范式吗>?

实际上,有些人故意违背条件2这个标准用数据冗余来达到优化数据的效果和降低编程的难度。
这种操作叫逆范式。(在自连接部分,我们来实现一下)
第三范式3NF
第三范式就是在满足第二范式的基础之上消除传递依赖!简单理解,就是把数据分到另外一张表当中,使用外键进行关联。
注意:
范式是一种理想的规范,不是绝对的标准,一般的做法是先满足数据库设计的要求,再进行优化处理!有时候为了提高效率或者使用方便,还会故意违反范式!
19.2 表关系之1对1
概念
一对一(1∶1)关系是指:
实体集 A 中的一个实体,在实体集 B 中最多对应一个实体;
反之,实体集 B 中的一个实体,在实体集 A 中也最多对应一个实体。
比如: 1个用户对应1个用户详情。
从技术实现的角度:从表中对应主表的那个字段,既是外键也是unique唯一索引

示例:
create table users( user_id int unsigned primary key auto_increment comment '用户id,pk', name varchar(20) not null comment '姓名', gender enum ('男','女') default '男');
create table user_details( details_id int unsigned primary key auto_increment comment '详情id,pk', user_id int unsigned comment '外键,用户id', unique index `uk_user_id` (`user_id`), # 加了外键约束 constraint `fk_user_id` foreign key (`user_id`) references `users` (`user_id`));
-- 外键的好处: 必须主表有记录后,从表才能加上对应的记录-- 外键除了联动表以外,还可以约束两张表之间的数据一致性insert into users(name)values ('张三');-- 如果只有1条记录,且Id为1,那么如下代码是错误的,因为2是不存在的用户insert into user_details(user_id)values (2); # errorinsert into user_details(user_id)values (1);19.3 表关系之1对多、多对1
概念
多对一(belongs to)
一对多(has many,1∶N)关系是指:
实体集 A 中的一个实体,可以对应实体集 B 中的多个实体;
而实体集 B 中的一个实体,最多只能对应实体集 A 中的一个实体。
比如: 1个学生具有多个科目成绩,多个科目成绩对应某一个学生
从技术实现的角度:从表中对应主表的那个字段,既是外键也是index普通索引

示例:
create table ai_students( sid int unsigned primary key auto_increment comment '学生id,pk', name varchar(20) not null comment '姓名');
create table ai_students_sorce( cid int unsigned primary key auto_increment comment '成绩id,pk', student_id int unsigned comment '学生id,fk,普通索引', course varchar(10) not null comment '科目', scorce decimal(4, 2) default 0, index `ik_student_id` (`student_id`), FOREIGN KEY (student_id) REFERENCES ai_students (`sid`));
insert into ai_students(name)values ('张三');insert into ai_students_sorce(student_id, course, scorce)values (1, '语文', 66.6);
-- 如果外键没有加上级联更新和级联删除,那么是不能先更新主表或者先删除主表的update ai_studentsset sid=2where sid = 1;deletefrom ai_studentswhere sid = 1;
-- 如果这是必须要删除,那么要先删除从表,再删除主表deletefrom ai_students_sorcewhere student_id = 1;deletefrom ai_studentswhere sid = 1;示例2(具有级联更新和级联删除)
create table ai_students( sid int unsigned primary key auto_increment comment '学生id,pk', name varchar(20) not null comment '姓名');
create table ai_students_sorce( cid int unsigned primary key auto_increment comment '成绩id,pk', student_id int unsigned comment '学生id,fk,普通索引', course varchar(10) not null comment '科目', scorce decimal(4, 2) default 0, index `ik_student_id` (`student_id`), # 级联更新或者级联删除,可以同时加,也可以选择其一 /* 只加级联删除,不加级联更新是没有级联更新的效果 constraint `fk_student_id` foreign key (`student_id`) references ai_students(`sid`) on DELETE cascade */
/* 只加级联更新,不加级联删是没有级联删除的效果 constraint `fk_student_id` foreign key (`student_id`) references ai_students(`sid`) on UPDATE cascade */ # 级联更新&级联删除同时添加 constraint `fk_student_id` foreign key (`student_id`) references ai_students (`sid`) on delete cascade on UPDATE cascade);
insert into ai_students(name)values ('张三');insert into ai_students_sorce(student_id, course, scorce)values (2, '语文', 66.6);insert into ai_students_sorce(student_id, course, scorce)values (2, '数学', 76.6);
-- 在有级联更新的情况下,我可以直接修改主表update ai_studentsset sid=3where sid = 2;
-- 在级联删除的情况下,也同时删除从表的数据(慎用)-- 由于级联删除是物理删除,数据无法恢复的,而开发中我们是使用软删除的-- 因此真正的开发,我不要加上级联删除这个选项deletefrom ai_studentswhere sid = 3;19.3 表关系之多对多
还有更复杂的表关系,例如:多对多。 这里会在后续大家涉及orm框架的时候更进一步实现。
多对多,其实就是中间多了一张中间表,但表中有两个外键或者两个以上的外键且外键是普通索引.

数据准备:
-- 学生表CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL);
CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL);
CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT, course_id INT, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id));
-- 插入数据INSERT INTO student(name) VALUES ('张三'), ('李四');INSERT INTO course(name) VALUES ('数学'), ('英语');
INSERT INTO student_course(student_id, course_id) VALUES(1, 1),(1, 2),(2, 1);案例1: 查出学生选的课程
SELECT s.id, s.name AS student_name, c.id, c.name AS course_nameFROM student sJOIN student_course sc ON s.id = sc.student_idJOIN course c ON sc.course_id = c.id;案例2:查询张三选了哪些课程
select '张三' as student_name, c.name as course_name from course c left join student_course scon c.id=sc.course_idwhere sc.student_id=(select id from student where name='张三')案例3:查询数学课有哪些学生
select '数学' as course_name , stu.name from student stu left join student_course sc on stu.id=sc.student_idwhere sc.course_id = (select id from course where name='数学');19.4 逆范式
省市行政区逆范式表示设计:


19.5 外键
外键本质上是从表向主表做出承诺. 如
学生表,课程表,选课表
选课表向学生表承诺当学生表删除学生时,选课表会自动删除相关记录,所以,外键应该创建在选课表中.
创建外键时,需要满足的条件
- 要求主表的相关列,必须要有唯一索引或主索引.
- 要求主表与从表的相关列,类型必须完全一致. int 与 int unsigned被视为不同的类型,是不可以创建外键关系的.
用于约束本表中的列.其增加,删除,修改需要参考外表的列,这取决于on delete和on update.
删除与更新的策略:
on delete 动作列表
-
restrict: (默认策略)禁止主表删除行.
-
cascade: 级联删除,主表删除相关行时,本表自动删除.
-
set null: 主表删除相关行时,从表关联列设置为null(这种情况下,要求从表关联列允许为null,也即不能为not null.)
-
no action: 等同于restrict
on update:
- restrict: (默认策略)禁止更新主表.
- cascade: 级联更新,主表更新相关行时,本表自动更新.
- set null: 主表更新相关行时,从表关联列设置为null(这种情况下,要求从表关联列允许为null,也即不能为not null.)
- no action: 等同于restrict
# create table 添加外键.sql
create table <从表名>( ..., [constraint <外键名>] foreign key(<本表列名>) references <主表名>(<主表字段列名>) [on delete <动作>] [on update <动作>])# alter table 添加外键.sql
alter table <从表名>add [constraint <外键名>]foreign key(<本表列名>) references <主表名>(<主表字段列名>)[on delete <动作>][on update <动作>]关于索引与外键的关系
# Mysql8会在从表的相关字段上,自动添加普通索引.create table users( id int primary key, name varchar(50));
create table orders( id int primary key, user_id int, # 创建表语句中没有对user_id添加任何索引. foreign key (user_id) references users (id));# 但是在show indexes中,可以发现user_id已经添加了普通索引.show indexes from orders;19.6 视图
- 视图是一种缓存吗? 不是,视图并非用于增加查询效率的,反而因为多套了一层,降低了查询效率.
- 视图中的增删改会影响物理表吗? 会,只不过效率低,并且不支持事务处理.
创建视图
create [or replace] view <视图名称> as<select查询>;
# MySQL自动生成CREATE ALGORITHM = UNDEFINED # 指定视图的处理算法: undefined: 默认值,让mysql自己决定如何处理这个视图,通常可省略. DEFINER =`pi`@`%` # 定义者/所有者是 pi SQL SECURITY DEFINER # 定义视图的案例执行上下文,definer意思就是说,当任何用户来查询这个视图时,都将使用pi用户的权限去执行视图中的sql语句. VIEW `v_course`ASselect `course`.`cid` AS `cid`, `course`.`cname` AS `cname`, `course`.`credit` AS `credit`from `course`修改视图
create or replace view <视图名称> as<select查询>;
alter view <视图名称> as<select查询>;删除视图
drop view [if exists] <视图名称>;显示视图定义
show create view <视图名称>;20.多表查询
20.1 联合查询union
UNION 用于把多条 SELECT 的结果“纵向合并”成一张结果集。本质:行合并(上下拼)
这个查询方式,我个人是很喜欢使用的。
数据准备:
-- 正式员工表CREATE TABLE emp_full( eid INT, ename VARCHAR(20), job VARCHAR(20));
-- 实习生表CREATE TABLE emp_intern( eid INT, ename VARCHAR(20), job VARCHAR(20));
INSERT INTO emp_full VALUES(1,'张三','开发'),(2,'李四','测试'),(3,'王五','运维');
INSERT INTO emp_intern VALUES(10,'赵六','实习开发'),(11,'孙七','实习测试'),(2,'李四','测试'); -- 故意重复| 关键字 | 是否去重 | 性能 |
|---|---|---|
UNION | ✅ 去重 | 较慢 |
UNION ALL | ❌ 不去重 | ✅ 更快 |
示例:
-- 不去除重复select * from emp_fullunion allselect * from emp_intern;
-- 去重select * from emp_fullunionselect * from emp_intern;在数据清洗的技巧中,有一种按月分表场景会使用到这个联合查询。但一般是在其他的数据处理仓库使用该场景,单纯的Mysql其实用得很少。
例如:
AnalyticDB (ADB) for MySQL/PostgreSQL(阿里云) --- 这个东西旧项目可能会更多一些
MaxCompute (ODPS)(阿里云) --- 这是阿里主推的数据仓库
1782629122064
20.2 交叉查询(cross join)
交叉查询会返回两张表的笛卡尔积(Cartesian Product)
即:左表的每一行 × 右表的每一行。
特点如下:
无条件连接
结果行数 = 表A行数 × 表B行数
数据准备:
-- 尺寸表CREATE TABLE size( sid INT, sname VARCHAR(10));
-- 颜色表CREATE TABLE color( cid INT, cname VARCHAR(10));
INSERT INTO size VALUES(1,'S'),(2,'M'),(3,'L');
INSERT INTO color VALUES(1,'红'),(2,'蓝');示例:
-- 显式的使用cross joinselect * from sizecross join color;
-- 隐式的,没有cross join(这个我个人是不喜欢的)select * from size,color;20.3 自然链接(了解)
自然连接(NATURAL JOIN)是一种特殊的等值连接方式,数据库会自动根据两张表中列名相同的列进行匹配,并在结果集中合并这些同名列。
虽然自然连接语法简洁,但由于其依赖列名而非明确的连接条件,一旦表结构发生变化,可能导致查询结果错误或难以排查。因此,在实际开发中,更推荐使用 join…on / Inner join …on的等价内连接代替。
数据准备:
-- 部门表CREATE TABLE dept( dept_id INT PRIMARY KEY, dept_name VARCHAR(20));
-- 员工表CREATE TABLE emp( emp_id INT PRIMARY KEY, emp_name VARCHAR(20), dept_id INT);
INSERT INTO dept VALUES(10,'研发部'),(20,'市场部');
INSERT INTO emp VALUES(1,'张三',10),(2,'李四',10),(3,'王五',20);示例:
SELECT *FROM empNATURAL JOIN dept;NATURAL JOIN = 以下三步
- 找出两张表中 列名相同的列
- 对这些列做
AND 列 = 列- 结果集中只保留一份同名列

dept_id只出现了一次
这个程序是有隐患的,因为mysql是自动匹配列名相同的列,如果员工表的dept_id字段名修改为depart_id,那么就出问题了,所以自然连接实际开发中我们最好不要使用。.
20.4 内连接
INNER JOIN(内连接)只返回两张表中“满足连接条件的数据”
有匹配的才显示
没匹配的两边都不显示
是最常用、最重要的多表连接方式
语法规则:
SELECT 列FROM 表AINNER JOIN 表BON 表A.关联列 = 表B.关联列;需求场景:查出部门的所有员工(1<多>多>)
数据准备:
INSERT INTO dept VALUES(30,'财务部'); -- 无员工
INSERT INTO emp VALUES(4,'赵六',NULL); -- 无部门示例:
select emp_id,emp_name,emp.dept_id,dept_namefrom emp inner join dept on emp.dept_id=dept.dept_id;
-- 也可以省略innerselect emp_id,emp_name,emp.dept_id,dept_namefrom emp inner join dept on emp.dept_id=dept.dept_id;隐式内连接(这个写法以前在asp和php时代很多,个人不推荐你这样写):
SELECT e.emp_name, d.dept_nameFROM emp e, dept dWHERE e.dept_id = d.dept_id;相对自然连接来说,inner join … on之后的比对条件是我们自己写的,条件更加清晰
20.5 左右外连接
LEFT JOIN(左外连接)
LEFT JOIN 会返回左表的全部记录,即使右表中没有匹配的数据
✅ 左表有,右表没有 → 右表字段补
NULL✅ 左表没有,右表有 → 不返回
📌 口诀:
“左表全要,右表看缘分。”
RIGHT JOIN(右外连接)
RIGHT JOIN 会返回右表的全部记录,即使左表中没有匹配的数据
📌 口诀:
“右表全要,左表看缘分。”
语法规则:
SELECT 列FROM 表ALEFT JOIN 表BON 表A.关联列 = 表B.关联列;
SELECT 列FROM 表ARIGHT JOIN 表BON 表A.关联列 = 表B.关联列;示例:
select * from emp as e left join dept as d on e.dept_id=d.dept_id;
select * from emp as e right join dept as d on e.dept_id=d.dept_id;本质上,左右链接是一样的功能,它只是一个顺序的问题。根据从左到右的习惯,开发中left join出现频率会更高一些。
20.6 where和分组
经典场景 1:查询“没有对应数据”的记录
-- 没有员工的部门SELECT d.dept_nameFROM dept dLEFT JOIN emp e ON d.dept_id = e.dept_idWHERE e.emp_id IS NULL;场景 2:统计“含 0 的情况”
-- 统计每个部门有多少人SELECT d.dept_name, COUNT(e.emp_id) 人数FROM dept dLEFT JOIN emp e ON d.dept_id = e.dept_idGROUP BY d.dept_name;20.7 自连接
自连接(Self Join)不是一种新的连接类型,而是同一张表“自己连接自己”
还是内连接 / 外连接
只是 左表和右表是同一张表
通过 表别名 把它们当成两张表来用
什么时候需要自连接?
✅ 表中的某个字段的值,来源于本表的另一个字段
典型场景:
- 省市区(pid 指向本表的 id)
- 员工与上级(manager_id 指向 emp_id)
- 商品分类的父子关系
自连接实际上是逆范式的一种应用
数据准备
CREATE TABLE areas( id INT PRIMARY KEY, name VARCHAR(20), pid INT COMMENT '上级id,省为NULL');
-- 省INSERT INTO areas VALUES(1,'广东省',NULL),(2,'湖南省',NULL);
-- 市INSERT INTO areas VALUES(10,'广州市',1),(11,'深圳市',1),(12,'长沙市',2),(13,'岳阳市',2);案例 1:查询所有城市及其所属省份
select p.name,c.namefrom areas as p inner join areas as c on p.id = c.pid;案例2: 使用Group_Concat函数优化
select p.name as 省份,group_concat(c.name) as 城市from areas as p inner join areas as c on p.id = c.pidgroup by p.name;
21.子查询
子查询,又称“嵌套查询”,是指在一个 SQL 语句(SELECT、INSERT、UPDATE、DELETE)内部嵌入的另一个 SELECT 查询语句。
它是以一个查询的结果,作为另一个查询的条件或数据源。
21.1 准备员工表和部门表
-- ============================-- 创建 departments 表(部门表)-- ============================CREATE TABLE IF NOT EXISTS departments ( dept_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '部门ID', dept_name VARCHAR(50) NOT NULL COMMENT '部门名称') ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- ============================-- 创建 employees 表(员工表)-- ============================CREATE TABLE IF NOT EXISTS employees ( emp_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID', name VARCHAR(50) NOT NULL COMMENT '员工姓名', age INT COMMENT '年龄', dept_id INT COMMENT '所属部门ID') ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- ============================-- 插入部门测试数据-- ============================INSERT INTO departments (dept_name) VALUES ('技术部'), ('市场部'), ('财务部'), ('人力资源部'), ('运营部');
-- ============================-- 插入员工测试数据-- ============================INSERT INTO employees (name, age, dept_id) VALUES('张伟', 28, 1),('李娜', 25, 1),('王强', 35, 2),('赵敏', 27, 2),('陈浩', 24, 3),('刘芳', 32, 3),('孙鹏', 26, 4),('周婷', 29, 4),('吴磊', 23, 5),('郑秀英', 30, 5),('刘德华', 30, 0),('张学友', 30, 0);21.2 案例1:子查询返回1行1列数据
需求场景:获取大于公司员工平均年龄的员工
select *from employeeswhere age > (select avg(age) from employees)这种子查询称为标量子查询
21.3 案例2:子查询返回1列多行数据
需求场景:获取有部门的员工
这种子查询称为列子查询
21.4 案例3:子查询返回1行多列
需求场景:查询tb_students中年龄最小且分数最低的用户
数据准备
create table tb_students ( id int primary key auto_increment comment '主键', name varchar(20) not null comment '姓名', age tinyint default 0 comment '年龄', sorces tinyint default 0 comment '分数')charset=utf8 engine=innodb;INSERT INTO tb_students (name, age, sorces) VALUES ('张三', 18, 85), ('李四', 19, 92), ('王五', 20, 78), ('赵六', 18, 95), ('孙七', 21, 88), ('刘八', 17, 50);子查询3步走
这种子查询称为行子查询
21.5 子查询在select以外的使用
需求场景:复制数据到另外一张表
数据准备
CREATE TABLE IF NOT EXISTS tb_users ( emp_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID', name VARCHAR(50) NOT NULL COMMENT '员工姓名', age INT COMMENT '年龄', dept_id INT COMMENT '所属部门ID') ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;这个是最常用的,至于Delete和Update也可以使用子查询,但并不是很常用,可以自己通过AI尝试一下。
主要是现实的开发Delete,Update这些业务都是比较谨慎的,且是单一修改和开启事务处理的多,一般涉及不到套用子查询。
21.6 子查询作为临时表
准备数据
CREATE TABLE accounts ( emp_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, salary DECIMAL(10, 2), hire_date DATE, dept_id INT, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES departments(dept_id));
-- 插入数据INSERT INTO accounts (name, salary, hire_date, dept_id) VALUES('张三', 12000.00, '2020-03-15', 1),('李四', 15000.00, '2019-07-01', 1),('王五', 9000.00, '2021-01-10', 1),('赵六', 18000.00, '2018-11-20', 1),('孙七', 10000.00, '2020-05-08', 2),('周八', 13000.00, '2019-09-15', 2),('吴九', 8000.00, '2022-02-28', 2),('郑十', 11000.00, '2021-06-01', 3),('钱十一', 9500.00, '2022-08-12', 3),('陈十二', 8500.00, '2023-01-05', 4);-- 获取每个部门的平均工资select dept_name,ROUND(agv,2) as agv_salary from departments as d inner join(select dept_id,AVG(salary) as agv from accounts group by dept_id) as tmp on d.dept_id=tmp.dept_idwith tmp as ( select dept_id,AVG(salary) as agv from accounts group by dept_id)
select dept_name,ROUND(agv,2) as agv_salary from departments as d inner join tmp on d.dept_id=tmp.dept_id这种子查询被称为表子查询
22.窗口函数
22.1 快速上手
需求场景: 求员工工资的平均值和员工个人工资的平均值差额
select emp_id, name, salary, ROUND(avg(salary) over(),2) as avg_salary, ROUND(salary - avg(salary) over(),2) as my_salaryfrom accounts;这个场景很适合把查询用作临时表
22.2 三大排序函数
数据准备:
-- 成绩表CREATE TABLE window_student_scores ( id INT PRIMARY KEY AUTO_INCREMENT, student_name VARCHAR(20) NOT NULL, subject VARCHAR(20) NOT NULL, score INT NOT NULL);
INSERT INTO window_student_scores (student_name, subject, score) VALUES-- 数学:有并列第1名,也有并列第3名('张三', '数学', 100),('李四', '数学', 100),('王五', '数学', 95),('赵六', '数学', 90),('孙七', '数学', 90),('周八', '数学', 85),
-- 英语:无并列,方便对比('张三', '英语', 88),('李四', '英语', 92),('王五', '英语', 78),('赵六', '英语', 92),('孙七', '英语', 85),('周八', '英语', 88);22.4 理解三大排序函数
RANK() : 可并列,不可连续DENSE_RANK() : 可并列,还连续ROW_NUMBER(): 不并列,很连续如图所示:

语法:
RANK() + over()DENSE_RANK() + over()ROW_NUMBER() + over()22.5 over分组和排序说明
over如果里面什么都不写就是全表数据框选。
OVER() # 这个操作如果在排序中通常情况下达不到你要的效果。over如果写东西,最常见是是下面两个点:
PARTITION BY:
按谁分组(比如按科目、按部门), 作用等同于group by,但PARTITION BY只能用于窗口函数
ORDER BY asc|desc:在框里按什么排序(比如按分数高低)
OVER (PARTITION BY 分组字段 ORDER BY 排序字段)需求场景:实现按科目的分数高低啊排名

select student_name,subject,score, rank() over(partition by subject order by score desc) as `rank`, dense_rank() over(partition by subject order by score desc) as `dense_rank`, row_number() over(partition by subject order by score desc) as `dense_rank`from student_scores;22.6 topN问题
数据准备
CREATE TABLE topN_employee ( id INT PRIMARY KEY AUTO_INCREMENT comment '主键id', name VARCHAR(50) NOT NULL comment '姓名', hire_date DATE comment '入职时间', dept_name varchar(20) comment '部门名称');
INSERT INTO topN_employee (name, hire_date, dept_name) VALUES-- 研发部(5人)('张三', '2020-03-15', '研发部'),('李四', '2019-07-01', '研发部'),('王五', '2021-01-10', '研发部'),('赵六', '2018-11-20', '研发部'),('钱七', '2022-05-08', '研发部'),
-- 市场部(5人)('孙八', '2020-06-12', '市场部'),('周九', '2019-09-15', '市场部'),('吴十', '2021-03-22', '市场部'),('郑十一', '2018-08-01', '市场部'),('王十二', '2022-01-18', '市场部'),
-- 财务部(5人)('冯十三', '2020-04-10', '财务部'),('陈十四', '2019-11-05', '财务部'),('褚十五', '2021-07-19', '财务部'),('卫十六', '2018-12-25', '财务部'),('蒋十七', '2022-02-14', '财务部'),
-- 人事部(5人)('沈十八', '2020-08-23', '人事部'),('韩十九', '2019-05-30', '人事部'),('杨二十', '2021-09-11', '人事部'),('朱廿一', '2018-06-17', '人事部'),('秦廿二', '2022-04-03', '人事部');需求场景: 查找每个部门最早入职的2名员工
with employee_date_rank as ( select name,hire_date,dept_name, row_number() over(partition by dept_name order by hire_date asc) as 'date_rank' from topN_employee)
select * from employee_date_rank where date_rank in(1,2)22.7 over框选+聚合函数

--- 准备数据CREATE TABLE over_employee ( id INT PRIMARY KEY AUTO_INCREMENT comment '主键id', name VARCHAR(50) NOT NULL comment '姓名', salary DECIMAL(10, 2), hire_date DATE comment '入职时间', dept_name varchar(20) comment '部门名称');---- 插入测试数据
INSERT INTO over_employee (name, salary, hire_date, dept_name) VALUES-- 技术部 (5人)('张伟', 25000.00, '2020-03-15', '技术部'),('李强', 28000.00, '2019-07-21', '技术部'),('王磊', 22000.00, '2021-01-10', '技术部'),('赵鹏', 32000.00, '2018-11-03', '技术部'),('刘洋', 26500.00, '2020-09-28', '技术部'),
-- 市场部 (5人)('陈静', 18000.00, '2021-05-12', '市场部'),('杨敏', 19500.00, '2020-08-19', '市场部'),('黄丽', 21000.00, '2019-12-01', '市场部'),('周婷', 17500.00, '2022-03-25', '市场部'),('吴佳', 20000.00, '2021-07-14', '市场部'),
-- 财务部 (5人)('孙浩', 23000.00, '2018-04-10', '财务部'),('马飞', 21500.00, '2019-09-22', '财务部'),('朱明', 24000.00, '2020-06-15', '财务部'),('胡伟', 20500.00, '2021-02-28', '财务部'),('林杰', 25000.00, '2018-12-05', '财务部'),
-- 人力资源部 (5人)('郭芳', 17000.00, '2021-08-11', '人力资源部'),('何秀', 18500.00, '2020-03-30', '人力资源部'),('高远', 19000.00, '2019-10-16', '人力资源部'),('罗辉', 16500.00, '2022-01-20', '人力资源部'),('梁雪', 17800.00, '2021-06-07', '人力资源部'),
-- 运营部 (5人)('韩宁', 20000.00, '2020-02-14', '运营部'),('唐磊', 21500.00, '2019-05-23', '运营部'),('于波', 19500.00, '2021-09-18', '运营部'),('冯涛', 22000.00, '2018-07-09', '运营部'),('曹阳', 20500.00, '2020-11-30', '运营部'),
-- 产品部 (5人)('邓超', 26000.00, '2019-01-15', '产品部'),('许晴', 24500.00, '2020-04-22', '产品部'),('贾楠', 27500.00, '2018-08-10', '产品部'),('丁怡', 23000.00, '2021-03-17', '产品部'),('薛峰', 25000.00, '2019-12-05', '产品部'),
-- 销售部 (5人)('阎军', 18000.00, '2021-07-01', '销售部'),('崔莹', 19500.00, '2020-10-14', '销售部'),('任杰', 21000.00, '2019-06-25', '销售部'),('姚斌', 17500.00, '2022-02-18', '销售部'),('沈琪', 19000.00, '2021-09-08', '销售部'),
-- 行政部 (5人)('傅蓉', 16000.00, '2022-01-10', '行政部'),('潘昊', 17500.00, '2021-04-19', '行政部'),('蔡蕾', 16800.00, '2020-08-25', '行政部'),('余刚', 18200.00, '2019-11-12', '行政部'),('杜娟', 17000.00, '2021-05-03', '行政部');常见聚合函数+over的基本语法:
SUM()+over():求和AVG()+over():求平均COUNT(*)+over():计数MAX()+over():最大值MIN()+over():最小值....需求场景<随着员工的入职>随着员工的入职>,每入职一个员工,工资就累计放发的情况
select name,hire_date,dept_name,salary, sum(salary) over(partition by dept_name order by hire_date asc rows between unbounded preceding and current row ) as total_salaryfrom over_employee;如下代码也可以实行同等的效果
select name,hire_date,dept_name,salary, sum(salary) over(partition by dept_name order by hire_date asc) as total_salaryfrom over_employee;22.8 数据清洗的基本认识
-
问题来了:rows between unbounded preceding and current row 这种代码什么使用会使用?
-
答案:以上代码看似完美无限。但现实中,公司员工编号的顺序实际上就已经代表了它的入职时间,有写HR的后台入职时间有可能是留空或者随便填上去的。按照这种情况,再要完成上述的需求我们不可能再以时间排序的。

1782405388229
rows between unbounded preceding and current row 这类型的代码其实是“数据清洗”技术的一种方案.
-- 以下两种方式其实都能达到该效果
select id,name,hire_date,dept_name,salary, sum(salary) over(partition by dept_name rows between unbounded preceding and current row ) as total_salaryfrom over_employee;
select id,name,hire_date,dept_name,salary, sum(salary) over(partition by dept_name order by id asc ) as total_salaryfrom over_employee;
-- 所以一般的开发约定就是“数据清洗”我们就把两条语句同时写上,看起来就好像把两者合并在一起
select id,name,hire_date,dept_name,salary, sum(salary) over(partition by dept_name order by id asc rows between unbounded preceding and current row) as total_salaryfrom over_employee;注意:数据清洗是有很多方案的,这个只是比较典型的一种.
22.9 窗口函数应用案例2则
需求场景:针对全表,在不分组的情况下,计算每个员工和前后相邻员工的平均薪资
select name,dept_name,salary, round(avg(salary) over(rows between 1 preceding and 1 following ),2) as avg_salaryfrom over_employee;需求场景:每个部门,按入职时间排序,计算部门第一个员工到当前员工的累计薪资
select name,hire_date,dept_name,salary, round(avg(salary) over(partition by dept_name order by hire_date asc rows between unbounded preceding and current row ),2) as total_salaryfrom over_employee;进行数据清洗:
select name,hire_date,dept_name,salary, round(avg(salary) over(partition by dept_name order by id asc rows between unbounded preceding and current row ),2) as total_salaryfrom over_employee;支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!






















