dream

一个菜鸟程序员的成长历程

0%

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

DDD记账软件实战二

经过一开始的战略设计和战术设计以后,我们有了一个初步的服务划分

  • 用户服务:管理用户相关的信息
  • 记账服务:管理账本和收支相关的信息

并且,我们已经知道了我们要创建一个什么样的目录结构,每个服务的目录如下:

  • starter: 接口层,包括HTTP接口、队列的消费者、DTO、启动类
  • api: 接口层,提供RPC接口,包括外部RPC接口需要使用的DTO、枚举等
  • application:应用服务层,放应用服务,负责编排领域服务、聚合根等。
  • domain:领域服务层,放领域相关的一切信息,领域服务负责编排聚合根,聚合根负责完成自身的业务逻辑。
  • infrastructure: 基础设施层,放配置、仓储、工厂、对外部的请求、发送MQ消息等。
  • common: 放一些公共信息。

我们这个整体架构图如下:

架构图

除了我们一开始划分好的两个服务以外,还有一些支撑服务,属于不管干啥都需要用到的。

创建用户服务

首先,我们要创建一个spring项目。可以直接使用start.spring.io来创建。

我们选择的是

  • java
  • maven
  • 3.5.4版本
  • java21
  • Jar

接下来输入一些基础信息就可以了。

创建用户服务

点击下面的generate就可以把项目代码下载下来了。

下载下来以后,我们需要调整maven文件和目录结构。

最外层的maven文件内容如下,我们主要引入了下面的一些依赖

  • spring cloud
  • nacos: 配置管理和服务注册发现
  • mysql
  • mybatis plus
  • jwt
  • lombok
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>user</name>
<description>记账软件用户服务</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
<spring-cloud.version>2025.0.0</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version>
<project.version>0.0.1-SNAPSHOT</project.version>
</properties>

<modules>
<module>user-domain</module>
<module>user-application</module>
<module>user-infrastructure</module>
<module>user-starter</module>
<module>user-common</module>
</modules>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- 添加Spring Cloud Alibaba依赖管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- 内部模块依赖管理 -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-infrastructure</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 添加Nacos服务发现依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- 添加Nacos配置管理依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!-- 添加bootstrap配置支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>

<!-- 引入jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>

</project>

调整了目录结构,整体目录如下图

目录图

我们首先复制一份代码放到目录下面并改名为user-starter。然后修改maven文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>user-starter</artifactId>
<packaging>jar</packaging>
<name>user-starter</name>
<description>用户服务启动模块</description>

<dependencies>
<!-- 依赖应用层 -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-application</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

接下来用同样的方法复制出应用服务层。改名user-application.修改maven文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>user-application</artifactId>
<packaging>jar</packaging>
<name>user-application</name>
<description>用户服务应用层</description>

<dependencies>
<!-- 依赖领域层 -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-domain</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>

</project>

在复制出领域层,改名user-domain。maven文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>user-domain</artifactId>
<packaging>jar</packaging>
<name>user-domain</name>
<description>用户服务领域层</description>

<dependencies>
<!-- 依赖common -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- 领域层只需要基础的Java依赖,不需要Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

</project>

继续复制出基础设施层,改名user-infrastructure。maven文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>user-infrastructure</artifactId>
<packaging>jar</packaging>
<name>user-infrastructure</name>
<description>用户服务基础设施层</description>

<dependencies>
<!-- 依赖领域层 -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-domain</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>

<!-- 引入jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>

</project>

最后,复制出common层,即可。maven文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>user-common</artifactId>
<packaging>jar</packaging>
<name>user-common</name>
<description>用户服务公共层</description>

<dependencies>
</dependencies>

</project>

设置启动类

我们将启动类放到user-starter下面。内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zt.bookkeeping.user;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class UserApplication {

private static final Logger logger = LoggerFactory.getLogger(UserApplication.class);

public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
logger.info("用户服务启动成功");
}

}

目录如下图:

启动用户服务

接下来启动服务,启动成功,打印出用户服务启动成功。我们就成功创建了DDD结构的用户微服务了。

总结

本次我们梳理了整体的系统架构图,为接下来的工作做了准备,并且带大家从0-1创建了用户微服务。并且改造成了DDD项目的目录结构。

至于剩下的记账微服务。用同样的方式创建即可。遇到问题可以随时在评论区留言~

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

高阶应用实战

除了常用的增删改查以外,MySQL还有很多的功能。比如CTE窗口函数这种。再比如一些特性比如视图触发器等。

这次我们就来看一下对于这些如何使用。

有些功能用好了可以完成很多实际业务需求的。

CTE(common table expressions)

在MySQL中,CTE(Common Table Expressions,公共表表达式)是一种临时的结果集,可以在查询中被引用。CTE通常用于简化复杂的查询,使查询更易于理解和维护。CTE在MySQL 8.0及更高版本中得到了支持。

CTE的主要特点

临时结果集:

  • CTE是一个临时的结果集,可以在查询中被引用。它类似于子查询,但更易于阅读和维护。
    简化复杂查询:
  • CTE可以将复杂的查询分解为多个简单的部分,使查询更易于理解和维护。
    可重用性:
  • CTE可以被多次引用,减少了代码重复,提高了开发效率。
    递归查询:
  • CTE支持递归查询,可以用于处理层次结构或递归数据。

使用CTE实现获取每个课程中分数最高的学生信息。

通过WITH语句来声明一个临时表。表名cteSource,表的内容就是最的sid,通过SELECT MAX(sid) FROM enrolled查询出来的结果。字段名叫maxId

然后在查询语句里面就可以连接cteSource表,然后通过sid = cteSource.maxId 来获取到sid最大的用户信息。

1
2
3
4
5
WITH cteSource (maxId) AS (
SELECT MAX(sid) FROM enrolled
)
SELECT name FROM student, cteSource
WHERE student.sid = cteSource.maxId

还有一些其他的用法,比如:

1
2
3
4
5
6
7
WITH cte1 (col1) AS (
SELECT 1
),
cte2 (col2) AS (
SELECT 2
)
SELECT * FROM cte1, cte2;

CTE(公共表表达式)使用场景

  1. 递归查询(如树形结构)

CTE最常见的场景之一是递归查询,比如组织架构、分类目录、菜单结构等树状数据。例如,查询某个员工的所有下属员工。

1
2
3
4
5
6
7
8
9
10
11
12
WITH RECURSIVE subordinates AS (
SELECT id, name, manager_id
FROM employees
WHERE manager_id IS NULL -- 顶层经理

UNION ALL

SELECT e.id, e.name, e.manager_id
FROM employees e
INNER JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;
  1. 简化复杂查询逻辑,提高可读性

当查询逻辑复杂时,可以将中间结果用CTE分段处理,让SQL更易读。例如,先筛选出某类用户,再基于这些用户做进一步统计。

1
2
3
4
5
6
WITH active_users AS (
SELECT user_id
FROM user_logins
WHERE login_date > CURRENT_DATE - INTERVAL '30 days'
)
SELECT COUNT(*) FROM orders WHERE user_id IN (SELECT user_id FROM active_users);
  1. 多次引用中间结果,避免重复计算

如果某个子查询需要被多次引用,CTE可以定义一次,后续多次调用,避免重复书写和计算,提高效率。

1
2
3
4
5
6
7
WITH sales_summary AS (
SELECT product_id, SUM(amount) AS total_sales
FROM sales
GROUP BY product_id
)
SELECT * FROM sales_summary WHERE total_sales > 1000;
-- 也可以在后续多个查询中引用 sales_summary
  1. 分步处理数据,逐步细化结果

例如,先计算每个部门的总销售额,再筛选出销售额最高的部门。

1
2
3
4
5
6
7
8
9
10
11
12
WITH dept_sales AS (
SELECT department_id, SUM(sales) AS total_sales
FROM employees
GROUP BY department_id
),
top_dept AS (
SELECT department_id
FROM dept_sales
ORDER BY total_sales DESC
LIMIT 1
)
SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM top_dept);
  1. 数据去重和排名

CTE可以配合窗口函数实现数据去重、分组排名等需求。

1
2
3
4
5
6
WITH ranked_orders AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
FROM orders
)
SELECT * FROM ranked_orders WHERE rn = 1;

实战案例

比如我之前的业务。有一个公司表,一个公司合同表,一个公司合同申请表。在后台的公司合同列表里面,要展示最新的申请记录和申请状态。

如果没有CTE的话,我们就需要使用子查询来实现。这对于一个列表来说,性能是很低的。

表结构假设:

  • company:公司表,字段如 company_id, company_name 等。
  • company_contract:公司合同表,字段如 contract_id, company_id, contract_no 等。
  • company_contract_application:公司合同申请表,字段如 application_id, contract_id, apply_time, status 等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SELECT
c.company_id,
c.company_name,
cc.contract_id,
cc.contract_no,
cca.application_id,
cca.apply_time,
cca.status
FROM company c
JOIN company_contract cc ON c.company_id = cc.company_id
LEFT JOIN company_contract_application cca
ON cc.contract_id = cca.contract_id
AND cca.apply_time = (
SELECT MAX(apply_time)
FROM company_contract_application
WHERE contract_id = cc.contract_id
);

而有了CTE以后,我们可以通过CTE来实现,这样可以提升性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WITH latest_applications AS (
SELECT
application_id,
contract_id,
apply_time,
status,
ROW_NUMBER() OVER (PARTITION BY contract_id ORDER BY apply_time DESC) AS rn
FROM company_contract_application
)
SELECT
c.company_id,
c.company_name,
cc.contract_id,
cc.contract_no,
la.application_id,
la.apply_time,
la.status
FROM company c
JOIN company_contract cc ON c.company_id = cc.company_id
LEFT JOIN latest_applications la ON cc.contract_id = la.contract_id AND la.rn = 1;

窗口函数

窗口函数(Window Function)是SQL标准中的一种功能强大的工具,它允许在查询中对一组行进行计算,而不会像聚合函数那样消除行的个数。窗口函数在MySQL 8.0及更高版本中得到了支持,它们可以用于计算移动平均值、累积和、排名等复杂的分析任务。

窗口函数的主要特点

行级计算:

  • 窗口函数在每一行上执行计算,同时可以访问同一组中的其他行。
  • 这与聚合函数不同,聚合函数会将多行数据合并为一行。
    分区和排序:
  • 窗口函数可以使用PARTITION BY子句将数据分成多个分区,每个分区独立计算。
  • 可以使用ORDER BY子句在每个分区内对数据进行排序。
    灵活的范围定义:
  • 窗口函数可以定义计算的范围,如当前行的前几行或后几行。
  • 使用ROWS或RANGE子句可以指定计算的范围。
    多种功能:
  • 窗口函数提供了多种功能,如ROW_NUMBER()、RANK()、DENSE_RANK()、NTILE()、SUM()、AVG()、LEAD()、LAG()等。

ROW_NUMBERRANK都需要和OVER一起使用。

  • ROW_NUMBER(): 显示当前行号
  • RANK() : 显示排序后的排名,如果没有排序,都是1
  • OVER()
    • PARTITION BY 进行分组
    • GROUP BY 进行分组
    • ORDER BY 排序

001

002

003

实战:获取每个课程中分数最高的学生信息

下面的SQL,在postgresql中执行成功,mysql8执行报错。

首先查询所有课程信息,并按照课程分组,按照分数排序。

1
2
3
SELECT *,
RANK() OVER (PARTITION BY cid ORDER BY grade ASC)
AS rank FROM enrolled

004

接着搜索上表中分数为1,也就是分数最高的学生。也就是每个课分数最高的学生信息。

1
2
3
4
5
6
SELECT * FROM (
SELECT *,
RANK() OVER (PARTITION BY cid
ORDER BY grade ASC)
AS rank FROM enrolled) AS ranking
WHERE ranking.rank = 1

005

视图

在MySQL中,视图(View)是一种虚拟表,其内容由SQL查询定义。视图并不存储实际的数据,而是根据定义的查询动态生成数据。视图可以简化复杂的SQL操作,提供数据的逻辑抽象,并且可以限制对某些数据的访问,从而增强数据的安全性。

MySQL中的视图是虚拟视图,说白了就是一条SQL语句,当查询视图的时候执行SQL语句而已。

除此之外,还有一个东西叫做物化视图,MySQL并没有实现这个东西,物化视图就是一张真的表,而不是一个SQL语句,因此查询效率更好。

视图的主要特点

虚拟表:

  • 视图是一个虚拟表,其内容由SQL查询定义。视图本身并不存储数据,而是根据定义的查询动态生成数据。

简化复杂查询:

  • 视图可以简化复杂的SQL操作,将复杂的查询逻辑封装起来,使用户可以像查询普通表一样查询视图。

数据抽象:

  • 视图提供数据的逻辑抽象,隐藏了底层表的复杂性,使用户可以更直观地访问数据。

安全性:

  • 视图可以限制对某些数据的访问,增强数据的安全性。通过视图,用户只能访问视图定义的特定数据,而不能访问底层表的全部数据。

更新限制:

  • 视图可以是可更新的,也可以是不可更新的。可更新视图允许用户通过视图插入、更新或删除数据,但需要满足一定的条件。不可更新视图则不允许用户通过视图修改数据。

创建一个视图。下面的语句,创建一个视图,视图名称是sales_employees,内容就是后面的Select语句的结果。当原始表employees变化以后,视图的内容也会跟着变化。

1
2
3
4
CREATE VIEW sales_employees AS
SELECT name, salary
FROM employees
WHERE department = 'Sales';

实战:交易聚合

