MySql数据库系统基础

MySql数据库系统基础

MySql数据库系统基础

课程目标

  • 如何使用MySQL数据库(如何安装和基本语法,工具的使用)
  • 如何设计数据库(一种策略、思想,与经验有关)
  • SQL编程
text
我们要学四种语言:
1.标签语言:HTML & CSS(执行在浏览器端)
2.逻辑语言: Python,Java,Go,PHP...(执行在服务器端)
3.MySQL编程语言(执行在MySQL服务器端)
4.JS,TS逻辑语言(执行在浏览器端)

在今天的AI时代,实际上学习数据库是比以前容易了很多。

今天目标(2026-07-01):

sql
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数据库系统来说,它是如下模式:

text
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和向量数据库

https://db-engines.com/en/ranking

2. 关系型的定义

所谓的关系型数据库,就是基于关系模型的数据库,一个关系模型其实就是一张二维表!

而一张二维表往往对应着现实世界中的一个实体集!

什么是实体和实体集?

实体是人类观念世界中描述客观事物的一种概念,可以是具体的事物,比如一本书,一个人,一个手机,一条街等,也可以是抽象的事物,比如一种感受,一个电子订单!

同一类实体的所有实例就构成了一个实体集,实体集就是实体的集合,每一个实体都是该实体集的一个实例,实体与实体集之间的关系有点类似于数学上的元素与集合的关系!

实体集反映到数据库中,就是一张一张的二维表!

比如,我们现在的有2种实体集:

学生实体集,教师实体集,也就分别对应着数据库中的2张数据表:学生表,教师表!

1782428659676

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

img

思考:假如为一个酒店管理系统设计一个数据库,需要设计哪些表?

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版本

年份版本标志性事件
19951.0诞生
20055.1支持视图、存储过程
20105.5InnoDB 成默认引擎
20155.7原生 JSON(曾经的"生产主力")
20188.0🔥架构级重构,当前教学/生产首选
20248.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是非常简单,如下图所示:

1782432916570

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

1782432981175

3.4 mysql的配置文件

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

1782439396014

4.mysql8的命令行

4.1.打开mysql8命令行

先找到你安装phpstudy的地方,例如我的安装目录是:D:\tools\phpstudy_pro

1782433626856

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

1782433769871

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

1782438312063

然后输入cmd命令,回车

1782433867153

就能打开mysql的命令行

1782438341336

注意: 务必先启动mysql8,否则无法后面的操作

注意:phpstudy还是最好不要加mysql8的环境变量

4.2 打开mysql控制台

在phpstudy中,安装了mysql8后,默认的用户名和密码都是root,我们可以通过如下命令打开控制台:

powershell
# 方式一
mysql -uroot -proot [数据库名]
# 方式二
mysql -u <用户名> -p [数据库名]

1782438706526

回车就可以进入控制台:

1782438759111

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 地址用于唯一标识网络中的每一台设备。它充当设备的“地址”,使得数据能够在网络中准确地找到目标设备。
properties
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、SocketUDPHTTPFTP 等,它们规定了数据如何被分割、传输、接收和重组。

在计算机当中127.0.0.1这个是一个默认的本地IP地址,也可以用localhost来表示。

Mysql是采用了Tcp/IP协议,它自定义了应用层.

MySQL 使用的是基于 TCP 的自定义应用层协议

本地连接可以用 Unix Domain Socket(常被误叫“Socket 协议”)

1782807309714

5.命令行基本操作

5.1 show命令

powershell
-- 展示所有的数据库有哪些
show databases;  

1782440725877

powershell
-- 展示引擎有哪些
show engines;

1782440803311

在开发中我们一般使用InnoDB比较多,你简单理解就是这个引擎的功能更完整

powershell
-- 展示数据库创建的语句
show create database demo; 

# 额外补充
# 还有一种显示表定义的语句,只不过不是DDL,而是结构化输出,是给人看的
desc <表名>
describe <表名>

1782441063015

powershell
-- 展示当前数据库使用默认编码字符集
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE '%char%';

1782441378456

这种命令你有个印象就差不多,在AI时代的今天,你是可以vibe coding的(比如:通义灵码)或者你问豆包/元宝也可以。

5.2 \G显示方式

1782440978852

5.3 exit命令

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

1782442062438

6.存储引擎

6.1 修改存储引擎

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

1782439396014

1782442335006

注意:改完记得保存

1782442428969

在命令行中,输入show engines

1782442594174

6.2 Innodb和MyISAM引擎

6.2.1 简单理解Innodb和MyISAM引擎

白话理解innodb: 它是一个功能齐全的存储引擎且安全性高. 但查询性能稍差.

白话理解MyISAM: 它是一个功能简单的存储引擎,无安全能力. 但查询性能很强.

选用存储引擎示例:

登录:

1. 输入手机号+密码. 
1. 检验手机号是否存在(查询). 
1. 判断密码是否正确(查询)

结论: 选用MyISAM,原因是需要高速查询.

支付: 张三给李四转账100

  1. 判断张三是否有100元(查询)
  2. 在张三账户-100,在李四账户+100(修改,写,需要事务处理(保持原子操作).)
  3. 返回李四的余额和张三的余额(查询)

结论: 选用Innodb: 第二步需要事务处理(原子操作),若出现问题,银行直接倒闭.

