innodb主键的长度为什么不能大于767字节

本文解析MySQL中索引长度限制的由来,探讨不同行格式下主键及索引长度的变化,尤其针对767与3072字节限制进行深入分析。

一、前言

      这篇文章是基于之前的一篇:导入sql文件报错:1071 Specified key was too long; max key length is 767 bytes ,当时只是知道主键长度不能超过767字节,但是并不知道为什么会这么规定,这个767有什么特殊的含义吗?这篇文章会尽量去解读一下这个767是什么,为什么是它。

二、长度限制767的来源

1、compact行格式溢出

      主键是聚集索引,也是其他二级索引的基础,所以每个主键要在一个数据页存储,不能分布在其他的溢出页,不能像其他大字段一样溢出数据页。

      就是说博主认为,这个主键部分,包括单列索引的部分,它们都是一个整体的,不会像是其他数据文件一样溢出。而对于mysql5.7之前,默认使用的行格式是compact模式。对于这个模式大家可以百度学习下,这里不做详细解答。我们只需要知道这个行格式的溢出设定:当某一列中的数据非常多的话,在数据页中只会存储该列的前767个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做行溢出,存储超出767字节的那些页面也被称为溢出页。

如图(偷来的图):
在这里插入图片描述

      这个768字节是不是跟767字节很接近了,博主看到这个概念的时候就在想,主键长度就像我们上面说的那样,超过这个阈值就会溢出到地址页,当然,这只是猜想,下面咱们尽量验证下。

2、查询之前报错的数据库,版本是小于5.7的,也就是使用的是compact行格式。

3、按照当时的计算

//这里是由于定义了多个字段共同组成主键索引
(255+10+10)*3 = 825  //在用utf8作为字符集的时候,超过了规定的767 bytes
(255+10+10)*2 = 550  //当该用gbk作为字符集的时候
(255+10+10)*4 = 1100  //当用utf8m4作为字符集的时候,也超标了

      这里的主键长度,后来经过查询mysql手册,并没有明确的说限制它的长度。但是我们发现了另外两个概念:单列索引和索引前缀

4、单列索引和索引前缀

      翻阅诸多资料,大家的一致说法是当单列索引长度大于767的时候,会取前767个字节作为索引前缀,会有个warning但不会直接报错。针对组合索引,比如a,b,c三个字段的组合索引,要求索引长度加起来不能超过3072字节。

767的另一种计算方式:256×3-1

      这个3是字符最大占用空间(utf8)。但是在5.5以后,开始支持4个字节的utf8mb4,因此单纯的767在某些情况下就不是那么通用了,mysql新增一个参数叫做 innodb_large_prefix,可以允许索引达到3072字节。

      这里可以推断,我们一直说的主键长度,其实算是主键索引,而主键索引也是属于单列索引之内的,因为只有一列数据嘛。也就是符合以上mysql对于单列索引长度的限制。

5、为什么主键长度超标会报错,单列索引只是warning呢

      这部分,博主推断是由于主键部分是聚集索引,聚集索引的特性要求它必须是最为高效的结构。按照上面compact行格式的定义,超过阈值之后,主键索引也会分布到其他的页面,这对于主键索引的完整性来说是不可接受的,因此主键长度超标就会报错。而普通的单列索引定义没有主键索引这么严格。

================ 2019/7/26更新 ================================

6、mysql手册证实行格式和索引关键字限制的关系

      在手册中有一句话:记录的值的数据长度是由767个字节的索引关键字限制的限制 COMPACT和REDUNDANT排格式,并为3072个字节DYNAMIC和 COMPRESSED列格式。

链接:https://dev.mysql.com/doc/refman/5.7/en/create-table-secondary-indexes.html

这句话充分验证了我们的猜想,索引前缀的长度限制确实是和行格式有关的。

7、那么行格式的溢出阈值是768字节,为什么索引长度限制是767呢

      本来这个问题博主也是百思不得其解,准备以后再来解答。但是群上有小伙伴对这个也有疑问,那就只能硬着头皮上了。在翻阅compact行格式定义的时候,在手册上发现一句话:

对于每个非NULL可变长度字段,记录头包含一个或两个字节的列长度。如果列的一部分存储在溢出页面的外部,或者最大长度超过255个字节且实际长度超过127个字节,则只需要两个字节。对于外部存储列,2字节长度表示内部存储部分的长度加上指向外部存储部分的20字节指针。内部部分为768字节,因此长度为768 + 20。20字节指针存储列的真实长度。

手册链接: https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html

      恍然大悟,如梦初醒。原来还要有1-2个字节要存储列长度,妥妥的。