我之前有一个业务,存在两个表,一个是买单表,代表买方要买一个产品出的价格。一个是卖单表,代表卖方要卖一个产品出的价格。

现在有一个需求是:在C端的一个页面上,要求混合展示买单信息和卖单信息,按照价格从低到高排序

数据库假设:

  • buy: 买单表
  • sell: 卖单表
  • order: 交易视图
1
2
3
4
CREATE VIEW order AS
SELECT *
FROM buy, sell
order by price asc;

触发器

在MySQL中,触发器(Trigger)是一种特殊的存储过程,它在特定的数据库操作(如INSERT、UPDATE、DELETE)发生时自动执行。触发器可以用于实现复杂的业务逻辑,确保数据的完整性和一致性,以及自动维护数据的同步。

触发器的主要特点

自动执行:

  • 触发器在特定的数据库操作发生时自动执行,无需显式调用。这使得触发器可以用于实现自动化的数据处理和维护。

数据完整性:

  • 触发器可以用于确保数据的完整性和一致性。例如,可以在插入或更新数据时自动检查数据的有效性,或者在删除数据时自动清理相关数据。

业务逻辑:

  • 触发器可以用于实现复杂的业务逻辑。例如,可以在插入或更新数据时自动计算某些字段的值,或者在删除数据时自动更新相关表的数据。

数据同步:

  • 触发器可以用于自动维护数据的同步。例如,可以在插入或更新数据时自动更新相关表的数据,或者在删除数据时自动清理相关表的数据。

触发器的类型

  • BEFORE INSERT:
    • 在插入数据之前执行触发器逻辑。
  • AFTER INSERT:
    • 在插入数据之后执行触发器逻辑。
  • BEFORE UPDATE:
    • 在更新数据之前执行触发器逻辑。
  • AFTER UPDATE:
    • 在更新数据之后执行触发器逻辑。
  • BEFORE DELETE:
    • 在删除数据之前执行触发器逻辑。
  • AFTER DELETE:
    • 在删除数据之后执行触发器逻辑。

触发器的限制

性能影响:

  • 触发器的执行会增加数据库操作的开销,可能会影响性能。因此,应谨慎使用触发器,避免在高频操作的表上定义过多的触发器。
    复杂性:
  • 触发器的逻辑可以非常复杂,但过多的复杂逻辑可能导致触发器难以维护和调试。因此,应尽量保持触发器的逻辑简单明了。
    调试困难:
  • 触发器的调试相对困难,因为它们在特定的操作发生时自动执行,难以直接观察和调试。因此,建议在开发和测试阶段充分测试触发器的逻辑。

因此,实际开发中基本不使用触发器。

存储过程

在MySQL中,存储过程(Stored Procedure)是一种预编译的SQL语句集合,它存储在数据库中,可以通过调用其名称并传递参数来执行。存储过程可以包含复杂的逻辑和多个SQL语句,用于完成特定的任务。它们类似于其他编程语言中的函数或方法。

可以把存储过程想成一个函数。只不过是在MySQL中的函数,这个函数可以实现各种功能。可以实现一些复杂的SQL处理,这样可以简化调用。

存储过程的主要特点

预编译:

  • 存储过程在创建时被预编译并存储在数据库中,这使得它们的执行速度比单独的SQL语句更快。
    代码重用:
  • 存储过程可以被多次调用,减少了代码重复,提高了开发效率。
    减少网络流量:
  • 存储过程在服务器端执行,减少了客户端和服务器之间的网络流量,因为只需要发送存储过程的名称和参数,而不是大量的SQL语句。
    安全性:
  • 存储过程可以限制用户对底层数据的直接访问,只允许通过存储过程进行数据操作,从而增强数据的安全性。
    事务管理:
  • 存储过程可以包含事务控制语句,如COMMIT和ROLLBACK,确保数据操作的完整性和一致性。

存储过程的限制

性能影响:

  • 存储过程的执行会增加数据库操作的开销,可能会影响性能。因此,应谨慎使用存储过程,避免在高频操作的表上定义过多的存储过程。
    复杂性:
  • 存储过程的逻辑可以非常复杂,但过多的复杂逻辑可能导致存储过程难以维护和调试。因此,应尽量保持存储过程的逻辑简单明了。
    调试困难:
  • 存储过程的调试相对困难,因为它们在服务器端执行,难以直接观察和调试。因此,建议在开发和测试阶段充分测试存储过程的逻辑。

实战案例

存储过程的实际应用场景有限。

我们之前有一个场景,是需要批量处理数据,有几十行SQL语句。因此将这些SQL封装到了一个存储过程中,这样只需要执行这个存储过程就可以了。

这里给出一个简单的实战案例:

创建一个存储过程get_employee_details,用于根据员工ID获取员工的详细信息:

DELIMITER用来设置结束符,比如正常的句子结束符是句号。代码结束符是分号;

IN代表输入参数,也就是这个函数有一个输入参数emp_id,是int类型。
还有out代表输出参数,用于返回结果。
INOUT代表既可以输入参数也可以是输出参数。

1
2
3
4
5
6
7
8
9
10
DELIMITER //

CREATE PROCEDURE get_employee_details(IN emp_id INT)
BEGIN
SELECT name, department, salary
FROM employees
WHERE id = emp_id;
END //

DELIMITER ;

上面这个存储过程的函数体,就是一段select查询语句。

存储函数

在MySQL中,存储函数(Stored Function)是一种预编译的SQL语句集合,类似于存储过程,但它返回一个值。存储函数可以被SQL语句直接调用,就像调用普通的函数一样。存储函数通常用于封装复杂的逻辑,并在查询中重用这些逻辑。

存储函数同样是一个函数,和上面的存储过程差不多。

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

区别类别 存储过程 存储函数
返回值 1. 可以返回多个值,通过OUT或INOUT参数返回。
2. 可执行多个SQL语句,但不直接返回单一值。
1. 必须返回一个单一的值。
2. 可以被SQL语句直接调用,就像普通函数一样。
调用方式 1. 通过CALL语句调用。
2. 可执行复杂逻辑,包括多个SQL语句和事务控制。
1. 可直接在SQL语句中调用,如普通函数。
2. 通常用于封装复杂逻辑,并在查询中重用。

存储函数的主要特点

主要特点 说明
返回值 存储函数必须返回一个值,这使得它们可以被SQL语句直接调用。
代码重用 存储函数可以被多次调用,减少了代码重复,提高了开发效率。
减少网络流量 存储函数在服务器端执行,减少了客户端和服务器之间的网络流量,因为只需要发送函数的名称和参数,而不是大量的SQL语句。
安全性 存储函数可以限制用户对底层数据的直接访问,只允许通过函数进行数据操作,从而增强数据的安全性。
事务管理 存储函数可以包含事务控制语句,如COMMIT和ROLLBACK,确保数据操作的完整性和一致性。

实战案例

创建一个存储函数get_employee_salary,用于根据员工ID获取员工的薪资:

可以看到和上面存储过程的区别,声明了一个返回值,类型是DECIMAL,最后通过return返回了,并且声明了一个变量。参数也没有IN、OUT这种了。

1
2
3
4
5
6
7
8
9
10
11
12
13
DELIMITER //

CREATE FUNCTION get_employee_salary(emp_id INT)
RETURNS DECIMAL(10, 2)
BEGIN
DECLARE emp_salary DECIMAL(10, 2);
SELECT salary INTO emp_salary
FROM employees
WHERE id = emp_id;
RETURN emp_salary;
END //

DELIMITER ;

总结

本次讲解了MySQL的高级功能和实战应用的一些场景。

只有知道了场景才能活学活用。这里面最常用的其实还是CTE和窗口函数。

可以多多练习,希望遇到这些场景的时候,大家能想起来使用这些功能。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

DDD战术设计代码结构

之前讲过了如何进行领域建模,也就是将真实世界映射到业务领域中。

接下来,看一下如何将业务领域落地到代码实现里面。

只有战略设计战术设计结合到一起,才能做好DDD,不能只注重领域建模而忽视了代码实现,也不能只注重代码实现而忽视了领域建模。

层次结构

贫血三层架构

一般情况下,我们使用的是贫血三层架构。也就是

  • controller:负责接收参数,参数校验,调用service处理业务逻辑。
  • service:处理业务逻辑
  • Model:和数据库交互,可能是Mybatics。

贫血三层架构

DDD四层架构

而对于DDD来说,使用的则是经典四层架构。

  • 用户接口层:作为程序的入口,controller就属于这一层。
  • 应用服务层:负责领域编排,不负责具体的业务逻辑处理。通过调用领域服务层来完成业务逻辑处理。
  • 领域服务层:负责具体的业务逻辑处理。可以将service的业务逻辑下沉到这一层,而业务逻辑编排则放到应用服务层。
  • 基础设施层:提供一些基础设施,包括数据库持久化,缓存,RPC调用等等。Model就属于这一层。

DDD四层架构

如果是重构项目的话,可以将三层架构拆解成四层架构,映射关系如下:

映射关系

用户接口层

用户接口层的命名可以是starter或者是interface等等。

用户接口层的职责就是整个程序的入口,对外部提供接口,包括HTTP接口RPC接口

这一层的结构很简单:

  • controller: 就是传统的controller
  • dto: dto里面包括了请求参数和返回参数
  • converter:负责将请求参数转换成应用服务的入参,将应用服务的出参转换成返回参数
  • consumer:消费者,负责消费MQ的消息

我们还可以将这一层进一步划分为两个目录,一个是HTTP接口的starter目录,一个是RPC接口的api目录。

因为一般来说rpc会对外部提供一个包。因此可以将这两个分开。

而且对于RPC接口的包来说,consumer这个目录是没有用的,因此,只需要三个目录就可以了。

对于HTTP接口来说,因此请求和返回都是JSON格式的对象,因此,可以省略dto和converter这两个步骤。

用户接口层

应用服务层

应用服务的职责边界很清晰,就是调用领域服务、聚合等完成业务操作。这一层只做服务编排,而不处理具体的业务逻辑。

  1. 代码结构

因此,应用服务层的代码结构也很简单,可以放到目录application里面。

  • service: 应用服务,应用服务不需要和领域服务一对一的关系,也不需要和controller一对一的关系。根据业务来创建即可。
  • dto: 这个里面存放应用服务层的入参和出参。可以根据CQRS规则来设置,创建、修改、删除的入参是Command。查询的入参是Query
  • event:应用服务层的事件发布。

应用服务层

  1. 代码示例:

这里,用注册用户来做一个简单的示例。看一下应用服务层是如何做服务编排的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Service
@Slf4j
public class MobileRegisterApplicationService {

@Resource
private UserAggService userAggService;

@Resource
private ApplicationEventPublisher eventPublisher;

@Resource
private SnowFlakeGenerator snowFlakeGenerator;

public Long register(MobileRegisterRequest command) {
// 1. 校验验证码是否正确
checkVerifyCode(command.getMobile(), command.getCode());

// 2. 调用用户领域服务 判断能否注册用户
userAggService.canRegister(command.getMobile());

// 3. 验证码正确 用户可以注册 生成用户聚合
UserAgg userAgg = UserAgg.init(command.getMobile(), snowFlakeGenerator.nextId("user"));

// 4. 调用用户领域服务 注册用户
userAggService.save(userAgg);

// 5. 发送注册成功领域事件
eventPublisher.publishEvent(new UserRegisteredEvent(userAgg.getId(), userAgg.getUsername(), null, LocalDateTime.now()));
return userAgg.getId();
}

private void checkVerifyCode(String mobile, String verifyCode) {
// 调用RPC接口来校验验证码

}
}

领域服务层

领域服务层是DDD的核心,所有的业务逻辑都以充血模型的形式放到聚合根、实体、值对象里面。

领域服务层的入参和出参都应该是领域模型。

  1. 代码结构

领域服务层的代码结构相对复杂。领域服务层的代码目录首先按照聚合来区分。其次,每个聚合下面有自己的领域模型和领域服务。可以放到目录domain里面。

  • 聚合
    • service: 存储领域服务,领域服务的职责是编排聚合。调用聚合来完成业务逻辑。
    • entity:这个目录存储所有的聚合根、实体和值对象。
    • event:存放领域事件
    • repository:存放仓储的接口,仓储的具体实现放在基础设施层。
    • factory:存放工厂的接口,工厂的具体实现放在基础设施层。
    • enums:存放一些领域层使用的枚举
    • gateway:存放一些RPC请求的接口,具体实现放在基础设施层。

领域服务层

  1. 代码示例

可以看到领域服务里面调用了Repository来重建聚合,然后判断聚合是否存在来完成校验。

1
2
3
4
5
6
7
8
9
public void canRegister(String mobile) {
// 根据要注册的手机号获取用户信息
UserAgg userAgg = userRepository.getUserByMobile(mobile);
// 校验用户是否存在
if (userAgg != null) {
log.warn("用户已存在");
throw new DomainException(ResultCode.USER_ALREADY_EXISTS);
}
}

再来看一下判断能否登陆的方法。这里调用的是聚合的方法来校验用户状态,而不是通过领域服务来校验。这就是编排聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void canLogin(UserAgg userAgg) throws UserLoginException {
// 0. 校验用户存在
if (userAgg == null) {
log.warn("用户不存在");
throw new UserLoginException(ResultCode.USER_NOT_FOUND);
}

// 2. 校验用户状态
if (!userAgg.validateStatus()) {
log.warn("用户状态错误:{}", userAgg.getUserStatus());
throw new UserLoginException(ResultCode.USER_STATUS_ERROR);
}
}

基础设施层

基础设施层为所有层提供基础的技术支持,包括但不限于数据库、缓存、队列、RPC请求、HTTP请求等等。

