MYSQL LAST_INSERT_ID()/LAST_INSERT_ID(expr)函数介绍

简介

本文翻译自https://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_last-insert-id,即MYSQL5.7官方文档的一个小部分的一个小节,英文ok的同学移步上述链接即可。

主要内容梗概:

  1. LAST_INSERT_ID(), 返回本连接(connection)下,上一次成功插入操作的自增列的值,若插入操作为多行插入,则会返回第一行成功插入的自增值;
  2. 如1中所述,此函数是连接维度的,不同的客户端连接,在MYSQL服务器端维护着不同的值。若同一客户端连接断了,再次重建,值也会重置,新建立的连接是不存在“上一次成功插入”的;
  3. 是连接维度的,不是表维度的,表A表B都有自增列,SELECT LAST_INSERT_ID()就是上一次成功插入操作的自增值,跟表无关;
  4. LAST_INSERT_ID(expr),原返expr值,并使得下一次系统自增值产生前调用LAST_INSERT_ID()也返回expr,可用于做自增序列,Spring里有一个内置的实现,类名MySQLMaxValueIncrementer,后边会补一篇介绍。

内容

不带参数的LAST_INSERT_ID()函数返回一个64位无符号的长整型(BIGINT UNSIGNED 64-bit),这个值是针对一个自增长列(AUTO_INCREMENT)的最近一次插入(INSERT)操作所成功插入的自动生成的第一个值。如果一行都没有插入成功,那么LAST_INSERT_ID的值不会发生变化。

带参数的LAST_INSERT_ID(), 即LAST_INSERT_ID(expr)返回一个无符号整型, 原返expr,并使不带参数调用LAST_INSERT_ID()也返回此值(expr)。

举例来讲,在插入一行包含自增长列(AUTO_INCREMNET)的记录后,你会得到如下结果:

mysql> SELECT LAST_INSERT_ID();
        -> 195
# 博主注:这个例子举得不好,第一次读可能懵逼,仅仅能说明这个函数返回一个值。其实际含义为本连接内上一次系统产生的自增值为195,LAST_INSERT_ID()返回了这个值。

当前正在执行的语句不会对LAST_INSTERT_ID()的值产生影响。假如你使用语句A创建了一个自增值,然后用语句B对一个含有自增列的表插入多行,语句B中使用LAST_INSERT_ID()。LAST_INSERT_ID()在语句B中值会保持不变;即被插入的多行中,第二行第三行不会受到第一行已经被插入的影响。(但是,如果混合使用了LAST_INSERT_ID()和LAST_INSERT_ID(expr),会产生undefined(没明白))。

如果上一条语句返回error,那么LAST_INSERT_ID会返回undefined。在事务表中,如果语句因为错误发生了回滚,LAST_INSERT_ID()也会是undefined。在手动回滚的场景,LAST_INSERT_ID的值不会回到事务开始之前,他会保持回滚发生时的值。

在MySQL 5.7.3之前,使用复制过滤规则,无法正确复制此函数。

在存储例程(存储过程或函数)或触发器中,LAST_INSERT_ID()的值的变化方式与在这些语句块外执行的变化方式相同;存储例程/触发器中对LAST_INSERT_ID()早成的变化对后续语句的可见性取决于例程的类型:

  • 在存储过程中,单条语句改变了LAST_INSERT_ID()的值,对后续语句是可见的
  • 在函数/触发器当中改变LAST_INSERT_ID()的值,它的值在函数/触发器结束时会重置,后续语句无法得到改变的值,是不可见的

LAST_INSERT_ID()返回的这个ID值是以连接维度(per-connection)存储在数据库服务端的,这意味着它返回的值是本客户端本次连接上一个插入语句所产生的第一个自增值,就算其他客户端也对自增值进行了操作,本客户端的这个值也不会受到影响。这个特性保证了每个客户端可以取回自己当前的ID值,而不用与其他客户端进行任何状态交互,也不用加锁,也不用事务。

如果你把一行数据的自增列设置为一个非魔法值(non-“magic”,非NULL且非0),LAST_INSERT_ID()的值不会发生变化。(博主:读了一遍没明白什么意思,经实测验证,如果插入行的时候,自增列上自定义设置了非NULL或0的值,那么LAST_INSERT_ID()的返回值不会发生变化,也就是说自增列上的值如果不是系统自动生成的,那么LAST_INSERT_ID()的值就不会变)

重要
如果你在一条插入语句中插入多行记录,LAST_INSER_ID()返回的值是第一个被插入的行的自增值。这种设定的原因是便于在其他服务器上重现相同的插入语句。(博主还没弄明白最后这句话什么意思,这个other server的意思可能是其他RDBMS?)

例子:

mysql> USE test;

mysql> CREATE TABLE t (
       id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
       name VARCHAR(10) NOT NULL
       );

mysql> INSERT INTO t VALUES (NULL, 'Bob');

mysql> SELECT * FROM t;
+----+------+
| id | name |
+----+------+
|  1 | Bob  |
+----+------+

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