新解释: 这个768是行格式规定的溢出阈值,对于compact行格式,我们存储一个varchar的列,实际存储数据占用的空间包括:实际数据+真实数据占用字节的长度+null值信息,当规定not null的时候,null值信息不占用空间,那么实际能存储的就是:实际数据 + 真实数据占用字节的长度,这里的真实数据占用字节的长度一般是1-2个字节代表列长度,所以:

767限制 = 768字节 - 1字节的列长度

      也就是说不管是对于什么编码存储,最少是要有一个字节来标识列长度的,所以768-1 = 767,也就是最多会留767字节来存储实际数据,这也是mysql规定索引前缀长度是767的原因。

三、升级mysql5.7之后的长度限制为3072字节

1、mysql5.7的行格式dynamic

      在mysql5.7之后,默认的行格式变成了dynamic行格式,它和compact行格式的大部分特性都相同,只是针对于溢出页的定义是不一样的,如图:

在这里插入图片描述

2、dynamic行格式特点

      对于dynamic行格式而言,假如数据量大导致溢出的话,那么数据页中只会保存一个溢出页的地址,大概是20个字节的指针,而对应的数据都只会存储在溢出页。

      网上查询到的资料是主键长度扩展到了3072字节,经过翻看手册,发现如果innodb_large_prefix启用(默认值),则对于InnoDB使用DYNAMICCOMPRESSED 行格式的表, 索引键前缀限制为3072字节。如果 innodb_large_prefix禁用,则对于任何行格式的表,索引键前缀限制为767字节。

mysql5.7默认的innodb_large_prefix是启用状态,所以对应的索引键前缀是可以设置为3072字节的,我们也可以愉快的设置:varchar(255)了。

3、对3072字节的猜想

      首选我们知道,innodb的每个数据页大小是16KB,也就是16384字节。innodb规定每个数据页至少要有两行记录,而每行记录主要是:行数据+27个字节的额外信息。每个数据页还有页头页尾等信息,总计136字节,所以实际存储数据的N大小为:

				136 + 2*(27+N) < 16384     N=8098字节

      假设在数据量的大小不足以溢出数据页的时候,那么每行的长度至少要小于8098字节,mysql给出的索引长度小于这个值即可。至于为什么要是3072字节,在手册上有这样一句话:

      如果通过在创建MySQL实例时指定选项将InnoDB 页面大小减小到8KB或4KB innodb_page_size,则索引键的最大长度将按比例降低,基于16KB页面大小的3072字节限制。也就是说,当页面大小为8KB时,最大索引密钥长度为1536字节,当页面大小为4KB时,最大索引密钥长度为768字节。

      我们可以理解为是为了当缩小数据页的大小为4KB的时候,我们还可以按照原有的习惯去建索引,而不至于因为索引长度过小,无法正常建索引等。( PS: 这部分也是猜测,后续有新的理解就补充上去)

四、查看当前行格式,修改行格式等

1、查看当前的行格式

mysql> show table status like '表名'\G;
*************************** 1. row ***************************
           Name: 表名
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic   =============就是它
           Rows: 5
 Avg_row_length: 3276
    Data_length: 16384
Max_data_length: 0
   Index_length: 98304
      Data_free: 0
 Auto_increment: 62
    Create_time: 2019-07-11 19:38:50
    Update_time: 2019-07-22 09:59:27
     Check_time: NULL
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)

2、新增行格式

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称

3、修改行格式

ALTER TABLE 表名 ROW_FORMAT=行格式名称

======================================================================

高危预警: 这篇文章,博主经过各种谷歌之后,只是得到了心中的一部分答案。而且并没有较权威的例子来证明这些东西。顶多算是抛砖引玉吧,博主心中的疑惑解开了一些,但是并没有得到充分的验证。希望这篇文章能给你带来一点灵感,如果有什么更好的理解,请务必告诉我,非常感谢,一起学习!

end