基础设施层有两个重要的概念

  • Repository:仓储,为领域模型提供持久化的功能,入参是领域模型,将领域模型转换成数据模型,在进行持久化。至于是缓存还是数据库,是MySQL还是PostgreSQL,上层并不关心。同时,也负责从持久化中恢复领域模型。
  • Factory:工厂,和工厂模式的工厂不同,这里的工厂是负责创建领域模型的。有些领域模型的创建较为复杂,可以封装到工厂里面。
  1. 基础设施层的目录

基础设施层的代码结构可以按照职能区分。其中Repository和聚合是一对一的关系,一个聚合有一个Repository负责持久化和恢复。可以放到目录infrastructure里面。

  • config: 放置项目的配置文件。
  • repository: 放仓储的具体实现类,一个仓储对应一个聚合,负责持久化聚合里面的多个表数据。
  • factory: 工厂的具体实现类,负责创建领域模型。
  • gateway:放对外部的请求,包括HTTP请求和RPC请求。
  • util:放一些工具类
  • listener: 放一些领域事件的监听者,有一些领域事件是放到队列中其他服务监听,也有一些领域事件是自己服务监听的。
  • producer: 生产者,负责向队列中发送消息

基础设施层

  1. 代码示例

以用户聚合对应的用户仓储来看,需要创建用户的时候传入用户聚合。然后转换成数据模型,当插入数据库以后会更新用户聚合的ID。

1
2
3
4
5
6
7
@Override
public void insert(UserAgg userAgg) {
UserPO userPO = this.toPO(userAgg);
userMapper.insert(userPO);
// 回填id
userAgg.setId(userPO.getId());
}

当需要查询数据的时候,传入查询条件,返回用户聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public UserAgg getUserByMobile(String mobile) {
// 1. 查询用户信息
LambdaQueryWrapper<UserPO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserPO::getMobile, mobile);
UserPO userPO = userMapper.selectOne(queryWrapper);
if (userPO == null) {
return null;
}

// 2. 转化成聚合返回
return this.toEntity(userPO);
}

四层架构之间的依赖关系

四层架构的目录结构已经出来了

  • starter: 放置启动类和HTTP请求以及队列的消费者。
  • api: 放置RPC接口以及外部需要使用的一些DTO。
  • application:应用服务层的代码
  • domain: 领域服务层的代码
  • infrastructure: 基础设施层的代码
  • common: 可以放置一些公共的代码,比如工具类、exception、枚举等。

总的目录结构如下,外层的user代表服务名称,这个是用户服务,也就是用户领域。

目录

用户接口层

  • 依赖应用服务层:调用应用服务实现功能。
1
2
3
4
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-domain</artifactId>
</dependency>

应用服务层

  • 依赖领域服务层:应用服务的职责就是编排领域服务和领域模型。
1
2
3
4
5
<!-- 依赖领域层 -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-domain</artifactId>
</dependency>

领域服务层

  • 什么也不依赖,领域服务层作为最底层的服务,什么也不依赖,如果需要使用基础设施的话,接口定义在领域层,实现放在基础设施层。

基础设施层

  • 依赖领域服务层:因为接口要放在领域服务层,实现放在基础设施层
1
2
3
4
5
<!-- 依赖领域层 -->
<dependency>
<groupId>com.zt.bookkeeping.user</groupId>
<artifactId>user-domain</artifactId>
</dependency>

整体依赖关系如下:

整体依赖关系

如何确定代码放在哪一层?

写代码的时候如何确定这个代码该放到哪一层呢?

如果这个代码是负责和外部交互的,那么就放到基础设施层。

如果这个代码包含业务逻辑,那么就放到领域层,将业务逻辑拆分到聚合、实体、值对象中,如果逻辑复杂,就通过领域服务进行编排。

应用层代码只编排领域服务和领域模型。

接口层代码只暴露接口给外部。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

用 @TypeHandler 做自动转换进阶:MyBatis + 自定义 TypeHandler

之前讲过通过枚举+值对象的方式来构建用户状态和用户类型。

但是每次查询的时候我们都需要手动的将数据库中的1,2,3来映射成值对象。

我们可以使用TypeHandler来实现自动转换,就不需要每次都手动执行了。

场景目标

已经使用了 UserStatus 值对象 + UserStatusEnum 枚举来表达用户状态(如 ACTIVE、FROZEN、DELETED),现在希望:

  • 数据库存储 Integer 类型(如:1、2、3)
  • MyBatis 能自动将 Integer → UserStatus
  • 不再每次手动 UserStatus.of(statusInt) 转换

最佳方案:MyBatis 自定义 @TypeHandler

✅ 1. 项目结构

1
2
3
4
5
6
7
8
9
10
11
domain.model.user
└── UserStatus.java(值对象)
└── UserStatusEnum.java(枚举)

infrastructure.mybatis.typehandler
└── UserStatusTypeHandler.java(自定义 TypeHandler)

infrastructure.repository.mybatis
└── UserMapper.java
└── UserPO.java
└── UserMapper.xml

✅ 2. 代码实现

✅ 值对象:UserStatus(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UserStatus {
private final UserStatusEnum status;

public static final UserStatus ACTIVE = new UserStatus(UserStatusEnum.ACTIVE);
public static final UserStatus FROZEN = new UserStatus(UserStatusEnum.FROZEN);
public static final UserStatus DELETED = new UserStatus(UserStatusEnum.DELETED);

private UserStatus(UserStatusEnum status) {
this.status = status;
}

public static UserStatus of(int code) {
return new UserStatus(UserStatusEnum.fromCode(code));
}

public int getCode() {
return status.getCode();
}

public UserStatusEnum getStatusEnum() {
return status;
}

@Override
public boolean equals(Object o) {
return (o instanceof UserStatus that) && this.status == that.status;
}

@Override
public int hashCode() {
return status.hashCode();
}
}

✅ 枚举类:UserStatusEnum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum UserStatusEnum {
ACTIVE(1, "启用"),
FROZEN(2, "冻结"),
DELETED(3, "注销");

private final int code;
private final String label;

UserStatusEnum(int code, String label) {
this.code = code;
this.label = label;
}

public int getCode() { return code; }

public static UserStatusEnum fromCode(int code) {
for (UserStatusEnum e : values()) {
if (e.code == code) return e;
}
throw new IllegalArgumentException("未知状态码: " + code);
}
}

✅ 自定义 TypeHandler:UserStatusTypeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(UserStatus.class)
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {

@Override
public void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}

@Override
public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return UserStatus.of(code);
}

@Override
public UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return UserStatus.of(code);
}

@Override
public UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return UserStatus.of(code);
}
}

✅ 3. PO 层配置 TypeHandler

1
2
3
4
5
6
7
8
9
10
11
public class UserPO {
private Long id;
private String username;
private String passwordHash;

@TableField(typeHandler = UserStatusTypeHandler.class)
private UserStatus status;

// getter/setter
}

如果你用 XML 而不是注解,也可以在 XML 中配置:

1
<result column="status" property="status" typeHandler="com.example.infrastructure.mybatis.typehandler.UserStatusTypeHandler"/>

✅ 4. 注册 TypeHandler(推荐方式)
Spring Boot + MyBatis:
确保你在配置类里添加扫描:

1
2
3
4
5
6
7
8
9
10
11
@MapperScan("com.example.infrastructure.repository.mybatis")
@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.getTypeHandlerRegistry().register(UserStatusTypeHandler.class);
};
}
}

或者:使用 Spring Boot 自动注册(只要包结构正确 + 被 Spring 管理)

✅ 5. 使用效果

现在,MyBatis 自动完成:

  • SELECT 时:从 int 字段转成 UserStatus 值对象
  • INSERT/UPDATE 时:从 UserStatus 转为 int

你无需手动 .of() 转换,仓储代码更简洁:

1
2
3
4
5
6
7
8
UserPO userPO = mapper.selectByUsername("jack");
User user = new User(
userPO.getId(),
userPO.getUsername(),
userPO.getPasswordHash(),
userPO.getStatus() // ✅ 直接是 UserStatus
);

总结

问题 建议
如何让 MyBatis 自动转换值对象? ✅ 自定义 @TypeHandler 并在字段或 XML 中指定
推荐封装格式? ✅ 使用 enum + ValueObject 组合建模
是否适用于其他值对象? ✅ 适用于所有 “数据库是整数/字符串” 的值对象场景

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

用户状态(UserStatus) 和 用户类型(UserType) 应该如何在领域模型中表达?

这是一个很有代表性的建模问题:用户状态(UserStatus) 和 用户类型(UserType) 应该如何在领域模型中表达?

✅ 最佳实践结论(DDD 建模角度):

✅ 建议建模为单独的值对象(Value Object),而不是直接使用 Integer 类型。

接下来分析一下为什么

✅ 原因分析:为什么不用普通的 Integer?

问题 使用 Integer 的弊端
可读性差 status == 1 到底代表啥?新手难以理解
安全性差 任何地方都可以传入不合法值,如 status = 999
违反封装 User 实体暴露了状态内部的具体编码实现
业务语义不明确 Integer 不能表达“启用/冻结/注销”等领域含义

✅ 为什么用值对象更合适?

在 DDD 中,值对象(Value Object)是具有领域语义但无身份标识的对象,适合表达用户的“状态”、“类型”、“级别”等固定枚举值的场景。

它能提供:

✅ 明确的领域语言(如:UserStatus.ACTIVE、UserType.ADMIN)
✅ 不变性(状态是不可变的值对象)
✅ 合法性控制(通过私有构造 + 工厂控制范围)
✅ 方便扩展(可以加业务方法,如 canLogin())

将用户状态设置为值对象以后,我们能明确的表达状态的含义,是正常冻结还是封禁等等。且避免了非法值。并且如果要变更的话,是变更整个值对象。

在领域建模中,你可以一下子明白这个实体中这个值的含义。

在DDD中,不要使用传统的数据建模思想,而是要使用领域建模思想。

传统的数据建模思想的话,用户状态存储在数据库中通常都是1,2,3,4这样的Int值。

所以实体中一般也直接使用Int类型的值作为存储。

领域建模思想则强调将真实世界中的业务映射成领域模型。数据库仅仅是领域模型持久化的一个地方而已。

✅ 示例:建模用户状态为值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.accountbook.domain.model.user;

import java.util.Objects;

public class UserStatus {

private final int code;

public static final UserStatus ACTIVE = new UserStatus(1);
public static final UserStatus FROZEN = new UserStatus(2);
public static final UserStatus DELETED = new UserStatus(3);

private UserStatus(int code) {
this.code = code;
}

public static UserStatus of(int code) {
return switch (code) {
case 1 -> ACTIVE;
case 2 -> FROZEN;
case 3 -> DELETED;
default -> throw new IllegalArgumentException("非法用户状态: " + code);
};
}

public boolean isActive() {
return this == ACTIVE;
}

public int getCode() {
return code;
}

// equals / hashCode for value object identity

@Override
public boolean equals(Object o) {
return (o instanceof UserStatus that) && this.code == that.code;
}

@Override
public int hashCode() {
return Objects.hash(code);
}
}

聚合中使用值对象

1
2
3
4
5
6
7
8
9
10
public class User {
private Long id;
private String username;
private String passwordHash;

private UserStatus status; // ✅ 状态是值对象
private UserType type; // ✅ 类型是值对象

}

用 enum 枚举 + 值对象混合的高级方案

一般来说,用户状态和用户类型,我们都会创建一个枚举类。因此我们可以使用枚举+值对象的方式来重新构建一下用户状态。

我们现在深入 DDD 实践:用 Enum 枚举 + 值对象(Value Object)混合建模方式 表达如用户状态 / 用户类型,并展示如何在 MyBatis 中映射为字段、在 Repository 中自动转换为领域值对象。

✅ 为什么用「Enum + 值对象」组合建模?

