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

相关推荐
小码的头发丝、20 分钟前
Django中ListView 和 DetailView类的区别
数据库·python·django
小兜全糖(xdqt)28 分钟前
mysql数据同步到sql server
mysql·adb
Karoku06630 分钟前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
白云如幻1 小时前
MySQL的分组函数
数据库·mysql
荒川之神1 小时前
ORACLE 闪回技术简介
数据库·oracle
时差9533 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
让学习成为一种生活方式3 小时前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
秋意钟3 小时前
MySQL日期类型选择建议
数据库·mysql
Dxy12393102164 小时前
python下载pdf
数据库·python·pdf