别踩坑!使用MySQL唯一索引请注意「建议收藏」

别踩坑!使用MySQL唯一索引请注意「建议收藏」背景在程序设计中了,我们往往需要确保数据的唯一性,比如在常见的注册模块,我们需要确保一个手机号只能注册为一个账号。这种情况下,我们的程序往往是第一道关卡,用户来注册之前,首先判断这个手机号是否已经注册,如果已经注册则返回错误信息。但是我们不能确保同时有两个人使用同一个手机号注册到我们的系统中,因此这里就需要在更深的层次去确保手机号的唯一性了。不同存储方案,解决方式不一样,这里以MySQL为例,我…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

背景

在程序设计中,我们往往需要确保数据的唯一性,比如在常见的注册模块,我们需要确保一个手机号只能注册为一个账号。这种情况下,我们的程序往往是第一道关卡,用户来注册之前,首先判断这个手机号是否已经注册,如果已经注册则返回错误信息,或直接去登录。但是我们不能确保同时有两个人使用同一个手机号注册到我们的系统中,因此这里就需要在更深的层次去确保手机号在系统的唯一性了。不同存储方案,解决方式不一样。对于常用的MySQL数据库,我们可以使用唯一索引的方式来作为我们的最后一道防线。

但是最近在使用数据库的唯一索引时,发现一个比较奇怪的现象。MySQL数据库,使用InnoDB存储引擎,创建了唯一索引时,在insert操作时,如果唯一索引上的字段有为NULL的情况,则可以无限插入。这有点匪夷所思,但是现实就是这么一个情况。现在就来具体分析这样的一个案例,来看看底层对于唯一索引是怎么设计的,来规避在数据库设计上犯错和踩坑。

案例

假设现在有一个用于保存用户信息的数据表user,是使用email注册的,当前使用email作为唯一索引,同时这一基本规则也被其他依赖系统作为设计数据模型的设计基础。假设现在设计这样一个user表:

CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `email` varchar(32) NOT NULL DEFAULT '' COMMENT 'email',
  `name` varchar(11) DEFAULT '' COMMENT 'name',
  `age` int(11) DEFAULT NULL COMMENT 'age',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk-email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

1@user.com来注册,执行insert语句,执行成功

INSERT INTO user (email,name,age) VALUES ('1@user.com','h1',18);

1@user.com再来注册,则再次执行,则报错。成功规避了用户多次创建导致系统产生脏数据问题。

Duplicate entry '1@user.com' for key 'uk-email'

从这里看,user表的设计是符合业务要求的,并没有出现同一个email出现多行的情况。随着业务发展,单单email注册的模式并不适合移动互联网时代,所以现在的要求在原有基础上增加了手机号的字段,并要求手机号也是唯一的。于是添加phone字段,并将原有唯一索引删除,为email和phone设置新的唯一索引。

ALTER TABLE `user` ADD COLUMN `phone` varchar(11) default NULL AFTER `age`;

DROP INDEX `uk-email` ON `user`;

ALTER TABLE `user` ADD UNIQUE KEY `uk-email-phone` (`email`,`phone`);

假设用户1再来用同样的email注册,可以注册成功:

INSERT INTO user (email,name,age,phone) VALUES (‘1@user.com’,‘h1’,18,NULL);

查询数据库数据,得到以下结果:
mysql-query-user-uk-index-1
有两个email为1@user.com的记录,他们的phone都是NULL,这怎么可能存在?!难道是MySQL出问题了?!不可能,我们再试另外一个数据

INSERT INTO user (email,name,age,phone) VALUES ('2@user.com','h2',18,'18812345678');

连续执行两次,第一次执行成功,第二次报错:

Duplicate entry ‘2@user.com-18812345678’ for key ‘uk-email-phone’

查询user结果集,得到
mysql-query-user-uk-index-2
从结果看这样MySQL的唯一索引也算是正常的啊,那这到底是怎么一回事呢?

原因探寻

业务中希望建立的唯一索引是email + phone的组合,但是由于phone一开始是没有数据的,所以新建字段时默认允许为NULL来兼容老数据。如果程序没有控制好,数据操作直接打到数据库,就产生了两条email为“1@user.com”且phone为NULL的数据,那么就会发生这种数据错乱的情况。

我从 MySQL 5.7官方文档 中找到了这个:

Unique Indexes

A UNIQUE index creates a constraint such that all values in the index must be distinct. An error occurs if you try to add a new row with a key value that matches an existing row. If you specify a prefix value for a column in a UNIQUE index, the column values must be unique within the prefix length. A UNIQUE index permits multiple NULL values for columns that can contain NULL.