对象类型 作用
enum 表达离散枚举值(常量集合)
Value Object 表达领域语义 + 方法(如 isActive()

🌟 组合起来,既有类型安全、又有语义表达、也便于持久化。

✅ 枚举类:UserStatusEnum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public enum UserStatusEnum {
ACTIVE(1, "启用"),
FROZEN(2, "冻结"),
DELETED(3, "注销");

private final int code;
private final String label;

UserStatusEnum(int code, String label) {
this.code = code;
this.label = label;
}

public int getCode() {
return code;
}

public static UserStatusEnum fromCode(int code) {
for (UserStatusEnum status : values()) {
if (status.code == code) return status;
}
throw new IllegalArgumentException("未知状态: " + code);
}
}

✅ 值对象:UserStatus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class UserStatus {

private final UserStatusEnum status;

private UserStatus(UserStatusEnum status) {
this.status = status;
}

public static UserStatus of(UserStatusEnum status) {
return new UserStatus(status);
}

public static UserStatus of(int code) {
return new UserStatus(UserStatusEnum.fromCode(code));
}

public boolean isActive() {
return this.status == UserStatusEnum.ACTIVE;
}

public int getCode() {
return this.status.getCode();
}

public String getLabel() {
return this.status.name();
}

public UserStatusEnum getStatusEnum() {
return this.status;
}

@Override
public boolean equals(Object o) {
return (o instanceof UserStatus that) && this.status == that.status;
}

@Override
public int hashCode() {
return status.hashCode();
}
}

✅ MyBatis 持久化对象(PO):UserPO

1
2
3
4
5
6
7
8
public class UserPO {
private Long id;
private String username;
private String passwordHash;
private Integer status; // 状态值字段
// ... 省略 getter/setter
}

✅ Mapper XML 映射

1
2
3
4
5
6
7
<mapper namespace="com.example.repository.mybatis.UserMapper">
<select id="selectByUsername" resultType="com.example.repository.mybatis.UserPO">
SELECT id, username, password_hash, status
FROM users
WHERE username = #{username}
</select>
</mapper>

✅ 5. 仓储实现:UserRepositoryImpl → 转换 PO → 聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Repository
public class UserRepositoryImpl implements UserRepository {

private final UserMapper mapper;

public UserRepositoryImpl(UserMapper mapper) {
this.mapper = mapper;
}

@Override
public Optional<User> findByUsername(String username) {
UserPO userPO = mapper.selectByUsername(username);
return Optional.ofNullable(userPO).map(this::toEntity);
}

private User toEntity(UserPO doObj) {
return new User(
doObj.getId(),
doObj.getUsername(),
doObj.getPasswordHash(),
UserStatus.of(doObj.getStatus()) // ← 关键转换点
);
}
}

✅ 领域聚合实体:User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class User {
private Long id;
private String username;
private String passwordHash;
private UserStatus status; // 值对象

public User(Long id, String username, String passwordHash, UserStatus status) {
this.id = id;
this.username = username;
this.passwordHash = passwordHash;
this.status = status;
}

public boolean canLogin() {
return status.isActive();
}

public int getStatusCode() {
return status.getCode();
}

public String getStatusLabel() {
return status.getLabel();
}

// 省略其他 getter...
}

✅ 保存数据时:实体 → PO

1
2
3
4
5
6
UserPO userPO = new UserPO();
userPO.setId(user.getId());
userPO.setUsername(user.getUsername());
userPO.setPasswordHash(user.getPasswordHash());
userPO.setStatus(user.getStatus().getCode()); // ← 值对象 → DB 字段

✅ 总结对比

Integer enum Value Object + enum ✅ 推荐
类型安全
语义表达力 ⚠️ 限
是否可添加行为 ⚠️(有限) ✅(如 canLogin()
可维护性 一般

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

DDD领域驱动设计记账软件实战教程–划分领域和聚合

本文章为实战教程,主要内容包括:

  • 从零使用DDD构建一个记账软件

你是否有以下疑问?

  • 你是一个DDD软件开发人员,但是只是按照现有的项目去写,如果自己搭建一个新项目就无从下手了?
  • 你是一个传统软件开发人员,想使用DDD重构软件,却不知道如何入手?
  • 你看过很多DDD的文章,了解DDD概念,却没有一个从头到尾的完整教程,无法把碎片化的知识串起来?

本文章带你从头构建一个完整的DDD项目。由点到面的串联起来所有的知识。达成一个完整的知识图谱。

事件风暴

首先需要进行事件风暴。整理出来所有的命令业务流事件

  • 命令:可以简单理解为一些行为。这些命令可以映射成代码模型中的一个个方法。
  • 业务流:可以按照场景进行分析业务流,比如对于记账软件来说:记账场景、查账场景就是两个不同的场景,可能有不同的业务流。业务流可以映射成代码模型中的接口。
  • 事件:可以简单理解成上面的命令产生的一些事件。会有一些监听者异步监听这些事件并实现一些逻辑。

我们来分析一些记账场景会有哪些业务流。

首先用户需要注册登陆。因此我们可以识别出注册业务流登陆业务流。继而识别出注册命令登陆命令

对于登陆业务流中来说,除了登陆命令,还可以有查询用户、校验用户信息、生成token等命令。这些命令共同完成了一次登陆业务流。

此外,还会产生对应的事件。比如用户已注册事件已登陆事件等。

对于已注册事件,我们可以发送注册成功的消息提醒。还可以参与注册活动等。

对于已登陆事件,我们可以写入登陆成功日志,发送登陆成功消息提醒等。

登陆成功以后,首先需要创建一个账本。接下来可以更新账本信息。更新预算。设置角色信息。然后就可以添加收支记录在账本上了。

DDD领域其实是现实世界的映射。这里我们要思考现实世界的记账是怎么样的。
现实世界需要一个账本、需要记录收支信息、账本的最上方可能会写上本月的预算。记录收支信息的时候会写上分类、角色、金额等信息。

场景分析

在这里我仅仅分析了记账场景的业务流。其实还有查账场景的业务流。此外还有一些其他的通用业务流。比如监控,数据统计等。

大家可以自己分析一下将结果发到评论区。我会一一回复的。

纸上得来终觉浅,绝知此事要躬行

领域建模

当我们分析完所有的命令事件以后。我们就可以根据上面分析出来的这些进行领域建模了。通过这些我们可以分析出领域对象。

比如,登陆注册行为是围绕用户领域对象来完成的行为。写入登陆日志、登陆日志事件是围绕用户日志领域对象的行为。

此外,我们还可以从上面的分析中找到账本预算角色收支记录这几个领域对象。

如下图:

领域建模

领域对象也分三种类型

  • 聚合根:聚合的入口,操作聚合下所有的实体和值对象都需要通过聚合根来操作
  • 实体:具有唯一ID的实体,使用充血模型实现,也就是要包含属性和方法。所有对于实体的操作都应该放到这个实体中。可以映射为数据库的一个表。
  • 值对象:值对象分为简单值对象和复杂值对象。值对象和实体最大的区别在于可变性,值对象不能单独存在,只能作为实体的一部分存在。同样的,值对象不支持变更,如果需要变更则是整个值对象删除再创建一个新的。
    • 简单值对象:基本类型的属性可以视为简单值对象。
    • 复杂值对象:引用其他类(自定义的类)作为属性的值,可以视为复杂值对象。

接下来我们就可以分析出上面的领域对象哪些是实体,哪些是值对象了。

实体:

  • 用户实体:用户实体包括用户信息和用户的一些操作。这里可以将最新的一个登陆日志作为值对象放到用户实体中。而不是将所有的登陆日志都作为值对象放到用户实体中,因为那样的话太庞大了。
  • 用户登陆日志实体:存储登陆日志的信息,虽然从用户的角度来看,登陆日志不能单独存在。但是如果从数据分析的角度看,登陆日志则可以单独存在了。因此可以作为一个实体存在。
  • 账本实体:账本作为一个现实世界记账必须的物品,自然也要作为一个实体存在,有账本ID保证唯一性。并且可以修改账本的部分信息。
  • 收支记录实体:每一个收支记录都是账本上的一条信息,都是可以独立修改的个体。比如修改某一个记录的金额等。所以收支记录也作为一个实体存在。

值对象:

  • 预算值对象:预算信息是依赖于账本的一个信息,修改预算就是修改预算这个整体。因此预算作为账本的一个值对象存在。
  • 角色值对象:多个人记账,每个人记账的时候的角色都是不一样的,修改角色也是修改角色这个整体,角色离开账本以后就没有存在的意义了,因此角色也作为值对象存在。

聚合建模

经过上面的步骤,我们已经找到了我们需要的实体值对象。接下来我们可以根据他们之间的关系,来划分成一个个聚合了。

如果把实体理解成一个个的人,那么聚合就相当于部落家族等等。相近的一些人聚集在一起,就变成了一个聚合。

变成聚合以后,当对外沟通的时候,需要一个话事人,也就是聚合根

首先我们要有用户聚合,用户聚合来管理用户信息。用户实体就是用户聚合的聚合根。

其次,用户登陆日志也作为一个单独的聚合存在。可以思考一下。

✅ 1. 领域含义:登录日志是用户行为的“历史轨迹”,而非用户本身的组成部分

  • 用户(User)是一个业务主语,它的职责是认证、权限、信息管理
  • 登录日志是用户行为产生的事实事件,它与用户的生命周期边界无关

✅ 2. 技术设计上:日志数据量大、写入频繁、读取模式完全不同

特性 用户聚合(User) 登录日志(LoginLog)
写入频率 中低(修改信息、改密码) 高(每次登录都写)
数据量 小(一个用户一条) 非常大(每用户数百/数千条)
删除时是否联动 删除用户可保留日志 有审计/合规要求时不能删除
查询模式 通过用户 ID 查询 分页查询 / 最近10条等

这说明 LoginLog 应该用独立表、独立聚合、独立读写模型管理。

✅ 3. 聚合的职责原则(Evans 原则)

一个聚合应尽可能小,保持一致性边界清晰,仅包含保护不变式所必须的部分。

登录日志并不影响用户不变式(用户名、密码唯一性、身份验证),因此它不应该进聚合。

所以,我们把登陆日志放到一个单独的聚合中。登陆日志就是聚合根。

接下来,账本实体、预算角色值对象一起构成了账本聚合。账本作为聚合根存在。

剩下的收支记录作为一个聚合。

领域建模

有了4个聚合。我们还需要划分领域。将相近的聚合放到一个领域中。

领域可以简单理解为粗粒度的聚合。聚合中是实体和值对象。而领域中则是聚合。

我们将4个聚合划分成两个领域,分别是用户领域记账领域

总结

至此,我们完成了整个领域建模的工作,下一步就是将领域建模转换成代码建模了。

下一遍文章会将我们这次的领域建模转换成代码建模,并用spring框架来实现整个代码。

大家可以先自己将领域建模转换成代码建模,并将结果发到评论区一起讨论~

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

高阶应用实战

除了常用的增删改查以外,MySQL还有很多的功能。比如CTE窗口函数这种。再比如一些特性比如视图触发器等。

这次我们就来看一下对于这些如何使用。

有些功能用好了可以完成很多实际业务需求的。

CTE(common table expressions)

在MySQL中,CTE(Common Table Expressions,公共表表达式)是一种临时的结果集,可以在查询中被引用。CTE通常用于简化复杂的查询,使查询更易于理解和维护。CTE在MySQL 8.0及更高版本中得到了支持。

CTE的主要特点

临时结果集:

  • CTE是一个临时的结果集,可以在查询中被引用。它类似于子查询,但更易于阅读和维护。
    简化复杂查询:
  • CTE可以将复杂的查询分解为多个简单的部分,使查询更易于理解和维护。
    可重用性:
  • CTE可以被多次引用,减少了代码重复,提高了开发效率。
    递归查询:
  • CTE支持递归查询,可以用于处理层次结构或递归数据。

使用CTE实现获取每个课程中分数最高的学生信息。

通过WITH语句来声明一个临时表。表名cteSource,表的内容就是最的sid,通过SELECT MAX(sid) FROM enrolled查询出来的结果。字段名叫maxId

然后在查询语句里面就可以连接cteSource表,然后通过sid = cteSource.maxId 来获取到sid最大的用户信息。

1
2
3
4
5
WITH cteSource (maxId) AS (
SELECT MAX(sid) FROM enrolled
)
SELECT name FROM student, cteSource
WHERE student.sid = cteSource.maxId

还有一些其他的用法,比如:

1
2
3
4
5
6
7
WITH cte1 (col1) AS (
SELECT 1
),
cte2 (col2) AS (
SELECT 2
)
SELECT * FROM cte1, cte2;

CTE(公共表表达式)使用场景

  1. 递归查询(如树形结构)

CTE最常见的场景之一是递归查询,比如组织架构、分类目录、菜单结构等树状数据。例如,查询某个员工的所有下属员工。

1
2
3
4
5
6
7
8
9
10
11
12
WITH RECURSIVE subordinates AS (
SELECT id, name, manager_id
FROM employees
WHERE manager_id IS NULL -- 顶层经理

UNION ALL

SELECT e.id, e.name, e.manager_id
FROM employees e
INNER JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;
  1. 简化复杂查询逻辑,提高可读性

当查询逻辑复杂时,可以将中间结果用CTE分段处理,让SQL更易读。例如,先筛选出某类用户,再基于这些用户做进一步统计。

1
2
3
4
5
6
WITH active_users AS (
SELECT user_id
FROM user_logins
WHERE login_date > CURRENT_DATE - INTERVAL '30 days'
)
SELECT COUNT(*) FROM orders WHERE user_id IN (SELECT user_id FROM active_users);
  1. 多次引用中间结果,避免重复计算

如果某个子查询需要被多次引用,CTE可以定义一次,后续多次调用,避免重复书写和计算,提高效率。

1
2
3
4
5
6
7
WITH sales_summary AS (
SELECT product_id, SUM(amount) AS total_sales
FROM sales
GROUP BY product_id
)
SELECT * FROM sales_summary WHERE total_sales > 1000;
-- 也可以在后续多个查询中引用 sales_summary
  1. 分步处理数据,逐步细化结果

例如,先计算每个部门的总销售额,再筛选出销售额最高的部门。

1
2
3
4
5
6
7
8
9
10
11
12
WITH dept_sales AS (
SELECT department_id, SUM(sales) AS total_sales
FROM employees
GROUP BY department_id
),
top_dept AS (
SELECT department_id
FROM dept_sales
ORDER BY total_sales DESC
LIMIT 1
)
SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM top_dept);
  1. 数据去重和排名

CTE可以配合窗口函数实现数据去重、分组排名等需求。

1
2
3
4
5
6
WITH ranked_orders AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
FROM orders
)
SELECT * FROM ranked_orders WHERE rn = 1;

实战案例

比如我之前的业务。有一个公司表,一个公司合同表,一个公司合同申请表。在后台的公司合同列表里面,要展示最新的申请记录和申请状态。

如果没有CTE的话,我们就需要使用子查询来实现。这对于一个列表来说,性能是很低的。

表结构假设:

  • company:公司表,字段如 company_id, company_name 等。
  • company_contract:公司合同表,字段如 contract_id, company_id, contract_no 等。
  • company_contract_application:公司合同申请表,字段如 application_id, contract_id, apply_time, status 等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SELECT
c.company_id,
c.company_name,
cc.contract_id,
cc.contract_no,
cca.application_id,
cca.apply_time,
cca.status
FROM company c
JOIN company_contract cc ON c.company_id = cc.company_id
LEFT JOIN company_contract_application cca
ON cc.contract_id = cca.contract_id
AND cca.apply_time = (
SELECT MAX(apply_time)
FROM company_contract_application
WHERE contract_id = cc.contract_id
);

而有了CTE以后,我们可以通过CTE来实现,这样可以提升性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WITH latest_applications AS (
SELECT
application_id,
contract_id,
apply_time,
status,
ROW_NUMBER() OVER (PARTITION BY contract_id ORDER BY apply_time DESC) AS rn
FROM company_contract_application
)
SELECT
c.company_id,
c.company_name,
cc.contract_id,
cc.contract_no,
la.application_id,
la.apply_time,
la.status
FROM company c
JOIN company_contract cc ON c.company_id = cc.company_id
LEFT JOIN latest_applications la ON cc.contract_id = la.contract_id AND la.rn = 1;

窗口函数

窗口函数(Window Function)是SQL标准中的一种功能强大的工具,它允许在查询中对一组行进行计算,而不会像聚合函数那样消除行的个数。窗口函数在MySQL 8.0及更高版本中得到了支持,它们可以用于计算移动平均值、累积和、排名等复杂的分析任务。

窗口函数的主要特点

行级计算:

  • 窗口函数在每一行上执行计算,同时可以访问同一组中的其他行。
  • 这与聚合函数不同,聚合函数会将多行数据合并为一行。
    分区和排序:
  • 窗口函数可以使用PARTITION BY子句将数据分成多个分区,每个分区独立计算。
  • 可以使用ORDER BY子句在每个分区内对数据进行排序。
    灵活的范围定义:
  • 窗口函数可以定义计算的范围,如当前行的前几行或后几行。
  • 使用ROWS或RANGE子句可以指定计算的范围。
    多种功能:
  • 窗口函数提供了多种功能,如ROW_NUMBER()、RANK()、DENSE_RANK()、NTILE()、SUM()、AVG()、LEAD()、LAG()等。

ROW_NUMBERRANK都需要和OVER一起使用。

  • ROW_NUMBER(): 显示当前行号
  • RANK() : 显示排序后的排名,如果没有排序,都是1
  • OVER()
    • PARTITION BY 进行分组
    • GROUP BY 进行分组
    • ORDER BY 排序

001

002

003

实战:获取每个课程中分数最高的学生信息

下面的SQL,在postgresql中执行成功,mysql8执行报错。

首先查询所有课程信息,并按照课程分组,按照分数排序。

1
2
3
SELECT *,
RANK() OVER (PARTITION BY cid ORDER BY grade ASC)
AS rank FROM enrolled

004

接着搜索上表中分数为1,也就是分数最高的学生。也就是每个课分数最高的学生信息。

1
2
3
4
5
6
SELECT * FROM (
SELECT *,
RANK() OVER (PARTITION BY cid
ORDER BY grade ASC)
AS rank FROM enrolled) AS ranking
WHERE ranking.rank = 1

005

视图

在MySQL中,视图(View)是一种虚拟表,其内容由SQL查询定义。视图并不存储实际的数据,而是根据定义的查询动态生成数据。视图可以简化复杂的SQL操作,提供数据的逻辑抽象,并且可以限制对某些数据的访问,从而增强数据的安全性。

MySQL中的视图是虚拟视图,说白了就是一条SQL语句,当查询视图的时候执行SQL语句而已。

除此之外,还有一个东西叫做物化视图,MySQL并没有实现这个东西,物化视图就是一张真的表,而不是一个SQL语句,因此查询效率更好。

视图的主要特点

虚拟表:

  • 视图是一个虚拟表,其内容由SQL查询定义。视图本身并不存储数据,而是根据定义的查询动态生成数据。

简化复杂查询:

  • 视图可以简化复杂的SQL操作,将复杂的查询逻辑封装起来,使用户可以像查询普通表一样查询视图。

数据抽象:

  • 视图提供数据的逻辑抽象,隐藏了底层表的复杂性,使用户可以更直观地访问数据。

安全性:

  • 视图可以限制对某些数据的访问,增强数据的安全性。通过视图,用户只能访问视图定义的特定数据,而不能访问底层表的全部数据。

更新限制:

  • 视图可以是可更新的,也可以是不可更新的。可更新视图允许用户通过视图插入、更新或删除数据,但需要满足一定的条件。不可更新视图则不允许用户通过视图修改数据。

创建一个视图。下面的语句,创建一个视图,视图名称是sales_employees,内容就是后面的Select语句的结果。当原始表employees变化以后,视图的内容也会跟着变化。

1
2
3
4
CREATE VIEW sales_employees AS
SELECT name, salary
FROM employees
WHERE department = 'Sales';

实战:交易聚合

我之前有一个业务,存在两个表,一个是买单表,代表买方要买一个产品出的价格。一个是卖单表,代表卖方要卖一个产品出的价格。

现在有一个需求是:在C端的一个页面上,要求混合展示买单信息和卖单信息,按照价格从低到高排序

数据库假设:

  • buy: 买单表
  • sell: 卖单表
  • order: 交易视图
1
2
3
4
CREATE VIEW order AS
SELECT *
FROM buy, sell
order by price asc;

触发器

在MySQL中,触发器(Trigger)是一种特殊的存储过程,它在特定的数据库操作(如INSERT、UPDATE、DELETE)发生时自动执行。触发器可以用于实现复杂的业务逻辑,确保数据的完整性和一致性,以及自动维护数据的同步。

触发器的主要特点

自动执行:

  • 触发器在特定的数据库操作发生时自动执行,无需显式调用。这使得触发器可以用于实现自动化的数据处理和维护。

数据完整性:

  • 触发器可以用于确保数据的完整性和一致性。例如,可以在插入或更新数据时自动检查数据的有效性,或者在删除数据时自动清理相关数据。

业务逻辑:

  • 触发器可以用于实现复杂的业务逻辑。例如,可以在插入或更新数据时自动计算某些字段的值,或者在删除数据时自动更新相关表的数据。

数据同步:

  • 触发器可以用于自动维护数据的同步。例如,可以在插入或更新数据时自动更新相关表的数据,或者在删除数据时自动清理相关表的数据。

触发器的类型

  • BEFORE INSERT:
    • 在插入数据之前执行触发器逻辑。
  • AFTER INSERT:
    • 在插入数据之后执行触发器逻辑。
  • BEFORE UPDATE:
    • 在更新数据之前执行触发器逻辑。
  • AFTER UPDATE:
    • 在更新数据之后执行触发器逻辑。
  • BEFORE DELETE:
    • 在删除数据之前执行触发器逻辑。
  • AFTER DELETE:
    • 在删除数据之后执行触发器逻辑。

触发器的限制

性能影响:

  • 触发器的执行会增加数据库操作的开销,可能会影响性能。因此,应谨慎使用触发器,避免在高频操作的表上定义过多的触发器。
    复杂性:
  • 触发器的逻辑可以非常复杂,但过多的复杂逻辑可能导致触发器难以维护和调试。因此,应尽量保持触发器的逻辑简单明了。
    调试困难:
  • 触发器的调试相对困难,因为它们在特定的操作发生时自动执行,难以直接观察和调试。因此,建议在开发和测试阶段充分测试触发器的逻辑。

因此,实际开发中基本不使用触发器。

存储过程

在MySQL中,存储过程(Stored Procedure)是一种预编译的SQL语句集合,它存储在数据库中,可以通过调用其名称并传递参数来执行。存储过程可以包含复杂的逻辑和多个SQL语句,用于完成特定的任务。它们类似于其他编程语言中的函数或方法。

可以把存储过程想成一个函数。只不过是在MySQL中的函数,这个函数可以实现各种功能。可以实现一些复杂的SQL处理,这样可以简化调用。

存储过程的主要特点

预编译:

  • 存储过程在创建时被预编译并存储在数据库中,这使得它们的执行速度比单独的SQL语句更快。
    代码重用:
  • 存储过程可以被多次调用,减少了代码重复,提高了开发效率。
    减少网络流量:
  • 存储过程在服务器端执行,减少了客户端和服务器之间的网络流量,因为只需要发送存储过程的名称和参数,而不是大量的SQL语句。
    安全性:
  • 存储过程可以限制用户对底层数据的直接访问,只允许通过存储过程进行数据操作,从而增强数据的安全性。
    事务管理:
  • 存储过程可以包含事务控制语句,如COMMIT和ROLLBACK,确保数据操作的完整性和一致性。

存储过程的限制

性能影响:

  • 存储过程的执行会增加数据库操作的开销,可能会影响性能。因此,应谨慎使用存储过程,避免在高频操作的表上定义过多的存储过程。
    复杂性:
  • 存储过程的逻辑可以非常复杂,但过多的复杂逻辑可能导致存储过程难以维护和调试。因此,应尽量保持存储过程的逻辑简单明了。
    调试困难:
  • 存储过程的调试相对困难,因为它们在服务器端执行,难以直接观察和调试。因此,建议在开发和测试阶段充分测试存储过程的逻辑。

实战案例

存储过程的实际应用场景有限。

我们之前有一个场景,是需要批量处理数据,有几十行SQL语句。因此将这些SQL封装到了一个存储过程中,这样只需要执行这个存储过程就可以了。

这里给出一个简单的实战案例:

创建一个存储过程get_employee_details,用于根据员工ID获取员工的详细信息:

DELIMITER用来设置结束符,比如正常的句子结束符是句号。代码结束符是分号;

IN代表输入参数,也就是这个函数有一个输入参数emp_id,是int类型。
还有out代表输出参数,用于返回结果。
INOUT代表既可以输入参数也可以是输出参数。

1
2
3
4
5
6
7
8
9
10
DELIMITER //

CREATE PROCEDURE get_employee_details(IN emp_id INT)
BEGIN
SELECT name, department, salary
FROM employees
WHERE id = emp_id;
END //

DELIMITER ;

上面这个存储过程的函数体,就是一段select查询语句。

存储函数

在MySQL中,存储函数(Stored Function)是一种预编译的SQL语句集合,类似于存储过程,但它返回一个值。存储函数可以被SQL语句直接调用,就像调用普通的函数一样。存储函数通常用于封装复杂的逻辑,并在查询中重用这些逻辑。

存储函数同样是一个函数,和上面的存储过程差不多。

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

区别类别 存储过程 存储函数
返回值 1. 可以返回多个值,通过OUT或INOUT参数返回。
2. 可执行多个SQL语句,但不直接返回单一值。
1. 必须返回一个单一的值。
2. 可以被SQL语句直接调用,就像普通函数一样。
调用方式 1. 通过CALL语句调用。
2. 可执行复杂逻辑,包括多个SQL语句和事务控制。
1. 可直接在SQL语句中调用,如普通函数。
2. 通常用于封装复杂逻辑,并在查询中重用。

存储函数的主要特点

主要特点 说明
返回值 存储函数必须返回一个值,这使得它们可以被SQL语句直接调用。
代码重用 存储函数可以被多次调用,减少了代码重复,提高了开发效率。
减少网络流量 存储函数在服务器端执行,减少了客户端和服务器之间的网络流量,因为只需要发送函数的名称和参数,而不是大量的SQL语句。
安全性 存储函数可以限制用户对底层数据的直接访问,只允许通过函数进行数据操作,从而增强数据的安全性。
事务管理 存储函数可以包含事务控制语句,如COMMIT和ROLLBACK,确保数据操作的完整性和一致性。

实战案例

创建一个存储函数get_employee_salary,用于根据员工ID获取员工的薪资:

可以看到和上面存储过程的区别,声明了一个返回值,类型是DECIMAL,最后通过return返回了,并且声明了一个变量。参数也没有IN、OUT这种了。

1
2
3
4
5
6
7
8
9
10
11
12
13
DELIMITER //

CREATE FUNCTION get_employee_salary(emp_id INT)
RETURNS DECIMAL(10, 2)
BEGIN
DECLARE emp_salary DECIMAL(10, 2);
SELECT salary INTO emp_salary
FROM employees
WHERE id = emp_id;
RETURN emp_salary;
END //

DELIMITER ;

总结

本次讲解了MySQL的高级功能和实战应用的一些场景。

只有知道了场景才能活学活用。这里面最常用的其实还是CTE和窗口函数。

可以多多练习,希望遇到这些场景的时候,大家能想起来使用这些功能。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

增删改查实战

MySQL是最常用的关系型数据库之一,掌握基本的增、删、改、查(CRUD)操作是学习MySQL的基础。本篇文章将通过详细示例,帮助零基础的读者快速上手MySQL的增、删、改、查操作。无论是新手还是有一定数据库基础的开发者,都能从中受益。

1. MySQL环境准备

在进行任何操作之前,首先需要确保你已经成功安装了MySQL数据库,并能顺利连接到数据库。如果没有安装MySQL,请参考MySQL官方网站或相关教程进行安装。

连接到MySQL

在终端中使用以下命令登录MySQL:

1
mysql -u root -p

输入密码后,成功登录后会进入MySQL命令行界面。

创建数据库

在MySQL中,所有的操作都发生在数据库中,因此我们首先需要创建一个数据库:

1
CREATE DATABASE mydb;

然后进入到mydb数据库:

1
USE mydb;

创建表

在进行增删改查操作之前,先创建一个简单的表。假设我们要管理一个学生信息表,表结构如下:

1
2
3
4
5
6
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
age INT,
grade VARCHAR(10)
);

