什么?Mysql不设置主键竟然这么多问题

发布时间 2024-01-13 00:10:45作者: 銘聊技术

什么?Mysql不设置主键竟然这么多问题

大家好,我是銘,全栈开发程序员。

Mysql大家不陌生吧,平时用的最多的数据库差不多就是它了,创建表的时候,没有主键好似也没啥问题,那今天我们聊一聊Mysql如果不设置主键会怎么样。

InnoDB索引

在聊这个话题之前,先说一下InnoDB索引,当Mysql数据表使用InnoDB作为存储引擎的时候,数据结构使用的就是B+树,而表的所有数据存储在主键索引上,也就是通常说的聚簇索引,也就是每个表需要有个聚簇索引树,对于InnoDB,主键对应的索引就是聚簇索引,而除了聚簇索引的其他索引存储的只是主键的引用,所以,查询的时候对于普通索引需要进行回表才能取到具体数据。

创建表没有主键,Mysql怎么处理呢

既然InnoDB对于数据的存储必须依赖主键,那没有主键的表,Mysql该怎么办呢?

当使用InnoDB作为引擎时,如果定义了主键,那么InnoDB会使用主键作为聚簇索引,如果没有定义主键,那么会使用第一非空的唯一索引NOT NULL and UNIQUE INDEX)作为聚簇索引,如果既没有主键,也没有合适的非空索引,那么InnoDB会自动生成一个包含ROW_ID值的列作为聚簇索引(就是InnoDB会自动帮我们生成一个隐藏列充当主键),插入新数据时,就会生成一个新的递增的ROW_ID,所以根据ROW_ID排序的行,本质上是按照插入顺序排序,很明显,没有主键的表,InnoDB就会内置一列用于聚簇索引来组织数据,没有建立主键的话就没法通过主键来进行索引,查询的时候是全表扫面的,数据量小的时候没问题,数据量多了性能就会出现问题。

隐式ROW_ID怎么实现

InnoDB帮我们生成的ROW_ID的列(就是隐藏的主键),既不能被任何查询访问,也不能被内部使用,。更坑爹的是,*所有用ROW_ID列的表,共享同一个被保存在数据字典中的全局序列数*,且下一个将要使用的值被保存在系统表空间的page 7(type SYS),数据字段头里(字段名为DICT_HDR_ROW_ID)。

这个全局序列计数器被dict_sys->mutex保护它的线程安全问题,在include/dict0boot.ic中的核心实现源码如下:

UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t        id;
    mutex_enter(&(dict_sys->mutex));
    id = dict_sys->row_id;
    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
            dict_hdr_flush_row_id();
    }
    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

很多小伙伴可能注意到了,这段代码对row_id只是一味的递增,没有任何48位溢出的保护。事实上也没有必要,因为即使只有48位,假设每秒插入10万次,需要90年才会耗尽ROW_ID的48位空间。所以,基本上够用!

另外我们从这段代码可知,每生成256次ROW_ID,计数器就会被刷到磁盘持久化(dict_hdr_flush_row_id()),这个频率通过字段DICT_HDR_ROW_ID_WRITE_MARGIN定义,并且被保存在事务日志中。在启动的时候,InnoDB将保存在磁盘上的DICT_HDR_ROW_ID增加256,这样就能确保已经生成的ID,不管是否被持久化到磁盘上,都会小于新生成的值,所以生成的ID不会有任何冲突。

InnoDB中很多地方的代码,包括刚才提到的ROW_ID的生成都是通过dict_sys->mutex保证线程安全问题,因此,我可以说任何用ROW_ID作为隐式聚簇索引键的表,都可能随机性的碰到插入停顿问题。多张这种表并行插入就会遇到性能限制,因为共享计数器的共享互斥锁和缓存争用是串行的。此外,每生成256个ID就会需要日志写入和刷新,这些都会引起性能毛刺问题。

总结

  • 不创建主键时,使用不了主键索引,查询会进行全表扫描,影响性能
  • 插入数据需要生成ROW_ID,而生成的ROW_ID是全局共享的,并发会导致锁竞争,影响性能

所以在建表的时候还是要设置主键,无论表设计有无合适的唯一字段,都需要设置一个主键,提高性能的同时也是一种好的习惯,对于后续的拓展以及表之间关联都有一定的拓展性。否则,数据量大了,坑的可就是自己了。