diff --git a/Database.md b/Database.md index 9308a2d..9a88cb5 100644 --- a/Database.md +++ b/Database.md @@ -346,9 +346,9 @@ ON A.PK = B.PK; -## 全连接(FULL OUTER JOIN) +## 全连接(FULL JOIN) -FULL OUTER JOIN 一般被译作外连接、全连接,实际查询语句中可以写作 FULL OUTER JOIN 或 FULL JOIN。外连接查询能返回左右表里的所有记录,其中左右表里能关联起来的记录被连接后返回。(**MySQL不支持FULL OUTER JOIN**) +FULL JOIN 一般被译作外连接、全连接,实际查询语句中可以写作 FULL OUTER JOIN 或 FULL JOIN。外连接查询能返回左右表里的所有记录,其中左右表里能关联起来的记录被连接后返回。(**MySQL不支持FULL OUTER JOIN**) **文氏图** @@ -461,7 +461,7 @@ WHERE A.PK IS NULL; -## FULL OUTER JOIN EXCLUDING INNER JOIN +## FULL JOIN EXCLUDING INNER JOIN 返回左表和右表里没有相互关联的记录集。 @@ -610,167 +610,204 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID; **原子性(Atomicity)** -事务包含的操作要么全部成功,要么全部失败回滚。 +事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做。 **一致性(Consistency)** +事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。 + 一个事务执行前后,应该使数据库从一个一致性状态转换为另一个一致性状态。比方说假设A、B两个人,共有5000元。那么无论A给B转多少钱,转多少次,总数仍然是5000没有改变。 **隔离性(Isolation)** -多个用户并发访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务,不能被其他事务的操作干扰。多个并发事务间需要隔离。 +一个事务的执行不能受到其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 **持久性(Durability)** -一旦一个事务被提交,对数据库中的数据改变是永久的,即使数据库系统故障,也不会丢失提交事务操作。 +指一个事务一旦提交,它对数据库中数据的改变就是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。 ## 隔离级别 -Oracle数据库中,仅有Serializable(串行化)和Read Committed(读已提交)两种隔离方式,默认选择读已提交的方式。不做隔离操作则会出现: +数据库事务隔离级别有4种,由低到高为:**Read uncommitted** 、**Read committed** 、**Repeatable read** 、**Serializable** 。而且,在事务的并发操作中可能会出现 **脏读、不可重复读、幻读** 问题。不做隔离操作则会出现: -- **脏读**:读到未提交更新的数据 +- **脏读**:事务A中读到了事务B中未提交的更新数据内容 +- **不可重复读**:读到其它事务已经提交后的**更新**数据,即一个事务范围内两个相同的查询却返回了不同数据 +- **幻读**:事物A执行select后,事物B**增或删**了一条数据,事务A再执行同一条SQL后发现多或少了一条数据 - **第一类丢失更新**:A事务撤销时,把已经提交的B事务的更新数据覆盖了 - **第二类丢失更新**:A事务提交时,把已经提交的B事务的更新数据覆盖了 -- **不可重复读**:读到已经提交**更新**的数据,但一个事务范围内两个相同的查询却返回了不同数据 -- **幻读**: 事物A在用一个表,此时事物B在表中**增加或删除**了一条数据,A发现多了/少了一条数据,即为幻读 + + + +**默认隔离级别** + +- Oracle仅有Serializable(串行化)和Read Committed(读已提交)两种隔离方式,默认选择**读已提交**的方式 +- MySQL默认为**Repeatable Read(可重读)** **InnoDB存储引擎下**的四种隔离级别发生问题的可能性如下: -| 隔离级别 | 第一类丢失更新 | 第二类丢失更新 | 脏读 | 不可重复读 | 幻读 | -| ---------------------------- | -------------- | -------------- | ---- | ---------- | ---- | -| SERIALIZABLE (串行化) | × | × | × | × | × | -| REPEATABLE READ(可重复读) | × | × | × | × | √ | -| READ COMMITTED (读已提交) | × | √ | × | √ | √ | -| READ UNCOMMITTED(读未提交) | × | √ | √ | √ | √ | +| 隔离级别 | 第一类丢失更新 | 第二类丢失更新 | 脏读 | 不可重复读 | 幻读 | +| -------------------------- | -------------- | -------------- | -------- | ---------- | -------- | +| Read Uncommitted(读未提交) | 不可能 | **可能** | **可能** | **可能** | **可能** | +| Read Committed(读已提交) | 不可能 | **可能** | 不可能 | **可能** | **可能** | +| Repeatable Read(可重复读) | 不可能 | 不可能 | 不可能 | 不可能 | **可能** | +| Serializable(串行化) | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 | +### Read Uncommitted(读未提交) +**即读取到了其它事务未提交的内容**。在该隔离级别,**所有事务都可以看到其他未提交事务的执行结果**。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为**脏读(Dirty Read)**。 -### Serializable(串行化) +**特点**:最低级别,任何情况都无法保证 -指一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。 +**读未提交的数据库锁情况** +- 事务中读取数据:**未加锁** +- 事务中更新数据:**只对数据增加行级共享锁** -**特点**:避免脏读、不可重复读、幻读 -**可序列化的数据库锁情况** +### Read Committed(读已提交) -- 事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放 -- 事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放 +**即读取到了其它事务已提交的内容**。一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的**不可重复读(Nonrepeatable Read)**,因为同一事务的其他实例在该实例处理期间可能会有新的commit,所以同一select可能返回不同结果。 +**特点**:避免脏读 +**读已提交的数据库锁情况** -### Repeatable Read(可重复读) +- 事务中读取数据:**加行级共享锁(读到时才加锁),读完后立即释放** +- 事务中更新数据:**在更新时的瞬间对其加行级排它锁,直到事务结束才释放** -由于提交读隔离级别会产生不可重复读的读现象,所以比提交读更高一个级别的隔离级别就可以解决不可重复读的问题,这种隔离级别就叫可重复读(Repeatable reads) +**Read Committed隔离级别下的加锁分析** -**特点**:MYSQL默认选择为可重复读。避免脏读、不可重复读 +隔离级别的实现与锁机制密不可分,所以需要引入锁的概念,首先我们看下InnoDB存储引擎提供的两种标准的行级锁: -**可重复读的数据库锁情况** +- **共享锁(S Lock)**:又称为读锁,可以允许多个事务并发的读取同一资源,互不干扰。即如果一个事务T对数据A加上共享锁后,其他事务只能对A再加共享锁,不能再加排他锁,只能读数据,不能修改数据 +- **排他锁(X Lock)**: 又称为写锁,如果事务T对数据A加上排他锁后,其他事务不能再对A加上任何类型的锁,获取排他锁的事务既能读数据,也能修改数据 -- 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放 -- 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放 +**注意**: 共享锁和排他锁是不相容的。 -### Read Committed(读已提交) +### Repeatable Read(可重复读) -提交读(Read committed)也可以翻译成“读已提交”,通过名字也可以分析出,在一个事务修改数据过程中,如果事务还没提交,其他事务不能读该数据。 +**它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行**。但会导致**幻读 (Phantom Read)**问题。 +**幻读** 是户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当该用户再读取该范围的数据行时,会发现有新的“幻影” 行。 +**特点**:避免脏读、不可重复读。MySQL默认事务隔离级别 -**特点**:避免脏读 +**可重复读的数据库锁情况** -**提交读的数据库锁情况** +- 事务中读取数据:**开始读取的瞬间对其增加行级共享锁,直到事务结束才释放** +- 事务中更新数据:**开始更新的瞬间对其增加行级排他锁,直到事务结束才释放** -- 事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁 -- 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放 +### Serializable(可串行化) -**Read Committed隔离级别下的加锁分析** +**指一个事务在执行过程中完全看不到其他事务对数据库所做的更新**。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。 -隔离级别的实现与锁机制密不可分,所以需要引入锁的概念,首先我们看下InnoDB存储引擎提供的两种标准的行级锁: +**特点**:避免脏读、不可重复读、幻读 -- **共享锁(S Lock)**:又称为读锁,可以允许多个事务并发的读取同一资源,互不干扰。即如果一个事务T对数据A加上共享锁后,其他事务只能对A再加共享锁,不能再加排他锁,只能读数据,不能修改数据 -- **排他锁(X Lock)**: 又称为写锁,如果事务T对数据A加上排他锁后,其他事务不能再对A加上任何类型的锁,获取排他锁的事务既能读数据,也能修改数据 +**可序列化的数据库锁情况** -**注意**: 共享锁和排他锁是不相容的。 +- 事务中读取数据:**先对其加表级共享锁 ,直到事务结束才释放** +- 事务中更新数据:**先对其加表级排他锁 ,直到事务结束才释放** -### Read uncommitted(读未提交) +## SpringBoot Transaction -未提交读(Read uncommitted)是最低的隔离级别。通过名字咱们就可以知道,在这种事务隔离级别下,一个事务可以读到另外一个事务未提交的数据。 +查看 `mysql` 事务隔离级别:`show variables like 'tx_iso%';`。 +### 实现方式 +在Spring中事务有两种实现方式: -**特点**:最低级别,任何情况都无法保证 +- **编程式事务管理**: 编程式事务管理使用`TransactionTemplate`或直接使用底层的`PlatformTransactionManager` +- **声明式事务管理**: 建立在`AOP`之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理不需要入侵代码,通过`@Transactional`就可以进行事务操作,更快捷而且简单 -**未提交读的数据库锁情况** -- 事务在读数据的时候并未对数据加锁 -- 事务在修改数据的时候只对数据增加行级共享锁 +### 提交方式 +**默认情况下,数据库处于自动提交模式**。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。 +对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring会将底层连接的自动提交特性设置为false。也就是在使用spring进行事物管理的时候,spring会将是否自动提交设置为false,等价于JDBC中的 `connection.setAutoCommit(false);`,在执行完之后在进行提交,`connection.commit();` 。 -## SpringBoot Transaction -查看 `mysql` 事务隔离级别:`show variables like 'tx_iso%';`。 +### 事务隔离级别 +隔离级别是指若干个并发的事务之间的隔离程度。 -### 事务管理方式 +```java +@Transactional(isolation = Isolation.READ_UNCOMMITTED) +public void addGoods(){ + ...... +} +``` -在Spring中,事务有两种实现方式,分别是编程式事务管理和声明式事务管理两种方式。 +枚举类Isolation中定义了五种隔离级别: -- **编程式事务管理**: 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate -- **声明式事务管理**: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理不需要入侵代码,通过@Transactional就可以进行事务操作,更快捷而且简单,推荐使用 +- `DEFAULT`:默认值。表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是**READ_COMMITTED** +- `READ_UNCOMMITTED`:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别 +- `READ_COMMITTED`:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 +- `REPEATABLE_READ`:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读 +- `SERIALIZABLE`:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 -### 事务提交方式 +### 事务传播行为 -默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。 -对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring会将底层连接的自动提交特性设置为false。也就是在使用spring进行事物管理的时候,spring会将是否自动提交设置为false,等价于JDBC中的 `connection.setAutoCommit(false);`,在执行完之后在进行提交,`connection.commit();` 。 +事务的传播性一般用在事务嵌套的场景,如一个事务方法里面调用了另外一个事务方法,那两个方法是各自作为独立的方法提交还是内层事务合并到外层事务一起提交,这就需要事务传播机制配置来确定怎么样执行。 +```java +@Transactional(propagation=Propagation.REQUIRED) +public void addGoods(){ + ...... +} +``` +枚举类Propagation中定义了七种事务传播机制如下: -### 事务隔离级别 +- `REQUIRED`(required) -隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量: + Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行 -- **TransactionDefinition.ISOLATION_DEFAULT**:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED -- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED**:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别 -- **TransactionDefinition.ISOLATION_READ_COMMITTED**:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 -- **TransactionDefinition.ISOLATION_REPEATABLE_READ**:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读 -- **TransactionDefinition.ISOLATION_SERIALIZABLE**:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 +- `REQUIRES_NEW`(requires_new,新创建事务) + 该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可 +- `SUPPORTS`(supports) -### 事务传播行为 + 如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务 + +- `NOT_SUPPORTED`(not_supported,传播机制不支持事务) + + 该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码 + +- `NEVER`(never) + + 该传播机制不支持外层事务,即如果外层有事务就抛出异常 -所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量: +- `MANDATORY`(mandatory) -- **TransactionDefinition.PROPAGATION_REQUIRED**:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值 -- **TransactionDefinition.PROPAGATION_REQUIRES_NEW**:创建一个新的事务,如果当前存在事务,则把当前事务挂起 -- **TransactionDefinition.PROPAGATION_SUPPORTS**:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 -- **TransactionDefinition.PROPAGATION_NOT_SUPPORTED**:以非事务方式运行,如果当前存在事务,则把当前事务挂起 -- **TransactionDefinition.PROPAGATION_NEVER**:以非事务方式运行,如果当前存在事务,则抛出异常。 -- **TransactionDefinition.PROPAGATION_MANDATORY**:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 -- **TransactionDefinition.PROPAGATION_NESTED**:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED + 与NEVER相反,如果外层没有事务,则抛出异常 + +- `NESTED`(nested,嵌套事务) + + 该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。 @@ -784,14 +821,37 @@ Oracle数据库中,仅有Serializable(串行化)和Read Committed(读已 ### 事务常用配置 -- **readOnly**:该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true) -- **rollbackFor**: 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class}) -- **rollbackForClassName**: 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”}) -- **noRollbackFor**:该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) -- **noRollbackForClassName**:该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”}) -- **propagation** : 该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true) -- **isolation**:该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置 -- **timeout**:该属性用于设置事务的超时秒数,默认值为-1表示永不超时 +- **readOnly** + + 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true) + +- **rollbackFor** + + 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class}) + +- **rollbackForClassName** + + 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”}) + +- **noRollbackFor** + + 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) + +- **noRollbackForClassName** + + 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”}) + +- **propagation** + + 该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true) + +- **isolation** + + 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置 + +- **timeout** + + 该属性用于设置事务的超时秒数,默认值为-1表示永不超时 @@ -1288,62 +1348,132 @@ alter table `表名` drop index 索引名; -## 索引规范 +## 索引优缺点 + +### 索引优点 + +- 提高数据检索的效率,降低检索过程中必须要读取得数据量,降低数据库IO成本 +- 降低数据库的排序成本。因为索引就是对字段数据进行排序后存储的,如果待排序的字段与索引键字段一致,就在取出数据后不用再次排序了,因为通过索引取得的数据已满足排序要求。另外,分组操作是先排序后分组,所以索引同样可以省略分组的排序操作,降低内存与CPU资源的消耗 + -- **【强制】业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。** - 说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明 显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必 然有脏数据产生。 +### 索引缺点 -- **【强制】超过三个表禁止join。join字段数据类型必须绝对一致;多表关联查询时, 保证被关联字段需要有索引。** +- 索引会增加 增、删、改操作所带来的IO量与调整索引的计算量 +- 索引要占用空间,随着数据量的不断增大,索引还会带来存储空间的消耗 - 说明:即使双表 join 也要注意表索引、SQL 性能。 -- 【**强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度即可。** - 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分 度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度 来确定。 +## 失效场景 -- **【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。** +**场景一:where语句中包含or时,可能会导致索引失效** - 说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索 引。 +使用or并不是一定会使索引失效,你需要看or左右两边的查询列是否命中相同的索引。 + +```sql +-- 假设user表中的user_id列有索引,age列没有索引 +-- 能命中索引 +select * from user where user_id = 1 or user_id = 2; +-- 无法命中索引 +select * from user where user_id = 1 or age = 20; +-- 假设age列也有索引的话,依然是无法命中索引的 +select * from user where user_id = 1 or age = 20; +``` + +可以根据情况尽量使用union all或者in来代替,这两个语句的执行效率也比or好些。 -- **【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合 索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。** - 正例:where a=? and b=? order by c; 索引:a_b_c - 反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。 +**场景二:where语句中索引列使用了负向查询,可能会导致索引失效** -- **【推荐】利用覆盖索引来进行查询操作,避免回表。** +负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。其实负向查询并不绝对会索引失效,这要看MySQL优化器的判断,全表扫描或者走索引哪个成本低了。 - 说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览 一下就好,这个目录就是起到覆盖索引的作用。 正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查 询的一种效果,用 explain 的结果,extra 列会出现:using index。 -- **【推荐】利用延迟关联或者子查询优化超多分页场景。** - 说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过 特定阈值的页数进行 SQL 改写。 正例:先快速定位需要获取的 id 段,然后再关联: SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id +**场景三:索引字段可以为null,使用is null或is not null时,可能会导致索引失效** -- **【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。** +其实单个索引字段,使用is null或is not null时,是可以命中索引的。 - 说明: - 1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据 - 2)ref 指的是使用普通的索引(normal index) +**场景四:在索引列上使用内置函数,一定会导致索引失效** - 3)range 对索引进行范围检索 +比如下面语句中索引列login_time上使用了函数,会索引失效: + +```sql +select * from user where DATE_ADD(login_time, INTERVAL 1 DAY) = 7; +``` - 反例:explain 表的结果,type=index,索引物理文件全扫描,速度非常慢,这个 index 级 别比较 range 还低,与全表扫描是小巫见大巫。 -- **【推荐】建组合索引的时候,区分度最高的在最左边。** - 正例:如果 where a=? and b=? ,如果 a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。 +**场景五:隐式类型转换导致的索引失效** + +如下面语句中索引列user_id为varchar类型,不会命中索引: + +```mysql +select * from user where user_id = 12; +``` + + + +**场景六:对索引列进行运算,一定会导致索引失效** + +运算如+,-,\*,/等,如下: + +```mysql +select * from user where age - 1 = 10; +``` + +优化的话,要把运算放在值上,或者在应用程序中直接算好,比如: + +```sql +select * from user where age = 10 - 1; +``` + + + +**场景七:like通配符可能会导致索引失效** + +like查询以%开头时,会导致索引失效。解决办法有两种: + +- 将%移到后面,如: + +```sql +select * from user where `name` like '李%'; +``` + +- 利用覆盖索引来命中索引: + +```sql +select name from user where `name` like '%李%'; +``` + + + +**场景八:联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效** + +当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。比如下面的语句就不会命中索引: + +```sql +select * from t where k2=2; +select * from t where k3=3; +select * from t where k2=2 and k3=3; +``` + +下面的语句只会命中索引(k1): + +```sql +select * from t where k1=1 and k3=3; +``` + - 说明:存在非等号和等号混合时,在建索引时,请把等号条件的列前置。如:where c>? and d=? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即索引 idx_d_c。 -- **【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。** +## 优化建议 -- **【参考】创建索引时避免有如下极端误解:** - - **认为一个查询就需要建一个索引** - - **认为索引会消耗空间、严重拖慢更新和新增速度** - - **认为业务的惟一性一律需要在应用层通过“先查后插”方式解决** +- **禁止在更新十分频繁、区分度不高的属性上建立索引** + - 更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能。 + - “性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似。 +- **建立组合索引,必须把区分度高的字段放在前面**