MySQL唯一索引失效的注意点


【1.实验环境】

Docker环境下的MySQL
MySQL版本: 5.7.37

【2.表结构】

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个配置项

[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 表里初始化数据

#! /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 条数据

mysql> SELECT count(1) FROM t_1;
+----------+
| count(1) |
+----------+
|    49826 |
+----------+
1 row in set (0.51 sec)

【5.操作】

首先, 查询前10条数据

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 一样的数据

-- 本插入语句使用的 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; 插入语句
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)

插入成功了

我们验证下

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 的执行计划

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)

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

你可能感兴趣的:(mysql,数据库,唯一索引)