表结构说明:

  • id:学生的唯一标识,使用AUTO_INCREMENT自增。
  • name:学生的姓名,VARCHAR(50)表示最多存储50个字符。
  • age:学生的年龄,使用INT表示整数类型。
  • grade:学生的年级,VARCHAR(10)表示最多存储10个字符。

2. 增:插入数据(INSERT)

在MySQL中,使用INSERT INTO语句来插入数据。我们可以插入一条或多条记录。

插入一条记录

插入一条记录到students表:

1
2
3
INSERT INTO students (name, age, grade) 
VALUES ('John Doe', 20, 'Sophomore');

  • INSERT INTO students (name, age, grade):指定插入的数据列。
  • VALUES ('John Doe', 20, 'Sophomore'):插入的数据值,注意顺序要和列名匹配。

插入多条记录

我们也可以一次性插入多条记录:

1
2
3
4
5
6
INSERT INTO students (name, age, grade) 
VALUES
('Alice', 19, 'Freshman'),
('Bob', 21, 'Junior'),
('Charlie', 22, 'Senior');

  • 通过多个VALUES列表插入多条记录,减少了插入的语句数量,提升了效率。

插入数据并返回自动生成的ID

如果表中有AUTO_INCREMENT字段(如id),可以使用LAST_INSERT_ID()函数获取插入后生成的ID:

1
2
3
INSERT INTO students (name, age, grade) 
VALUES ('David', 23, 'Graduate');
SELECT LAST_INSERT_ID();

3. 查:查询数据(SELECT)

MySQL使用SELECT语句从表中查询数据。可以查询全部列,也可以选择特定列,还可以进行条件过滤、排序等。

查询所有记录

查询students表中所有的数据:

1
SELECT * FROM students;
  • *表示查询所有列的数据。

查询特定列

查询nameage列的数据:

1
SELECT name, age FROM students;

使用WHERE条件筛选数据

查询年纪大于20的学生:

1
SELECT * FROM students WHERE age > 20;
  • WHERE用于添加条件,过滤符合条件的记录。

排序查询结果

按年龄升序排列学生:

1
SELECT * FROM students ORDER BY age ASC;
  • ASC表示升序(默认),如果要降序排列,可以使用DESC
1
SELECT * FROM students ORDER BY age DESC;

使用LIMIT限制返回结果数量

如果只想查看前两条记录:

1
SELECT * FROM students LIMIT 2;

模糊查询(LIKE)

使用LIKE进行模糊查询,查找名字中包含“a”的学生:

1
SELECT * FROM students WHERE name LIKE '%a%';
  • %是通配符,表示任意字符。

聚合函数

MySQL提供了多种聚合函数,如COUNT()AVG()SUM()等。

查询学生人数:

1
2
SELECT COUNT(*) FROM students;

查询平均年龄:

1
2
SELECT AVG(age) FROM students;

4. 改:更新数据(UPDATE)

使用UPDATE语句修改表中已有的数据。可以根据条件更新特定记录,也可以批量更新。

更新单条记录

更新id=1的学生姓名为“John Smith”:

1
2
3
4
UPDATE students 
SET name = 'John Smith'
WHERE id = 1;

  • SET用于指定需要更新的列和新的值。
  • WHERE用于指定要更新的记录,确保不修改所有记录。

更新多条记录

更新所有年龄大于20的学生年级为“Graduate”:

1
2
3
4
UPDATE students 
SET grade = 'Graduate'
WHERE age > 20;

更新多个字段

同时更新多个字段:

1
2
3
4
UPDATE students 
SET age = 23, grade = 'Postgraduate'
WHERE id = 2;

5. 删:删除数据(DELETE)

使用DELETE语句删除表中的记录,可以删除特定记录或删除所有记录。

删除单条记录

删除id=1的学生:

1
DELETE FROM students WHERE id = 1;
  • WHERE子句用于指定删除条件,确保只删除满足条件的记录。

删除多条记录

删除所有年龄大于20的学生:

1
DELETE FROM students WHERE age > 20;

删除所有记录

删除students表中的所有记录(注意:表结构不变):

1
DELETE FROM students;

删除表中的记录(TRUNCATE)

TRUNCATE语句与DELETE类似,但其删除的是所有数据并且不记录日志,因此效率更高:

1
TRUNCATE TABLE students;
  • TRUNCATE会立即清空表中的所有数据,并且不能恢复。

删除表(DROP)

如果需要删除整个表,包括表结构:

1
DROP TABLE students;
  • DROP会删除表以及表中所有的数据,无法恢复,因此要小心使用。

总结

在MySQL中,增、删、改、查(CRUD)操作是数据库管理的基础。通过上述操作,我们能够对数据库表中的数据进行增删改查,并且可以通过各种条件、排序和聚合功能来灵活查询数据。

这里,要特别注意更新删除操作。一定要加条件,不然就会更新全表数据,或者删除全表数据。大家有没有误操作过呢?一起分享下。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。

MySQL零基础教程

本教程为零基础教程,零基础小白也可以直接学习,有基础的可以跳到后面的原理篇学习。
基础概念和SQL已经更新完成。

接下来是应用篇,应用篇的内容大致如下图所示。

应用学习

索引设计

之前已经讲过表设计了,这次来说说索引设计。

索引设计也是一个比较重要的东西,可谓是重中之重。想要学好,很难。也是面试的常见问题。

在MySQL数据库中,索引是提升查询效率的核心工具。没有索引的查询,就像是在浩瀚的书海中翻书,效率低下。而良好的索引设计,则能帮助我们高效地找到我们需要的信息,避免无谓的资源浪费。因此,理解索引的原理、种类以及如何设计索引是每个开发者必须掌握的基础技能。

索引是什么?

索引是一种数据结构,它通过特定的方式排列数据,以便于快速查询。可以把索引想象成一本书的目录,通过目录可以快速找到你想要的章节,而不用从头到尾翻书。

在MySQL中,索引通常是通过B树或B+树等数据结构实现的,数据库查询引擎可以利用这些结构快速查找数据。

我们先看一下索引都有哪些类型,最常用的当然是b+树索引啦

  • B+树索引:B+树是一种多路自平衡的树结构,所有的叶子节点都在同一层,并且叶子节点通过链表连接。它是MySQL中最常用的索引类型,支持范围查询、精确查找等操作。
  • 哈希索引:哈希索引基于哈希表的原理,通过哈希函数对索引列进行计算,得到一个哈希值,哈希值对应的桶中存储数据的指针。
  • 全文索引:全文索引是专门用于文本检索的索引结构,通常用于TEXT类型的数据。MySQL会将文本字段中的每个单词进行索引,创建倒排索引,以提高文本搜索的效率。
  • 空间索引: 空间索引是用于地理空间数据类型(如POINTLINESTRINGPOLYGON)的索引结构,通常使用R树(或改进版的R+树)来存储和查询空间数据。
  • 倒排索引:倒排索引主要用于文本搜索。它将文档中每个词的出现位置存储在一个列表中,从而支持高效的文本检索。
  • 位图索引:位图索引使用位图(bitmap)来表示数据列中各个值的存在情况。例如,假设某一列有3个不同的值,可以用3个位来表示每个值的出现情况。

这些主要是根据索引结构来划分的。接下来详细看一下各个结构的优缺点,这样我们才能更好的进行选择。

B+树索引(B+ Tree Index)

结构:

  • B+树是一种多路自平衡的树结构,所有的叶子节点都在同一层,并且叶子节点通过链表连接。
  • 它是MySQL中最常用的索引类型,支持范围查询、精确查找等操作。

优点:

  • 高效的范围查询:由于B+树是顺序存储的,支持范围查询,查询某个区间的数据时非常高效。
  • 高效的精确查询:B+树通过逐层查找,可以快速定位到数据行。
  • 支持排序:由于叶子节点按顺序排列,B+树非常适合用于ORDER BYGROUP BY等操作。
  • 空间利用高:每个非叶子节点仅包含键,而不包含数据,可以节省存储空间。

缺点:

  • 性能受页大小影响:B+树的性能与页的大小(每页存储的记录数)密切相关,过小的页会导致频繁的I/O操作,过大的页则会浪费内存。
  • 写入性能较低:对于频繁插入、删除操作的场景,B+树索引的维护成本较高,可能导致性能下降。

应用场景:

  • 最常用于主键索引、普通索引和唯一索引。尤其适用于需要进行范围查询、排序的场景。

特点:

  • b+ tree 删除和插入的复杂度都是O(log n), b 是 balance (平衡),推荐阅读paper: the ubiquitous B-tree
  • b+ tree,保证每个节点都必须是半满的,对于存放在节点中的key数量来说,key数量至少为M/2 - 1个,M为树的高度,key的数量必须小于 M - 1,如果当删除数据以后导致key数量小于M/2 - 1个,就会进行平衡,使他满足M/2 - 1个。

    M/2 - 1 ≤ key数量 ≤ M - 1

  • 如果一个中间节点有k个key,那你就会有k+1个非空孩子节点,也就是k+1个指向下方节点的指针。每个节点的内容是一个指针和一个key
  • 叶子节点之间有连接叶子节点的兄弟指针,这个想法来源于b link tree。每个节点的内容是一个数据和一个key,数据可以是一个record id 也可以是一个 tuple
  • b+ tree 标准填充容量大概是67% - 69%,对于一个大小是8kb的page来说,如果高度为4,大约能记录30 0000个键值对。
  • b+ tree的节点大小,机械硬盘的大小最好在1M,ssd的大小在10KB

b tree 和 b+ tree 的区别

  • b tree的中间节点也可以存数据,所以key是不重复的
  • b+ tree的中间节点没有数据,所有数据都在叶子节点,所以key有可能既存在中间节点也存在叶子节点。会重复
  • b tree的性能在并行处理上更差,因为修改以后需要向上传播也需要向下传播修改,这个时候两边都要增加锁
  • b+ tree的性能更好,因为只修改叶子节点,所以只需要向上传播,只需要增加一个锁

b+ tree 插入

  1. 向下扫描,找到对应的叶子节点
  2. 如果可以插入就直接插入
  3. 如果不可以插入,那么从中间分开,变成两个叶子节点,并将中间的key传递给父节点,插入父节点。
  4. 如果父节点可以插入就直接插入并分出一个指针指向新的叶子节点
  5. 如果父节点不可以插入重复上述操作3

b+ tree 删除

  1. 向下扫描,找到对应的叶子节点,这个时候就会增加latch,因为不知道需不需要合并,操作以后才会释放
  2. 如果可以删除就直接删除
  3. 如果删除后导致key数量 < M/2 - 1,那么就会出发合并,因为不满足key数量啦
  4. 进行合并的时候删除这个key,然后先查看左右的兄弟节点,是否能直接把数据插入过来,如果可以的话就掠夺一个key过来,然后向上传播
  5. 如果不能掠夺,那么就合并到兄弟节点,然后向上传播。

b+ tree的查找

  • 对于<a,b,c>,查找a=5 and b=3也是可以走索引的,但是hash索引就不行,有些数据库还支持b=3的搜索走索引,比如oracle和sql server

推荐书籍 Modern B-Tree Techniques

哈希索引(Hash Index)

结构:

  • 哈希索引基于哈希表的原理,通过哈希函数对索引列进行计算,得到一个哈希值,哈希值对应的桶中存储数据的指针。

优点:

  • 非常高效的等值查询:哈希索引的查找时间复杂度是O(1),因此对于精确查找(=)非常高效。
  • 查询速度快:对于大量的数据,哈希索引可以快速找到对应的记录。

缺点:

  • 不支持范围查询:由于哈希是通过哈希值计算的,哈希索引不支持范围查询(BETWEEN、>、<等)。
  • 空间浪费:哈希表可能会出现哈希冲突,造成空间浪费和性能下降。
  • 不支持排序:哈希索引无法用于ORDER BY、GROUP BY等排序操作。

应用场景:

  • 适合用于精确查找,但不适合范围查询和排序操作。MySQL中内存表(MEMORY存储引擎)默认使用哈希索引。

哈希索引的应用场景有限,因此实际工作中使用较少。

哈希分为静态hash实现和动态hash实现。

静态hash

  • liner probe hashing
    • 如果要插入的位置有值了,就往下扫描,扫描到空的位置插入
    • 删除的时候可以增加一个墓碑标记,这样就知道这里是有数据的不是空,查找的时候就会继续往下扫描而不会是没找到
    • 删除的时候还可以把后面的数据往前移动,但是这样有的数据就不再原来的位置了,就找不到了。因为只会往下扫描不会往上扫描
  • robin hood hashing
    • 记录距离数,表示插入的位置和应该插入的位置的距离。从0开始。
    • 插入的时候判断距离数,进行劫富济贫,如果你向下扫描到距离数为3的地方插入,而在距离数为2的地方的数据x,x的距离数比你小,比如是0,1.那么你就占据这里,你插入距离数为2的地方,而将x插入你下面,x的距离数会+1.
    • 从整体来看,这个方法牺牲了插入的效率,将数据的距离数变得更加平均
  • cuckoo hashing
    • 该方法使用两个或多个hash table来记录数据,对A进行两次hash,得出两个hash table中的插入位置,随机选择一个进行插入
    • 如果选择的插入位置已经有数据了,就选择另一个插入
    • 如果两个都有数据了,就占据一个,然后对这个位置上之前的数据B再次hash选择其余位置。

