feat:更新MySQL博客
This commit is contained in:
62
docs/Web-Backend/MySQL/Flyway.md
Normal file
62
docs/Web-Backend/MySQL/Flyway.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Flyway简单使用
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  Flyway 是一个开源的数据库版本控制工具,它极大地简化了数据库的迁移和版本管理。它的核心思想是像**用 Git 管理代码一样来管理数据库的结构**。
|
||||
|
||||
# 二、原理
|
||||
  Flyway 通过在数据库中创建一个名为 flyway_schema_history的特殊表来工作
|
||||
| 列名 | 含义 |
|
||||
| - | - |
|
||||
| installed_rank | 执行顺序 |
|
||||
| version | 脚本的版本号 |
|
||||
| description | 脚本的描述 |
|
||||
| type | 脚本类型(通常是 SQL) |
|
||||
| script | 脚本文件名 |
|
||||
| checksum | 脚本文件的校验和(用于检测篡改) |
|
||||
| installed_by | 执行人 |
|
||||
| installed_on | 执行时间 |
|
||||
| execution_time | 执行耗时(毫秒) |
|
||||
| success | 是否成功 |
|
||||
|
||||
  工作流程:
|
||||
1. 应用启动时,Flyway 会检查配置的数据库路径。
|
||||
2. 检查目标数据库中的 flyway_schema_history表。
|
||||
3. 将数据库路径下的迁移脚本与 flyway_schema_history表中的记录进行对比。
|
||||
4. 按照版本号顺序执行那些尚未执行的迁移脚本。
|
||||
5. 执行成功后,将记录插入 flyway_schema_history表。
|
||||
|
||||
## 2.1 校验和计算
|
||||
  Flyway 使用 CRC32 算法 计算 SQL 脚本文件的校验和(Checksum)。Javs使用32位有符号整数存储,因此有时会得到负数。可以通过`mvn flyway:info`查看每个脚本的校验和。
|
||||
|
||||
# 三、与SpringBoot集成
|
||||
## 3.1 添加依赖
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 3.2 配置数据源
|
||||
```yml
|
||||
spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true # 如果数据库非空,且无 flyway_schema_history 表,则先创建基线版本
|
||||
```
|
||||
  `baseline-on-migrate: true`,当数据库已经存在数据,但是没有flyway_schema_history表时,Flyway会插入一条基线数据,并标记为1.0版本,则后续的迁移脚本只会执行比1.0版本更高的数据库文件。
|
||||
  **如果设置为false,如果数据库已经存在数据时,Flyway会报错。**
|
||||
  因此需要避免这种情况的存在,在**初始发布应用时,要保证数据库为空**。
|
||||
|
||||
## 3.3 创建数据库脚本
|
||||
  在项目的资源目录 src/main/resources下创建文件夹 db/migration。
|
||||
  Flyway 的 SQL 脚本文件名有严格的命名规则:`V<Version>__<Description>.sql`
|
||||
  例如V1.0.0_001__20251027.sql,表示v1.0.0版本的第一个sql,日期为2025年10月27日。
|
||||
  **创建了迁移脚本,一旦应用,就不可修改,否则会导致校验错误。如果确实要修改,请再创建一个脚本。**
|
||||
|
||||
## 3.4 启动程序
|
||||
  启动程序后。Flyway会自动在数据库中创建flyway_schema_history表,然后扫描db/migration目录下的所有脚本,按顺序执行sql文件,并记录到flyway_schema_history表中。
