MySQL之select原理,UUID主键分析,插入或更新
1 MySQL查询
MySQL
作为互联网行业使用最多的关系型数据库之一,与其免费、开源的特性是密不可分的。然而,很多小伙伴工作了很多年,只知道使用 MySQL
进行 CRUD
操作,这也导致很多小伙伴工作多年后,想跳槽进入大厂,却在面试的时候屡屡碰壁。
问个简单的问题:select 语句是如何在 MySQL 中执行的? 这也是很多面试官喜欢问的问题,如果你连这个简单的问题都不能回答的话,那就要好好规划下自己的职业生涯了。
好了,今天我们就一起来聊聊 select 语句是如何在 MySQL 中执行的
1.1 sql语句的执行顺序
FROM
<left_table>
ON
<join_condition>
<join_type>
JOIN
<right_table>
WHERE
<where_condition>
GROUP BY
<group_by_list>
HAVING
<having_condition>
SELECT
DISTINCT
<select_list>
ORDER BY
<order_by_condition>
LIMIT
<limit_number>
1.2 频繁使用的select语句
为了更好地贯穿全文,这里先来列举一个最简单的 select
查询语句,例如:查询 user
表中 id
为 1001
的用户信息,使用下面的 SQL 语句进行查询。
select * from user where user_id = 1001;
当我们在 MySQL
的命令行中输入上述 SQL
语句时,这条 SQL
语句到底在 MySQL
中是如何执行的呢?接下来,我们就以这条 SQL
语句为例,说说 select 语句是如何在 MySQL
中执行的。
1.3 MySQL 逻辑架构
在介绍 select
语句在 MySQL
中的执行流程之前,我们先来看看 MySQL
的逻辑架构,因为任何 SQL
语句的执行都离不开 MySQL
逻辑架构的支撑。也就是说, SQL
语句在 MySQL
中的执行流程与 MySQL
的逻辑架构是密不可分的
在上图中,我们简单地画了下 MySQL
的逻辑架构图,并且给出了逻辑分层和每层中各部分的功能。从逻辑上,我们可以将 MySQL
粗略地分成三层:Server 层
、存储引擎层
和系统文件层
,而 Server
层中又可以分成网络连接层(连接器)
和数据服务层(Server 层)
Server 层
中包含了连接器、查询缓存、分析器、优化器和执行器等 MySQL 的核心组成部分,另外,在 Server 层
中还包含了所有的内置函数(比如:日期时间函数、加解密函数、聚合函数、数学函数等),存储引擎、触发器、视图等等。
存储引擎层主要负责和系统文件层进行交互,存储引擎层本身是插件式的架构设计,支持 InnoDB、MyISAM、Archive、Memory 等存储引擎。在 MySQL 5.5.5 及以后的版本中,MySQL 的默认存储引擎是 InnoDB。
系统文件层主要负责存储实际的数据,将数据以文件的形式存储到服务器的磁盘上。
接下来,我们就来说说一条 select 语句在 MySQL 的逻辑架构的每一部分到底是如何执行的
1.4 连接器是如何授权的
首先,我们先来看看在服务器命令行输入连接 MySQL
的命令时,MySQL
的连接器是如何进行验证的。比如,我们在服务器的命令行输入了如下命令
mysql -ubinghe -p
执行“回车”后,输入 binghe
账户的密码,与 MySQL
进行连接。此时,连接的过程需要完成经典的 TCP
握手操作。之后,连接器就开始认证连接的身份是否合法,最直接的就是验证用户名和密码是否正确。
如果用户名或者密码错误,MySQL
会提示 Access denied for user
。如果用户名和密码正确,则连接器会到 MySQL 的权限表中查询当前连接拥有的权限。查询到权限之后,只要这个连接没有断开,则这个连接涉及到的权限操作都会依赖此时查询到的权限。
换句话说,一个用户登录 MySQL
并成功连接 MySQL
后,哪怕是管理员对当前用户的权限进行了修改操作,此时只要这个用户没有断开 MySQL
的连接,就不会受到管理修改权限的影响。管理员修改权限后,只有对新建的连接起作用。
如果客户端连接 MySQL
后,长时间没有执行任何操作,则连接器会自动断开与这个客户端的连接。具体多长时间断开是由 MySQL 的参数wait_timeout
控制的,这个值默认是 8小时
。我们可以根据实际业务需要,自行调整这个参数的值,以使 MySQL 能够满足我们的实际业务场景。
由于客户端与 MySQL 的连接是比较复杂的,这个过程也是比较耗时的,它会涉及 TCP 的握手操作,还会查询当前连接的权限信息等。往往在实际的工作过程中,我们会使用数据库连接池的方式,将数据库的连接缓存起来,这就意味着我们是使用长连接与 MySQL 进行交互的。
但是使用长连接连接 MySQL 也会有一个问题:那就是有时候会发现 MySQL 占用的内存涨得特别快,这是因为 MySQL 在执行的过程中,使用的临时内存是在连接对象里面进行管理的
。这些占用的资源只有在连接断开的时候,才会被释放。如果连接长时间不释放,就会出现大量的临时内存占用内存空间。如果时间久了,可能会导致占用过多的内存,从而被操作系统“消灭”了,给人的感觉就是 MySQL
意外重启了。
我们可以使用如下的方案来解决这个问题:
- 定期或者执行过一个比较占内存的查询操作后,断开连接,以后再重新建立和
MySQL
的连接 - 如果使用
MySQL 5.7
或更新的MySQL
版本,可以通过执行mysql_reset_connection
重新初始化 MySQL 的资源。重新初始化的过程不会重新连接 MySQL,也不会重新做权限的验证操作。
1.5 查询缓存的作用是什么
登录 MySQL
后,客户端就会与 MySQL
建立连接,此时执行 select 语句时,首先会到查询缓存中查询是否执行过当前 select 语句。如果之前执行过相应的 select 语句,则执行过的 select 语句和查询结果会以 key-value 的形式存放在查询缓存中,其中,key 是查询语句,value 是查询的结果数据。
如果在查询缓存中没有找到相应的数据,则会继续执行后续的查询阶段。执行完成后,会将结果缓存到查询缓存中。后续的查询如果命中缓存,则直接返回查询缓存中的数据,性能还是挺高的。
但是,大多数时候我不太建议小伙伴们开启查询缓存,为啥?原因很简单:查询缓存失效的频率是非常频繁的,只要对一个表进行更新操作,则这张表上所有的查询缓存都会被清空
。 而且在 MySQL 8.0 中,直接删除了查询缓存的功能
1.6 逻辑架构中各种分析器
1.6.1 分析器
分析器主要是对 select
语句进行 词法分析和语法分析
操作
如果 select
语句没有命中缓存,则首先会由分析器对其进行词法分析
操作,此时,MySQL
会识别 select 语句中的每个字符串代表什么含义
例如,MySQL
会通过select
关键字识别出这是一个查询语句,也会把user
识别为数据表名 user
,把id
识别成字段名 id
。接下来,就要进行“语法分析了”,根据语法规则,判断 select
语句是否满足 MySQL
的语法。如果判断出输入的 SQL
语句不满足语法规则,则 MySQL
会提示相应的错误信息。
1.6.2 优化器
对 select
语句进行了词法分析和语法分析后,还要经过优化器的优化处理才能执行。比如,我们的 select
语句中如果使用了多个索引,则优化器会决定使用哪个索引来查询数据;再比如,在 select
语句中,有多表关联的操作,优化器会决定各表的连接顺序,数据表的连接顺序不同,对于执行的效率会大不相同,优化器往往会选择使用查询效率高的连接顺序。
如果 select
语句经过优化器的优化之后,就会进入执行阶段了。
1.6.3 执行器
进入执行阶段的 select
语句,首先,执行器会对当前连接进行权限检查,最直接的方式就是检查当前连接是否对数据表 user
具有查询权限。如果当前连接对数据表 user
没有查询权限,就会返回没有权限的错误。例如,会返回如下错误。
ERROR 1142 (42000): SELECT command denied to user 'binghe'@'localhost' for table 'user'
如果当前连接具有对数据表 user 的查询权限,则会继续执行。首先会进行打开数据表的操作,此时优化器会根据创建表时使用的存储引擎,使用相应存储引擎的接口执行查询操作。这里,我们举一个例子:
假设,我们在 id 字段上没有建立索引,执行器执行的流程大致如下所示。
- 通过存储引擎读取数据表
user
的第一行数据,判断当前行的 id 值是否等于 1001,如果不等于 1001,则继续读取下一行数据;如果等于 1001,则将当前行放入结果集中。 - 继续通过存储引擎读取下一行数据,执行与(1)相同的逻辑判断,直到处理完 user 表中的所有数据。
- 处理完所有的数据后,执行器就会将结果集中的数据返回给客户端。
如果在 id 字段上有索引的话,执行的整体逻辑与 id 字段上没有索引大体一致。
如果开启了慢查询的话,执行 select
语句时,会在慢查询日志中输出一个 rows_examined
字段,这个字段表示 select 语句在执行的过程中扫描了数据表中的多少行数据。不过在有些场景下,执行器调用一次,存储引擎内部会会扫描多行,这就导致存储引擎扫描的行数与 rows_examined
字段标识的行数并不完全相同
转载于:https://mp.weixin.qq.com/s/bxVOPXU_ftZEVTxmW_6NAw
1.7 查询问题分析
1.7.1 limit耗时分析
1.7.2 查询分组后前三条数据
1.7.3 SQL模式匹配
1.7.3.1 SQL模式
SQL的模式匹配允许你使用_
匹配任何单个字符
,而%
匹配任意数目字符(包括零个字符)
。在 MySQL
中,SQL
的模式缺省是忽略大小写的。下面显示一些例子。注意在你使用SQL模式时,你不能使用=或!=
;而使用LIKE
或NOT LIKE
比较操作符。
SELECT 字段 FROM 表 WHERE 某字段 Like 条件
其中关于条件,SQL提供了四种匹配模式:
%
:表示任意个或多个字符。可匹配任意类型和长度的字符。
比如SELECT * FROM [user] WHERE u_name LIKE ‘%三%’
将会把u_name为“张三”,“张猫三”、“三脚猫”,“唐三藏”等等有“三”的记录全找出来。另外,如果需要找出u_name中既有“三”又有“猫”的记录,请使用and条件SELECT * FROM [user] WHERE u_name LIKE ‘%三%’ AND u_name LIKE ‘%猫%’
_
:表示任意单个字符。匹配单个任意字符,它常用来限制表达式的字符长度
语句:(可以代表一个中文字符)
比如SELECT * FROM [user] WHERE u_name LIKE ‘_三_’
只找出“唐三藏”这样u_name为三个字且中间一个字是“三”的;
再比如SELECT * FROM [user] WHERE u_name LIKE ‘三__’;
只找出“三脚猫”这样name为三个字且第一个字是“三”的;
1.7.3.2 正则模式
由MySQL
提供的模式匹配的其他类型是使用扩展正则表达式。当你对这类模式进行匹配测试时,使用REGEXP
和NOT REGEXP
操作符(或RLIKE
和NOT RLIKE
,它们是同义词)。
扩展正则表达式的一些字符是:
.
匹配任何单个的字符。(单字节字符)
一个字符类[…]
匹配在方括号内的任何字符。例如,[abc]
匹配“a”、“b”或“c”。为了命名字符的一个范围,使用一个-
。[a-z]
匹配任何小写字母,而[0-9]
匹配任何数字。*
匹配零个
或多个
在它前面的东西。例如,x*
匹配任何数量的x
字符,[0-9]*
匹配的任何数量的数字,而.*
匹配任何数量的任何东西。
正则表达式是区分大小写的,但是如果你希望,你能使用一个字符类匹配两种写法。例如,“[aA]”匹配小写或大写的“a”而“[a-zA-Z]”匹配两种写法的任何字母。如果它出现在被测试值的任何地方,模式就匹配(只要他们匹配整个值,SQL模式匹配)。
为了定位一个模式以便它必须匹配被测试值的开始或结尾,在模式开始处使用^
或在模式的结尾用$
。
为了说明扩展正则表达式如何工作,上面所示的LIKE
查询在下面使用REGEXP
重写:
为了找出以三
开头的名字,使用^
匹配名字的开始。
FROM [user] WHERE u_name REGEXP ‘^三’;
将会把u_name为 “三脚猫”等等以“三”开头的记录全找出来。
为了找出以三
结尾的名字,使用$
匹配名字的结尾。
FROM [user] WHERE u_name REGEXP ‘三$’;
将会把u_name为“张三”,“张猫三”等等以“三”结尾的记录全找出来。
也可以使用{n}
“重复n次”操作符重写先前的查询:FROM [user] WHERE u_name REGEXP ‘b{2}$’;
2 MySQL不能用UUID做主键
2.1 前言
在mysql
中设计表的时候,mysql
官方推荐不要使用uuid
或者不连续不重复
的雪花id
(long
形且唯一,单机递增),而是推荐连续自增的主键id,官方的推荐是auto_increment
,那么为什么不建议采用uuid
,使用uuid
究竟有什么坏处?
2.2 mysql和程序实例
2.2.1 准备工作
要说明这个问题,我们首先来建立三张表
分别是user_auto_key
,user_uuid
,user_random_key
分别表示自动增长
的主键,uuid
作为主键,随机key
作为主键,其它我们完全保持不变.
根据控制变量法,我们只把每个表的主键使用不同的策略生成,而其他的字段完全一样,然后测试一下表的插入速度和查询速度:
注:这里的随机key
其实是指用雪花算法算出来的前后不连续不重复无规律的id,一串18位长度的long值
id自动生成表:
create table user_key_auto(
id int UNSIGNED not null auto_increment,
user_id BIGINT(64) not NULL DEFAULT 0,
user_name VARCHAR(64) not NULL DEFAULT '',
sex int(2) not NULL,
address VARCHAR(255) not null DEFAULT '',
city VARCHAR(64) not NULL DEFAULT '',
email VARCHAR(64) not nulT DEFAULT '',
state int(6) not NULL DEFAULT 0,
PRIMARY KEY(id),
key user_name_key(user_name)
)ENGINE=INNODB
用户uuid表
create table user_uuid(
id VARCHAR(36) not null,
user_id BIGINT(64) not NULL DEFAULT 0,
user_name VARCHAR(64) not NULL DEFAULT '',
sex int(2) not NULL,
address VARCHAR(255) not null DEFAULT '',
city VARCHAR(64) not NULL DEFAULT '',
email VARCHAR(64) not null DEFAULT '',
state int(6) not NULL DEFAULT 0,
PRIMARY KEY(id),
key user_name_key(user_name)
)ENGINE=INNODB
随机主键表:
create table user_random_key(
id BIGINT(64) not null DEFAULT 0,
user_id BIGINT(64) not NULL DEFAULT 0,
user_name VARCHAR(64) not NULL DEFAULT '',
sex int(2) not NULL,
address VARCHAR(255) not null DEFAULT '',
city VARCHAR(64) not NULL DEFAULT '',
email VARCHAR(64) not null DEFAULT '',
state int(6) not NULL DEFAULT 0,
PRIMARY KEY(id),
key user_name_key(user_name)
)ENGINE=INNODB
2.2.2 开始测试
光有理论不行,直接上程序,使用spring的jdbcTemplate
来实现增查测试:
技术框架:springboot+jdbcTemplate+junit+hutool
,程序的原理就是连接自己的测试数据库,然后在相同的环境下写入同等数量的数据,来分析一下insert
插入的时间来进行综合其效率,为了做到最真实的效果,所有的数据采用随机生成,比如名字、邮箱、地址都是随机生成。
import cn.hutool.core.collection.CollectionUtil;
import com.wyq.mysqldemo.databaseobject.UserKeyAuto;
import com.wyq.mysqldemo.databaseobject.UserKeyRandom;
import com.wyq.mysqldemo.databaseobject.UserKeyUUID;
import com.wyq.mysqldemo.diffkeytest.AutoKeyTableService;
import com.wyq.mysqldemo.diffkeytest.RandomKeyTableService;
import com.wyq.mysqldemo.diffkeytest.UUIDKeyTableService;
import com.wyq.mysqldemo.util.JdbcTemplateService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.StopWatch;
import java.util.List;
@SpringBootTest
class MysqlDemoApplicationTests {
@Autowired
private JdbcTemplateService jdbcTemplateService;
@Autowired
private AutoKeyTableService autoKeyTableService;
@Autowired
private UUIDKeyTableService uuidKeyTableService;
@Autowired
private RandomKeyTableService randomKeyTableService;
@Test
void testDBTime() {
StopWatch stopwatch = new StopWatch("执行sql时间消耗");
/**
* auto_increment key任务
*/
final String insertSql = "INSERT INTO user_key_auto(user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?)";
List<UserKeyAuto> insertData = autoKeyTableService.getInsertData();
stopwatch.start("自动生成key表任务开始");
long start1 = System.currentTimeMillis();
if (CollectionUtil.isNotEmpty(insertData)) {
boolean insertResult = jdbcTemplateService.insert(insertSql, insertData, false);
System.out.println(insertResult);
}
long end1 = System.currentTimeMillis();
System.out.println("auto key消耗的时间:" + (end1 - start1));
stopwatch.stop();
/**
* uudID的key
*/
final String insertSql2 = "INSERT INTO user_uuid(id,user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?,?)";
List<UserKeyUUID> insertData2 = uuidKeyTableService.getInsertData();
stopwatch.start("UUID的key表任务开始");
long begin = System.currentTimeMillis();
if (CollectionUtil.isNotEmpty(insertData)) {
boolean insertResult = jdbcTemplateService.insert(insertSql2, insertData2, true);
System.out.println(insertResult);
}
long over = System.currentTimeMillis();
System.out.println("UUID key消耗的时间:" + (over - begin));
stopwatch.stop();
/**
* 随机的long值key
*/
final String insertSql3 = "INSERT INTO user_random_key(id,user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?,?)";
List<UserKeyRandom> insertData3 = randomKeyTableService.getInsertData();
stopwatch.start("随机的long值key表任务开始");
Long start = System.currentTimeMillis();
if (CollectionUtil.isNotEmpty(insertData)) {
boolean insertResult = jdbcTemplateService.insert(insertSql3, insertData3, true);
System.out.println(insertResult);
}
Long end = System.currentTimeMillis();
System.out.println("随机key任务消耗时间:" + (end - start));
stopwatch.stop();
String result = stopwatch.prettyPrint();
System.out.println(result);
}
2.2.3 程序写入结果
user_key_auto写入结果:
user_random_key写入结果:
user_uuid表写入结果:
2.2.4 效率测试结果
在已有数据量为130W
的时候:我们再来测试一下插入10w
数据,看看会有什么结果:
可以看出在数据量100W
左右的时候,uuid
的插入效率垫底,并且在后序增加了130W
的数据,uuid
的时间又直线下降
时间占用量总体可以打出的效率排名为:auto_key>random_key>uuid
,uuid
的效率最低,在数据量较大的情况下,效率直线下滑。那么为什么会出现这样的现象呢?带着疑问,我们来探讨一下这个问题:
2.3 使用uuid和自增id的索引结构对比
2.3.1 自增id
使用自增id的内部结构
自增的主键的值是顺序的,所以Innodb
把每一条记录都存储在一条记录的后面。当达到页面的最大填充因子时候(innodb
默认的最大填充因子是页大小的15/16
,会留出1/16
的空间留作以后的修改):
- 下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费
- 新插入的行一定会在原有的最大数据行下一行,
mysql
定位和寻址很快,不会为计算新行的位置而做出额外的消耗 - 减少了页分裂和碎片的产生
2.3.2 uuid
使用uuid的索引内部结构
因为uuid
相对顺序的自增id
来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以innodb
无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。
这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:
- 写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,
innodb
在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机IO
- 因为写入是
乱序的
,innodb
不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,一次插入最少需要修改三个页以上 - 由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片
- 在把随机值(
uuid和雪花id
)载入到聚簇索引(innodb
默认的索引类型)以后,有时候会需要做一次OPTIMEIZE TABLE
来重建表并优化页的填充,这将又需要一定的时间消耗。
结论
:使用innodb
应该尽可能的按主键的自增顺序插入,并且尽可能使用单调的增加的聚簇键的值来插入新行
2.4 自增id缺点
那么使用自增id
就完全没有坏处了吗?并不是,自增id
也会存在以下几点问题:
- 别人一旦爬取你的数据库,就可以根据数据库的自增id获取到你的业务增长信息,很容易分析出你的经营情况
- 对于高并发的负载,
innodb
在按主键进行插入的时候会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入都发生在这里,并发插入会导致间隙锁竞争 Auto_Increment
锁机制会造成自增锁的抢夺,有一定的性能损失
Auto_increment
的锁争抢问题,如果要改善,需要调优innodb_autoinc_lock_mode
的配置
参考连接:https://mp.weixin.qq.com/s/px-vRWikt8xp7b3ZCr27uw
2.5 雪花算法
点击了解MySQL分布式环境下生成全局自增有序ID(雪花算法Snowflake)
3 插入或更新
在MySQL
中假如想要在插入时没有就插入有则更新,可以用MyISAM
引擎的merge
或者InnoDB
引擎的on duplicate key
,现在主要是用InnoDB
,下面主要介绍on duplicate key
3.1 on duplicate key
3.1.1 定义
如果在INSERT
语句末尾指定了 on duplicate key update
,并且插入行后会导致在一个UNIQUE
索引或PRIMARY KEY
中出现重复值,则在出现重复值的行执行UPDATE
;如果不会导致唯一值列重复的问题,则插入新行。
3.1.2 values函数
values
函数使用说明:在一个 INSERT … ON DUPLICATE KEY UPDATE …
语句中,可以在 UPDATE
子句中使用 VALUES(col_name )
函数,用来访问来自该语句的 INSERT
部分的列值。换言之,UPDATE
子句中的 VALUES(col_name )
访问需要被插入的 col_name
的值 , 并不会发生重复键冲突。这个函数在多行插入中特别有用。
values(col_name)
函数只是取当前插入语句中的插入值,并没有累加功能。 如:count = values(count)
取前面 insert into
中的 count
值,并更新。
当有多条记录冲突,需要插入时,前面的更新值都被最后一条记录覆盖
,所以呈现出取最后一条更新的现象。如:count = count + values(count)
依然取前面 insert into
中的 count
值,并与原记录值相加后更新回数据库,这样,当多条记录冲突需要插入时,就实现了不断累加更新的现象
注意
:VALUES()
函数只在 INSERT ... UPDATE
语句中有意义,而在其它情况下只会返回 NULL
3.1.3 注意事项
注意事项:
insert into ... on duplicate key update ... values()
这个语句,尽管在冲突时执行了更新,并没有插入,但是发现依然会占用id 序号(自增)
,出现很多丢失的id
值- 因为这是个插入语句,所以不能加
where
条件 - 如果是插入操作,受到影响行的值为
1
;如果更新操作,受到影响的行的值为2
;如果更新的数据和已有的数据比对一样(就相当于没变,所有值保持不变),受到影响的行的值为0 - 更新的内容中
unique key
或者primary key
最好按照一个来判断是否重复,不然不能保证语句执行正确(有任意一个unique key
重复就会走更新);尽量不对存在多个唯一键的talbe
使用该语句,避免可能导致数据错乱 - 在有可能有并发事物执行的
insert
语句下不要使用该语句,可能导致产生dead lock
- 如果数据表
id
是自动递增的不建议使用该语句;id
不连续,如果前面更新的比较多,新增的下一条会相应跳跃的更大 - 该语句是
mysql
独有的语法,如果可能涉及到其他数据库语言要慎重使用
3.1.4 操作分析
创建案例表 word_count(单词计数表)
use test;
CREATE TABLE IF NOT EXISTS word_count (
id int(11) NOT NULL AUTO_INCREMENT,
word varchar(64) NOT NULL,
count int(11) DEFAULT 0,
date date NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY word (word, date) // (word,date) 两字段组合唯一
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
执行第一次:(首次数据库表中没有数据,正常插入)
insert into word_count (word, count, date) values
('a',5,curdate())
on duplicate key update count=values(count);
# 结果显示:
id word count date
1 a 5 2023-03-11
执行第二次:(与第一次的唯一(word,date)冲突,执行更新)
insert into word_count (word, count, date) values
('a',6,curdate())
on duplicate key update count=values(count);
# 结果显示:
id word count date
1 a 6 2023-03-11 (更新)
执行第三次:
insert into word_count (word, count, date) values
('a',6,curdate()-1), // 取前一天,不会冲突
('a',7,curdate()) // 冲突
on duplicate key update count=values(count);
# 结果显示:
id word count date
1 a 7 2023-03-11 (更新)
3 a 6 2023-03-10 (新插入)
执行第四次:(更新冲突的最后一条插入值)
insert into word_count (word, count, date) values
('a',2,curdate()), // 冲突
('a',1,curdate()) // 冲突
on duplicate key update count=values(count);
# 结果显示:
id word count date
1 a 1 2023-03-11 (更新最后一条插入值)
3 a 6 2023-03-10 (不变)
执行第五次:(更新冲突的累加插入值)
insert into word_count (word, count, date) values
('a',2,curdate()),
('a',1,curdate())
on duplicate key update count=count+values(count); // 实现每行累加
# 结果显示:
id word count date
1 a 4 2023-03-11
3 a 6 2023-03-10
执行第六次:(无冲突插入,观察 id 键值,出现了很多丢失,id 直接跳到了 9)
insert into word_count (word, count, date) values
('b',2,curdate())
on duplicate key update count=count+values(count);
# 结果显示:
id word count date
1 a 4 2019-06-26
3 a 6 2019-06-25
9 b 2 2019-06-26
3.2 replace info
3.2.1 定义
咱们在运用数据库时能够会常常遇到这种状况。若是一个表在一个字段上建立了索引,当咱们再向这个表中运用现已存在的键值插进一条记载,那将会抛出一个主键抵触的过错。当然,咱们能够想用新记载的值来掩盖本来的记载值。若是运用传统的做法,有必要先运用 DELETE
句子删去原先的记载,然后再运用 INSERT
插进新的记载。而在MySQL
中为咱们供给了一种新的解决方案,这即是 REPLACE
句子。
运用REPLACE
插进一条记载时,若是不重复,REPLACE
就和INSERT
的功用相同,若是有重复记载,REPLACE
就运用新记载的值来更换本来的记载值。
运用REPLACE
的最大优点即是能够将DELETE
和INSERT
合二为一,构成一个原子操作。这样就能够不用思考在一起运用DELETE
和INSERT
时增加业务等杂乱操作了。
在运用REPLACE
时,表中有必要有索引,并且这个索引地点的字段不能答应空值,不然REPLACE
就和INSERT
彻底相同的
在履行REPLACE
后,体系回来了所影响的行数,若是回来1,阐明在表中并没有重复的记载,若是回来2,阐明有一条重复记载,体系主动先调用了DELETE
删去这条记载,然后再记载用INSERT
来刺进这条记载。若是回来的值大于2,那阐明有多个仅有索引,有多条记载被删去和刺进。
replace
具备替换拥有唯一索引
或者主键索引
重复数据的能力,也就是如果使用replace into
插入的数据的唯一索引或者主键索引与之前的数据有重复的情况,将会删除原先的数据,然后再进行添加。
语法:replace into table( col1, col2, col3 ) values ( val1, val2, val3 )
语义:向table表中col1, col2, col3列replace数据val1,val2,val3
3.2.2 操作
先查询:select * from word_count
# 结果显示:
1 a 9 2023-03-11
3 a 7 2023-03-10
12 c 4 2023-03-11
13 b 1 2023-03-11
执行replace into 语句:
replace into word_count (word, count, date) values ('b',1,curdate());
再次查询:select * from word_count
# 结果显示:
1 a 9 2023-03-11
3 a 7 2023-03-10
12 c 4 2023-03-11
14 b 1 2023-03-11
3.2.3 MySQL中特殊插入语法
MySQL
中的INSERT
句子和规范的INSERT
不太相同,在规范的SQL
句子中,一次刺进一条记载的INSERT句子只要一种办法。
INSERT INTO tablename(列名…) VALUES(列值);
而在MySQL
中还有别的一种办法,如下的方法把列名和列值成对呈现和运用,如下面的句子将产生中样的作用
INSERT INTO tablename SET column_name1 = value1, column_name2 =
value2,…;
3.3 INSERT IGNORE
在MySQL
中,可以使用INSERT IGNORE
语句来忽略重复的记录。如果尝试插入的数据会导致重复键错误,那么INSERT IGNORE
会跳过这条记录,而不是报错。
例如:
INSERT IGNORE INTO table (column1, column2) VALUES ('value1', 'value2');
在这个例子中,如果('value1', 'value2')会导致重复键错误,那么这条插入语句就会被忽略,而不会插入数据。同时,这条SQL语句也不会报错,而是会继续执行下一条SQL语句(如果有的话)。
需要注意的是,INSERT IGNORE
不仅会忽略重复键错误,还会忽略其他一些错误(例如数据溢出)。因此,应当确保要插入的数据确实是想要插入的,并且确实希望在出错时跳过插入,而不是报错。