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

相关推荐
Karoku06628 分钟前
【企业级分布式系统】ELK优化
运维·服务器·数据库·elk·elasticsearch
小技与小术2 小时前
数据库表设计范式
数据库·mysql
安迁岚2 小时前
【SQL Server】华中农业大学空间数据库实验报告 实验三 数据操作
运维·服务器·数据库·sql·mysql
安迁岚2 小时前
【SQL Server】华中农业大学空间数据库实验报告 实验九 触发器
数据库·sql·mysql·oracle·实验报告
Loganer2 小时前
MongoDB分片集群搭建
数据库·mongodb
LKID体2 小时前
Python操作neo4j库py2neo使用之创建和查询(二)
数据库·python·neo4j
刘大浪2 小时前
后端数据增删改查基于Springboot+mybatis mysql 时间根据当时时间自动填充,数据库连接查询不一致,mysql数据库连接不好用
数据库·spring boot·mybatis
一只爱撸猫的程序猿2 小时前
简单实现一个系统升级过程中的数据平滑迁移的场景实例
数据库·spring boot·程序员
无敌岩雀2 小时前
MySQL中的索引
数据库·mysql
a_安徒生3 小时前
linux安装TDengine
linux·数据库·tdengine