MySQL唯一索引失效的注意点

【1.实验环境】

Docker环境下的MySQL

MySQL版本: 5.7.37

【2.表结构】

sql 复制代码
CREATE TABLE `t_1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `student_id` char(120) DEFAULT NULL,
  `course_id` char(120) DEFAULT NULL,
  `is_delete` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_student_id_is_delete` (`student_id`,`is_delete`)
  UNIQUE KEY `uk_student_id_course_id` (`student_id`,`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

在 student_id 和 course_id 两个字段上创建了唯一索引

【3.my.cnf配置】

向 my.cnf 文件中添加以下4个配置项

bash 复制代码
[mysqld]

innodb_buffer_pool_size = 64M
innodb_buffer_pool_load_at_startup = 0
innodb_buffer_pool_dump_at_shutdown = 0
innodb_buffer_pool_dump_pct = 0

MySQL 和 RDS MySQL 数据库的 innodb_buffer_pool_load_at_startup 和 innodb_buffer_pool_dump_at_shutdown 默认值都是1

MySQL 的 innodb_buffer_pool_load_at_startup
RDS MySQL 开放参数一览表

【4.初始化数据】

使用 Python 脚本向 t_1 表里初始化数据

python 复制代码
#! /usr/bin/env python

"""
pip install pymysql
"""

import pymysql
import random
import os

def t():
    i = 0
    while i < 50000:
        try:
            id = random.randint(1000,8000000)
            student_id = str(random.randint(1000000000000000,8000000000000000))
            course_id = str(random.randint(1000000000000000,8000000000000000))

            sql = "INSERT IGNORE INTO t_1(id,student_id,course_id) VALUES(%s,%s,%s) " % (id, student_id, course_id)
            os.system('mysql -uroot -p9527 -h172.31.3.199 -P3306 -e "use db0; %s"' % (sql))
        except:
            pass
        i = i + 1


if __name__ == '__main__':
    t()

保证聚簇索引中前边的数据与后边的数据所在叶子节点的页相差很远

我向表里初始化了 49826 条数据

sql 复制代码
mysql> SELECT count(1) FROM t_1;
+----------+
| count(1) |
+----------+
|    49826 |
+----------+
1 row in set (0.51 sec)

【5.操作】

首先, 查询前10条数据

sql 复制代码
mysql> SELECT * FROM t_1 ORDER BY id ASC LIMIT 10 ;
+------+------------------+------------------+-----------+
| id   | student_id       | course_id        | is_delete |
+------+------------------+------------------+-----------+
| 1146 | 1663872557190860 | 4217153589627926 |         0 |
| 1158 | 3252641372188845 | 1885989893713950 |         0 |
| 1170 | 3505508562693832 | 5842914532945726 |         0 |
| 1176 | 4735722558899119 | 6217057537289160 |         0 |
| 1915 | 1183711356591177 | 1086968443403080 |         0 |
| 1920 | 4745308528623498 | 6039228952996318 |         0 |
| 2129 | 2938861665097838 | 3951826741079136 |         0 |
| 2522 | 3131140464950062 | 2272868851197166 |         0 |
| 2803 | 3015035454377989 | 3334778163743394 |         0 |
| 2820 | 3768213727956738 | 1119168911648982 |         0 |
+------+------------------+------------------+-----------+

构造一个与 id = 2820 一样的数据

sql 复制代码
-- 本插入语句使用的 id = 1000000, 所以要保证 t_1 表里没有 id = 1000000的数据, 如果 t_1 表里有 id = 1000000的数据, 可以继续使用 id = 1000001依此类推.
INSERT INTO t_1(id,student_id,course_id,is_delete) VALUES(1000000,'3768213727956738','1119168911648982',0) ;

以上插入语句, 构造了一个 t_1 表里已经有 student_id = 3768213727956738 , course_id = 1119168911648982 的数据.

因为 在 student_id 和 course_id 两个字段上创建了唯一索引, 接下来我们看一下, id = 1000000的数据是否会插入成功.

重启MySQL服务

登录数据库, 依次执行 set unique_checks=0; use db0; 插入语句

sql 复制代码
mysql> set unique_checks=0;
Query OK, 0 rows affected (0.00 sec)

mysql> use db0;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> INSERT INTO t_1(id,student_id,course_id,is_delete) VALUES(1000000,'3768213727956738','1119168911648982',0) ;
Query OK, 1 row affected (0.01 sec)

插入成功了

我们验证下

sql 复制代码
mysql> select * from t_1 where student_id='3768213727956738' and course_id='1119168911648982';
+------+------------------+------------------+-----------+
| id   | student_id       | course_id        | is_delete |
+------+------------------+------------------+-----------+
| 2820 | 3768213727956738 | 1119168911648982 |         0 |
+------+------------------+------------------+-----------+
1 row in set (0.00 sec)

哎, 这里只查询到了 id = 2820 的数据, 没有查询到我们新插入的 id = 1000000的数据.

看一下该 SQL 的执行计划

sql 复制代码
mysql> explain select * from t_1 where student_id='3768213727956738' and course_id='1119168911648982';
+----+-------------+-------+------------+-------+-------------------------+-------------------------+---------+-------------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys           | key                     | key_len | ref         | rows | filtered | Extra |
+----+-------------+-------+------------+-------+-------------------------+-------------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | t_1   | NULL       | const | uk_student_id_course_id | uk_student_id_course_id | 962     | const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+-------------------------+-------------------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

以上 SQL 使用了 uk_student_id_course_id 索引, MySQL回表只会取一条数据

接下来强制使用主键索引 , 采用 force index(primary)

sql 复制代码
mysql> select * from t_1 force index(primary) where student_id='3768213727956738' and course_id='1119168911648982';
+---------+------------------+------------------+-----------+
| id      | student_id       | course_id        | is_delete |
+---------+------------------+------------------+-----------+
|    2820 | 3768213727956738 | 1119168911648982 |         0 |
| 1000000 | 3768213727956738 | 1119168911648982 |         0 |
+---------+------------------+------------------+-----------+
2 rows in set (0.44 sec)

查询到了2条数据, 它们的 student_id 和 course_id 一样. 唯一索引失效了 .

然而这并不是MySQL的bug, 出现这样的问题, 责任在于使用者自身 .

【6.解惑】

首先, 这样要说一下MySQL里的ChangeBuffer. 在我们的 t_1 表上有一个普通的二级索引 idx_student_id_is_delete, 当向 t_1 表里更新数据的时候( update t_1 set is_delete = 1 where student_id = '1663872557190860' ), 除了要更新聚簇索引上的数据, 还要更新二级索引 idx_student_id_is_delete 上的数据, 为了提高更新二级索引的性能, 引入了ChangeBuffer. 在更新二级索引的时候, 不需要先从磁盘读取二级索引数据页( 读取二级索引是随机读,性能差 ), 而是先把数据放在ChangeBuffer里, MySQL会在合适的时机将ChangeBuffer里的数据更新到二级索引数据页上. 然而这里说的二级索引不包含唯一索引, 假如我们要更新唯一索引, 为了保证唯一性, 就不能把数据放在ChangeBuffer里, 必须要读取磁盘,进行唯一性判断, 这样就会导致更新性能差.

于是乎MySQL提供了一个配置项 unique_checks , 默认 unique_checks = 1, 也就是插入唯一索引时,必须进行唯一性校验, 需要读取磁盘. 当 unique_checks = 0, 在插入唯一索引时, 就会使用到 ChangeBuffer 了, 这样就会造成, 重复的数据会插入到聚簇索引的数据页上, 即便MySQL在合适的时机将ChangeBuffer里的数据插入到唯一索引的数据页上时, 发现重复了, 也于事无补了. 聚簇索引的数据页和唯一索引的数据页不是同一个数据页, 在 unique_checks = 0 时两个数据页互不影响.

当使用者将 unique_checks = 0 时, 需要使用者自己保证插入的数据没有重复的 .

外链

1.ChangBuffer

2.官方 unique_checks

相关推荐
.Shu.1 小时前
Mysql InnoDB 底层架构设计、功能、原理、源码系列合集【五、InnoDB 高阶机制与实战调优】
数据库·mysql
新法国菜2 小时前
MySql知识梳理之DDL语句
数据库·mysql
DarkAthena3 小时前
【GaussDB】全密态等值查询功能测试及全密态技术介绍
数据库·gaussdb
ShawnLeiLei4 小时前
2.3 Flink的核心概念解析
数据库·python·flink
石皮幼鸟4 小时前
数据完整性在所有场景下都很重要吗?
数据库·后端
大只鹅5 小时前
Centos7.9 Docker26容器化部署 MySql9.4 一主一从的同步复制部署
mysql·centos
叁沐6 小时前
MySQL 28 读写分离有哪些坑?
mysql
nightunderblackcat6 小时前
新手向:异步编程入门asyncio最佳实践
前端·数据库·python
DarkAthena6 小时前
【GaussDB】使用MySQL客户端连接到GaussDB的M-Compatibility数据库
数据库·mysql·gaussdb
livemetee7 小时前
Flink2.0学习笔记:使用HikariCP 自定义sink实现数据库连接池化
大数据·数据库·笔记·学习·flink