官方的文档中明确说明在唯一索引中是允许存在多行值为NULL的数据存在的。

当然我们会认为这是MySQL的一个bug,其实早有人这么认为了,并给MySQL提出了这个问题https://bugs.mysql.com/bug.php?id=8173。但是MySQL的开发者并不认为这是一个bug,而是本身的一种设计。额,这么说,好像也说得过去。那这里就有一个问题了,我们知道索引是使用B+树来维护的,但是对于这种非唯一索引是怎么维护的?

带着这个问题,我觉得有两种可能:

(1)唯一索引时另外一种数据类型,正好把有值为NULL的字段过滤掉了,无需特殊处理。

(2)还是用的B+树索引,但是对于NULL的索引特殊处理了。

于是我对email=2@user.com且phone= 18812345678的数据执行了Explain执行计划

explain select * from user where `email` = '2@user.com' and `phone` = '18812345678';

mysql-query-user-uk-index-3
这个查询正好用到了唯一索引uk-email-phone,索引长度是134。

对email=1@user.com且phone为NULL的执行类似Explain执行计划

explain select * from user where `email` = '1@user.com' and `phone` is   NULL;

mysql-query-user-uk-index-4

对比上面两次不同数据的explain执行结果,可以看到其实都用了uk-email-phone的唯一索引,不同的是第一个type是const(通过一次索引就可以找到,用于primary key或unique index),第二个type是ref(非唯一性索引扫描),且rows为2。所以猜测这里极有可能是对NULL进行的特殊处理,唯一索引树还是用的和非NULL一样的唯一索引树。

源码分析

上面利用explain,测试结果是符合自己的猜测行为而已。也许只有源码中才能比较好的知道答案,基于此,在github上找到MySQL相关的源码(在此感谢DBA同学在唯一索引源码分析上的指点)。在这段源码https://github.com/mysql/mysql-server/blob/8e797a5d6eb3a87f16498edcb7261a75897babae/storage/innobase/row/row0ins.cc中,有一个方法 row_ins_scan_sec_index_for_duplicate(),这里会扫描唯一非聚簇索引树,来确定是否会发生唯一性的冲突。源码内有一段注释

/* If the secondary index is unique, but one of the fields in the
  n_unique first fields is NULL, a unique key violation cannot occur,
  since we define NULL != NULL in this case */

在继续往下有一段这样的逻辑

	 cmp = cmp_dtuple_rec(entry, rec, index, offsets);

    if (cmp == 0 && !index->allow_duplicates) {
      if (row_ins_dupl_error_with_rec(rec, entry, index, offsets)) {
        err = DB_DUPLICATE_KEY;

        thr_get_trx(thr)->error_info = index;

        /* If the duplicate is on hidden FTS_DOC_ID,
        state so in the error log */
        if (index == index->table->fts_doc_id_index &&
            DICT_TF2_FLAG_IS_SET(index->table, DICT_TF2_FTS_HAS_DOC_ID)) {
          ib::error(ER_IB_MSG_958) << "Duplicate FTS_DOC_ID"
                                      " value on table "
                                   << index->table->name;
        }

        goto end_scan;
      }
    } else {
      ut_a(cmp < 0 || index->allow_duplicates);
      goto end_scan;
    }

跳转到row_ins_dupl_error_with_rec()方法中有一段这样的逻辑

/* In a unique secondary index we allow equal key values if they
  contain SQL NULLs */

  if (!index->is_clustered() && !index->nulls_equal) {
    for (i = 0; i < n_unique; i++) {
      if (dfield_is_null(dtuple_get_nth_field(entry, i))) {
        return (FALSE);
      }
    }
  }

在唯一索引中有字段为NULL的情况下,返回FALSE,代码中就没有抛出DB_DUPLICATE_KEY的异常了。所以从源码来看,这里实现了唯一索引允许为NULL的情况了,而且可以知道,这个唯一索引树和其他的二级索引基本上是没什么区别的。这也是前面explain时及时我们查询非唯一索引中另一个字段为空的记录,也还是用到了同样的索引和相同的索引长度。

反观来看,如果是我们在未知实现的情况下,要我们来设计,怎么实现允许有字段为NULL的唯一索引呢?是否还有比现有MySQL更好的方式来实现?

结论

所以其实MySQL在唯一索引中允许存在值为NULL的字段。NULL值在MySQL可以代表是任意值,并且在有字段值为NULL时,不会参与校验这个组合的唯一索引,所以可能插入业务上不允许重复的数据,导致脏数据。