|
||||
458
docs/Web-Backend/MySQL/MyBatis.md
Normal file
458
docs/Web-Backend/MySQL/MyBatis.md
Normal file
@@ -0,0 +1,458 @@
|
||||
---
|
||||
title: MyBatis简介和使用
|
||||
date: 2025-11-27
|
||||
---
|
||||
|
||||
# 一、简介
|
||||
  [MyBatis](https://mybatis.org/mybatis-3/)是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
|
||||
|
||||
# 二、安装
|
||||
### 2.1 引入依赖
|
||||
  在`pom.xml`文件中,引入依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>x.x.x</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
  可以在Github中查看[MyBatis](https://github.com/mybatis/mybatis-3)最新版本号。
|
||||
|
||||
### 2.2 配置文件
|
||||
  在`resource`文件夹中新建mybatis-config.xml文件和mapper->BlogMapper.xml映射文件:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
|
||||
<configuration>
|
||||
<!-- 环境配置 -->
|
||||
<environments default="development">
|
||||
<!-- 环境名称 -->
|
||||
<environment id="development">
|
||||
<!-- 事务管理器配置 -->
|
||||
<transactionManager type="JDBC"/>
|
||||
<!-- 数据源配置 -->
|
||||
<dataSource type="POOLED">
|
||||
<!-- JDBC驱动名称 -->
|
||||
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
|
||||
<!-- 数据库地址 -->
|
||||
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT"/>
|
||||
<!-- 数据库用户名 -->
|
||||
<property name="username" value="root"/>
|
||||
<!-- 数据库密码-->
|
||||
<property name="password" value="123456"/>
|
||||
</dataSource>
|
||||
</environment>
|
||||
</environments>
|
||||
|
||||
<!-- 映射器 -->
|
||||
<mappers>
|
||||
<!-- mapper文件 -->
|
||||
<mapper resource="mapper/BlogMapper.xml"/>
|
||||
</mappers>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
  注:如果使用MySql数据库,需要增加MySql驱动依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.12</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 2.3 定义映射语句
|
||||
  新建dao->BlogDao.java:
|
||||
|
||||
```java
|
||||
public interface BlogDao {
|
||||
Blog selectBlog(@Param("id") Integer id);
|
||||
}
|
||||
```
|
||||
|
||||
  BlogMapper.xml:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.example.mybatislearn.dao.BlogDao">
|
||||
<select id="selectBlog" resultType="com.example.mybatislearn.entity.Blog">
|
||||
select * from Blog where author_id = #{id}
|
||||
</select>
|
||||
</mapper>
|
||||
```
|
||||
|
||||
### 2.4 执行SqlSession
|
||||
```java
|
||||
// 配置文件路径
|
||||
String resource = "mybatis-config.xml";
|
||||
try {
|
||||
// 读取配置文件
|
||||
InputStream inputStream = Resources.getResourceAsStream(resource);
|
||||
// 构建SqlSession工厂
|
||||
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
|
||||
// 获取SqlSession
|
||||
SqlSession session = sqlSessionFactory.openSession();
|
||||
// 获取映射文件
|
||||
BlogDao blogDao = session.getMapper(BlogDao.class);
|
||||
// 执行已映射的SQL语句
|
||||
Blog blog = blogDao.selectBlog(101);
|
||||
System.out.println(blog);
|
||||
// 关闭SqlSession
|
||||
session.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
```
|
||||
|
||||
  每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。
|
||||
  SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。
|
||||
  而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
|
||||
|
||||
### 2.5 作用域和生命周期
|
||||
1. SqlSessionFactoryBuilder
|
||||
|
||||
  这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
|
||||
|
||||
2. SqlSessionFactory
|
||||
|
||||
  SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
|
||||
|
||||
3. SqlSession
|
||||
|
||||
  每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。
|
||||
|
||||
# 三、注入SpringBoot框架
|
||||
## 3.1 引入依赖
|
||||
  将之前MyBatis的依赖替换成MyBatis的SpringBoot Starter:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 3.2 配置文件
|
||||
  在Resource文件夹下新建application.yml:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/mybatis_learn?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
mybatis:
|
||||
# mapper文件路径
|
||||
mapper-locations: classpath*:mapper/*Mapper.xml
|
||||
configuration:
|
||||
# 开启日志
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
```
|
||||
|
||||
  注:这里的url的写法和XML文件中的写法不一致。
|
||||
|
||||
# 3.3 定义映射语句
|
||||
  在原映射接口文件BlogDao.java中添加@Mapper注解
|
||||
|
||||
## 3.4 实现原理
|
||||
  引入mybatis-spring-boot-starter模块之后,其可以:
|
||||
|
||||
1. **自动检测DataSource**
|
||||
2. **使用SqlSessionFactoryBean注册SqlSessionFactory 实例,并设置DataSource数据源**
|
||||
3. **基于SqlSessionFactory自动注册SqlSessionTemplate实例**
|
||||
4. **自动扫描@Mapper注解类,并通过SqlSessionTemplate注册到Spring Context中**
|
||||
|
||||
  每次执行@Mapper映射文件中的接口时,都会自动开启一个SqlSession并在执行结束时关闭。
|
||||
|
||||
## 3.5 执行映射语句
|
||||
```java
|
||||
Blog blog = blogDao.selectBlog(101);
|
||||
System.out.println(blog);
|
||||
```
|
||||
|
||||
  相较之前的写法,节省了大量的配置工作。
|
||||
|
||||
# 四、高级特性
|
||||
## 4.1 动态参数
|
||||
```xml
|
||||
#{}是参数占位符的标记,它可以防止SQL注入,当使用#{}时,MyBatis会自动处理参数的数据类型,
|
||||
如果参数是字符串,它会给传入的值加上引号,这样可以有效地防止SQL注入攻击。
|
||||
|
||||
${}则是直接将参数值嵌入SQL语句中。当使用${}时,传入的参数会直接显示在SQL中,
|
||||
MyBatis不会对参数进行任何类型转换或加引号处理。一般用在动态表名、列名或数据库名称中。
|
||||
```
|
||||
|
||||
## 4.2 SQL片段
|
||||
  可以用来定义可重复的SQL代码片段:
|
||||
|
||||
```xml
|
||||
<sql id="userColumns">
|
||||
id, username, email, phone
|
||||
</sql>
|
||||
|
||||
<select id="findAllUsers" resultType="User">
|
||||
SELECT <include refid="userColumns" /> FROM users
|
||||
</select>
|
||||
```
|
||||
|
||||
## 4.3 批量操作
|
||||
  推荐使用集合方式批量操作:
|
||||
|
||||
```java
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
Integer insertUsers(@Param("list") List<User> userList);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```xml
|
||||
<insert id="insertUsers">
|
||||
INSERT INTO user (username, password)
|
||||
VALUES
|
||||
<foreach collection ="list" item="item" separator =",">
|
||||
(#{item.username}, #{item.password})
|
||||
</foreach>
|
||||
</insert>
|
||||
```
|
||||
|
||||
## 4.4 结果映射
|
||||
  复杂结果缓存
|
||||
|
||||
```xml
|
||||
<!-- 非常复杂的结果映射 -->
|
||||
<resultMap id="detailedBlogResultMap" type="Blog">
|
||||
<!-- 一般不需要 -->
|
||||
<constructor>
|
||||
<idArg column="blog_id" javaType="int"/>
|
||||
</constructor>
|
||||
<result property="title" column="blog_title"/>
|
||||
<!-- 复杂对象 1对1 -->
|
||||
<association property="author" javaType="Author">
|
||||
<id property="id" column="author_id"/>
|
||||
<result property="username" column="author_username"/>
|
||||
<result property="password" column="author_password"/>
|
||||
<result property="email" column="author_email"/>
|
||||
<result property="bio" column="author_bio"/>
|
||||
<result property="favouriteSection" column="author_favourite_section"/>
|
||||
</association>
|
||||
<!-- 列表 1对多-->
|
||||
<collection property="posts" ofType="Post">
|
||||
<id property="id" column="post_id"/>
|
||||
<result property="subject" column="post_subject"/>
|
||||
<association property="author" javaType="Author"/>
|
||||
<collection property="comments" ofType="Comment">
|
||||
<id property="id" column="comment_id"/>
|
||||
</collection>
|
||||
<collection property="tags" ofType="Tag" >
|
||||
<id property="id" column="tag_id"/>
|
||||
</collection>
|
||||
<discriminator javaType="int" column="draft">
|
||||
<case value="1" resultType="DraftPost"/>
|
||||
</discriminator>
|
||||
</collection>
|
||||
</resultMap>
|
||||
```
|
||||
|
||||
  其中`<collection>`也可以使用嵌套查询:
|
||||
|
||||
```xml
|
||||
<collection property="posts" ofType="Post" select="queryPost"/>
|
||||
|
||||
<resultMap id="postResultMap" type="Post">
|
||||
<id property="id" column="post_id"/>
|
||||
<result property="subject" column="post_subject"/>
|
||||
<association property="author" javaType="Author"/>
|
||||
<collection property="comments" ofType="Comment">
|
||||
<id property="id" column="comment_id"/>
|
||||
</collection>
|
||||
<collection property="tags" ofType="Tag" >
|
||||
<id property="id" column="tag_id"/>
|
||||
</collection>
|
||||
<discriminator javaType="int" column="draft">
|
||||
<case value="1" resultType="DraftPost"/>
|
||||
</discriminator>
|
||||
</resultMap>
|
||||
|
||||
<select id="queryPost" resultMap="postResultMap">
|
||||
|
||||
</select>
|
||||
|
||||
```
|
||||
|
||||
  如果需要传递参数,可以在`<collection>`添加`column`属性:
|
||||
|
||||
```xml
|
||||
<!-- 单个参数 -->
|
||||
<collection property="posts" column="name" ofType="Post" select="queryPost"/>
|
||||
<!-- 多个参数 -->
|
||||
<collection property="posts" column="{param1=param_1, param2=param_2}" ofType="Post" select="queryPost"/>
|
||||
```
|
||||
|
||||
  注:建立在非列表数据时使用嵌套查询,否则每查到一个数据都会进行一次子查询操作。
|
||||
|
||||
## 4.5 一二级缓存
|
||||
  默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>
|
||||
|
||||
+ 映射语句文件中的所有 select 语句的结果将会被缓存。
|
||||
+ 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
|
||||
+ 一级缓存和二级缓存区别在于一级缓存只针对一次SqlSession,二级缓存针对全局范围。
|
||||
|
||||
## 4.6 动态SQL
|
||||
+ if :是/否
|
||||
|
||||
```xml
|
||||
<select id="findActiveBlogWithTitleLike" resultType="Blog">
|
||||
SELECT * FROM BLOG
|
||||
WHERE state = 'ACTIVE'
|
||||
<if test="title != null">
|
||||
AND title like #{title}
|
||||
</if>
|
||||
</select>
|
||||
```
|
||||
|
||||
+ choose、when、otherwise:选择其中一个
|
||||
|
||||
```xml
|
||||
<select id="findActiveBlogLike" resultType="Blog">
|
||||
SELECT * FROM BLOG WHERE state = 'ACTIVE'
|
||||
<choose>
|
||||
<when test="title != null">
|
||||
AND title like #{title}
|
||||
</when>
|
||||
<when test="author != null and author.name != null">
|
||||
AND author_name like #{author.name}
|
||||
</when>
|
||||
<otherwise>
|
||||
AND featured = 1
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
```
|
||||
|
||||
+ where、set:解决SQL语法问题
|
||||
|
||||
```xml
|
||||
<select id="findActiveBlogLike" resultType="Blog">
|
||||
SELECT * FROM BLOG
|
||||
<where>
|
||||
<if test="state != null">
|
||||
state = #{state}
|
||||
</if>
|
||||
<if test="title != null">
|
||||
AND title like #{title}
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<update id="updateAuthorIfNecessary">
|
||||
update Author
|
||||
<set>
|
||||
<if test="username != null">username=#{username},</if>
|
||||
<if test="password != null">password=#{password},</if>
|
||||
</set>
|
||||
where id=#{id}
|
||||
</update>
|
||||
```
|
||||
|
||||
  where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
|
||||
|
||||
# 五、自定义类型处理器
|
||||
  MyBatis 在预处理语句(PreparedStatement)中设置参数时,会从 Java 类型(javaType)转换为 JDBC 类型(jdbcType);而从结果集中取出值时,会将 JDBC 类型转换为 Java 类型。这个转换工作就是由 TypeHandler来完成的。
|
||||
  需要创建一个类来实现 org.apache.ibatis.type.TypeHandler接口,或者继承 org.apache.ibatis.type.BaseTypeHandler类实现自定义类型处理器。
|
||||
  需要实现的方法:
|
||||
```java
|
||||
/**
|
||||
* 将Java对象设置到PreparedStatement中(Java类型 → JDBC类型)
|
||||
* @param ps PreparedStatement对象
|
||||
* @param i 参数位置(从1开始)
|
||||
* @param parameter 要设置的Java对象(非空)
|
||||
* @param jdbcType JDBC类型
|
||||
*/
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
|
||||
// 实现转换逻辑:T → JDBC类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列名从ResultSet中获取值(JDBC类型 → Java类型)
|
||||
* @param rs ResultSet对象
|
||||
* @param columnName 列名
|
||||
* @return 转换后的Java对象
|
||||
*/
|
||||
@Override
|
||||
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
// 实现转换逻辑:JDBC类型 → T
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列索引从ResultSet中获取值
|
||||
* @param rs ResultSet对象
|
||||
* @param columnIndex 列索引(从1开始)
|
||||
* @return 转换后的Java对象
|
||||
*/
|
||||
@Override
|
||||
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
// 实现转换逻辑:JDBC类型 → T
|
||||
}
|
||||
|
||||
/**
|
||||
* 从CallableStatement中获取值(用于存储过程)
|
||||
* @param cs CallableStatement对象
|
||||
* @param columnIndex 列索引
|
||||
* @return 转换后的Java对象
|
||||
*/
|
||||
@Override
|
||||
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
// 实现转换逻辑:JDBC类型 → T
|
||||
}
|
||||
```
|
||||
|
||||
  例如自定义一个处理 MEDIUMBLOB 字段与 Base64 字符串的转换:
|
||||
```java
|
||||
/**
|
||||
* 处理 MEDIUMBLOB 字段与 Base64 字符串的转换
|
||||
*/
|
||||
@MappedJdbcTypes(JdbcType.BLOB)
|
||||
@MappedTypes(String.class)
|
||||
public class BlobToBase64TypeHandler extends BaseTypeHandler<String> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||||
// Base64字符串 -> 数据库的byte[]
|
||||
if (parameter != null && !parameter.trim().isEmpty()) {
|
||||
byte[] bytes = Base64.getDecoder().decode(parameter);
|
||||
ps.setBytes(i, bytes);
|
||||
} else {
|
||||
ps.setBytes(i, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
// 数据库的byte[] -> Base64字符串
|
||||
byte[] bytes = rs.getBytes(columnName);
|
||||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
byte[] bytes = rs.getBytes(columnIndex);
|
||||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
byte[] bytes = cs.getBytes(columnIndex);
|
||||
return bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
520
docs/Web-Backend/MySQL/MySQL-Knowledge.md
Normal file
520
docs/Web-Backend/MySQL/MySQL-Knowledge.md
Normal file
@@ -0,0 +1,520 @@
|
||||
---
|
||||
title: MySQL知识点
|
||||
date: 2025-11-26
|
||||
---
|
||||
|
||||
# 一、基础知识
|
||||
## 1.1 数据类型
|
||||
### 1.1.1 汇总
|
||||
|类型|存储空间|范围|适用场景|
|
||||
| :-------------------------------------------------------------------------------------------------: | :-----------------: | :--------------------------: | :-----------------------: |
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">TINYINT</span>|1字节|-128 \~ 127|状态码、年龄、布尔值|
|
||||
|SMALLINT|2字节|-32,768 \~ 32,767|小范围计数、年份||
|
||||
|MEDIUMINT|3字节|-8,388,608 \~ 8,388,607|中型ID、访问量统计|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">INT/INTEGER</span>|4字节|-2\^31 \~ 2\^31-1|用户ID、订单号(常用)|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">BIGINT</span>|8字节|-2\^63 \~ 2\^63-1|分布式ID、大数据量计数|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">DECIMAL(M,D)</span>|变长(M+2字节)||金融金额、精确计算|精确小数,M\=总位数,D\=小数位|
|
||||
|FLOAT|4字节||科学测量、非精确计算|
|
||||
|DOUBLE|8字节||地理坐标、高精度计算|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">CHAR(M)</span>|255字符||固定长度编码、MD5哈希|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">VARCHAR(M)</span>|65,535字节||用户名、地址等变长数据|
|
||||
|TINYTEXT|255字节||短标题、简介|
|
||||
|TEXT|65,535字节||文章内容、评论|
|
||||
|MEDIUMTEXT|16MB (2\^24-1)||博客文章、产品描述|
|
||||
|LONGTEXT|4GB (2\^32-1)||电子书、大型文档|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">DATE</span>|||纯日期|
|
||||
|TIME(fsp)|||可指定微秒精度(0-6)|
|
||||
|<span data-type="text" style="background-color: var(--b3-card-error-background); color: var(--b3-card-error-color);">DATETIME(fsp)</span>|||高精度时间记录|
|
||||
|TIMESTAMP(fsp)|||自动时区转换,4字节存储|
|
||||
|ENUM|||性别、状态等有限选项|`gender ENUM('M','F','O')`|
|
||||
|SET|||用户兴趣、文章标签|`tags SET('red','green','blue')`|
|
||||
|TINYBLOB|255字节||微小二进制对象|
|
||||
|BLOB|65KB||标准二进制对象|
|
||||
|MEDIUMBLOB|16MB||中等二进制对象|
|
||||
|LONGBLOB|4GB||超大二进制对象|
|
||||
|
||||
### 1.1.2 法则
|
||||
  **整数选择**:优先INT,大数量用BIGINT,布尔值用TINYINT(1)或BIT(1)
|
||||
  **小数选择**:金融金额必须用DECIMAL,非精确测量可用FLOAT/DOUBLE
|
||||
  **字符串选择**:定长编码用CHAR,变长文本用VARCHAR,大文本用TEXT系列
|
||||
  **时间选择**:日期用DATE,精确时间用DATETIME,自动更新用TIMESTAMP
|
||||
  **特殊场景**:多选项用SET,结构化数据用JSON
|
||||
|
||||
## 1.2 常用函数
|
||||
### 1.2.1 字符串函数
|
||||
1. CONCAT(str1, str2, ...):连接字符串
|
||||
```sql
|
||||
SELECT CONCAT('Hello', ' ', 'World');
|
||||
```
|
||||
2. LENGTH(str):返回字符串的长度(字节数)
|
||||
```sql
|
||||
SELECT LENGTH('Hello');
|
||||
```
|
||||
3. SUBSTRING(str, pos, len):从字符串 str 的 pos 位置开始,截取长度为 len 的子字符串。
|
||||
```sql
|
||||
SELECT SUBSTRING('Hello World', 7, 5); -- 返回 'World'
|
||||
```
|
||||
|
||||
### 1.2.2 日期/时间函数
|
||||
1. NOW():返回当前日期和时间(yyyy-MM-DD HH:mm:ss)。
|
||||
2. CURDATE():返回当前日期,不带时间部分(yyyy-MM-DD)。
|
||||
3. DATE_FORMAT(date, format):按照指定的格式返回日期值。
|
||||
```sql
|
||||
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
|
||||
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d');
|
||||
```
|
||||
4. DATEDIFF(date1, date2):返回两个日期之间的天数差。
|
||||
```sql
|
||||
SELECT DATEDIFF('2024-12-31', '2024-01-01'); -- 返回364
|
||||
```
|
||||
5. YEAR(date) / MONTH(date) / DAY(date):分别返回日期的年、月、日部分。
|
||||
6. TIME(date):从日期或时间值中提取时间部分。
|
||||
7. TIMESTAMPDIFF(unit, datetime1, datetime2):返回两个日期或时间的差值,单位可以是 SECOND, MINUTE, HOUR, DAY, MONTH, YEAR 等。
|
||||
8. STR_TO_DATE(str, format):根据给定的格式将字符串转换为日期。
|
||||
```sql
|
||||
SELECT STR_TO_DATE('01-09-2024', '%d-%m-%Y'); -- 返回 '2024-09-01'
|
||||
```
|
||||
|
||||
### 1.2.3 数值函数
|
||||
1. ROUND(x, d):将数值 x 四舍五入到 d 位小数。
|
||||
```sql
|
||||
SELECT ROUND(123.4567, 2); -- 返回 123.46
|
||||
```
|
||||
2. FLOOR(x) / CEIL(x):返回小于或等于 x 的最大整数(向下取整)或大于或等于 x 的最小整数(向上取整)。
|
||||
```sql
|
||||
SELECT FLOOR(2.9); -- 返回 2
|
||||
SELECT CEIL(2.1); -- 返回 3
|
||||
```
|
||||
|
||||
### 1.2.4 聚合函数
|
||||
1. COUNT(expression):返回某列中的记录数。
|
||||
2. SUM(expression):返回某列中数值的总和。
|
||||
3. AVG(expression):返回某列中数值的平均值。
|
||||
4. MAX(expression) / MIN(expression):返回某列的最大值或最小值。
|
||||
|
||||
### 1.2.5 控制流函数
|
||||
1. IF(condition, true_value, false_value):如果 condition 为真,返回 true_value,否则返回 false_value。
|
||||
```sql
|
||||
SELECT IF(1 > 0, 'Yes', 'No'); -- 返回 'Yes'
|
||||
```
|
||||
2. CASE:用于条件判断,类似于多路选择。
|
||||
```sql
|
||||
SELECT
|
||||
CASE
|
||||
WHEN salary > 10000 THEN 'High'
|
||||
WHEN salary BETWEEN 5000 AND 10000 THEN 'Medium'
|
||||
ELSE 'Low'
|
||||
END AS salary_range
|
||||
FROM employees;
|
||||
```
|
||||
|
||||
# 二、索引
|
||||
  索引是对数据库表中的一列或多列值进行**排序**的一种结构,使用索引可以快速访问数据库表中的特定信息。
|
||||
  **索引相当于图书上的目录,可以根据目录上的页码快速找到所需的内容,提高性能(查询速度)**
|
||||
|
||||
## 2.1 优缺点
|
||||
优点:
|
||||
* **查询速度起飞 (主要目的)** :通过索引,数据库可以**大幅减少需要扫描的数据量**,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘 I/O 次数。
|
||||
* **保证数据唯一性**:通过创建**唯一索引 (Unique Index)** ,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户ID、邮箱等。**主键本身就是一种唯一索引**。
|
||||
* **加速排序和分组**:如果查询中的 ORDER BY 或 GROUP BY 子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作,从而提升性能。
|
||||
|
||||
缺点:
|
||||
* **创建和维护耗时**:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行**增、删、改 (DML操作)** 时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会**降低这些 DML 操作的执行效率**。
|
||||
* **占用存储空间**:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会**额外占用一定的磁盘空间**。索引越多、越大,占用的空间也就越多。
|
||||
* **可能被误用或失效**:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
|
||||
|
||||
## 2.2 适用场景
|
||||
适用场景
|
||||
* **频繁作为查询条件的字段**应该创建索引
|
||||
* **查询中排序的字段**创建索引将大大提高排序的速度(索引就是排序加快速查找)
|
||||
* **查询中统计或者分组的字段**
|
||||
|
||||
不适用场景
|
||||
* **频繁更新的字段不适合创建索引**,因为每次更新不单单是更新记录,还会更新索引,保存索引文件
|
||||
* **表记录太少,不需要创建索引**;
|
||||
* **数据重复且分布平均的字段**,因此为经常查询的和经常排序的字段建立索引。注意某些数据包含大量重复数据,因此他建立索引就没有太大的效果,例如性别字段,只有男女,不适合建立索引。
|
||||
|
||||
## 2.3 数据结构
|
||||
  在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构。
|
||||

|
||||
  例如要查找id=75的用户:`SELECT * FROM users WHERE id = 75`
|
||||
  查找步骤:
|
||||
1. 从根节点开始,75在50~100之间,走中间分支
|
||||
2. 到达叶子节点,找到id=75的数据位置
|
||||
3. 根据位置直接获取完整的用户数据
|
||||
|
||||
  整个过程只需要3次IO操作,而全表扫描可能需要很多次。
|
||||
|
||||
|特点|优势|实际效果|
|
||||
| :--------------: | :------------: | :--------------------: |
|
||||
|多路平衡|树的高度很低|减少磁盘访问次数|
|
||||
|叶子节点连接|支持范围查询|ORDER BY、分页查询快|
|
||||
|只在叶子存数据|内部节点小|更多索引数据放入内存|
|
||||
|
||||
## 2.4 索引类型
|
||||
1. 主键索引
|
||||
主键索引是最特殊的索引。
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(50),
|
||||
email VARCHAR(100)
|
||||
);
|
||||
|
||||
SELECT * FROM users WHERE id = 12345;
|
||||
```
|
||||
|
||||
2. 唯一索引
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_email ON users(email);
|
||||
```
|
||||
  插入重复的邮箱会报错
|
||||
|
||||
3. 普通索引
|
||||
```sql
|
||||
CREATE INDEX idx_name ON users(name);
|
||||
|
||||
SELECT * FROM users WHERE name = "张三";
|
||||
```
|
||||
|
||||
4. 复合索引
|
||||
多个字段组合的索引
|
||||
```sql
|
||||
CREATE INDEX idx_name_age_city ON users(name, age, city);
|
||||
|
||||
SELECT * FROM users WHERE name = '张三';
|
||||
SELECT * FROM users WHERE name = '张三' AND age = 25;
|
||||
SELECT * FROM users WHERE name = '张三' AND age = 25 AND city = '北京';
|
||||
SELECT * FROM users WHERE age = 25; -- 无法使用索引
|
||||
SELECT * FROM users WHERE city = '北京'; -- 无法使用索引
|
||||
```
|
||||
  复合索引的使用最左前缀原则。
|
||||
  `CREATE INDEX idx_name_age_city ON users(name, age, city);`相当于创建了3个索引:`users(name),users(name, age),users(name, age, city)`。
|
||||
|
||||
## 2.5 索引设计
|
||||
* 为WHERE条件添加索引
|
||||
```sql
|
||||
SELECT * FROM orders WHERE user_id = 123;
|
||||
SELECT * FROM orders WHERE status = 'paid';
|
||||
SELECT * FROM orders WHERE create_time > '2024-01-01';
|
||||
|
||||
CREATE INDEX idx_user_id ON orders(user_id);
|
||||
CREATE INDEX idx_status ON orders(status);
|
||||
CREATE INDEX idx_create_time ON orders(create_time);
|
||||
```
|
||||
|
||||
* 为ORDER BY字段添加索引
|
||||
```sql
|
||||
SELECT * FROM articles ORDER BY create_time DESC LIMIT 10;
|
||||
|
||||
CREATE INDEX idx_create_time ON articles(create_time);
|
||||
```
|
||||
|
||||
* 复合索引的顺序很关键
|
||||
```sql
|
||||
SELECT * FROM users WHERE city = '北京' AND age > 25 ORDER BY create_time;
|
||||
|
||||
-- 索引字段顺序应该是:过滤性强的字段在前
|
||||
CREATE INDEX idx_city_age_create_time ON users(city, age, create_time);
|
||||
```
|
||||
|
||||
* 限制每张表上的索引数量,建议单张表索引不超过 5 个。
|
||||
* 被频繁更新的字段应该慎重建立索引,虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
|
||||
* 尽可能的考虑建立联合索引而不是单列索引。因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
|
||||
|
||||
## 2.6 索引优化
|
||||
1. 监控慢查询
|
||||
```sql
|
||||
-- 开启慢查询日志
|
||||
SET GLOBAL slow_query_log = 'ON';
|
||||
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录下来
|
||||
|
||||
-- 查看慢查询
|
||||
SHOW VARIABLES LIKE 'slow_query_log_file';
|
||||
```
|
||||
|
||||
2. 合理使用前缀索引
|
||||
```sql
|
||||
-- 对于很长的字符串字段,使用前缀索引
|
||||
CREATE INDEX idx_title_prefix ON articles(title(20)); -- 只索引前20个字符
|
||||
```
|
||||
|
||||
## 三、事务
|
||||
  事务是数据库操作的基本单位,它是一组原子性的 SQL 语句,或者说是一个独立的工作单元。事务内的所有操作**要么全部成功,要么全部失败**。
|
||||
|
||||
## 3.1 特性
|
||||
* **原子性**(`Atomicity`):原子性确保事务中的所有操作要么全部完成,要么全部不完成。如果事务执行过程中发生错误,所有已执行的操作都会回滚。
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
INSERT INTO orders (user_id, amount) VALUES (1, 100);
|
||||
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1;
|
||||
-- 如果任何一步失败,整个事务都会回滚
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* **一致性**(`Consistency`):一致性确保数据库从一个一致的状态转换到另一个一致的状态。事务执行前后,数据库的完整性约束不会被破坏。
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
-- 确保账户余额不会出现负数
|
||||
UPDATE accounts SET balance = balance - 100
|
||||
WHERE id = 1 AND balance >= 100;
|
||||
UPDATE accounts SET balance = balance + 100
|
||||
WHERE id = 2;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* **隔离性**(`Isolation`):隔离性确保并发执行的事务之间不会相互影响。每个事务都感觉不到其他事务的存在。
|
||||
```sql
|
||||
-- 事务1
|
||||
START TRANSACTION;
|
||||
SELECT balance FROM accounts WHERE id = 1;
|
||||
-- 其他事务的修改不会影响这个查询结果
|
||||
COMMIT;
|
||||
|
||||
-- 事务2
|
||||
START TRANSACTION;
|
||||
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
* **持久性**(`Durability`):持久性确保一旦事务提交,其所做的修改就会永久保存到数据库中。
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
INSERT INTO logs (message) VALUES ('重要操作');
|
||||
COMMIT;
|
||||
-- 提交后,数据已经持久化到磁盘
|
||||
```
|
||||
|
||||
## 3.2 隔离级别
|
||||
### 3.2.1 概念
|
||||
* **脏读 (Dirty Read):** 事务A读取了事务B**尚未提交**的修改数据。如果事务B最终回滚,事务A读到的就是无效的"脏"数据。
|
||||
* **不可重复读 (Non-Repeatable Read):** 在同一个事务A中,**两次读取同一行数据**,得到的结果不同。这是因为在两次读取之间,该行数据被另一个提交了的事务B**修改**了。
|
||||
* **幻读 (Phantom Read):** 在同一个事务A中,**两次执行相同的查询**(通常是范围查询 `SELECT ... WHERE ...`),得到的结果集行数不同(出现了新的"幻影"行或原有行消失了)。这是因为在两次查询之间,另一个提交了的事务B**插入**了满足查询条件的新行或**删除**了原有的行。
|
||||
|
||||
### 3.2.2 类型
|
||||
1. 读未提交 (READ UNCOMMITTED)
|
||||
* 特点: 这是最低的隔离级别,允许读取尚未提交的数据变更。
|
||||
* 允许的问题:
|
||||
* **脏读:** 事务可以读取其他事务尚未提交的修改。
|
||||
* **不可重复读:** 可能发生。
|
||||
* **幻读:** 可能发生。
|
||||
* 并发性: **最高**。因为它几乎不加锁(或者锁持有时间非常短),事务之间等待最少。
|
||||
* 数据一致性: **最差**。读取的数据可能是临时的、无效的(如果其他事务回滚)或中间状态。
|
||||
* 使用场景: 非常罕见,通常仅在对数据准确性要求极低、需要极高吞吐量且能容忍脏数据的统计类场景(如实时大屏粗略计数)中考虑。**强烈不建议在要求数据准确性的业务中使用。**
|
||||
|
||||
2. 读已提交 (READ COMMITTED)
|
||||
* 特点: 允许读取并发事务已经提交的数据,这是许多数据库(如 Oracle, PostgreSQL)的默认隔离级别(但不是 MySQL 的默认)。
|
||||
* 允许的问题:
|
||||
* **脏读: ❌ 避免。** 事务只能读取其他事务**已经提交**的修改。
|
||||
* **不可重复读: ✔️ 可能发生。** 同一事务内多次读取同一行,结果可能不同(如果其他已提交事务修改了该行)。
|
||||
* **幻读: ✔️ 可能发生。** 同一事务内多次执行相同范围查询,结果集行数可能不同(如果其他已提交事务插入/删除了满足条件的行)。
|
||||
* 并发性: **较高**。避免了脏读带来的最基础问题,锁的持有时间通常比 `REPEATABLE READ` 短(行锁在语句执行后可能更快释放)。
|
||||
* 数据一致性: **较好**。保证了读取的数据是已提交的、有效的。但同一事务内的多次读取结果可能不一致。
|
||||
* 使用场景: 适用于大多数不需要在同一个事务内保证多次读取数据绝对一致的场景。例如,一个展示数据的列表页,每次查询都是独立的快照。
|
||||
|
||||
3. **可重复读 (REPEATABLE READ)**
|
||||
* 特点: 确保在同一事务中多次读取同一数据得到相同的结果。这是 **MySQL 的默认事务隔离级别**。通过 **MVCC (多版本并发控制)** 实现。
|
||||
* 允许的问题:
|
||||
* **脏读: ❌ 避免。**
|
||||
* **不可重复读: ❌ 避免。** 在同一事务内,**多次读取同一行**数据的结果保证是一致的(即使其他事务已提交修改)。MVCC 通过为事务提供**一致性视图 (Consistent Read View)** 来实现,该视图基于事务开始时的快照。
|
||||
* **幻读: ⚠️ 理论可能,但 MySQL InnoDB 很大程度上避免。** 这是关键点!标准的 SQL 定义中,`REPEATABLE READ` 允许幻读。但是,**MySQL 的 InnoDB 存储引擎通过 MVCC 和 Next-Key Locking(临键锁)的组合,在绝大多数情况下避免了幻读**。对于**快照读**(普通 `SELECT` 语句),MVCC 保证看到的是事务开始时的快照,因此不会看到新插入的行。对于**当前读**(`SELECT ... FOR UPDATE`, `SELECT ... LOCK IN SHARE MODE`, `UPDATE`, `DELETE`),Next-Key Locking 会锁定扫描到的索引范围,阻止其他事务在该范围内插入,从而避免幻读。
|
||||
* 并发性: **中等**。比 `READ COMMITTED` 稍低,因为锁(特别是 Next-Key Locks)可能持有更长时间,覆盖更大的范围(索引区间)。
|
||||
* 数据一致性: **好**。保证了事务内读取数据的稳定性(同一行可重复读),并通过机制有效防止了幻读,满足大多数应用的需求。
|
||||
* **使用场景:** MySQL 的默认选择,适用于绝大多数需要保证事务内数据读取一致性的场景,如订单处理、账户管理等。是兼顾一致性和并发性的良好平衡点。
|
||||
4. 串行化 (SERIALIZABLE)
|
||||
* 特点: 最高的隔离级别。它通过强制事务**串行执行**来实现,所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。
|
||||
* 允许的问题:
|
||||
* **脏读: ❌ 避免。**
|
||||
* **不可重复读: ❌ 避免。**
|
||||
* **幻读: ❌ 避免。**
|
||||
* **实现方式:** 简单理解,它会在读取的数据上自动加共享锁(`SELECT` 默认变成 `SELECT ... LOCK IN SHARE MODE`),在写入的数据上加排他锁。这些锁会持有到事务结束。这导致事务之间几乎完全串行化,读写相互阻塞非常严重。
|
||||
* 并发性: **最低**。性能开销巨大,吞吐量急剧下降。
|
||||
* 数据一致性: **最好**。完全保证事务的隔离性,不会出现任何并发问题。
|
||||
* 使用场景: 仅在对数据一致性要求极高,且完全不能接受任何并发副作用(如金融核心系统的某些极端操作),并且能承受极低并发性能的情况下使用。**实践中很少使用。**
|
||||
|
||||
## 3.3 MySQL锁
|
||||
* **表级锁:** MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
|
||||
* **行级锁:** MySQL 中锁定粒度最小的一种锁,是 **针对索引字段加的锁** ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
|
||||
* **共享锁(S 锁)** :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
|
||||
* **排他锁(X 锁)** :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
|
||||
|
||||
|SQL语句|默认锁类型|说明|
|
||||
| :-----------------------------: | :------------: | :----------------------------: |
|
||||
|SELECT|无锁(MVCC)|使用快照读取,不阻塞其他事务|
|
||||
|SELECT ... FOR UPDATE|排他锁(X)|阻塞其他事务修改这些行|
|
||||
|SELECT ... LOCK IN SHARE MODE|共享锁(S)|允许其他事务读取但阻塞修改|
|
||||
|INSERT|排他锁(X)|自动获取|
|
||||
|UPDATE|排他锁(X)|自动获取|
|
||||
|DELETE|排他锁(X)|自动获取|
|
||||
|
||||
# 四、日志
|
||||
  MySQL 的三大核心日志系统是保证数据一致性、实现故障恢复和提供复制功能的关键组件。这三大日志分别是:二进制日志(binlog)、错误日志(error log) 和 重做日志(redo log)。
|
||||
## 4.1 二进制日志(Binary Log,简称 binlog)
|
||||
### 4.1.1 概念
|
||||
  二进制日志是 MySQL 记录所有修改数据或可能修改数据的语句(或数据变更)的日志文件。它记录了数据库执行的更改操作(如 INSERT、UPDATE、DELETE 等 DML 操作,以及 CREATE、ALTER、DROP 等 DDL 操作),但**不记录 SELECT 这类不修改数据的查询操作**。
|
||||
|
||||
### 4.1.2 作用
|
||||
1. 主从复制(Replication):在主从架构中,主库将 binlog 发送给从库,从库通过读取并重放 binlog 来保持与主库的数据同步。
|
||||
2. 数据恢复(Point-in-Time Recovery):通过备份 + binlog 可以恢复到某个具体时间点。
|
||||
3. 审计:可以追踪数据库的所有变更操作。
|
||||
|
||||
## 4.2 重做日志(Redo Log)
|
||||
### 4.2.1 概念
|
||||
  重做日志是 InnoDB 存储引擎特有的日志,它记录的是 “物理级别” 上的页修改信息,主要用于 崩溃恢复(Crash Recovery)。
|
||||
  InnoDB 使用 WAL(Write-Ahead Logging,预写日志)技术,即 事务提交时,先写 redo log,再写磁盘数据页,从而保证即使系统崩溃,也能通过 redo log 恢复数据。
|
||||
|
||||
### 4.2.2 作用
|
||||
1. 实现事务的持久性(Durability):确保事务提交后,即使发生宕机,数据也不会丢失。
|
||||
2. 提高写入性能:数据不是每次修改都直接写磁盘,而是先写 redo log(顺序写,速度快),后续再异步刷盘。
|
||||
3. 支持 crash-safe:MySQL 宕机重启后,可通过 redo log 恢复未刷盘的数据。
|
||||
|
||||
## 4.3 回滚日志(Undo Log)
|
||||
### 4.3.1 概念
|
||||
  回滚日志也是 InnoDB 引擎特有 的日志,它记录的是 数据被修改前的原始值
|
||||
|
||||
### 4.3.2 作用
|
||||
1. 支持事务回滚:如果事务执行失败或调用了 ROLLBACK,可以通过 undo log 将数据恢复到修改之前的状态。
|
||||
2. 实现 MVCC(多版本并发控制):在读已提交(RC)、可重复读(RR)隔离级别下,undo log 用于提供历史版本数据,使得不同事务能看到不同的数据快照,而不需要加锁。
|
||||
|
||||
## 4.4 和事务、锁之间的关系
|
||||
  例如,当执行一条SQL语句时,事务、锁、日志等之间的关系:
|
||||
1. 发起一个事务
|
||||
2. 执行一系列的DML操作(如 INSERT/UPDATE/DELETE)
|
||||
  **涉及 锁:对操作的数据行或表加锁,防止其他事务干扰。**
|
||||
  **涉及 隔离级别:决定其他事务是否能“看到”你未提交的数据。**
|
||||
  **涉及 undo log:如果事务回滚,可以根据 undo log 恢复旧值。**
|
||||
3. 事务提交(commit) 或 回滚(rollback)
|
||||
  **redo log:保证即使宕机,已提交事务的修改也不丢失(持久性)。**
|
||||
  **binlog:记录数据变更,用于主从复制与时间点恢复。**
|
||||
  **undo log:用于实现事务回滚、MVCC。**
|
||||
4. **背后有日志系统默默记录一切,锁系统保障并发安全,隔离级别定义了“你能看到啥”。**
|
||||
|
||||
### 4.4.1 相互关系
|
||||
1. 事务是锁的使用者,锁是事务实现隔离性的手段。当一个事务对某行数据执行 UPDATE/DELETE 操作时,为了防止其他事务同时修改相同数据,InnoDB 会自动对该行或索引加 排他锁(X锁)。如果事务只是读取数据,根据隔离级别,可能会加 共享锁(S锁) 或使用 MVCC(不加锁)。
|
||||
2. 隔离级别定义了事务之间的可见性规则,是事务“隔离性”的具体体现。事务的隔离性是通过锁 + MVCC(多版本并发控制,依赖 undo log)+ 隔离级别共同实现的。
|
||||
3. 日志是事务实现 持久性、崩溃恢复、回滚 等能力的基石。redo log(重做日志)保障事务的持久性(Durability),事务提交时,先将数据页的变更记录到 redo log(顺序写,高性能),随后再异步刷盘到磁盘数据页。即使系统崩溃,重启后也能通过 redo log 恢复已提交但尚未刷盘的数据。undo log(回滚日志)支持事务回滚 和 MVCC。事务修改数据前,会先把原始数据保存到 undo log,如果事务回滚,可以用它恢复旧值。同时,undo log 也是 MVCC(多版本控制)实现的基础,用于提供历史版本数据。binlog(二进制日志)用于主从复制、时间点恢复。虽然 binlog 是 Server 层的日志,不属于 InnoDB 事务引擎的一部分,但在事务提交时,binlog 与 redo log 通过两阶段提交(2PC)保证一致性。
|
||||
4. 隔离级别决定了锁的粒度和行为,锁是隔离级别的底层实现机制之一。在 READ UNCOMMITTED 下,一般不加锁(脏读允许)。在 READ COMMITTED 和 REPEATABLE READ 下,InnoDB 通过 行锁 + MVCC 实现隔离,不一定非得阻塞其他事务。在 SERIALIZABLE 下,会自动为读操作也加上共享锁,相当于所有操作串行执行,隔离性最强,但并发性能最低。
|
||||
5. 锁保障并发安全,日志保障操作可恢复。两者从不同维度确保数据库的正确性。锁:是在运行时控制谁可以访问哪些数据,是 并发控制 的手段。日志:是在磁盘上记录操作过程,是 故障恢复 & 事务一致性 的手段。
|
||||
6. 隔离级别定义了事务间数据的可见性,而日志(尤其是 undo log 和 binlog)为这种“可见性”提供了实现基础。undo log 是实现 MVCC(多版本并发控制) 的基础,而 MVCC 是 REPEATABLE READ 等隔离级别的关键。binlog 虽不直接影响隔离性,但它记录了事务的变更历史,是构建主从环境、实现数据恢复的基础。
|
||||
|
||||
# 五、优化
|
||||
## 5.1 Explain 执行计划
|
||||
  `EXPLAIN`是 MySQL 自带的一个诊断工具,它可以模拟 MySQL 查询优化器的执行过程,对`SELECT`语句(在 MySQL 8.0 及以上版本,也支持对`UPDATE`、`DELETE`等语句使用)进行分析,并输出该语句的执行计划。
|
||||
### 5.1.1 基本用法
|
||||
  在select语句前面加上EXPLAIN关键字即可,例如:`EXPLAIN SELECT * FROM users WHERE age > 25;
|
||||
`
|
||||
|
||||
### 5.1.2 输出列说明
|
||||
| 列名 | 说明 | 示例 |
|
||||
| :-: | :-: | :-: |
|
||||
| id | 查询标识符 | 相同 id 表示同组查询,执行顺序从上到下;不同 id 值越大优先级越高 |
|
||||
| select_type | 查询类型 | SIMPLE(无子查询)、PRIMARY(外层查询)、SUBQUERY(子查询) |
|
||||
| table | 访问的表名 | |
|
||||
| partitions | 匹配的分区 | |
|
||||
| **type** | 访问类型 | system > const > eq_ref > ref > range > index > ALL(性能核心指标,从优到劣排序) |
|
||||
| possible_keys | 可能使用的索引 | |
|
||||
| **key** | 实际使用的索引 | |
|
||||
| key_len | 索引使用的字节数 | |
|
||||
| ref | 索引匹配的列或常量 | |
|
||||
| **rows** | 预估扫描行数 | 越小越好 |
|
||||
| filtered | 存储引擎返回数据后在 server 层过滤的比例 | |
|
||||
| **Extra** | 额外执行信息 | |
|
||||
|
||||
### 5.1.3 重要指标:Type
|
||||
| 类型 | 描述 | 性能 | 示例 |
|
||||
| - | - | - | - |
|
||||
| system | 系统表,仅一行 | 最优 | MyISAM 引擎的空表 |
|
||||
| const | 主键/唯一索引的常量查询 | 极优 | WHERE id = 1 |
|
||||
| eq_ref | JOIN 时主键/唯一索引关联 | 优 | JOIN ... ON t1.pk = t2.pk |
|
||||
| ref | 非唯一索引的等值查询 | 良 | WHERE index_col = 10 |
|
||||
| fulltext | 全文索引 | 中 | MATCH(...) AGAINST(...) |
|
||||
| ref_or_null | ref + NULL 值搜索 | 中 | WHERE col = 10 OR col IS NULL |
|
||||
| index_merge | 索引合并优化 | 中 | 多个索引条件组合 |
|
||||
| unique_subquery | 唯一索引子查询 | 中 | value IN (SELECT pk FROM ...) |
|
||||
| index_subquery | 非唯一索引子查询 | 中下 | value IN (SELECT index_col FROM ...) |
|
||||
| range |索引范围扫描 | 中下 | WHERE id > 10 |
|
||||
| index | 全索引扫描 | 差 | SELECT indexed_col FROM table |
|
||||
| ALL | 全表扫描 | 最差 | 无索引查询|
|
||||
|
||||
### 5.1.4 重要指标:Extra
|
||||
| 值 | 含义 | 优化建议 |
|
||||
| - | - | - |
|
||||
| Using index | 覆盖索引(无需回表) | 优,保持 |
|
||||
| Using where | Server 层过滤数据 | 检查索引使用 |
|
||||
| Using temporary | 使用临时表 | 优化 GROUP BY/ORDER BY |
|
||||
| Using filesort | 额外排序操作 | 为排序字段加索引 |
|
||||
| Select tables optimized away | 使用聚合函数优化 | 优 |
|
||||
| Using index condition | 索引条件下推(ICP) | MySQL 5.6+ 优化特性 |
|
||||
| Using join buffer | 使用连接缓冲区 | 增大 join_buffer_size |
|
||||
| Impossible WHERE | WHERE 条件永不成立 | 查询逻辑错误 |
|
||||
| Distinct | 优化 DISTINCT 操作 | |
|
||||
|
||||
# 六、执行过程
|
||||

|
||||
  MySQL架构分为两层,Service层和存储引擎层。
|
||||
  Server 层负责建立连接、分析和执行 SQL。存储引擎层负责数据的存储和提取。
|
||||
1. 连接器:建立连接、管理连接、校验用户身份。
|
||||
2. 查询缓存(MySQL8.0已删除)
|
||||
3. 解析SQL,通过解析器对SQL查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段和语句类型等。
|
||||
4. 执行SQL:分为预处理阶段、优化阶段和执行阶段。
|
||||
|
||||
# 七、视图
|
||||
## 7.1 定义
|
||||
  视图(View)是 MySQL 中的一种虚拟表,它的数据来源于一个或多个实际的表(基表),**其结构和数据是通过 SQL 查询语句定义的**。简单来说,视图就像是一个 “查询窗口”,你看到的是基表的数据,但**视图本身并不存储实际数据,每次访问视图时都会执行对应的查询语句**。
|
||||
|
||||
## 7.2 作用
|
||||
1. 简化复杂查询:将多表关联、聚合等复杂的 SQL 逻辑封装到视图中,后续使用时只需查询视图即可。
|
||||
2. 数据安全:可以只暴露基表中的部分列或部分行给用户,隐藏敏感数据。
|
||||
3. 数据一致性:如果业务逻辑发生变化,只需修改视图的定义,而无需修改所有使用该逻辑的查询。
|
||||
|
||||
## 7.3 使用
|
||||
  创建视图
|
||||
```sql
|
||||
-- 创建视图v_student_basic
|
||||
CREATE VIEW v_student_basic
|
||||
AS
|
||||
SELECT name, age, gender FROM student;
|
||||
```
|
||||
|
||||
  使用视图和正常使用数据表一致:
|
||||
```sql
|
||||
-- 查询视图(和查询普通表的方式完全一样)
|
||||
SELECT * FROM v_student_basic;
|
||||
```
|
||||
|
||||
# 八、事件
|
||||
## 8.1 定义
|
||||
  MySQL 事件(Event)也被称为事件调度器(Event Scheduler),是 `MySQL 中一种定时执行的数据库对象`,可以理解为数据库层面的 “定时任务” 或 “计划任务”。它能根据你设定的时间规则(一次性执行、周期性执行),自动触发并执行指定的 SQL 逻辑(如数据清理、统计报表生成、数据同步等)。
|
||||
|
||||
## 8.2 特点
|
||||
1. 定时执行:支持一次性执行(如某个具体时间点)和周期性执行(如每天凌晨 3 点、每小时执行一次)。
|
||||
2. 自动运行:依赖 MySQL 的事件调度器线程,只要调度器开启,事件就会按规则自动触发。
|
||||
3. 与存储过程结合:事件的执行逻辑可以是单条 SQL,也可以是复杂的存储过程(推荐用存储过程封装复杂逻辑)。
|
||||
|
||||
## 8.3 作用
|
||||
- 数据清理:定期删除过期数据(如删除 30 天前的日志表数据)。
|
||||
- 数据统计:定时生成业务统计报表(如每天凌晨统计前一天的订单数据)。
|
||||
- 数据同步:定期将 A 表的数据同步到 B 表。
|
||||
- 定时备份:定期执行数据库备份脚本(配合存储过程)。
|
||||
|
||||
## 8.4 使用
|
||||
  开启事件调度器:
|
||||
```sql
|
||||
-- 查看事件调度器状态(ON表示开启,OFF表示关闭)
|
||||
SHOW VARIABLES LIKE 'event_scheduler';
|
||||
|
||||
-- 临时开启(MySQL重启后会恢复为默认状态)
|
||||
SET GLOBAL event_scheduler = ON;
|
||||
-- 永久开启(需要修改my.cnf/my.ini配置文件,添加以下内容,然后重启MySQL)
|
||||
event_scheduler = ON # 放在[mysqld]节点下
|
||||
```
|
||||
|
||||
  每天凌晨 1 点执行的事件,删除日志表中 7 天前的过期数据:
|
||||
```sql
|
||||
-- 创建周期性事件,每天凌晨1点执行,无结束时间
|
||||
CREATE EVENT IF NOT EXISTS event_clear_log
|
||||
ON SCHEDULE EVERY 1 DAY STARTS '2025-12-22 01:00:00'
|
||||
COMMENT '每天凌晨1点删除7天前的日志'
|
||||
DO
|
||||
DELETE FROM log WHERE create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);
|
||||
```
|
||||
41
docs/Web-Backend/MySQL/SQL-Advance.md
Normal file
41
docs/Web-Backend/MySQL/SQL-Advance.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: SQL高阶用法
|
||||
date: 2025-12-21
|
||||
---
|
||||
|
||||
# 一、WITH
|
||||
## 1.1 定义
|
||||
  SQL 中的WITH子句也被称为**公用表表达式**(CTE,Common Table Expression),它的作用是**在执行主查询之前,先定义一个临时的结果集,这个结果集可以在后续的查询中被多次引用**,就像一个临时表一样。它能让复杂的 SQL 查询变得更清晰、更易读,还能简化嵌套查询的逻辑。
|
||||
|
||||
## 1.2 使用
|
||||
```sql
|
||||
-- 定义第一个CTE:数学高分学生
|
||||
WITH math_high_score AS (
|
||||
SELECT name, score FROM student_score WHERE subject = '数学' AND score > 85
|
||||
),
|
||||
-- 定义第二个CTE:语文高分学生
|
||||
chinese_high_score AS (
|
||||
SELECT name, score FROM student_score WHERE subject = '语文' AND score > 85
|
||||
)
|
||||
-- 主查询:查询既在数学高分又在语文高分的学生
|
||||
SELECT m.name
|
||||
FROM math_high_score m
|
||||
JOIN chinese_high_score c ON m.name = c.name;
|
||||
```
|
||||
|
||||
  递归 CTE:
|
||||
```sql
|
||||
WITH recursive dept_hierarchy AS (
|
||||
-- 锚点成员:查询顶级部门(parent_id为NULL)
|
||||
SELECT dept_id, dept_name, parent_id, 1 AS level
|
||||
FROM department
|
||||
WHERE parent_id IS NULL
|
||||
UNION ALL
|
||||
-- 递归成员:查询子部门,关联自身的dept_id和parent_id
|
||||
SELECT d.dept_id, d.dept_name, d.parent_id, dh.level + 1 AS level
|
||||
FROM department d
|
||||
JOIN dept_hierarchy dh ON d.parent_id = dh.dept_id
|
||||
)
|
||||
-- 主查询:获取所有部门的层级
|
||||
SELECT * FROM dept_hierarchy;
|
||||
```
|
||||
Reference in New Issue
Block a user