文章目录
- [《MySQL内核拆解:从SQL到InnoDB》(三) : 数据库设计 & 聚合查询 & 分组查询](#《MySQL内核拆解:从SQL到InnoDB》(三) : 数据库设计 & 聚合查询 & 分组查询)
-
- 1、数据库范式
- 2、设计过程
-
- 2.1、设计的三个步骤
- [2.2、三层映射:现实对象 → 概念类 → 表](#2.2、三层映射:现实对象 → 概念类 → 表)
- [2.3、E-R 图的基本组成](#2.3、E-R 图的基本组成)
- 2.4、、三种关系类型
-
- [2.4.1、 一对一(1 : 1)](#2.4.1、 一对一(1 : 1))
- [2.4.2、 一对多(1 : N)](#2.4.2、 一对多(1 : N))
- [2.4.3、 多对多(M : N)](#2.4.3、 多对多(M : N))
- [3、从 E-R 图到表结构](#3、从 E-R 图到表结构)
- 4、插入查询结果
- 5、聚合查询
- [6、GROUP BY分组查询](#6、GROUP BY分组查询)
-
- [6.1、GROUP BY子句](#6.1、GROUP BY子句)
- 6.2、having子句
- 6.3、where和having的区别
- 7、常见的内置函数
-
- 7.1、日期类函数
-
- 7.1.1、获取当前日期与时间
- [7.1.2、从 datetime 里取出日期](#7.1.2、从 datetime 里取出日期)
- [7.1.3、日期加减:ADDDATE 与 SUBDATE](#7.1.3、日期加减:ADDDATE 与 SUBDATE)
- 7.1.4、日期差:DATEDIFF
- 7.2、字符串处理函数
-
- 7.2.1、长度:字符数还是字节数
- [7.2.2、拼接:CONCAT 与 CONCAT_WS](#7.2.2、拼接:CONCAT 与 CONCAT_WS)
- [7.2.3、 NULL 陷阱(重点)](#7.2.3、 NULL 陷阱(重点))
- 7.2.4、大小写转换与十六进制
- 7.2.5、查找、截取与比较
- 7.2.6、插入与替换
- [7.2.7、清理:TRIM 家族](#7.2.7、清理:TRIM 家族)
《MySQL内核拆解:从SQL到InnoDB》(三) : 数据库设计 & 聚合查询 & 分组查询
1、数据库范式
数据库范式是一组规则,为了设计出合理的关系型数据库,我们遵从不同的规范,这些不同的规范要求被称为不同的范式。
第⼀范式(1NF)、第⼆范式(2NF)、第三范式(3NF)、巴斯-科德
范式(BCNF)、第四范式(4NF)和第五范式(5NF,⼜称完美范式),越⾼的范式数据库冗余越⼩。然⽽,普遍认为范式越⾼虽然对数据关系有更好的约束性,但也可能导致数据库IO更繁忙,因此在实际应⽤中,数据库设计通常只需满⾜第三范式即可
1.1、第一范式
1.1.1、定义
- 数据库表的每⼀列都是不可分割的原⼦数据项,⽽不能是集合,数组,对象等⾮原⼦数据。
例如 : 学生表中如果结构如下,最后一列中的学校就不符合第一范式,因为还能再进行拆分。

1.2、第二范式
1.2.1、定义
- 在满⾜第⼀范式的基础上,不存在⾮关键字段对任意候选键的部分函数依赖。存在于表中定义了复合主键的情况下
候选键:背unique限定的列或列的组合,可从这些列中选出一列或多列充当主键
上面的概念有些许抽象,我们重新赋予其一个理解:当表里有"复合主键"(由两个或多个字段一起组成主键)时,非关键字段必须依赖于这个复合主键的【全部】,而不能只依赖于其中的【一部分】
反例:

- 以上的表设计学号和课程名为复合主键,其中学分只依赖课程名字段,所以不符合第二范式。
1.2.2、不满足第二范式的问题
- 数据冗余 : 学生相关的信息多此重复出现,造成大量数据冗余
- 更新异常:在更新数据时,如果某几条数据更新失败,则会造成数据冲突,例如修改课程学分,可能会造成相同课程,但学分不同的情况
- 插入异常,成绩字段要在学生考完对应的科目才能有值,那么在取得考试成绩之后才能产生一条记录。
- 删除异常 : 把毕业学⽣的考试数据全都删除,此时课程和学分的信息也会被删除掉,有可能导致⼀段时间内,数据库⾥没有某⻔课程和学分的信息
所以针对需求应该拆成三张表 : 学生 、课程、成绩表 :



1.3、第三范式
1.3.1、定义
-
在满⾜第⼆范式的基础上,不存在⾮关键字段,对任⼀候选键的传递依赖。
-
意思就是字段要和主键强相关,否则进行拆分
2、设计过程
表结构建错了,改一次要动代码、动数据、动索引,代价远高于设计时多画一张图。E-R 图就是把"业务怎么想"翻译成"表怎么建"的那张中间图纸。本文把设计流程、E-R 图三要素、三种关系类型,以及每种关系怎么落成 SQL 表讲清楚。
2.1、设计的三个步骤
| 步骤 | 做什么 | 产出 |
|---|---|---|
| 1 | 从现实业务中抽象出概念类 | 一份类清单(实体候选) |
| 2 | 确定实体之间的关系,画出 E-R 图 | E-R 图 |
| 3 | 依据 E-R 图编写 SQL、创建数据库 | 表结构 |
第一步有个容易忽略的前提:概念类必须在需求分析阶段就确定下来,而不是等到写代码时边写边加表。原型阶段随手加一张表,往往就是后面数据冗余、字段打架的源头。
画成图的好处也很直接------项目经理、后端、前端、测试看的是同一张图,不用各自在脑子里建一套模型,沟通成本立刻降下来。
2.2、三层映射:现实对象 → 概念类 → 表
数据库设计要跨越三个"世界",每一层的元素是一一对应的:
| 现实世界 | 概念世界(E-R 图) | 数据世界(数据库) |
|---|---|---|
| 业务对象 | 实体 Entity | 表 Table |
| 对象的特征 | 属性 Attribute | 字段(列)Column |
| 对象之间的联系 | 关系 Relationship | 外键 / 中间表 |
一句话记住:类对应实体,实体对应表;类的属性对应实体的属性,实体的属性对应表中的列。
2.3、E-R 图的基本组成
E-R 图(Entity-Relationship Diagram),也叫实体联系模型,是描述数据模型的概念图,主要用于数据库设计阶段。它由三种基本成分构成:
| 成分 | 含义 | 图形表示 | 举例 |
|---|---|---|---|
| 实体 | 数据对象 | 矩形框 | 用户、学生、班级 |
| 属性 | 实体的特性 | 椭圆 / 圆角矩形 | 学生的姓名、年龄 |
| 关系 | 实体之间的联系 | 菱形框,框内写关系类型,用直线连到相关实体 | 选修、属于 |
画图时的两条约定:
- 菱形框里填的是动词(选修、拥有、属于),不是名词,这样关系一眼能看懂。
- 连接线上标出基数(1、N、M),这是关系类型唯一不靠文字也能读懂的地方。
2.4、、三种关系类型
2.4.1、 一对一(1 : 1)
针对一对一关系,设计表时,有两种方式:
- 把两个实体所有的信息全部放在一张表里
- 创建两张表,分别记录用户信息与账号信息,并把两张表做关联
一个用户 实体包含:用户昵称、真实姓名、手机号、邮箱地址、性别、学校。
一个账户实体包含:登录用户名、密码。
一个用户只对应一个账户,一个账户也只属于一个用户,所以是 1:1。
#mermaid-svg-5PwJDIh5duylPMfj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5PwJDIh5duylPMfj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5PwJDIh5duylPMfj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5PwJDIh5duylPMfj .error-icon{fill:#552222;}#mermaid-svg-5PwJDIh5duylPMfj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5PwJDIh5duylPMfj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5PwJDIh5duylPMfj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5PwJDIh5duylPMfj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5PwJDIh5duylPMfj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5PwJDIh5duylPMfj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5PwJDIh5duylPMfj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5PwJDIh5duylPMfj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5PwJDIh5duylPMfj .marker.cross{stroke:#333333;}#mermaid-svg-5PwJDIh5duylPMfj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5PwJDIh5duylPMfj p{margin:0;}#mermaid-svg-5PwJDIh5duylPMfj .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-5PwJDIh5duylPMfj .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-5PwJDIh5duylPMfj .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-5PwJDIh5duylPMfj .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-5PwJDIh5duylPMfj .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-5PwJDIh5duylPMfj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5PwJDIh5duylPMfj .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-5PwJDIh5duylPMfj .node rect,#mermaid-svg-5PwJDIh5duylPMfj .node circle,#mermaid-svg-5PwJDIh5duylPMfj .node ellipse,#mermaid-svg-5PwJDIh5duylPMfj .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5PwJDIh5duylPMfj .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-5PwJDIh5duylPMfj .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-5PwJDIh5duylPMfj .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5PwJDIh5duylPMfj .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-5PwJDIh5duylPMfj .edgeLabel .label text{fill:#333;}#mermaid-svg-5PwJDIh5duylPMfj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拥有
USER
string
nickname
用户昵称
string
realname
真实姓名
string
phone
手机号
string
email
邮箱地址
string
gender
性别
string
school
学校
ACCOUNT
string
username
登录用户名
string
password
密码
判定:A 的一条记录只能匹配 B 的一条记录,反过来也成立。
为什么要把账户单独拆出来:用户信息是展示用的,账户信息是认证用的,两者的读写频率、权限、加密要求完全不同。拆开的代价只是一次关联查询,收益是敏感字段可以单独管控。
2.4.2、 一对多(1 : N)
一个班级 实体包含:班级名、学生人数。
一个学生实体包含:真实姓名、学号、年龄、性别、入学时间。
一个班级里有多个学生,但一个学生只属于一个班级,所以班级与学生是 1:N。

注意关系的方向性 :同一个联系,从班级看是"一对多",从学生看就是"多对一"。描述关系时必须说清从哪一端看,否则 1:N 和 N:1 会写反,外键就会加错边。
2.4.3、 多对多(M : N)
一个学生 实体包含:真实姓名、学号、年龄、性别、入学时间。
一个课程实体包含:课程名。
一个学生可以选修多门课程,一门课程也可以被多名学生选修,所以学生与课程是 M:N。
#mermaid-svg-ugC2ID2ngHf9y6sN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ugC2ID2ngHf9y6sN .error-icon{fill:#552222;}#mermaid-svg-ugC2ID2ngHf9y6sN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ugC2ID2ngHf9y6sN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ugC2ID2ngHf9y6sN .marker.cross{stroke:#333333;}#mermaid-svg-ugC2ID2ngHf9y6sN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ugC2ID2ngHf9y6sN p{margin:0;}#mermaid-svg-ugC2ID2ngHf9y6sN .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ugC2ID2ngHf9y6sN .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-ugC2ID2ngHf9y6sN .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-ugC2ID2ngHf9y6sN .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-ugC2ID2ngHf9y6sN .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-ugC2ID2ngHf9y6sN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ugC2ID2ngHf9y6sN .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-ugC2ID2ngHf9y6sN .node rect,#mermaid-svg-ugC2ID2ngHf9y6sN .node circle,#mermaid-svg-ugC2ID2ngHf9y6sN .node ellipse,#mermaid-svg-ugC2ID2ngHf9y6sN .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ugC2ID2ngHf9y6sN .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ugC2ID2ngHf9y6sN .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ugC2ID2ngHf9y6sN .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ugC2ID2ngHf9y6sN .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-ugC2ID2ngHf9y6sN .edgeLabel .label text{fill:#333;}#mermaid-svg-ugC2ID2ngHf9y6sN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 选修
STUDENT
string
student_no
学号
string
realname
真实姓名
int
age
年龄
string
gender
性别
date
enroll_date
入学时间
COURSE
string
course_name
课程名
多对多不能直接用外键表达,必须引入一张中间表(也叫关联表、连接表)来记录"谁选了谁"。
而中间表的价值不止于此------它还能承载关系自身的属性。比如一个学生参加某门课程的考试得到成绩,"成绩"既不属于学生,也不属于课程,只属于"这一次选课"这个关系。所以它只能放在中间表里:
#mermaid-svg-O6AurY7kHtqq4LxQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O6AurY7kHtqq4LxQ .error-icon{fill:#552222;}#mermaid-svg-O6AurY7kHtqq4LxQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O6AurY7kHtqq4LxQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O6AurY7kHtqq4LxQ .marker.cross{stroke:#333333;}#mermaid-svg-O6AurY7kHtqq4LxQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O6AurY7kHtqq4LxQ p{margin:0;}#mermaid-svg-O6AurY7kHtqq4LxQ .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-O6AurY7kHtqq4LxQ .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-O6AurY7kHtqq4LxQ .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-O6AurY7kHtqq4LxQ .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-O6AurY7kHtqq4LxQ .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-O6AurY7kHtqq4LxQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O6AurY7kHtqq4LxQ .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-O6AurY7kHtqq4LxQ .node rect,#mermaid-svg-O6AurY7kHtqq4LxQ .node circle,#mermaid-svg-O6AurY7kHtqq4LxQ .node ellipse,#mermaid-svg-O6AurY7kHtqq4LxQ .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O6AurY7kHtqq4LxQ .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-O6AurY7kHtqq4LxQ .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O6AurY7kHtqq4LxQ .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-O6AurY7kHtqq4LxQ .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-O6AurY7kHtqq4LxQ .edgeLabel .label text{fill:#333;}#mermaid-svg-O6AurY7kHtqq4LxQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 参加考试
被记录成绩
STUDENT
string
student_no
PK
学号
string
realname
真实姓名
SCORE
string
student_no
PK,FK
学号
string
course_no
PK,FK
课程编号
decimal
score
成绩
COURSE
string
course_no
PK
课程编号
string
course_name
课程名
此时 E-R 图上是两个 1:N:学生 1:N 成绩,课程 1:N 成绩。多对多是通过中间表被拆成两个一对多的,这是理解关联表的关键。
3、从 E-R 图到表结构
三条落地规则,覆盖全部关系类型:
| 关系类型 | 落地方式 | 外键放在哪 |
|---|---|---|
| 1 : 1 | 主外键合一(直接并成一张表);或独立建表,外键列加 UNIQUE 约束 | 任一方 |
| 1 : N | 合并进"多"的一方,不需要新表 | 多的一方 |
| M : N | 拆出一张独立中间表 | 中间表持有双方主键 |
以学生选课系统为例,一次性把三类关系都建出来:
sql
-- 一对多:班级表("一"的一方,主表)
CREATE TABLE class (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
class_name VARCHAR(50) NOT NULL,
student_count INT UNSIGNED DEFAULT 0
);
-- 一对多:学生表("多"的一方,外键放在这里)
CREATE TABLE student (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
student_no CHAR(12) NOT NULL UNIQUE,
realname VARCHAR(20) NOT NULL,
gender CHAR(1),
enroll_date DATE,
class_id INT UNSIGNED,
CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES class(id)
);
-- 多对多:课程表
CREATE TABLE course (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
course_no CHAR(8) NOT NULL UNIQUE,
course_name VARCHAR(50) NOT NULL
);
-- 多对多:中间表,同时承载关系属性(成绩)
CREATE TABLE score (
student_id INT UNSIGNED,
course_id INT UNSIGNED,
score DECIMAL(5,2),
PRIMARY KEY (student_id, course_id), -- 联合主键:同一人同一课只能有一条记录
CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id),
CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id)
);
4、插入查询结果
想要复制表中的数据,我们有以下三种方法:
1、一条一条重新插入
2、把原来的数据导出,将表名进行修改,再改入到目标表中
3、可以使用insert into select 语句
语法:
sql
insert into table_name [(column [, column ...])] SELECT ...
- 此处的table_name 是目标表
例子:
sql
insert into stu1(id, name) select id,name from stu;
- 要插入的列和查询的列要保持一致
5、聚合查询
mysql中内置有一些函数,针对于某一列来进行运算
5.1、常用的聚合函数
| 函数 | 说明 |
|---|---|
COUNT([DISTINCT] expr) |
返回查询到的数据的数量 |
SUM([DISTINCT] expr) |
返回查询到的数据的总和,不是数字没有意义 |
AVG([DISTINCT] expr) |
返回查询到的数据的平均值,不是数字没有意义 |
MAX([DISTINCT] expr) |
返回查询到的数据的最大值,不是数字没有意义 |
MIN([DISTINCT] expr) |
返回查询到的数据的最小值,不是数字没有意义 |
- 统计表中的行数:推荐使用 * ,因为这种写法是sql标准中规定的
sql
select count(*) from exam;
- 统计有多少学生参加数学考试:查询的结果是一个临时表,不受原表中字段长度的约束
sql
select count(math) from exam_result;
- 统计英语成绩的平均分:
sql
select avg(english) from exam_result;
- 统计平均总分:
sql
select avg(chinese + english + math) as `总分` from exam_result;
- 统计英语成绩的总分:sum函数求和时,NULL值不参与运算
sql
select sum(english) from exam_result;
- 查询语文成绩的最高分和最低分
sql
select max(chinese),min(chinese) from exam_result;
注意 : 在使用关于运算的聚合函数时,不能用于非数值列上,比如sum(),avg(),max(),min().
6、GROUP BY分组查询
select 中使用 group by 子句可以对指定列进行分组查询,通过⼀定的规则将⼀个数据集划分成若⼲个⼩的分组,然后针对若⼲个分组进⾏数据处理
- 需要满足 : 使用该语句查询时,select指定的字段必须是分组
依据字段,其它字段 如果想出现在select的临时表中则必须包含在聚合函数中。
6.1、GROUP BY子句
语法:
sql
SELECT {col_name | expr} ,... ,aggregate_function (aggregate_expr)
FROM table_references
GROUP BY {col_name | expr}, ...
[HAVING where_condition]
- col_name|expr:要查询的列或表达式,可以有多个,必须在 GROUP BY ⼦句中作为分组的依
据 - aggregate_function:聚合函数,⽐如 COUNT(),SUM(),AVG(),MAX(),MIN()
- aggregate_expr:聚合函数传⼊的列或表达式,如果列或表达式不在 GOURP BY ⼦句中,必须包含中聚合函数中
- having : 在分组后,过滤分组结果(通常必须跟聚合函数,如 COUNT, SUM, AVG)。
示例:
sql
drop table if exists emp;
create table emp (
id bigint primary key auto_increment,
name varchar(20) not null,
role varchar(20) not null,
salary decimal(10, 2) not null
);
insert into emp values (1, '冯骥', '老板', 1500000.00);
insert into emp values (2, '码化腾', '老板', 1800000.00);
insert into emp values (3, '罗', '讲师', 10000.00);
insert into emp values (4, '陈川', '讲师', 12000.00);
insert into emp values (5, '比翼鸟', '学管', 9000.00);
insert into emp values (6, '亚当重锤', '学管', 8000.00);
insert into emp values (7, '孙悟空', 'npc', 956.8);
insert into emp values (8, '猪悟能', 'npc', 700.5);
insert into emp values (9, '沙和尚', 'npc', 333.3);
-
统计每种角色的人数
sqlselect role,count(*) from emp group by role; +--------+----------+ | role | count(*) | +--------+----------+ | 老板 | 2 | | 讲师 | 2 | | 学管 | 2 | | npc | 3 | +--------+----------+ -
统计每种角色的平均工资,最高工资,最低工资
sqlselect role,round(avg(salary),2) as '平均工资',round(max(salary),2) as '最高工资', round(min(salary),2) as '最低工资' from emp group by role; +--------+--------------+--------------+--------------+ | role | 平均工资 | 最高工资 | 最低工资 | +--------+--------------+--------------+--------------+ | 老板 | 1650000.00 | 1800000.00 | 1500000.00 | | 讲师 | 11000.00 | 12000.00 | 10000.00 | | 学管 | 8500.00 | 9000.00 | 8000.00 | | npc | 663.53 | 956.80 | 333.30 | +--------+--------------+--------------+--------------+
6.2、having子句
使⽤GROUP BY 对结果进⾏分组处理之后,对分组的结果进⾏过滤时,不能使⽤ WHERE ⼦句,⽽要使⽤ HAVING ⼦句
-
显示平均工资低于1500工资角色和平均工资
sqlselect role,round(avg(salary),2) as '平均工资' from emp group by role having avg(salary) < 1500; +------+--------------+ | role | 平均工资 | +------+--------------+ | npc | 663.53 | +------+--------------+
6.3、where和having的区别
- Having⽤于对分组结果的条件过滤
- Where⽤于对表中真实数据的条件过滤
7、常见的内置函数
7.1、日期类函数
| 函数 | 作用 | 返回 | 同义词 |
|---|---|---|---|
CURDATE() |
当前日期 | 2024-08-27 |
CURRENT_DATE、CURRENT_DATE() |
CURTIME() |
当前时间 | 10:26:33 |
CURRENT_TIME、CURRENT_TIME([fsp]) |
NOW() |
当前日期 + 时间 | 2024-08-27 10:26:47 |
CURRENT_TIMESTAMP、CURRENT_TIMESTAMP([fsp]) |
DATE(expr) |
从 date / datetime 表达式里取出日期部分 | 2024-08-27 |
无 |
ADDDATE(date, INTERVAL expr unit) |
日期加一个时间间隔 | 新日期 | DATE_ADD() |
SUBDATE(date, INTERVAL expr unit) |
日期减一个时间间隔 | 新日期 | DATE_SUB() |
DATEDIFF(expr1, expr2) |
两个日期相差多少天 | 整数,可负 | 无 |
一句话归类:取当下 (前三个)、取日期 (DATE)、算加减 (ADDDATE / SUBDATE)、算间隔(DATEDIFF)。
7.1.1、获取当前日期与时间
sql
SELECT CURDATE(); -- 2024-08-27
SELECT CURTIME(); -- 10:26:33
SELECT NOW(); -- 2024-08-27 10:26:47
三个函数的分工很明确:只关心"哪天"用 CURDATE(),只关心"几点"用 CURTIME(),两者都要用 NOW()。
关于 fsp:CURTIME([fsp]) 和 CURRENT_TIMESTAMP([fsp]) 可以带一个 0 ~ 6 的小数秒精度参数,省略则为 0。例如 CURRENT_TIMESTAMP(3) 返回 2024-08-27 10:26:47.123。取值范围与前面日期类型里的 fsp 是同一套规则。
补充 :
NOW()返回的是语句开始执行 的时刻,同一条 SQL 里调用多次结果完全相同;如果需要每次调用都取实时时间,用SYSDATE()。批量插入成千上万行时,这个差别会影响时间戳的精度一致性。
7.1.2、从 datetime 里取出日期
sql
SELECT DATE('2024-08-27 10:26:47'); -- 2024-08-27
DATE() 的作用就是"砍掉时分秒"。它既接受 datetime,也接受 date 和格式合法的字符串。
字段存的是 DATETIME、统计却要按天分组时,就靠它:
sql
SELECT DATE(created_at) AS d, COUNT(*) AS cnt
FROM orders
GROUP BY DATE(created_at);
补充 :只是想"取日期部分"用
DATE();如果要的是自定义显示格式(如2024年08月27日),那是DATE_FORMAT()的职责,两者别混用。
7.1.3、日期加减:ADDDATE 与 SUBDATE
结构固定为 函数(日期, INTERVAL 数值 单位),第三个位置必须写 INTERVAL 关键字。
sql
-- 加 31 天
SELECT ADDDATE('2024-01-02', INTERVAL 31 DAY); -- 2024-02-02
-- 减 1 个月
SELECT SUBDATE('2024-08-02', INTERVAL 1 MONTH); -- 2024-07-02
常用单位:
| 单位 | 含义 | 单位 | 含义 |
|---|---|---|---|
DAY |
天 | HOUR |
小时 |
WEEK |
周 | MINUTE |
分钟 |
MONTH |
月 | SECOND |
秒 |
QUARTER |
季度 | YEAR |
年 |
两点要注意:
- 单位不能加引号 ,写成
INTERVAL 31 'DAY'会报错;数值和unit之间也不要逗号。 - 加减会自动跨月跨年 ,
2024-01-02加 31 天得到2024-02-02,不需要自己判断月份天数。
补充 :
ADDDATE还有一个简写形式ADDDATE(date, days),第二个参数直接给天数,不用写INTERVAL,例如ADDDATE('2024-01-02', 31)。但DATE_ADD()没有这个简写,只能写完整形式------混用时容易踩坑。补充 :月末是一个特殊边界。
'2024-01-31'加 1 个月,2 月没有 31 号,MySQL 会截断到当月最后一天,得到2024-02-29,而不是报错或滚到 3 月。
7.1.4、日期差:DATEDIFF
sql
-- 相差 1 天
SELECT DATEDIFF('2024-12-31 23:59:59', '2024-12-30'); -- 1
-- 表达式 1 早于表达式 2 时返回负数
SELECT DATEDIFF('2024-11-30 23:59:59', '2024-12-31'); -- -31
规则只有三条:
- 计算方式是 expr1 − expr2,顺序决定正负。
- 只使用日期部分 ,时分秒被直接丢弃------上例中
23:59:59和00:00:00算出来一样是 1 天。 - 结果是整数天,不做四舍五入。
补充 :
DATEDIFF只能按"天"算。要按小时、月、年算间隔,用的是TIMESTAMPDIFF(unit, expr1, expr2),例如TIMESTAMPDIFF(HOUR, a, b)。
注意:
DATEDIFF忽略时分秒 :'2024-12-31 23:59:59'减'2024-12-30 00:00:00'结果仍是 1,不是"1 天 23 小时"。要精确到小时请用TIMESTAMPDIFF。- 参数顺序写反就是负数 :
DATEDIFF(a, b)是 a 减 b,别凭直觉认为"大的减小的"。 INTERVAL的单位不加引号 ,INTERVAL 31 DAY正确,INTERVAL 31 'DAY'报错。ADDDATE有简写、DATE_ADD没有 :ADDDATE(d, 31)合法,DATE_ADD(d, 31)不合法。- 月末加减会被截断 :
'2024-01-31' + INTERVAL 1 MONTH得到2024-02-29。 - 同义词带不带括号都能用 :
CURRENT_DATE、CURRENT_DATE()等价;但NOW不带括号会报错,它没有关键字形式。 NOW()在语句内固定 :同一条 SQL 里多列都写NOW(),拿到的是同一个时刻;要实时值用SYSDATE()。- 日期字符串统一用
-分隔 :'2024-01-02'是标准写法,'2024/01/02'之类的识别结果依赖sql_mode,跨环境容易出问题。
7.2、字符串处理函数
| 分组 | 函数 | 作用 | 同义词 |
|---|---|---|---|
| 长度 | CHAR_LENGTH(str) |
字符个数 | CHARACTER_LENGTH() |
LENGTH(str) |
字节数(与字符集有关) | --- | |
| 拼接 | CONCAT(str1, str2, ...) |
依次拼接 | --- |
CONCAT_WS(sep, str1, str2, ...) |
用 sep 作分隔符拼接 |
--- | |
| 大小写 | LCASE(str) |
转小写 | LOWER() |
UCASE(str) |
转大写 | UPPER() |
|
| 编码 | HEX(str) / HEX(N) |
转十六进制字符串 | --- |
| 查找 | INSTR(str, substr) |
substr 首次出现的位置 |
--- |
| 截取 | SUBSTR(str, pos) SUBSTR(str FROM pos FOR len) |
取子串 | SUBSTRING() |
LEFT(str, len) / RIGHT(str, len) |
取最左 / 最右 len 个字符 |
--- | |
| 插入替换 | INSERT(str, pos, len, newstr) |
在 pos 处覆盖 len 个字符并插入 |
--- |
REPLACE(str, from_str, to_str) |
把 from_str 全部换掉 |
--- | |
| 比较 | STRCMP(expr1, expr2) |
逐字符比较,返回 -1 / 0 / 1 | --- |
| 清理 | LTRIM(str) / RTRIM(str) / TRIM(str) |
去前导 / 末尾 / 两端空格 | --- |
TRIM([LEADING / TRAILING / BOTH [remstr] FROM] str) |
去两端指定字符串 | --- |
7.2.1、长度:字符数还是字节数
这是最容易搞混的一对函数,差别就在"以什么为单位数":
| 函数 | 计数单位 | '唐三藏' 的结果 |
说明 |
|---|---|---|---|
CHAR_LENGTH() |
字符 | 3 |
不管底层占几个字节 |
LENGTH() |
字节 | 6 |
取决于字符集 |
用示例数据看得更清楚:
sql
SELECT name, CHAR_LENGTH(name), LENGTH(name) FROM exam;
| name | char_length(name) | length(name) |
|---|---|---|
| 唐三藏 | 3 | 6 |
| 孙权 | 2 | 4 |
| 张飞 | 2 | 4 |
中文在 utf8mb4 下每个字符占 3 字节 ,所以 3 个汉字 = 3 字符 / 9 字节------但示例里是 6 字节,说明该表用的字符集下中文占 2 字节(如 gbk)。同一个 3 字姓名,换成 utf8mb4 后 LENGTH() 会变成 9,而 CHAR_LENGTH() 永远是 3。
选型判断很简单:校验"用户名不超过 20 个字"用 CHAR_LENGTH,估算"这个字段占多少存储"用 LENGTH。
7.2.2、拼接:CONCAT 与 CONCAT_WS
sql
-- 拼一句话
SELECT CONCAT(name, '的语文成绩:', chinese, '分,数学成绩:', math, '分')
FROM exam;
-- 唐三藏的语文成绩:134分,数学成绩:98分
-- 拼一行数据,用逗号分隔
SELECT CONCAT_WS(',', name, chinese, math, english) FROM exam;
-- 唐三藏,134,98,56
7.2.3、 NULL 陷阱(重点)
两个函数对 NULL 的反应完全不同:
sql
-- 张飞的 english 为 NULL
SELECT CONCAT(name, chinese, math, english) FROM exam; -- 张飞那行 → NULL
SELECT CONCAT_WS(',', name, chinese, math, english) FROM exam; -- 张飞,54,0
| 函数 | 遇到 NULL 时 | 后果 |
|---|---|---|
CONCAT |
整个结果变成 NULL |
一行里有一个空值,全行报废 |
CONCAT_WS |
跳过该值,分隔符照常 | 字段数对不上,容易错位 |
用 CONCAT 拼多个可能为空的字段时,先套 IFNULL(col, ''),否则整条记录都变成 NULL。
补充 :
CONCAT_WS的分隔符如果是NULL,结果才是NULL;其余参数为NULL一律跳过。这也意味着用它拼出来的字符串长度不固定,解析时不能按逗号个数硬编码。
7.2.4、大小写转换与十六进制
sql
SELECT LCASE('ABC'); -- abc
SELECT UCASE('abc'); -- ABC
SELECT HEX('Hello MySQL'); -- 48656C6C6F204D7953514C
SELECT HEX(15); -- F
大小写转换没什么可说的,LCASE 和 LOWER 完全等价。
HEX() 有两种用法,参数类型不同、含义也不同:传字符串时把每个字节转成十六进制(Hello MySQL 共 11 个字节,输出 22 个十六进制字符);传数字时按整数转(15 → F)。
补充 :
HEX()输出的是字节 的十六进制,所以中文会按字符集展开成多字节。反向还原用UNHEX(),两者常配合做二进制字段的调试。
7.2.5、查找、截取与比较
- INSTR:找位置
sql
SELECT INSTR('Hello MySQL', 'sql'); -- 9
注意这个例子:'sql' 是小写,目标串里是 'MySQL' 大写,结果依然是 9 ------说明 INSTR 的匹配不区分大小写 (取决于字段/参数的排序规则,_ci 结尾的都是大小写不敏感)。返回的是 substr 第一个字符的位置,从 1 开始数。
- 取子串
两种写法,效果等价:
sql
SELECT SUBSTR('Hello, MySQL', 8); -- MySQL
SELECT SUBSTR('Hello, MySQL' FROM 8 FOR 1); -- M
SELECT SUBSTR('Hello, MySQL' FROM 8 FOR 10); -- MySQL(不足 10 个,取到结尾)
pos从 1 开始,不是 0。FOR len超出剩余长度时不会报错,取到字符串结尾为止。- 不给
FOR就一路取到底。
补充 :
pos允许为负数,表示从末尾倒数,SUBSTR('Hello', -3)得到llo。另外还有逗号形式的三参数写法SUBSTR(str, pos, len),与FROM ... FOR ...等价。
- LEFT / RIGHT:两端取字符
sql
SELECT LEFT('Hello MySQL', 5); -- Hello
SELECT RIGHT('Hello MySQL', 5); -- MySQL
取手机号后四位、姓名首字这类需求,用它比算 SUBSTR 的下标更省事。
- STRCMP:比较
sql
SELECT STRCMP('text', 'text1'); -- -1
SELECT STRCMP('text', 'text'); -- 0
SELECT STRCMP('text1', 'text'); -- 1
逐个字符比较,expr1 小于、等于、大于 expr2 分别返回 -1 / 0 / 1。
补充 :返回值只表示大小关系,不代表差了几个字符 。它也不是"是否相等"的布尔值------判断相等直接用
=更直观,STRCMP更适合排序或三元比较场景。
7.2.6、插入与替换
- INSERT:按位置覆盖
参数是 INSERT(str, pos, len, newstr),语义是"从 pos 开始,删掉 len 个字符 ,再插入 newstr":
sql
-- len = 0:纯插入,什么都不删
SELECT INSERT('Hello, Database', 8, 0, 'MySQL '); -- Hello, MySQL Database
-- len = 20:从第 8 位往后不足 20 个,全删掉
SELECT INSERT('Hello, Database', 8, 20, 'MySQL'); -- Hello, MySQL
第二条是关键:len 表示要覆盖掉的字符数 ,剩余长度不够时就把后面全删了,不会报错。所以 len 给大了,等于截断。
- REPLACE:全部替换
sql
SELECT REPLACE('Hello Database', 'Database', 'MySQL'); -- Hello MySQL
把 from_str 出现的所有位置 都换成 to_str,并且区分大小写。
注意这里有个反差 :同样是做字符串匹配,
INSTR不区分大小写,REPLACE却区分。所以REPLACE('Hello', 'hello', 'X')不会替换任何内容,而INSTR('Hello', 'hello')能返回 1。混用两者做"先找再换"时容易出 bug,务必先统一大小写(LOWER())再处理。
7.2.7、清理:TRIM 家族
- 去空格
sql
SELECT LTRIM(' ABC'); -- 'ABC' 只去前导空格
SELECT RTRIM('ABC '); -- 'ABC' 只去末尾空格
SELECT TRIM(' ABC '); -- 'ABC' 两端都去
三个函数都只处理两端,夹在中间的空格一个都不会动。
- 去指定字符串
完整语法是 TRIM([{LEADING | TRAILING | BOTH} [remstr] FROM] str),三个方向对应三种效果:
sql
SELECT TRIM(BOTH 'xxx' FROM 'xxxABCxxx'); -- ABC 两端都去(BOTH 可省略)
SELECT TRIM(LEADING 'xxx' FROM 'xxxABCxxx'); -- ABCxxx 只去前导
SELECT TRIM(TRAILING 'xxx' FROM 'xxxABCxxx'); -- xxxABC 只去末尾
和去空格版一样,它也只削两端的连续匹配,不会动字符串中间的 xxx。
注意:
LENGTH()是字节数不是字符数 :'唐三藏'在 gbk 下是 6、在 utf8mb4 下是 9,而CHAR_LENGTH()恒为 3。校验字数必须用CHAR_LENGTH。CONCAT遇到 NULL 全行变 NULL :只要有一个参数是NULL,整个结果就是NULL,先在参数里套IFNULL()。CONCAT_WS遇到 NULL 会跳过:分隔符照常输出,但字段数变少,解析结果时不能按固定列数取。- 索引从 1 开始 :
INSTR、SUBSTR、INSERT的pos都是 1-based,习惯 0-based 的语言容易差一位。 INSTR不区分大小写、REPLACE区分 :做"查找后再替换"时两者行为不一致,先LOWER()统一。INSERT的len是覆盖长度 :len = 0才是纯插入;len超过剩余长度就直接截断到结尾。SUBSTR的FROM ... FOR ...不能写逗号 :SUBSTR(s FROM 8 FOR 1)正确,SUBSTR(s, FROM 8)是语法错误。TRIM不动中间内容:无论去空格还是去指定字符,都只处理两端。HEX()转的是字节 :中文会按字符集展开成多字节,CHAR_LENGTH与HEX结果长度不是一回事。STRCMP返回的是符号不是差值:只表示大小关系,不要当成"差几个字符"。