《MySQL内核拆解:从SQL到InnoDB》(三) : 数据库设计 & 聚合查询 & 分组查询

文章目录

《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. 把两个实体所有的信息全部放在一张表里
  2. 创建两张表,分别记录用户信息与账号信息,并把两张表做关联

一个用户 实体包含:用户昵称、真实姓名、手机号、邮箱地址、性别、学校。

一个账户实体包含:登录用户名、密码。

一个用户只对应一个账户,一个账户也只属于一个用户,所以是 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);
  • 统计每种角色的人数

    sql 复制代码
    select role,count(*) from emp group by role; 
    +--------+----------+
    | role   | count(*) |
    +--------+----------+
    | 老板   |        2 |
    | 讲师   |        2 |
    | 学管   |        2 |
    | npc    |        3 |
    +--------+----------+
  • 统计每种角色的平均工资,最高工资,最低工资

    sql 复制代码
    select 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工资角色和平均工资

    sql 复制代码
    select 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

规则只有三条:

  1. 计算方式是 expr1 − expr2,顺序决定正负。
  2. 只使用日期部分 ,时分秒被直接丢弃------上例中 23:59:59 和 00:00:00 算出来一样是 1 天。
  3. 结果是整数天,不做四舍五入。

补充 :DATEDIFF 只能按"天"算。要按小时、月、年算间隔,用的是 TIMESTAMPDIFF(unit, expr1, expr2),例如 TIMESTAMPDIFF(HOUR, a, b)。


注意:

  1. DATEDIFF 忽略时分秒 :'2024-12-31 23:59:59' 减 '2024-12-30 00:00:00' 结果仍是 1,不是"1 天 23 小时"。要精确到小时请用 TIMESTAMPDIFF。
  2. 参数顺序写反就是负数 :DATEDIFF(a, b) 是 a 减 b,别凭直觉认为"大的减小的"。
  3. INTERVAL 的单位不加引号 ,INTERVAL 31 DAY 正确,INTERVAL 31 'DAY' 报错。
  4. ADDDATE 有简写、DATE_ADD 没有 :ADDDATE(d, 31) 合法,DATE_ADD(d, 31) 不合法。
  5. 月末加减会被截断 :'2024-01-31' + INTERVAL 1 MONTH 得到 2024-02-29。
  6. 同义词带不带括号都能用 :CURRENT_DATE、CURRENT_DATE() 等价;但 NOW 不带括号会报错,它没有关键字形式。
  7. NOW() 在语句内固定 :同一条 SQL 里多列都写 NOW(),拿到的是同一个时刻;要实时值用 SYSDATE()。
  8. 日期字符串统一用 - 分隔 :'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。

注意:

  1. LENGTH() 是字节数不是字符数 :'唐三藏' 在 gbk 下是 6、在 utf8mb4 下是 9,而 CHAR_LENGTH() 恒为 3。校验字数必须用 CHAR_LENGTH。
  2. CONCAT 遇到 NULL 全行变 NULL :只要有一个参数是 NULL,整个结果就是 NULL,先在参数里套 IFNULL()。
  3. CONCAT_WS 遇到 NULL 会跳过:分隔符照常输出,但字段数变少,解析结果时不能按固定列数取。
  4. 索引从 1 开始 :INSTR、SUBSTR、INSERT 的 pos 都是 1-based,习惯 0-based 的语言容易差一位。
  5. INSTR 不区分大小写、REPLACE 区分 :做"查找后再替换"时两者行为不一致,先 LOWER() 统一。
  6. INSERT 的 len 是覆盖长度 :len = 0 才是纯插入;len 超过剩余长度就直接截断到结尾。
  7. SUBSTR 的 FROM ... FOR ... 不能写逗号 :SUBSTR(s FROM 8 FOR 1) 正确,SUBSTR(s, FROM 8) 是语法错误。
  8. TRIM 不动中间内容:无论去空格还是去指定字符,都只处理两端。
  9. HEX() 转的是字节 :中文会按字符集展开成多字节,CHAR_LENGTH 与 HEX 结果长度不是一回事。
  10. STRCMP 返回的是符号不是差值:只表示大小关系,不要当成"差几个字符"。
相关推荐
Decay__3 小时前
01_MySQL 体系架构全解 —— 连接层、SQL 层、存储引擎层与 InnoDB 深入
运维·数据库·sql·mysql·架构
Linux_cy3 小时前
nginx 502 排查手册:九种原因分类、定位命令与检查顺序
linux·运维·服务器·网络·数据库·nginx·ssh
在多学一点3 小时前
MySQL_DDL的基本命令
数据库·mysql·adb
9624563 小时前
补打一条卡,为什么要从前一天重新算:人工补录与实时重算业务复盘
java·mysql·kafka
是程序喵呀4 小时前
Redis实现持久化
数据库·redis·缓存
余槐i4 小时前
PostgreSQL 加索引后查询反而更慢:优化器弃用索引的 3 种情形
spring boot·redis·mysql·postgresql·事务
z06.4 小时前
MySQL存储引擎
数据库·mysql
小徐不徐说4 小时前
数据库存储基础:理解表结构、主键、事务与 BLOB
开发语言·数据库·c++·qt·程序设计
JackLam4 小时前
Gitea HTTPS 反向代理配置
数据库