6.2.2 面试八股文(Leet Code)

  1. 引擎的作用域

    所有的存储引擎是作用在表上,而不是数据库上.

    sql
    create database nice;
    
    use nice;
    
    create table users
    (
        id   int comment '用户编号',
        name varchar(20) comment '姓名'
    );
    
    # 在这里,可以发现 ENGINE=InnoDB
    show create table users;
    
    
    create table accounts
    (
        id   int comment '用户编号',
        name varchar(20) comment '姓名'
    ) engine = myisam;
    
    # 在这里,可以发现 ENGINE=MyISAM
    show create table accounts;
  2. innodb和MyISAM的文件结构

    mysql8中

    Innodb是以单个文件形式存储的,它会把数据与结构存放在一起,扩展名为.ibd.

    MyISAM是以多个文件形式存储的.

    • .sdi: 数据结构文件,在mysql8中结构被mysql使用json格式优化了.
    • .myi: 索引文件.
    • .myd: 数据文件.

    在mysql5中

    Innodb是以两个文件形式存储的,会将数据结构独立出来.

    • .ibd: 数据与索引文件
    • .frm: 数据结构文件

    MyISAM是以多个文件形式存储的.

    • .frm: 数据结构文件.
    • .myi: 索引文件.
    • .myd: 数据文件.
  3. MyISAM在mysql8之前的功能(了解)

    MyISAM是具有数据压缩功能和数据结构修复功能,但在mysql8中,功能被弃用.

    在mysql5中修复MyISAM表的命令

    • 在mysql5的bin目录下打开命令行
    powershell
    myisamchk <.myi文件路径>

    在mysql5中压缩MyISAM表的命令

    • 在mysql5的bin目录下打开命令行
    powershell
    myisampack <.myi文件路径>

    对于历史数据有用

    注意: 表在被修复和压缩过后,将会变为只读.

6.3 Arhive引擎

mysql8推荐用于取代MyISAM的方案

archive引擎同时具有安全写,高性能写入,压缩的功能.

不支持删除,修改.

6.4 并发

行锁:

MyISAM不支持行锁

innodb支持行锁功能

7.DataGrip和通义灵码

7.1 安装和绿化

请参考如下资料和观看视频进行实操:

1782442771481

7.2 使用DataGrip链接Mysql

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

1782443044059

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

1782894546961

1782894585605

1782894719700

1782894776489

1782894839037

1782894918361

1782895002955

7.4 通义灵码配置和连接Mysql

1782895441512

1782895490150

最新的2026版本的DataGrip是可以集成通义灵码的,如果有兴趣的同学可以去闲鱼找找

但DataGrip是不是最新版的其实无所谓,所以这里就使用通义灵码的IDEA辅助就可以了。

在Python阶段,可以在Pycharm中安装最新版本。

7.3 MySQL注释符

单行注释:

# 注释内容

– 注释内容,这里的—与注释内容之间必须有一个空格!

多行注释

/* 注释内容 */

8.DDL之数据库操作

8.1 创建数据库语法规则

sql
CREATE <DATABASE | SCHEMA> [IF NOT EXISTS] db_name
    [CHARACTER SET <charset_name>]
    [COLLATE <collation_name>];

DATABASESCHEMA完全等价,可互换使用。

需求场景:创建名为ecmall的数据库,创建名为easycms的数据库

sql
create database ecmall;
create schema easycms;

推荐写法

sql
create database if not exists itcast;

完整写法(了解,不常用):

sql
CREATE DATABASE IF NOT EXISTS test_db
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

8.2 use语句

格式:

sql
 use 数据库名字;

示例:

text
use demo;  # 使用或者切换数据库为demo

有时候我们会在登录控制台的时候就使用某一个数据库了:

powershell
mysql -uroot -proot echop

1782478368398

加上数据库不存在,那么mysql客户端会报错 ERROR 1049 (42000): Unknown database ‘aaaaabbbbcccc’

8.4 查看当前数据库

格式:

sql
select database();

1782478392929

8.5 删除数据库

格式:

sql
drop database [if exists]  数据库名称
drop schema [if exists]  数据库名称

示例:

sql
drop schema ecmall;
drop database itcast;

删除数据库尽量不要使用if exists ,最好让数据库有报错的可能性

8.6 修改数据库(了解)

sql
ALTER DATABASE test_db
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

这个学不学都无所谓

注意: 请不要忘记前面学过的show databases;命令

mysql中的数据库存储位置可以在my.ini中配置

ini
# 若缺省,则使用运行目录下的./data文件夹
datadir=D:/phpstudy_pro/Extensions/MySQL8.0.12/data/

每个数据库在data文件夹中都有一个单独的同名文件夹

今天目标(2026-07-02):

powershell
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的值签到二进制记录,布隆过滤器。

示例:

sql
格式: 字段名 数据类型 [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字符变长二进制字符串
tinytext0-255字符可变长度文本
text0-65 535字符可变长度文本文章内容,
mediumtext0-16 777 215字符可变长度文本
longtext0-4 294 967 295字符可变长度文本
tinyblob0-255字符可变二进制文本
blob0-65 535字符可变二进制文本小图标、二进制的认证信息
mediumblob0-16 777 215字符可变二进制文本
longblob0-4 294 967 295字符可变二进制文本
json0-4 294 967 295字符可变二进制json文本 (也叫bson: binary json)主要实现一些NoSQL数据的存储

这里有两个经典的面试八股文

1.char与varchar的区别:

text
存入 'abc':
CHAR(10)→ 存 'abc       '(补 7 个空格)
char若超过指定的长度,直接截断,不报错

VARCHAR(10)→ 存 'abc'(只占 3 个字符 + 长度标记)

2.varchar字符串和text文本的区别

text
varchar可指定n,text不能指定 
text类型不能有默认值,注意json也不能有默认值。(但是在8.0.13版本后,支持常量默认值default 'textDefaultValue')
varchar可直接创建索引,text创建索引要指定前多少个字符。varchar查询速度快于text。
索引:index,主要为了加快查询数据的数据的一种技术,类似书籍的目录。

9.3 日期类型

表示时间值的日期和时间类型为 DATETIME、DATE、TIMESTAMP、TIME 和 YEAR。

每个时间类型有一个有效值范围和一个"零"值,当指定不合法的 MySQL 不能表示的值时使用"零"值。

数据类型取值范围日期格式零值使用场景
year1901~2155YYYY0000电影年份,图书年份
date1000-01-01~9999-12-31YYYY-MM-DD0000-00-00生日
time-838:59:59~838:59:59HH:MM:SS00:00:00餐牌时间,会议时间
datetime1000-01-01 00:00:00~9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS0000-00-00 00:00:00添加时间,更新时间,删除时间,登陆时间
timestamp1970-01-01 00:00:01~2038-01-19 03:14:07YYYY-MM-DD HH:MM:SS0000-00-00 00:00:00添加时间,更新时间,删除时间,登陆时间

9.4 枚举与集合

enum中文名称叫枚举类型,它的值范围需要在创建表时通过枚举方式显示。ENUM只允许从值集合中选取单个值,而不能一次取多个值。SET和ENUM非常相似,也是一个字符串对象,里面可以包含0-64个成员。根据成员的不同,存储上也有所不同。set类型可以允许值集合中任意选择1或多个元素进行组合。对超出范围的内容将不允许注入,而对重复的值将进行自动去重

类型大小 (字节)用途
enum对1-255个成员的枚举需要1个字节存储; 对于255-65535个成员,需要2个字节存储; 最多允许65535个成员。单选:选择性别,现居地城市
set1-8个成员的集合,占1个字节 9-16个成员的集合,占2个字节 17-24个成员的集合,占3个字节 25-32个成员的集合,占4个字节 33-64个成员的集合,占8个字节多选:兴趣、爱好、标签

枚举是单选的

集合是多选的

集合的特殊用途

name, hobbies

张三 1,2

李四 1,3

王五 1,2,3

将所有值有1的人查找出来

sql
select * from users where find_in_set('1',hobbies)>0;

# find_in_set('<值>',<字段名>) # 返回结果: 位置索引(下标从1开始).

9.5 自增列

sql
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(创建表)

语法规则:

sql
CREATE TABLE [IF NOT EXISTS] 表名 (
    列定义1,
    列定义2,
    ...
    [表级约束]
) [ENGINE=InnoDB] [DEFAULT CHARSET=utf8mb4] [COMMENT='表注释'] [auto_increment = <>];

列定义规则:

sql
列名 数据类型 [NOT NULL | NULL] [DEFAULT 默认值]
[AUTO_INCREMENT]
[UNIQUE [KEY]]
[PRIMARY KEY]
[COMMENT '列注释']

示例:

sql
# 设计一张会员表

10.2 复制表结构

语法规则:

sql
create table <表名> like <源表名>;

示例:

sql
# 创建store_members表.
# 除了爱好字段以外,其它字段都与ai_members相同.
# 1. 复制表结构
create table store_members like ai_members;

10.3 ALTER TABLE(修改表)

一旦涉及到修改表,你当初设计表的时候肯定多少存在一些不太合理的地方。

语法规则:

sql
ALTER TABLE <表名> 操作类型;

示例:

sql
# 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 清空表

语法规则:

sql
delete from 表名; # 删除所有记录,保留表状态
truncate table 表名; # 截断表,不保留表状态

delete与truncate的区别

两个命令都会把数据清空,但是delete会删除数据,保留表的历史状态,而truncate会清空数据并清楚表的历史状态。

11. DML数据库表操作

DML 的核心操作无非“增删改”,而在应用开发中,这些操作大多已由 ORM 框架封装完成。至于查询(SELECT,隶属 DQL),在表关系简单的场景下,ORM 同样能胜任;但对于需要深入挖掘数据或性能优化的复杂查询,手写原生 SQL 仍是必修课。虽然 SELECT 语句的复杂度往往取决于业务场景与个人经验,但随着 AI 辅助编程工具的普及,即便是普通开发者,如今也能借助 AI 轻松驾驭高阶的查询逻辑。

11.1 insert语句(增)

insert语句就是对表进行记录添加操作.

语法

sql
# 单行插入与多行插入
insert into <表名>[(<字段列表>)] values (<值列表>)[,(<值列表>)][,(...)];
insert into <表名>[(<字段列表>)] values (<值列表>)[,(<值列表>)][,(...)];

# 复制表数据
# 当然,可以只复制某几列,或者给列指定一个计算列或者只复制某些行,只需要修改select就可以了.
# 若列名不同,则需要指定字段列表.
insert into <表名>[(<字段列表>)] select * from <表名> [where <条件>]

示例:

sql
# 单行插入
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_members
values (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_copy
select *
from pet;

11.2 select语句(查)

select语句就是对表进行查的操作.

示例:

sql
# 查询所有字段
select *
from ai_members;

在select中使用where子句

sql
# 一般查询
select id, mobile
from ai_members
where id = 5;

11.3 update语句(改)

update语句就是对表进行记录修改的操作.一般都需要配合where子句进行

语法

sql
update <表名> set <字段名> = <> [where <条件>];

11.4 delete语句(删)

update语句就是对表进行记录删除的操作(是物理删除,直接在磁盘中把记录删除了,一般是无法恢复的),在实际开发中,有90%以上的需求是软删除

示例

sql
delete from <表名> [where <条件>]

11.5 软删除操作

在基础课程中,我可以先跳过这个实操,后面可以配合Python的一些ORM做这个操作。

这里大家可以了解一下原理:

通过在表中添加支持null的字段delete_at来支持软删除.

当为null时,代表数据没有被删除,当该字段被赋予值时,代表已经被删除了.

当删除时,使用update 更新delete_at的值来代表软删除.

查询时,使用is null查询未删除的数据,使用is not null查询已删除的数据.

恢复数据时,只需要使用update将delete_at设置为null即可.

sql
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数据库的导出(备份)和导入(恢复)

这个技术是运维的工作.

bash
# 导出
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 / keyindex / key (字段名)给对应的字段的值设置添加索引(目的让当前字段的值在被删除,修改,查询时,加快执行速度)
外键索引foreign keyconstraint 外键名 foreign key 字段名 [,字段名2,…] references <主表名> 主键列1 [,主键列2,…]用来建立主表与从表的关联关系,为两个表的数据建立连接,约束两个表中数据的一致性和完整性。

12.1 默认值约束

default 的应用场景: 年龄默认值,性别默认值

示例:

sql
age tinyint unsigned default 18 # 年龄
gender enum('男','女','保密') dfault '保密' #性别

12.2 非空约束

not null的应用场景:账号,手机号码

示例:

text
username varchar(16) not null # 用户名
phone char(11) not null # 手机
idcard varchar(22) not null # 大陆身份证号码

12.3 唯一性约束(索引)

unique的应用场景: 手机号码,身份证号码

当表的相关列中已经有重复值时,无法对这些列创建唯一索引.

示例:

text
# 创建方式一: 
alter table <表名>
    add unique index <索引名> (id_card);
    
# 创建方式二: 注意这个是有on关键字的
create unique index <索引名> on <表名> (<单个字段>);

# 额外: 联合索引,两个索引使用同一个名字
create unique index <索引名> on <表名> (<字段名列表>);

当为某一字段创建了唯一性约束,同时也为这个字段标记为索引字段

12.4 主键约束(索引)

primary key的应用场景:id,username. 主键也是唯一的,同样起到了唯一性的作用。同时它具有not null的作用

示例:

sql
id int unsigned primary key auto_increment

12.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查看索引的情况

sql
# 如果有and,or,in,between,like等运算符,就是证明where子句具有逻辑
# 如果逻辑不当,可能索引会失效.
# 如果逻辑合理,但逻辑非常复杂,也有可能会导致索引失效.
# mysql8是拥有查询缓存的.
# 用上索引最好的结果是const,次结果是range,最差的结果是all.
explain
select * from user where id = 1 and username = 'sj';

13.3

14.索引的创建和删除

在 MySQL 中,创建索引主要有以下几种常用方式,简单说明如下:

14.1 建表时直接创建

sql
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 已存在的表添加索引(最常用)

sql
-- 普通索引
create index <索引名称> ON <表名>(<字段名>);

-- 唯一索引
create unique index <索引名称> ON <表名>(<字段名>);
CREATE UNIQUE INDEX uk_email ON user(email);

14.3 使用 ALTER TABLE 创建索引

sql
# alter table <表名> add [unique] index <索引名>(<字段名>)
ALTER TABLE user ADD INDEX idx_name (name);
ALTER TABLE user ADD UNIQUE INDEX uk_email (email);

14.4 查看/删除索引

sql
-- 查看
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 单表查询核心语法

sql
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 数据准备

sql
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:查询全部 / 指定列

sql
select * 
from students;

select name as cnname, score
from students

案例2:带条件查询

sql
-- 成绩 ≥ 80 的学生
select name as cnname, score
from students
where 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

sql
-- 男生且成绩80~90
# between是包括两个边界的,两个边界都会被返回.
select * from students
where gender='男'
  and score between 80 and 90;

-- 成绩是88或95
SELECT * FROM students
WHERE 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个)'_明%'李明、王明辉
'张__'张三丰(三个字)
sql
-- 查询姓“张”的学生
SELECT * FROM students
WHERE name LIKE '张%';

-- 查询名字中包含“明”的学生
SELECT * FROM students
WHERE name LIKE '%明%';

-- 查询名字以“丰”结尾的学生
SELECT * FROM students
WHERE name LIKE '%丰';

-- 查询姓名为两个字的学生
SELECT * FROM students
WHERE name LIKE '__';

-- 查询姓“张”且名字为三个字的学生
SELECT * FROM students
WHERE name LIKE '张__';

案例5:去重,排序

一般不会使用去重,因为去重量会导致索引失效,去重一般在程序中的set集合中去除.

sql
-- 去除重复的成绩
select distinct score from students;

-- asc表示从小到大(升序),desc表示从大到小(降序)
-- 按照分数的升序进行排序
select * from students order by score asc;
-- 如果省略asc或者desc,那么默认就是asc
select * 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开始.

sql
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层面,注入一个恒成立的条件,进面绕过字符串比对.

sql
-- 赋值逻辑的功能
select 1=1 as a;
select ''='' as b;

-- 创建一张表是会员登录表login_members,字段有id,username,password
create 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()求最小值

案例: 平均分、最高分、人数

sql
-- 平均分、最高分、人数
SELECT
  AVG(score) as 平均分,
  MAX(score) as 最高分,
  COUNT(*) as 人数
FROM students;

扩展:count(1)是什么意思

如果没有where 子句count的性能很高,因为是获取的optimized中的内容.

有where子句时,有可能会使用不到索引.

sql
# 获取表中行计数
select count(1) from <表名>;

-- count()是一个函数
-- * 是一个参数,count(*)统计表有多条记录
-- count并不是一定要用*来做占位的
select count(*) as rsCount from students;
select count('a') rstotal from students;

案例:求总分和平均分,并且小数保留2位

sql
# sum求总分
select sum(score) as 总分 from students;
# round一般会配合sum,avg使用,保留多少位小数
select round(avg(score),2) as 平均分 from students;

15.5 GROUP BY 分组查询

思考: 为什么需要分组(Group By) ?

前面学的聚合函数是 “整张表算一次”

实际业务中,往往需要:

“先分组,再分别统计”

例如:

按性别统计人数

按科目统计最高

语法规则:

sql
SELECT 分组列, 聚合函数()
FROM 表名
[WHERE 条件]
GROUP BY 分组列
[ORDER BY ];

案例 1:统计男女人数

sql
-- 如果没有分组,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 students
group by gender;

案例 2:按性别统计平均成绩

sql
select gender as 性别,round(avg(score),2) as 平均分 from students
group by gender;

案例 3:只统计成绩 ≥ 80 学生的平均分,再按性别分组

sql
-- 步骤1: 只统计成绩 ≥ 80 学生的平均分
select round(avg(score),2) as 平均分 from students where score>=80;

-- 步骤2: 只统计成绩 ≥ 80 男学生的平均分
select round(avg(score),2) as 平均分 from students
where score>=80 and gender='男'; #85.28
-- 步骤3: 只统计成绩 ≥ 80 女学生的平均分
select round(avg(score),2) as 平均分 from students
where score>=80 and gender='女'; #95.80

-- 只统计成绩 ≥ 80 学生的平均分,再按性别分组
select gender 性别,round(avg(score),2) 平均分 from students
where score>=80 group by gender;

WHERE 作用于 分组前

15.6 HAVING 分组后筛选

where用于筛选记录,having用于筛选分组.

sql
SELECT 分组列, 聚合函数()
FROM 表名
GROUP BY 分组列
HAVING 聚合条件;

基础案例:只显示平均成绩 ≥ 85 的性别组

sql
select
    gender,
    count(1) as 人数,
    avg(score) as 平均分
from students
group by gender
having avg(score)>=85;

优化代码后:

sql

综合案例

sql
SELECT
    gender AS 性别,
    COUNT(*) AS 人数,
    AVG(score) AS 平均成绩,
    MAX(score) AS 最高分,
    MIN(score) AS 最低分
FROM students
WHERE score IS NOT NULL
GROUP BY gender
HAVING AVG(score) >= 80
ORDER 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函数

数据准备:

sql
CREATE TABLE stu_course(
  stu_name VARCHAR(10),
  course VARCHAR(10)
);

INSERT INTO stu_course VALUES
('张三','语文'),
('张三','数学'),
('李四','英语'),
('李四','物理');

需求场景:一条记录显示每个学生的姓名和对应的课程

学生姓名课程列表
张三语文,数学
李四英语,物理
王五体育,音乐,艺术
sql
select stu_name,GROUP_CONCAT(course) as 选课列表 from stu_course group by stu_name;

重点2: find_in_set

功能强大,但性能一般,适合做内部系统场景

用于在集合中查询一个值

find_in_set始终无法利用索引.

语法规则:

sql
find_in_set(关键字,字段名)

数据准备:

sql
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`);
sql
# 其实这样也是是可以查出来的
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字段的多值索引.

sql
# 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函数返回的结果>)

数据准备:

sql
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是都不能建立索引

sql
# 创建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
# 这就是多值索引,数组中有多少个元素,就会拆成几条索引项,但都指向了同一行.
sql
SELECT * FROM bjxz_sickers_json WHERE JSON_CONTAINS(drugs, '"地芬尼多"'); # 注意必须是"地芬尼多"

-- 不过我喜欢写成这样子,不过Member of好像要8.0.17+才能支持
SELECT *
FROM bjxz_sickers_json
WHERE '地芬尼多' MEMBER OF (drugs);



-- 把用了"地芬尼多"或者"左旋多巴"的患者找出来,这样有可能用不上索引的
SELECT * FROM bjxz_sickers_json WHERE
    JSON_CONTAINS(drugs, '"地芬尼多"')
    or
    JSON_CONTAINS(drugs, '"左旋多巴"')
    ;

-- 你可以把上面的语句优化成下面这样子,就可以用上索引了
SELECT *
FROM bjxz_sickers_json
WHERE '地芬尼多' or '左旋多巴' MEMBER OF (drugs);

17.流程控制函数

数据准备:

sql
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块.

sql
select if(<条件>,<then >,<else >) as <列名>
from <表名>

示例:

sql
select
       name,
       if(gender='男','先生','小姐') as sex
from bjxz_sickers_json;

17.2 IFNULL函数

当列返回null时,将会使用给定的值进行替换.

sql
select ifnull(<字段名>,<>)
from <表名>

示例:

sql
select
  stu_name,  # course varchar(20) default '这家伙很懒,啥也没有选'
  ifnull(course,'这家伙很懒,啥也没有选') as lesson
from stu_course

17.3 CASE分支

sql
select 
	case
		when <条件1> then <then 1>
		when <条件2> then <then 2>
		...
		else <else块>
    end as <列名>
from <表名>

示例:

sql
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语句和分页

sql
page -> 表示当前页
# 路径参数
http://kulve.tech/news/:page
# 示例: 页为3
http://kulve.tech/news/3

# 查询参数
http://kulve.tech/news?page=<页码>
# 示例: 页为3
http://kulve.tech/news?page=3
sql
# 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

第一范式是最容易满足的,就是要求把各个数据设计成一个一个单独的信息,不能再进行拆分!

也就是说,字段里面的数据都可以直接被外部所调用,而不是提出出来之后还要进行分割!

1782619395720

上面的数据表就不满足第一范式!

解决方案:对上面的姓名和性别进行拆分即可!

1782619515874

很显而易见的事情是:就算你不知道范式,实际上你是不是也用上了?

第二范式2NF

第二范式就是在满足第一范式的基础之上,满足以下两个条件:

1.增加唯一性的标识。这个简单的理解就是加上主键。

2.记录之间不要具有依赖性(这是标准的),主要是因为这样会产生数据冗余

思考:下图的例子符合第2范式吗?

1782620309612

实际上,有些人故意违背条件2这个标准用数据冗余来达到优化数据的效果和降低编程的难度。

这种操作叫逆范式。(在自连接部分,我们来实现一下)

第三范式3NF

第三范式就是在满足第二范式的基础之上消除传递依赖!简单理解,就是把数据分到另外一张表当中,使用外键进行关联。

注意:

范式是一种理想的规范,不是绝对的标准,一般的做法是先满足数据库设计的要求,再进行优化处理!有时候为了提高效率或者使用方便,还会故意违反范式!

19.2 表关系之1对1

概念

一对一(1∶1)关系是指:

实体集 A 中的一个实体,在实体集 B 中最多对应一个实体;

反之,实体集 B 中的一个实体,在实体集 A 中也最多对应一个实体。

比如: 1个用户对应1个用户详情。

从技术实现的角度:从表中对应主表的那个字段,既是外键也是unique唯一索引

1782623761564

示例:

sql
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); # error
insert into user_details(user_id)
values (1); 

19.3 表关系之1对多、多对1

概念

多对一(belongs to)

一对多(has many,1∶N)关系是指:

实体集 A 中的一个实体,可以对应实体集 B 中的多个实体

而实体集 B 中的一个实体,最多只能对应实体集 A 中的一个实体

比如: 1个学生具有多个科目成绩,多个科目成绩对应某一个学生

从技术实现的角度:从表中对应主表的那个字段,既是外键也是index普通索引

1782624427223

示例:

sql
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_students
set sid=2
where sid = 1;
delete
from ai_students
where sid = 1;


-- 如果这是必须要删除,那么要先删除从表,再删除主表
delete
from ai_students_sorce
where student_id = 1;
delete
from ai_students
where sid = 1;

示例2(具有级联更新和级联删除)

sql
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_students
set sid=3
where sid = 2;

-- 在级联删除的情况下,也同时删除从表的数据(慎用)
-- 由于级联删除是物理删除,数据无法恢复的,而开发中我们是使用软删除的
-- 因此真正的开发,我不要加上级联删除这个选项
delete
from ai_students
where sid = 3;

19.3 表关系之多对多

还有更复杂的表关系,例如:多对多。 这里会在后续大家涉及orm框架的时候更进一步实现。

多对多,其实就是中间多了一张中间表,但表中有两个外键或者两个以上的外键且外键是普通索引.

1782804898849

数据准备:

sql
-- 学生表
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: 查出学生选的课程

sql
SELECT s.id, s.name AS student_name, c.id, c.name AS course_name
FROM student s
JOIN student_course sc ON s.id = sc.student_id
JOIN course c ON sc.course_id = c.id;

案例2:查询张三选了哪些课程

sql
select '张三' as student_name, c.name as course_name from course c left join student_course sc
on c.id=sc.course_id
where sc.student_id=(select id from student where name='张三')

案例3:查询数学课有哪些学生

sql
select '数学' as course_name , stu.name from student stu left join student_course sc on stu.id=sc.student_id
where sc.course_id = (select id from course where name='数学');

19.4 逆范式

省市行政区逆范式表示设计:

image-20260709194516586

image-20260709194527002

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
sql
# create table 添加外键.sql

create table <从表名>
(
    ...,
	[constraint <外键名>] foreign key(<本表列名>) references <主表名>(<主表字段列名>) 
    [on delete <动作>]
    [on update <动作>]
)
sql
# alter table 添加外键.sql

alter table <从表名>
add [constraint <外键名>]
foreign key(<本表列名>) references <主表名>(<主表字段列名>) 
[on delete <动作>]
[on update <动作>]

关于索引与外键的关系

sql
# 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 视图

  • 视图是一种缓存吗? 不是,视图并非用于增加查询效率的,反而因为多套了一层,降低了查询效率.
  • 视图中的增删改会影响物理表吗? 会,只不过效率低,并且不支持事务处理.

创建视图

sql
create [or replace] view <视图名称> as
<select查询>;

# MySQL自动生成
CREATE 
    ALGORITHM = UNDEFINED 	# 指定视图的处理算法: undefined: 默认值,mysql自己决定如何处理这个视图,通常可省略.
    DEFINER =`pi`@`%`	# 定义者/所有者是 pi
    SQL SECURITY DEFINER	# 定义视图的案例执行上下文,definer意思就是说,当任何用户来查询这个视图时,都将使用pi用户的权限去执行视图中的sql语句.
    VIEW `v_course` 
AS
select `course`.`cid` AS `cid`, `course`.`cname` AS `cname`, `course`.`credit` AS `credit`
from `course`

修改视图

sql
create or replace view <视图名称> as
<select查询>;

alter view <视图名称> as
<select查询>;

删除视图

sql
drop view [if exists] <视图名称>;

显示视图定义

sql
show create view <视图名称>;

20.多表查询

20.1 联合查询union

UNION 用于把多条 SELECT 的结果“纵向合并”成一张结果集。本质:行合并(上下拼)

这个查询方式,我个人是很喜欢使用的。

数据准备:

sql
-- 正式员工表
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❌ 不去重✅ 更快

示例:

sql
-- 不去除重复
select * from emp_full
union all
select * from emp_intern;

-- 去重
select * from emp_full
union
select * from emp_intern;

在数据清洗的技巧中,有一种按月分表场景会使用到这个联合查询。但一般是在其他的数据处理仓库使用该场景,单纯的Mysql其实用得很少。

例如:

AnalyticDB (ADB) for MySQL/PostgreSQL(阿里云) — 这个东西旧项目可能会更多一些

MaxCompute (ODPS)(阿里云) — 这是阿里主推的数据仓库

1782629122064

20.2 交叉查询(cross join)

交叉查询会返回两张表的笛卡尔积(Cartesian Product)

即:左表的每一行 × 右表的每一行。

特点如下:

无条件连接

结果行数 = 表A行数 × 表B行数

数据准备:

sql
-- 尺寸表
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,'蓝');

示例:

sql
-- 显式的使用cross join
select * from size
cross join color;

-- 隐式的,没有cross join(这个我个人是不喜欢的)
select * from size,color;

20.3 自然链接(了解)

自然连接(NATURAL JOIN)是一种特殊的等值连接方式,数据库会自动根据两张表中列名相同的列进行匹配,并在结果集中合并这些同名列。

虽然自然连接语法简洁,但由于其依赖列名而非明确的连接条件,一旦表结构发生变化,可能导致查询结果错误或难以排查。因此,在实际开发中,更推荐使用 join…on / Inner join …on的等价内连接代替。

数据准备:

sql
-- 部门表
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);

示例:

sql
SELECT *
FROM emp
NATURAL JOIN dept;

NATURAL JOIN = 以下三步

  1. 找出两张表中 列名相同的列
  2. 对这些列做 AND 列 = 列
  3. 结果集中只保留一份同名列

1782640584577

dept_id只出现了一次

这个程序是有隐患的,因为mysql是自动匹配列名相同的列,如果员工表的dept_id字段名修改为depart_id,那么就出问题了,所以自然连接实际开发中我们最好不要使用。.

20.4 内连接

INNER JOIN(内连接)只返回两张表中“满足连接条件的数据”

有匹配的才显示

没匹配的两边都不显示

最常用、最重要的多表连接方式

语法规则:

sql
SELECT 
FROM A
INNER JOIN B
ON A.关联列 = B.关联列;

需求场景:查出部门的所有员工(1:多)

数据准备:

sql

INSERT INTO dept VALUES(30,'财务部');  -- 无员工

INSERT INTO emp VALUES(4,'赵六',NULL); -- 无部门

示例:

sql
select
       emp_id,emp_name,emp.dept_id,dept_name
from emp inner join dept
         on emp.dept_id=dept.dept_id;
         
         
-- 也可以省略inner 
select
       emp_id,emp_name,emp.dept_id,dept_name
from emp inner join dept
         on emp.dept_id=dept.dept_id;

隐式内连接(这个写法以前在asp和php时代很多,个人不推荐你这样写):

sql
SELECT e.emp_name, d.dept_name
FROM emp e, dept d
WHERE e.dept_id = d.dept_id;

相对自然连接来说,inner join … on之后的比对条件是我们自己写的,条件更加清晰

20.5 左右外连接

LEFT JOIN(左外连接)

LEFT JOIN 会返回左表的全部记录,即使右表中没有匹配的数据

✅ 左表有,右表没有 → 右表字段补 NULL

✅ 左表没有,右表有 → 不返回

📌 口诀:

“左表全要,右表看缘分。”

RIGHT JOIN(右外连接)

RIGHT JOIN 会返回右表的全部记录,即使左表中没有匹配的数据

📌 口诀:

“右表全要,左表看缘分。”

语法规则:

sql
SELECT 
FROM A
LEFT JOIN B
ON A.关联列 = B.关联列;

SELECT 
FROM A
RIGHT JOIN B
ON A.关联列 = B.关联列;

示例:

sql
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:查询“没有对应数据”的记录

sql
-- 没有员工的部门
SELECT d.dept_name
FROM dept d
LEFT JOIN emp e ON d.dept_id = e.dept_id
WHERE e.emp_id IS NULL;

场景 2:统计“含 0 的情况”

sql
-- 统计每个部门有多少人
SELECT
  d.dept_name,
  COUNT(e.emp_id) 人数
FROM dept d
LEFT JOIN emp e ON d.dept_id = e.dept_id
GROUP BY d.dept_name;

20.7 自连接

自连接(Self Join)不是一种新的连接类型,而是同一张表“自己连接自己”

还是内连接 / 外连接

只是 左表和右表是同一张表

通过 表别名 把它们当成两张表来用

什么时候需要自连接?

表中的某个字段的值,来源于本表的另一个字段

典型场景:

  • 省市区(pid 指向本表的 id)
  • 员工与上级(manager_id 指向 emp_id)
  • 商品分类的父子关系

自连接实际上是逆范式的一种应用

数据准备

sql
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:查询所有城市及其所属省份

sql
select
  p.name,c.name
from areas as p
       inner join areas as c
    on p.id = c.pid;

案例2: 使用Group_Concat函数优化

sql
select
  p.name as 省份,group_concat(c.name) as 城市
from areas as p
       inner join areas as c
    on p.id = c.pid
group by p.name;

1782660991581

21.子查询

子查询,又称“嵌套查询”,是指在一个 SQL 语句(SELECT、INSERT、UPDATE、DELETE)内部嵌入的另一个 SELECT 查询语句。

它是以一个查询的结果,作为另一个查询的条件或数据源。

21.1 准备员工表和部门表

sql
-- ============================
-- 创建 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列数据

需求场景:获取大于公司员工平均年龄的员工

sql
select *
from employees
where age > (select avg(age)
             from employees)

这种子查询称为标量子查询

21.3 案例2:子查询返回1列多行数据

需求场景:获取有部门的员工

这种子查询称为列子查询

21.4 案例3:子查询返回1行多列

需求场景:查询tb_students中年龄最小且分数最低的用户

数据准备

sql
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;
sql
INSERT INTO tb_students (name, age, sorces) VALUES
  ('张三', 18, 85),
  ('李四', 19, 92),
  ('王五', 20, 78),
  ('赵六', 18, 95),
  ('孙七', 21, 88),
  ('刘八', 17, 50);

子查询3步走

sql

这种子查询称为行子查询

21.5 子查询在select以外的使用

需求场景:复制数据到另外一张表

数据准备

sql
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 子查询作为临时表

准备数据

sql
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);
sql
-- 获取每个部门的平均工资
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_id
sql
with 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 快速上手

需求场景: 求员工工资的平均值和员工个人工资的平均值差额

sql
select
       emp_id,
       name,
       salary,
       ROUND(avg(salary) over(),2) as avg_salary,
       ROUND(salary - avg(salary) over(),2) as my_salary
from accounts;

这个场景很适合把查询用作临时表

22.2 三大排序函数

数据准备:

sql
-- 成绩表
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 理解三大排序函数

sql
RANK() : 可并列,不可连续
DENSE_RANK() : 可并列,还连续
ROW_NUMBER(): 不并列,很连续

如图所示:

1782391439465

语法:

sql
RANK() + over()
DENSE_RANK() + over()
ROW_NUMBER() + over()

22.5 over分组和排序说明

over如果里面什么都不写就是全表数据框选。

text
OVER() # 这个操作如果在排序中通常情况下达不到你要的效果。

over如果写东西,最常见是是下面两个点:

PARTITION BY:

按谁分组(比如按科目、按部门), 作用等同于group by,但PARTITION BY只能用于窗口函数

ORDER BY asc|desc:在框里按什么排序(比如按分数高低)

powershell
OVER (PARTITION BY 分组字段 ORDER BY 排序字段)

需求场景:实现按科目的分数高低啊排名

1782393729589

sql
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问题

数据准备

sql
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名员工

sql
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框选+聚合函数

1782393729589

sql
--- 准备数据
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的基本语法:

sql
SUM()+over():求和
AVG()+over():求平均
COUNT(*)+over():计数
MAX()+over():最大值
MIN()+over():最小值
....

需求场景:随着员工的入职,每入职一个员工,工资就累计放发的情况

sql
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_salary
from over_employee;

如下代码也可以实行同等的效果

sql
select
    name,hire_date,dept_name,salary,
    sum(salary) over(partition by dept_name order by hire_date asc) as total_salary
from over_employee;

22.8 数据清洗的基本认识

  • 问题来了:rows between unbounded preceding and current row 这种代码什么使用会使用?

  • 答案:以上代码看似完美无限。但现实中,公司员工编号的顺序实际上就已经代表了它的入职时间,有写HR的后台入职时间有可能是留空或者随便填上去的。按照这种情况,再要完成上述的需求我们不可能再以时间排序的。

    1782405388229

rows between unbounded preceding and current row 这类型的代码其实是“数据清洗”技术的一种方案.

sql
-- 以下两种方式其实都能达到该效果

select
    id,name,hire_date,dept_name,salary,
    sum(salary) over(partition by dept_name rows between unbounded preceding and current row ) as total_salary
from over_employee;


select
    id,name,hire_date,dept_name,salary,
    sum(salary) over(partition by dept_name order by id asc ) as total_salary
from 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_salary
from over_employee;

注意:数据清洗是有很多方案的,这个只是比较典型的一种.

22.9 窗口函数应用案例2则

需求场景:针对全表,在不分组的情况下,计算每个员工和前后相邻员工的平均薪资

sql
select
       name,dept_name,salary,
       round(avg(salary) over(rows between 1 preceding and 1 following ),2) as avg_salary
from over_employee;

需求场景:每个部门,按入职时间排序,计算部门第一个员工到当前员工的累计薪资

sql
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_salary
from over_employee;

进行数据清洗:

sql
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_salary
from over_employee;