动态hash

  • chained hashing
    • 把所有相同hash的组成一个bucket链表,然后一直往后面增加
    • java的hash table默认就是这样的
  • extendible hashing
    • 对 chained hashing 的扩展
    • 有一个slot array,在slot array上有一个 counter, 如果counter = 2,代表看hash以后的数字的前两个bit,slot array就有4个位置,分别是00,01,10,11
    • 每个slot指向一个bucket
    • hash以后找到前两位对应的slot指向的bucket,将数据放进去,如果满了,放不下了就进行拆分
    • 将slot array的counter扩容为3,看前3个bit,slot array变成了8个位置
    • 只将这个满了的bucket拆分成2个,其余的不变,重新进行slot的映射
    • 再次hash这个值,看前3个bit找到对应的slot,在找到对应的bucket,然后插入进去
  • linear hashing
    • 对 extendible hashing 的扩展
    • 去掉了 conter,因为他每次加1,都会扩容一倍
    • 增加了split point,一开始指向0,然后每次overflow需要拆分的时候就拆分split point指向的那个bucket,然后slot array只扩容一个,这个时候出现第二个hash函数并将split point+1
    • 查询的时候如果slot array的位置小于split point,就使用第二个hash函数,因为被拆分了
    • 如果大于等于split point,就使用第一个hash函数

全文索引(Full-text Index)

结构:

  • 全文索引是专门用于文本检索的索引结构,通常用于TEXT类型的数据。
  • MySQL会将文本字段中的每个单词进行索引,创建倒排索引,以提高文本搜索的效率。

优点:

  • 高效的全文搜索:可以用于模糊匹配、大规模文本数据的快速搜索,支持MATCH…AGAINST查询。
  • 支持复杂查询:全文索引不仅支持简单的文本搜索,还可以支持布尔模式、自然语言模式等复杂的文本搜索。

缺点:

  • 只适用于TEXT字段:全文索引主要用于大文本字段,如TEXT、LONGTEXT等。
  • 更新开销大:对于频繁更新的表,全文索引的更新性能较差。
  • 无法使用普通的LIKE查询:只能通过MATCH…AGAINST进行查询。

应用场景:

  • 适用于需要进行全文搜索的场景,如文章搜索、日志分析等。

数据库全文索引适用于某个字段需要支持全文搜索。且不想进行大的改造。也没有太多的后续扩展需求。

如果对于全文搜索的要求比较高的话,还是推荐使用ES等搜索引擎进行改造的。

空间索引(Spatial Index)

结构:

  • 空间索引是用于地理空间数据类型(如POINT、LINESTRING、POLYGON)的索引结构,通常使用R树(或改进版的R+树)来存储和查询空间数据。

优点:

  • 高效的空间查询:能够高效地查询空间数据,支持ST_Within、ST_Intersects等空间查询操作。
  • 支持多维数据:可以存储和查询多维数据,如经纬度、位置坐标等。

缺点:

  • 空间数据类型限制:只适用于空间数据类型(如GEOMETRY、POINT等)。
  • 查询性能受数据分布影响:空间索引的性能受空间数据的分布和密度影响,密集区域查询可能性能不佳。

应用场景:

  • 适用于地理信息系统(GIS)等需要进行空间数据查询的应用场景,如地图服务、定位服务等。

倒排索引(Inverted Index)

结构:

  • 倒排索引主要用于文本搜索。它将文档中每个词的出现位置存储在一个列表中,从而支持高效的文本检索。

优点:

  • 高效的关键词检索:倒排索引非常适合处理大量文本数据,可以通过关键词快速查找相关文档。
  • 支持复杂查询:可以支持基于关键词的复杂查询,如布尔查询、短语查询等。

缺点:

  • 只适用于文本数据:倒排索引主要用于文本搜索,不能像B+树一样处理范围查询。
  • 内存消耗大:倒排索引需要占用较大的内存空间,尤其是在数据量较大时。

应用场景:

  • 主要应用于搜索引擎、内容管理系统、文档管理系统等需要高效检索文本内容的场景。

位图索引(Bitmap Index)

结构:

  • 位图索引使用位图(bitmap)来表示数据列中各个值的存在情况。例如,假设某一列有3个不同的值,可以用3个位来表示每个值的出现情况。

优点:

  • 适用于低基数数据:当字段的可能值较少(如性别、状态等字段)时,位图索引可以节省大量的存储空间。
  • 查询效率高:位图索引非常适合多条件组合查询,尤其是当查询涉及多个低基数列时。

缺点:

  • 适用范围有限:位图索引不适用于高基数的字段(如年龄、收入等),因为位图的大小随着基数增加会急剧增加,导致空间浪费。
  • 更新成本高:每次更新时,需要修改位图,更新开销较大。

应用场景:

  • 适用于低基数的字段,特别是用于多条件组合查询时。

索引的作用

  • 加速查询:索引最主要的作用就是提高查询速度。通过索引,MySQL可以直接定位到目标数据行,而不是扫描整个表。
  • 提高排序效率:在使用ORDER BYGROUP BY时,索引可以帮助MySQL高效地进行排序。
  • 优化连接操作:当查询涉及多个表的连接时,索引可以加速连接的过程。
  • 减少数据扫描量:在进行范围查询、匹配查询等操作时,索引可以大幅减少需要扫描的记录数。

索引的种类

上面已经介绍了索引的结构了,接下来看一下索引的类型,这些类型是逻辑上的。而结构则是物理上的。

这些索引类型下面都可以支持不同的索引结构,进行组合。

主键索引(Primary Key Index)

特点:主键索引是唯一的,且不允许NULL值。每个表只能有一个主键索引。
应用场景:主键索引用于表中唯一标识记录的字段。通常选择业务中最重要且唯一的字段作为主键,如id。

主键索引一般使用自增ID或者使用雪花算法,主要保证自增即可。

唯一索引(Unique Index)

  • 特点:唯一索引保证索引列的值唯一,但允许NULL值。
  • 应用场景:当某一列的值需要保证唯一性时,使用唯一索引。例如,邮箱、用户名等字段。

唯一索引的唯一约束是由数据库来保证的。

这样的好处是保证了数据的绝对唯一。并且加速了一些查询。比如手机号上有唯一索引,那么查询手机号=xxx这个条件的时候,找到一个就会停止继续扫描了。如果没有唯一索引,那么会继续扫描。

不好的地方在于减慢了插入更新操作的速度。因为每次插入的时候数据库都需要判断唯一性。

具体使用就需要个人来权衡了。

普通索引(Index)

特点:普通索引没有唯一性要求。它主要用于提高查询效率。
应用场景:当我们需要查询某些列的数据时,且这些列没有唯一性要求,使用普通索引就能提高查询性能。

全文索引(Fulltext Index)

特点:全文索引适用于文本数据的快速搜索,尤其在TEXT类型的列中非常有效。
应用场景:当需要对大量文本数据进行搜索(如文章内容搜索)时,使用全文索引。

联合索引(Composite Index)

特点:联合索引是由多个列组成的索引。MySQL通过联合索引来优化涉及多个列的查询。
应用场景:当查询经常涉及多个列的条件时,使用联合索引可以提高查询效率。

如果涉及多个列的查询,可以不用设置多个索引,而是设置一个联合索引。

index merge 优化

index merge就是多个索引并发扫描,再将扫描结果合并

索引合并不适用于全文索引。

索引合并访问方法检索具有多个范围扫描的行,并将其结果合并为一个。此访问方法只合并单个表的索引扫描,而不合并多个表的扫描。合并可以产生其底层扫描的并集、交集或交集的并集。

可以使用索引合并的查询示例:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20;

SELECT * FROM tbl_name
WHERE (key1 = 10 OR key2 = 20) AND non_key = 30;

SELECT * FROM t1, t2
WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')
AND t2.key1 = t1.some_col;

SELECT * FROM t1, t2
WHERE t1.key1 = 1
AND (t2.key1 = t1.some_col OR t2.key2 = t1.some_col2);

如果你的查询有一个带有深度AND/OR嵌套的复杂WHERE子句,并且MySQL没有选择最佳计划,请尝试使用以下恒等转换来分发术语:

1
2
(x AND y) OR z => (x OR z) AND (y OR z)
(x OR y) AND z => (x AND z) OR (y AND z)

在EXPLAIN输出中,Index Merge方法在type列中显示为index_merge。在本例中,key列包含使用的索引列表,key_len包含这些索引的最长键部分列表。

索引合并访问方法有几种算法,它们显示在EXPLAIN输出的Extra字段中:

  • intersect:对多个and条件生效
  • union:对多个or条件生效
  • sort_union:sort-union算法和union算法之间的区别在于,sort-union算法必须首先获取所有行的行ID,并在返回任何行之前对其进行排序。

索引合并的使用取决于optimizer_switch系统变量的index_merge、index_merge_intersection、index_merge_union和index_merge_sort_union标志的值。默认情况下,所有这些标志都是打开的。

如何设计索引?

设计索引不仅仅是为每个字段都加上索引,而是要根据查询的特点来合理选择索引。以下是一些常见的索引设计思路:

优先选择经常作为查询条件的字段

在WHERE子句中频繁使用的字段,通常应该创建索引。
经常用来连接(JOIN)的字段也应该加索引。
如果某个字段的值具有较高的唯一性(如id、手机号等),它是理想的索引候选。

合理选择索引的顺序

对于联合索引,列的顺序非常重要。MySQL通过最左前缀匹配原则来匹配索引,因此选择索引列的顺序时要考虑到查询的条件顺序。
例如,在查询WHERE name='John' AND age>25时,若创建了(name, age)的联合索引,那么查询会使用该索引。如果把age放在前面,查询可能无法利用该索引。

不要过度使用索引

索引虽然提高了查询效率,但也带来了一些负担。每次插入、更新、删除数据时,索引都会被更新。过多的索引会导致写操作的性能下降。因此,应当只为常用的查询字段建立索引,避免为每个字段都加索引。

避免在频繁更新的字段上创建索引

对于一些经常进行更新操作的字段,不建议创建索引,因为每次更新时都会重新计算索引,这会影响性能。

使用覆盖索引(Covering Index)

覆盖索引是一种特殊的索引,它能满足查询的所有需求,即查询字段完全在索引中,可以避免访问数据表的操作。通过合理设计索引列,尽量使查询能够通过索引完成。

例如,查询SELECT id, name FROM users WHERE age > 30时,可以创建一个包含id、name和age的联合索引,这样查询就可以直接通过索引返回结果。

索引优化案例

案例1:快速查找用户信息

假设有一个users表,结构如下:

1
2
3
4
5
6
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100),
age INT
);

如果我们经常需要查询某个email对应的用户,可以为email列添加一个唯一索引:

1
CREATE UNIQUE INDEX idx_email ON users(email);

这样,查询SELECT * FROM users WHERE email='test@example.com';时,就能迅速找到对应的记录。

优化多条件查询

假设我们有一个orders表,用于记录订单信息,查询经常会使用status和order_date这两个字段。如果我们创建一个联合索引:

1
CREATE INDEX idx_status_date ON orders(status, order_date);

这样,查询SELECT * FROM orders WHERE status='shipped' AND order_date='2025-06-30';时,MySQL会利用该索引提高查询效率。

案例3:避免全表扫描

假设我们有一个大表products,其中包含了成千上万的商品信息。如果我们需要根据name来查询某个商品,通过给name字段创建索引:

1
CREATE INDEX idx_name ON products(name);

查询时,MySQL就能通过该索引快速定位到商品,而不是扫描整个表。

总结

索引设计是MySQL数据库优化的核心技术之一。通过合理的索引设计,我们可以极大地提高查询性能,但过多或不恰当的索引设计可能带来负面影响。掌握索引的基本原理与设计策略,将帮助你在MySQL数据库的应用中更加高效地管理和优化数据查询。

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习

大家好,我是大头,职高毕业,现在大厂资深开发,前上市公司架构师,管理过10人团队!
我将持续分享成体系的知识以及我自身的转码经验、面试经验、架构技术分享、AI技术分享等!
愿景是带领更多人完成破局、打破信息差!我自身知道走到现在是如何艰难,因此让以后的人少走弯路!
无论你是统本CS专业出身、专科出身、还是我和一样职高毕业等。都可以跟着我学习,一起成长!一起涨工资挣钱!
关注我一起挣大钱!文末有惊喜哦!

抖音为啥总让你“上头”?揭秘它偷摸给你“画像”的全过程!

“救命!这说的不就是我吗?”

1
2
3
4
5
“晚上11点,你躺在床上打开抖音,心想‘就刷10分钟’——结果一抬头,凌晨1点半了🙃。”

“明明只想看个搞笑视频,却被推送了‘如何暴瘦20斤’,你吓得手机一抖:它怎么知道我最近胖了?!”

“更离谱的是,你刚和朋友聊到想养猫,第二天抖音就塞给你100个猫片,仿佛在你家装了窃听器…”
  • “抖音人均‘熬夜冠军’:90%的用户每天刷视频超过1小时,60%的人会因‘再刷一条’熬夜到凌晨。”
  • “但比熬夜更可怕的是——它总能猜中你心里那点小秘密:暗恋的人、想买的东西、甚至你妈都不知道的生活习惯!”

你是否有以下疑问?

“明明没填过年龄、职业、爱好,抖音为啥比亲妈还懂你?”
“真相是:它早就在你眼皮底下,偷偷画了一张‘数字分身’的像!”
“今天我们就来扒一扒,抖音是怎么‘算计’你的——看完你可能再也不敢乱点赞了…”

“别慌!这不是科幻片,3分钟带你看懂背后的套路——从此刷抖音,你才是操控算法的人👊。”

什么是用户画像?——用“人话”解释专业概念

  1. 先举个“栗子”,秒懂用户画像