因此在创建属于唯一索引的列时,最好指定字段值不能为空,在已有值为NULL的情况下,创建的字段不允许为空,且默认值为空字符。如果已经创建了默认值为NULL的字段,则先将其update为空字符,然后再修改为NOT NULL DEFAULT ‘’。如上述情况建表语句改为

CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `email` varchar(32) NOT NULL DEFAULT '' COMMENT 'email',
  `name` varchar(11) DEFAULT '' COMMENT 'name',
  `age` int(11) DEFAULT NULL COMMENT 'age',
  `phone` varchar(11) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk-email-phone` (`email`,`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

并非所有数据库都是这样,SQL Server 2005及更老的版本,只允许有一个NULL值出现。从https://sqlite.org/faq.html#q26 了解到ANSI SQL-92标准:

A unique constraint is satisfied if and only if no two rows in a table have the same non-null values in the unique columns.(如果且仅当表中没有两行在唯一列中具有相同的非空值时,才满足唯一约束。)

除了MySQL之外,sqlLite、PostgreSQL、Oracle和FireBird也是允许唯一索引上存在多行为NULL。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/191435.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 利用Python制作微信机器人(一)

    利用Python制作微信机器人(一)双十一时候,阿里云服务器打折。于是直接买了三年的阿里云服务器。自己也明白有一个云服务器说白了就是有一个公网IP+7*24h不关机的电脑。但买完服务器后,就不知道用这服务器来做点什么炫酷的事情了。这两天看到有某位海王做了个自动回复消息的机器人来给女朋友们回消息,是否可以用这个服务器来做一个自动回复消息的机器人。…

    2022年6月23日
    26
  • java枚举类型enum用法(java定义枚举常量类)

    文章目录枚举类的使用如何定义枚举类方式一:jdk5.0之前,自定义枚举类方式二:jdk5.0,可以使用enum关键字定义枚举类Enum类的主要方法toString()values()valueOf(StringobjName)使用enum关键字定义的枚举类实现接口的情况情况一:实现接口,在enum类中实现抽象方法情况二:让枚举类的对象分别实现接口中的抽象方法枚举类的使用枚举类的理解:类的对象只有有限个,确定的。我们称此类为枚举类当需要定义一组常量时,强烈建议使用枚举类如果枚举类中只有一个对象,则

    2022年4月16日
    39
  • pycharm怎么配置tensorflow环境_linux系统物理机安装步骤

    pycharm怎么配置tensorflow环境_linux系统物理机安装步骤Tensorflow详细安装步骤及PyCharm配置Tensorflow是谷歌开源的深度学习框架,分为两个版本,GPU和CPU,主要的区别在于计算速度,GPU版本要比CPU计算速度更快,适用于处理大量复杂的数据,但需要计算机配置独立NVIDIA显卡。CPU版本没有显卡要求,安装更简单,合适新手小白和学生党,下面介绍CPU版本Tensorflow的详细安装步骤系统环境:Windows10第一步:安装Anaconda两种方式:直接在Anaconda官方网站下载,但速度很慢;建议第二种,选择镜像网站下载,

    2022年8月28日
    6
  • 颜色校准调整伽马_色彩gamma什么意思

    颜色校准调整伽马_色彩gamma什么意思目录1、色彩矫正(CCM)2、伽马校正(Gamma)1、色彩矫正(CCM)色彩校正(ColorCorrection)是指用相同的方法改变图像中的所有像素的颜色值,以得到不同得显示效果。图像采集系统在获得数字图像时,由于一起或环境光照或人为因素的影响,采集的图像往往与原始图像有很大差别。颜色校正可以在一定程度上减少这种差别。利用RGB颜色模型可以方便地调整图像的RGB分量值,这对校正偏色很有用。色彩校正的基本原理如下:其中,Mij…

    2022年9月16日
    4
  • PyCharm 插件推荐[通俗易懂]

    PyCharm 插件推荐[通俗易懂]安装插件的教程请看文章:https://blog.csdn.net/weixin_39020133/article/details/1056537941、主题插件MaterialThemeUI,效果图如下:2、远程调试服务器插件AlibabaCloudToolkit…

    2022年6月24日
    24
  • java CAS详解[通俗易懂]

    java CAS详解[通俗易懂]CAS解释:CAS(compareandswap),比较并交换。可以解决多线程并行情况下使用锁造成性能损耗的一种机制.CAS操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。一个线程从主内存中得到num值,并对num进行操作,写入值的时候,线程会把第一次取到的num值和主内存中num值进行比较,如果相等,就会将改变后的num写入主内存,如果不相等,则一直循环对比,知道成功为止。CAS

    2022年7月9日
    26

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号