mysql> INSERT INTO t VALUES
       (NULL, 'Mary'), (NULL, 'Jane'), (NULL, 'Lisa');

mysql> SELECT * FROM t;
+----+------+
| id | name |
+----+------+
|  1 | Bob  |
|  2 | Mary |
|  3 | Jane |
|  4 | Lisa |
+----+------+

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                2 |
+------------------+

如上例所示,第二条插入语句往表t里插入了三条数据,接下来的SELECT语句LAST_INSERT_ID()返回的是值为2,即三条数据中第一个成功插入行的ID。

如果你使用INSERT IGNORE语法,并且要插入的行被ignore了,那么LAST_INSERT_ID()的值不会变(如果本次连接还没有进行过一次成功的插入会返回0)。对于不支持事务的表(non-transational tables),AUTO_INCREMENT计数器不会增长。对于InnoDB表,若innodb_autoinc_lock_mode被设置为1或2,AUTO_INCREMENT计数器会增长。示例如下:

mysql> USE test;

mysql> SELECT @@innodb_autoinc_lock_mode;
+----------------------------+
| @@innodb_autoinc_lock_mode |
+----------------------------+
|                          1 |
+----------------------------+

mysql> CREATE TABLE `t` (
       `id` INT(11) NOT NULL AUTO_INCREMENT,
       `val` INT(11) DEFAULT NULL,
       PRIMARY KEY (`id`),
       UNIQUE KEY `i1` (`val`)
       ) ENGINE=InnoDB DEFAULT CHARSET=latin1;

# 插入两行

mysql> INSERT INTO t (val) VALUES (1),(2);

# 在auto_increment_offset=1下, AUTO_INCREMENT的值变为3

mysql> SHOW CREATE TABLE t\G
*************************** 1. row ***************************
       Table: t
Create Table: CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `val` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `i1` (`val`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1

# LAST_INSERT_ID() 返回第一个自动为自增列生成的自增值

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

# 这次在IGNORE模式下尝试插入重复的行(注:唯一键冲突,但忽略冲突错误)   

mysql> INSERT IGNORE INTO t (val) VALUES (1),(2);
Query OK, 0 rows affected (0.00 sec)
Records: 2  Duplicates: 2  Warnings: 0

# 在innodb_autoinc_lock_mode=1下, AUTO_INCREMENT计数器也记录了被忽略的行,值变为5

mysql> SHOW CREATE TABLE t\G
*************************** 1. row ***************************
       Table: t
Create Table: CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `val` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `i1` (`val`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1

# The LAST_INSERT_ID()的值并没有变化,因为上一条插入语句没有成功

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

更多相关信息,参见14.6.1.4节, “AUTO_INCREMENT Handling in InnoDB”

如果给LAST_INSERT_ID()设置参数,即LAST_INSERT_ID(expr),expr值会作为函数的返回值原返,并且下次调用LAST_INSERT_ID()时,会返回expr值(而不再是上一次插入的自增列的自增值),这个特性可以用来模拟序列

  • a. 创建一张表去维护一个序列计数器并初始化它:
    mysql> CREATE TABLE sequence (id INT NOT NULL);
    mysql> INSERT INTO sequence VALUES (0);
    
  • b. 用这张表生产数字序列:
    mysql> UPDATE sequence SET id=LAST_INSERT_ID(id+1);
    mysql> SELECT LAST_INSERT_ID();
    

UPDATE语句增加了计数器值,并且让下次调用LAST_INSERT_ID()时返回更新的计数器值。而后用SELECT语句拿到这个值。C语言API中的mysql_insert_id()函数也可以用来获取此值,参见Section 27.8.7.38, “mysql_insert_id()”

好像也可以不使用LAST_INSERT_ID()来生成序列,但使用此函数好处还是很明显的,它所产生的ID是维护在服务器中的自动产生的自增值。它是多用户安全的,因为不同的客户端都可以在调用UPDATE语句后再用SELECT语句(或者my_insert_id())获取这个序列值,而不会影响到其他的客户端或者被其他客户端影响。

注意mysql_insert_id()的值只在INSERT语句或UPDATE语句后更新,所以你无法使用C语言API函数去获取SELECT或SET语句等语句中调用LAST_INSERT_ID(expr)所设置的值。

废话时间

光阴似箭,日月如梭,虽然小学已经毕业了十几年,但每每想要感叹时光飞逝,还是只能想起这个经典的开头,博客已经一年半以上木有更新了,真是令人汗颜。工作这一年多,遇到了很多问题,相当一部分解决方案都是来源于网络博客,每每想要总结些什么,但总觉得网上已经有解决了我的问题的博客存在,再写出来难免有些画蛇添足。

公司的大佬告诉我,有用的博客有两种,一种是纯干货技术博客,一种是单纯给出解决方案的博客。回首往昔,自己学的东西、写的东西大多浅尝辄止,后者居多,随着工作的深入,或许不能再不求甚解下去了,毕竟曾经标榜过自己是对技术有追求的人士啊(滑稽)。