“你网购时的样子,就是用户画像的答案!”

  • 场景1:你经常买咖啡豆、搜“办公室提神神器” → 淘宝立马给你贴标签:“熬夜打工人,咖啡续命党”。

  • 场景2:你总看母婴测评、收藏育儿干货 → 小红书默默标记你:“新手宝妈,爱囤货的细节控”。

一句话总结:

用户画像 = 平台用你的行为数据,“拼”出的一本个人说明书,上面写满了你的喜好习惯,甚至小心思。

  1. 别怕!它画的不是你的脸,而是你的“影子”
  • 误区澄清:
    • “画像不是偷拍你的身份证,也不会知道你叫张三李四。”
    • 它只关心你做了什么,而不是你是谁
  • 举个反向例子🌰:
    • 如果你用爸妈手机刷了一天广场舞视频 → 抖音可能误判机主是退休阿姨,第二天狂推养生茶丝巾(爸妈:???)。
    • 结论:用户画像可能出错,但它会跟着你的行为“随时改作业”!
  1. 你的“数字分身”是怎么被画出来的?
  • 第一步:暗中观察你的“蛛丝马迹”
    • 明面操作:点赞、评论、关注 → 你主动告诉平台“我爱这个!”
    • 暗地追踪:
      • 你在一只柯基视频上停留了20秒 → 系统:“这人喜欢狗,记下来!”
      • 你总在晚上11点后刷美食视频 → 系统:“夜宵爱好者,深夜放毒目标用户!”
  • 第二步:给你贴满“隐形标签”
    • 标签类型:
      • 基础款:性别年龄(猜的)、地理位置(比如“北京通州打工人”)。
      • 进阶款:“社恐宅家党”“刘畊宏女孩”“甄嬛传十级学者”。
    • 终极目标:用无数个标签把你“拆解”得明明白白。
  1. 用户画像能有多离谱?网友亲身经历炸锅

爆笑案例:

  • 网友A:搜过一次“痔疮膏”,从此抖音推荐全是“肛肠医院专家号”(本人:我只是手滑!)。
  • 网友B:借朋友手机看了一次挖掘机视频,结果算法认定他是“蓝翔潜在学员”(朋友:你害我!)。

严肃提醒:

用户画像不一定100%准确,但会像牛皮糖一样粘着你——你越刷,它越懂(或越瞎猜)。

  1. 为什么平台非要给你“画像”?真相扎心了

对你:

  • 方便!不用手动搜索,喜欢的内容自动喂到嘴边。

对平台:

  • 留住你:让你刷到停不下来 → 增加广告曝光 → 赚更多钱。

说白了:

你以为在刷视频,其实是你的“画像”在帮抖音“打工”!

抖音如何“偷摸”给你画像?——3步拆解

步骤1:暗中观察——你的“小动作”全被记在小本本上

你以为你在玩手机?不,你在给抖音“交作业”!

  • 明面上的“送分题”:
    • 点赞 → “这个视频我超爱!”
    • 收藏 → “好东西必须囤着!”
    • 关注 → “这博主是我亲爹/妈!”
  • 暗地里的“监控大法”:
    • 停留时长:
      • 看帅哥美女视频停留30秒 → 系统:“这人好色,记下来!”
      • 看科普视频3秒就划走 → 系统:“学渣,下次少推!”
    • 重复观看:
      • 同一条修狗视频看了5遍 → 系统:“狗党实锤,明天首页全变宠物乐园!”
    • 搜索关键词:
      • 搜过“脱发自救” → 第二天推荐生发液广告+秃头博主励志故事(扎心了💔)。

步骤2:算法“算命”——找到你的“网络双胞胎”

套路1:协同过滤——拉群找“同类”

  • “你爱看腹肌男,我也爱看→咱俩就是异父异母的亲兄弟!”
    • 你点赞了“刘畊宏跳操” → 抖音找到100个同样爱健身的人 → 把她们喜欢的蛋白粉广告、瑜伽裤测评全塞给你。
  • 副作用:
    • 不小心点了一次广场舞视频 → 接下来一周都是“老年Disco”(年轻人:救救我😇)。

套路2:深度学习——系统比你更懂你

  • “你连刷10条露营视频?懂了,你上辈子是棵树!”
    • 系统发现你爱看“野外生存” → 自动关联推荐帐篷、烧烤架,甚至防狼喷雾(虽然你可能只是云露营)。
  • 离谱案例:
    • 网友@小陈:拍过一次穿JK制服的照片 → 抖音判定她是“二次元宅女”,狂推动漫混剪和手办福袋(实际是帮妹妹拍的)。

步骤3:24小时“PUA”——越刷越上瘾的终极陷阱

实时更新画像:今天你是宝妈,明天变驴友

  • 周一刷美妆教程 → 画像:“精致都市丽人”。
  • 周二突然看育儿知识 → 画像秒变:“新手妈妈”(哪怕你单身🐶)。
  • 系统逻辑:宁可错杀一千,绝不放过一个!

多维度“绑架”你的注意力:

  • BGM操控:你常听周杰伦 → 同类音乐视频优先推送。
  • 时间陷阱:发现你每晚10点活跃 → 准时投放“助眠神器”广告(结果你越看越精神)。
  • 地域追踪:IP在北京 → 推荐“环球影城攻略”;切换到老家县城 → 立刻变成“相亲神曲合集”。

举个真实场景,让你脊背发凉

“一条视频引发的血案”:

你偶然点开“考研英语”视频,看了15秒 → 系统标记“潜在考生”。

第二天首页出现“考研逆袭故事” → 你忍不住看完。

一周后,抖音开始推荐“考研辅导班”“防脱发洗发水”,甚至“二战考研租房攻略”……

结果:你本来只想随便看看,现在满脑子都是“我是不是该考研了?”

总结:抖音的“画像公式”

你的行为数据 + 算法的“脑补” = 一个比你妈还操心的“数字管家”

  • 温馨提示:

如果不想被“画像”绑架,试试这招👇:

长按视频 → 点击“不感兴趣” → 让算法怀疑人生!

(但大概率你会继续刷,因为——真香!)

为什么我们心甘情愿被“操控”?——人性的弱点

多巴胺陷阱:下一个视频永远在“勾引”你

“像赌博一样上瘾”:

  • 每次下滑都像“开盲盒”——可能是搞笑段子、萌宠暴击,或是心动帅哥,这种不确定的惊喜感让大脑疯狂分泌多巴胺。

案例:

网友@小鱼:“明明困得要死,但总感觉下一个视频会更精彩,结果刷到天亮……我像个赌徒!”

生理真相:

  • 刷抖音时的大脑活跃区和吸毒时高度相似——你不是在玩手机,是在“嗑电子药”。

即时满足:快乐来得太容易,谁还愿意“吃苦”?

“3秒一个爽点,谁扛得住?”

  • 看书要专注1小时,追剧要等更新,但抖音的快乐“即刷即有”:
  • 想放松?10秒就能笑到喷饭。
  • 想学习?1分钟“速成”Excel技巧(虽然转头就忘)。

对比伤害:

现实中的努力需要坚持,但抖音的快乐“一键直达”——人天生偏爱“捷径”。

信息茧房:温柔地“杀死”你的好奇心

“你以为你在选内容?其实是算法给你造了个温室”:

  • 爱看美妆就推全妆教程,爱看游戏就塞电竞直播……你越满意,系统越不敢让你看别的。

副作用:

网友@阿凯:“刷了3个月抖音,我以为全世界都爱看钓鱼,直到发现我妈的首页全是婆媳伦理剧……”

心理机制:

  • 人本能地抗拒“认知失调”——看到不同观点会难受,而抖音让你永远“舒适”。

社交货币:不刷抖音,你就“没朋友”

“梗都不知道,还怎么混圈子?”:

  • 当全网都在跳“科目三”、玩“小土豆梗”时,你不刷抖音=聊天插不上话=被社交圈抛弃。

案例:

大学生@露露:“室友们讨论抖音热梗哈哈大笑,我只能假装听懂,默默打开手机开刷……”

平台心机:

  • 抖音刻意制造“全民爆梗”,让你觉得:不参与=落伍=孤独。

虚拟认同:点赞和关注,成了“电子氧气”

“现实中当小透明,抖音里当赛博顶流”:

  • 发一条自拍视频,收到100个点赞 → 感觉自己被全世界喜爱(尽管陌生人根本不认识你)。

心理补偿:

  • 工作被老板骂?感情不顺?抖音里发条视频,评论区总有人夸你“姐妹好美!”——这种即时认同让人欲罢不能。

偷走时间还不让你愧疚:碎片化的“正当理由”

“我就刷5分钟,放松一下怎么了?”:

  • 等公交、上厕所、吃饭……所有碎片时间都被抖音填满,你以为在“高效利用时间”,实际是被切割成无法专注的废片。

真实伤害:

  • 研究显示:频繁切换注意力,会导致记忆力下降、焦虑加剧——但你根本停不下来,因为“5分钟”太容易骗过自己。

总结:我们不是输给抖音,是输给人性

“抖音不可怕,可怕的是它比你还懂你”:

  • 它利用人性的懒惰、贪婪、虚荣、恐惧,织了一张温柔的网——你明知道是陷阱,却自愿跳进去。

终极矛盾:

  • 我们既渴望抖音提供的快乐,又厌恶被控制的无力感,最后在“真香”和“戒断”之间反复横跳。

警惕!被“画像”背后的代价

你以为的“贴心”,可能是“掏空你”的陷阱

代价1:隐私裸奔——你的生活成了“透明超市”

案例:

1
2
3
和朋友微信聊“想买吸尘器”,第二天抖音就推荐10款 → “它怎么听到的?!”(其实是通过输入法、跨APP数据共享)。

搜过“租房攻略”,立刻被中介广告轰炸 → 你的焦虑成了别人赚钱的工具。

扎心真相:

  • 免费刷视频的代价,可能是你的隐私被“标价售卖”——广告主花钱就能精准找到你。

代价2:信息茧房——活成“数字井底之蛙”

  • “你看到的世界,是算法想让你看到的”:
    • 爱看正能量 → 首页全是人间美好 → 你以为社会一片和谐。
    • 爱看负面新闻 → 推送越来越极端 → 你变得焦虑、厌世。

网友自嘲:

  • “在抖音待了3年,我以为全国女生都月入5万,只有我穷得真实😭”

代价3:钱包被“绑架”——算法让你花钱更上头

套路拆解:

  • 先给你推“普通人逆袭”故事(制造焦虑)。
  • 再塞给你“3天速成副业”课程(提供希望)。
  • 最后弹出“限时折扣”支付页面(逼你冲动下单)。

血泪教训:

网友@娜娜:“刷抖音学了‘配音赚钱’,花2998元买课,最后只赚了9块8……”

防坑指南——普通人如何“反杀”算法?

招式1:给算法“喂假饭”

操作手册:

  • 偶尔故意乱刷:猛看完全不感兴趣的内容(比如广场舞、拖拉机维修)。
  • 随机点赞:给宠物视频点❤️,再给财经分析点❤️ → 让系统怀疑人生。

效果:

  • 算法画像混乱 → 推荐内容“精神分裂”,你成功隐身!

招式2:关闭“监控开关”(以抖音为例)

步骤:

  • 点击“我”→右上角三条杠→【设置】。
  • 找到【隐私设置】→ 关闭【个性化内容推荐】。
  • 【广告偏好设置】→ 关闭【程序化广告展示】。

副作用预警:

首页会变成无聊大杂烩(但至少你的数据安全了)。

招式3:定期“数据大扫除”

  • 清理缓存:设置→清理占用空间→勾选【临时文件】【资源文件】。
  • 重置广告ID(安卓/苹果通用):
  • 手机设置→隐私→广告→重置广告标识符 → 让平台重新“认识你”。

终极灵魂拷问:逃离算法,我们能赢吗?

现实很骨感:

  • 你可以关掉抖音,但外卖APP、购物平台、甚至导航软件都在给你画像 → “全网皆牢笼”。

清醒一点:

  • 完全抵制不现实,但可以“让算法为我所用”:
  • 想学习?主动搜索干货 → 让系统推荐知识类视频。
  • 想戒剁手?多刷差评揭露视频 → 算法帮你“拔草”。

你的人生不该被“标签”定义

  • “抖音里的你,是爱吃瓜的乐子人、是职场小白、是深夜emo怪……但那只是数据的‘碎片’。”
  • “真实的你,远比算法想象的更丰富、更自由——别让屏幕里的‘画像’,框住你的人生。”

“试过以上防坑招式的,评论区举手!没试过的……现在立刻去关掉个性化推荐,回来打卡!”

总结

把选择权“抢”回自己手里.

  • “刷抖音时,到底是你在找快乐,还是算法在‘投喂’快乐?”
  • 用算法看世界 → 世界越来越小,小到只剩你喜欢的“巴掌大”。
  • 自己探索世界 → 可能踩雷,但会发现更多意想不到的惊喜。

“别让算法替你决定‘该看什么’——你的人生,不该活成数据的提线木偶。”

“你是哪种抖音‘上头人’?对号入座👇
1️⃣ “永远在等下一个更香” 多巴胺赌徒
2️⃣ “不刷梗就社恐” 跟风焦虑党
3️⃣ “被算法PUA还夸它贴心” 清醒沉沦者
❤️ 评论区认领你的身份,找找同类!”

“关不掉抖音?至少可以‘反操控’它!”

  • 神操作:长按视频 → 【不感兴趣】 → 连点10次,算法会哭着求你别走。

“你在抖音的每一次滑动,都在为你的‘数字画像’描边——是时候告诉自己:我的人生,绝不止算法定义的这一个版本。”

文末福利

关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。

概念学习

概念学习

概念学习

概念学习