“给每个表格加一个自增id作为主键”在书籍、教程中常见,有时候甚至被写入编程规范。但是,你有没有想过,这是为什么呢?
以下分析基于MySQL、Innodb存储引擎。
理由一:浪费存储空间
两个表格,一个有自增id,一个没有自增id,其它字段相同。哪个占用更多存储空间?
先说结论:很可能是没有自增id的表格。
这是因为,在没有主键、也没有非空唯一索引的情况下,Innodb会自动生成一个单调递增的6字节Row ID
字段用作聚簇索引[1]:
If the table has no PRIMARY KEY or suitable UNIQUE index, InnoDB internally generates a hidden clustered index named GEN_CLUST_INDEX on a synthetic column containing row ID values. The rows are ordered by the ID that InnoDB assigns to the rows in such a table. The row ID is a 6-byte field that increases monotonically as new rows are inserted. Thus, the rows ordered by the row ID are physically in insertion order.
如果自增id使用的是4字节的int类型,那就比6字节的Row ID
更省存储空间。
那如果使用业务字段(例如手机号、身份证号等)作为主键或者唯一索引,Innodb就不用生成Row ID
了,这样应该更省空间吧?
结论是:不一定。
Innodb在每个数据页预留了一部分空闲的空间,用于将来的数据增长(插入和更新)[2]。如果是按顺序(升序或降序)插入记录,预留的空间是1/16,如果不是按顺序插入的,预留的空间是1/16 ~ 1/2.
When new records are inserted into an InnoDB clustered index, InnoDB tries to leave 1/16 of the page free for future insertions and updates of the index records. If index records are inserted in a sequential order (ascending or descending), the resulting index pages are about 15/16 full. If records are inserted in a random order, the pages are from 1/2 to 15/16 full.
由于手机号、身份证号通常不是有序插入的,实际存储时会有较多的“空洞”占用存储空间。
理由二:性能不好
如果插入记录时,主键的顺序不是有序插入的,为了维护索引的有序性,可能会发生页分裂(Page Split)。
假设某个数据页中的数据是1、3、5、9,且数据页满了,现在准备插入一个数据7,则需要把数据页分割为两个数据页:
出现页分裂时,需要将一个页的记录移动到另外一个页,性能会受到影响,同时页空间的利用率下降,造成存储空间的浪费。
而如果记录是顺序插入的,例如插入数据11,则只需开辟新的数据页,也就不会发生页分裂:
理由三:外键难维护
假设使用手机号(而不是用户ID)作为用户表的主键,其它评论表、文章表等也使用手机号作为外键与用户表关联。如果用户修改手机号,则评论表、文章表等表格也需要跟着修改,不仅麻烦,在记录较多时还会导致性能问题。使用用户ID作为用户表的主键时,则没有这个问题,修改手机号时,只需要更新用户表即可。
参考
[1] Clustered and Secondary Indexes
[2] The Physical Structure of an InnoDB Index