<think>嗯,用户问的是银行表适合用什么主键类型,这确实是个很关键的设计问题。从对话历史看,用户之前研究过MySQL安装和索引原理,还特意引用了聚集索引的资料,说明ta对数据库底层结构是有理解的。 银行场景对主键的要求特别严格啊。首先得绝对唯一,总不能让两个账户共用同一个ID吧。其次要稳定,想象一下如果主键会变化,转账记录就全乱套了。还得考虑性能,银行每天几百万笔交易,主键设计不好会拖慢整个系统。 用户引用的材料[1]提到聚集索引的特性很值得参考——主键直接关联数据存储位置。这意味着主键长度会影响所有二级索引的大小。比如如果用100字节字段主键,每建一个索引就要多存这100字节。所以UUID虽然唯一但太占空间,对银行海量数据不太友好。 材料[2]展示的浮点数精度问题也提醒我们,主键必须精确。银行系统里0.01元的误差都可能引发审计问题,所以像浮点型这种有舍入误差的类型绝对不适合做主键。 考虑到银行账户的核心属性:每个账户都有唯一账号,由央行或银行总行统一分配。这个账号本身符合主键的所有要求:天然唯一、终身不变、长度固定(通常19位数字)。用数字类型存储只要8字节,比字符串节省一半空间。而且数字比较速度比字符串快得多,这对高频交易的索引查找很重要。 不过得提醒用户注意特殊情况。有些银行把卡号当账号,但补卡时卡号会变。这时候就需要引入独立的account_id字段主键,把业务编号(卡号)作为普通索引。材料[3]提到的CHECK约束可以确保这个ID始终大于0。 最后想到材料[4]强调的事务特性。主键选择还会影响锁机制,自增数字主键插入时只锁末页,而随机主键可能导致页分裂。这对每秒处理上千交易的银行核心系统很关键。所以综合来看,独立数字主键是最平衡的选择。</think>在银行系统中,主键设计需兼顾**唯一性、稳定性、性能和业务需求**。根据引用[1]中聚集索引的特性(主键直接影响数据物理存储)和银行业务特点,推荐以下主键方案: --- ### **推荐方案:独立数字主键 (BIGINT AUTO_INCREMENT)** ```sql CREATE TABLE bank_account ( account_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 独立数字主键 account_number VARCHAR(20) NOT NULL UNIQUE, -- 业务账号(唯一索引) user_id VARCHAR(18) NOT NULL, -- 用户身份证号 balance DECIMAL(15,2) NOT NULL, -- 其他字段... ); ``` #### **优势分析** 1. **性能最优** - `BIGINT`仅需 **8字节存储**(引用[1]),比字符串类型(如UUID的32字节)节省60%以上空间,减少索引体积,提升查询速度。 - 自增主键插入时**避免页分裂**(数据按顺序写入),符合聚集索引特性[^1]。 2. **业务隔离** - 业务账号 (`account_number`) 与主键解耦: - 主键 `account_id` 仅用于内部关联(如外键、JOIN操作)。 - 业务账号可独立变更(如换卡号不影响主键)。 3. **符合银行业务规范** - 账户号通常为数字+字母组合(如"6230 5881 00XX XXXX"),用`VARCHAR`存储更灵活。 - 通过`UNIQUE`约束保证账号唯一性[^2]。 --- ### **其他方案对比** | **方案** | **问题** | **适用场景** | |-----------------------|--------------------------------------------------------------------------|--------------------------| | **身份证号做主键** | 长度大(18字节)、敏感信息泄露风险、公民换证时需级联更新 | 不推荐 | | **UUID做主键** | 存储大(36字节)、随机写入导致页分裂、索引碎片化(引用[1]性能问题) | 分布式系统(非银行核心表) | | **复合主键** | 增加复杂度、影响外键设计、聚集索引效率降低 | 需强制多字段唯一时 | --- ### **关键设计原则** 1. **主键与业务分离** - 主键仅作技术标识,业务标识(账号、卡号)通过`UNIQUE`约束实现[^2]。 2. **精度与范围** - 使用`BIGINT`而非`INT`:`INT`最大21亿(可能溢出),`BIGINT`支持**千万亿级数据**(\(9.2 \times 10^{18}\))。 - 金额用`DECIMAL`防止浮点误差(引用[2]精度验证)[^2]。 3. **约束保障** ```sql ALTER TABLE bank_account ADD CONSTRAINT chk_balance CHECK (balance >= 0); -- 余额非负约束[^3] ``` --- ### **示例:账户交易表设计** ```sql CREATE TABLE transaction ( tx_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 独立主键 from_account BIGINT UNSIGNED NOT NULL, -- 关联账户主键 amount DECIMAL(15,2) NOT NULL, FOREIGN KEY (from_account) REFERENCES bank_account(account_id) ) ENGINE=InnoDB; ``` > **说明**:通过数字主键关联,避免直接使用业务账号,确保外键效率与稳定性。 --- ### **为何不用业务账号做主键?** - **稳定性风险**:银行卡号升级(如16位→19位)需重建所有关联表。 - **效率问题**:字符串比较比数字慢3-5倍,影响JOIN性能。 - **存储浪费**:`VARCHAR(20)`主键比`BIGINT`多占用150%空间(引用[1]索引结构)[^1]。 --- **结论**:银行表主键应选择 **`BIGINT AUTO_INCREMENT`**,业务账号作为独立唯一字段。此设计兼顾InnoDB存储特性、性能与业务扩展性[^1][^3]。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁柱同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值