资讯动态

MySQL Key列PRI/UNI/MUL判定与SHOW INDEX排查

发布时间:2026/10/29 21:22:43 来源:尧图企业网站定制
打开一张表的结构目光扫到Key列看到PRI、UNI、MUL三个大写字母然后脑子里冒出一堆问号——这个场景我相信每个和 MySQL 打交道的人都经历过。有人以为MUL是“多重索引”有人以为UNI就是“这列不能重复”还有人看到联合唯一索引的首列显示UNI回去就把业务代码里的去重逻辑删了结果线上炸出一堆脏数据。这三个字母看着简单实际上它只是 MySQL 给你的一层“显示结论”背后藏着一整套判定规则而且这些规则和你的直觉经常是反着来的。我打算把这件事一次讲透PRI、UNI、MUL各自的判定条件是什么为什么同一列只能显示一个字母联合索引和非空约束会怎么影响显示结果以及最关键的——怎么用SHOW INDEX把这层“显示结论”撕开看到真正的索引结构。内容会从规则讲到建表验证再讲到几个我踩过的真实坑适合刚接触 MySQL 索引的新手也适合那些“用了一年但一直没细看 Key 列”的开发者。1. 先把这三个字母的身份定位搞对1.1 它不是索引类型是一句“结论性的备注”很多人第一反应是把PRI、UNI、MUL当成三种索引类型这从根上就错了。MySQL 的索引类型只有PRIMARY、UNIQUE、FULLTEXT、SPATIAL、普通索引这几种你在SHOW INDEX里看INDEX_NAME和NON_UNIQUE才能看到真身。而PRI/UNI/MUL是DESC 表名或SHOW COLUMNS FROM 表名输出结果里Key这一列的值它本质上是“MySQL 根据这张表上所有索引的定义倒推出来给这一列贴的一个标签”。理解这个定位非常关键。标签是结论索引定义才是事实。结论可能被简化、被覆盖、被多种索引定义挤压成同一个字母所以当你看到UNI的时候正确的反应不是“这列唯一”而是“这列参与了某个唯一索引而且排在第一顺位”。这两句话差别巨大后面我会用一个具体例子把这个差别撕开给你看。DESC的完整输出有六列Field、Type、Null、Key、Default、Extra。Key只是其中一列它的取值只有三种有效字母加上空值。命令行里空值显示为空白很多图形化客户端会渲染成NULL或者干脆空着。这个“空白”其实也是信息代表“这一列参与了索引但既不是主键、也不是唯一索引的首列、也不是普通索引的首列”。1.2 判定优先级PRI 压 UNIUNI 压 MUL同一个列可能同时挂着好几个索引比如一列既是主键、又建了个唯一索引、还顺手加了个普通索引。但Key列只能填一个值所以 MySQL 内部有明确的优先级顺序第一优先PRI第二优先UNI第三优先MUL兜底空这个优先级不是随便定的它反映的是约束的强弱。主键约束最强唯一且非空唯一索引次之唯一但可空普通索引最弱不唯一。列被贴标签时按“最强约束”优先展示方便你一眼识别出这一列最重要的身份。提示正因为存在优先级覆盖Key列永远无法告诉你“这一列一共有几个索引”。想知道全貌必须走SHOW INDEX FROM 表名或者查information_schema.STATISTICS。1.3 一张表先把规则固定下来下面这张表是我自己整理速查用的把三个字母的判定条件、容易误解的点、以及它到底保证了什么一次性列清楚。Key 值触发条件保证什么不保证什么PRI该列是PRIMARY KEY的一部分含联合主键的每一列全表唯一且一定非空不保证是单列主键也不保证自增UNI该列是某个UNIQUE索引的第一列含单列唯一索引在索引定义范围内唯一不保证单列唯一不保证非空不保证整个索引只有它一列MUL该列是某个非唯一索引的第一列或该列上只有普通索引这一列的值可以重复不保证一定可重复联合唯一索引的非首列也可能显示 MUL空该列只出现在多列索引的非首列位置且没有独立索引无额外约束不保证没索引它可能在联合索引里排第二、第三这张表建议直接存进备忘录。我见过太多人把UNI等同于“唯一约束”然后在联合唯一索引上栽跟头。2. PRI 的真实边界它只认真正的 PRIMARY KEY2.1 只有主键配叫 PRI没有例外PRI的判定条件非常干净这一列是不是PRIMARY KEY的组成部分。是就打PRI不是无论它上面挂了多少个唯一索引都轮不到PRI。这里有个常见的误解需要澄清。有些资料说“如果一个表没有主键但有唯一非空索引InnoDB 会把它当聚簇索引所以显示 PRI”。前半句是对的——InnoDB 在没有显式主键时确实会找一个所有列都NOT NULL的唯一索引来当聚簇索引如果找不到就隐式生成一个 6 字节的ROW_ID。但DESC的Key列不会因此显示PRI因为PRI严格对应PRIMARY KEY这个约束本身。没有主键的表Key列里一个PRI都不会出现。这个细节在实际排查中很有用。当你接手一张老表DESC看下来全是UNI和MUL、一个PRI都没有那基本可以判定这表没有主键。这种表在 InnoDB 里是隐式ROW_ID聚簇主从复制走 row 格式时如果ROW_ID冲突会导致主从不一致是个实打实的隐患。2.2 联合主键每一列都被打上 PRI这是我个人觉得最容易让人困惑的一点。假设建了这么一张表CREATE TABLE t_order_item ( order_id BIGINT UNSIGNED NOT NULL, item_id BIGINT UNSIGNED NOT NULL, sku VARCHAR(32) NOT NULL, qty INT NOT NULL DEFAULT 1, PRIMARY KEY (order_id, item_id), KEY idx_sku (sku) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;DESC t_order_item的输出里order_id和item_id两列都会显示PRI。第一次看到这个结果的人经常会问“item_id明明可以重复啊怎么也是主键”答案是联合主键的约束单位是“组合”不是“单列”。item_id单独看当然可以重复但(order_id, item_id)这个组合不能重复。所以PRI的准确语义是“这一列参与了主键约束”而不是“这一列单独唯一”。在联合主键表上如果你只拿item_id去做INSERT ... ON DUPLICATE KEY UPDATE或者以为它能当唯一键用逻辑就会出错。判断联合主键的各列顺序靠DESC没用得看SHOW INDEX FROM t_order_item WHERE Key_name PRIMARY里的Seq_in_index。2.3 主键隐含 NOT NULL写了 NULL 也会被改建表的时候如果写了id INT PRIMARY KEY NULLMySQL 不会报错它会默默把NULL改成NOT NULL并给一条 warning。所以任何标了PRI的列Null那一栏一定是NO。反过来如果你看到Null是YES而Key是UNI那这列就允许存 NULL而且允许存多个NULL——这一点是UNI和PRI最本质的行为差异下一节展开。注意TEXT、BLOB类型的列不能直接做主键除非指定前缀长度JSON列完全不能做索引。建表时如果想让一个大字段参与唯一约束只能先转成VARCHAR落一个哈希列或者用生成列。3. UNI 最容易骗人的地方3.1 判定条件唯一索引的“第一列”UNI的官方判定是该列是某个UNIQUE索引的第一列。注意“第一列”这三个字它是整个UNI语义的核心也是九成误判的来源。单列唯一索引UNIQUE KEY uk_mobile (mobile)mobile是第一列显示UNI没毛病。但联合唯一索引UNIQUE KEY uk_user_city (user_id, city)里面user_id是第一列它也会显示UNI。而city是第二列它不显示UNI如果city上没有别的独立索引Key那一栏就是空白如果它恰好又是另一个普通索引的首列就显示MUL。这个规则放到业务里看就很吓人了。假设你有一张用户城市绑定表(user_id, city)唯一意思是“同一个用户同一个城市只能有一条记录”。但DESC显示user_id是UNI新来的同事一看以为user_id只能出现一次于是在代码里写user_id查不到就去 Insert结果一个用户绑三个城市立刻触发唯一键冲突。这种问题排查起来极快但写的时候是真的容易踩。3.2 允许 NULL 的唯一索引依然是 UNI第二个高频误解UNI不等于非空。UNI只保证“在索引定义范围内非 NULL 的值不重复”而 SQL 标准里NULL ! NULL所以唯一索引允许多行存 NULL。CREATE TABLE t_user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, email VARCHAR(64) DEFAULT NULL, mobile VARCHAR(20) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_email (email), UNIQUE KEY uk_mobile (mobile) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;DESC t_user里email和mobile都显示UNI但email的Null是YESmobile的Null是NO。行为上可以插入 10 行email NULL不会冲突插入第 2 行mobile 13800000000立刻报Duplicate entry所以拿到一张表判断某列到底能不能重复不能只看Key是不是UNI必须把Key和Null两列一起看UNINO才是真正的单列唯一非空约束UNIYES意味着“非空值唯一NULL 可以无限重复”。提示这个特性在某些场景下是白送的便利。比如软删除场景用deleted_at做唯一索引的一部分未删除时是 NULL 不参与唯一性判断删除后写时间戳天然避开重复问题。但反过来如果业务上要求email必填唯一那就必须显式写NOT NULL否则脏数据会从 NULL 这个口子溜进来。3.3 版本与客户端的显示差异别硬背结论这里要老实说一句不同版本、不同客户端对Key列的渲染并不完全一致尤其是“唯一索引 允许 NULL”这种组合以及函数索引、前缀索引这些场景个别环境下的显示会有出入。我不建议把这套规则当成绝对真理背下来。真正可靠的做法是Key列用来快速扫一眼真要下判断就走SHOW INDEX FROM 表名。它的输出里有Non_unique0 表示唯一索引1 表示非唯一、Seq_in_index列在索引里的位置、Column_name列名、Sub_part前缀长度、Nullable是否可空这五个字段组合起来索引的真实结构一目了然比Key列那一个字母靠谱得多。4. MUL 是个“兜底标签”别过度解读4.1 三种典型来源MUL的含义最容易被简化成“这列可以重复”这个简化在大部分情况下碰巧是对的但它是结论而非原因。MUL的实际触发条件有这么几种该列是某个非唯一索引的第一列比如KEY idx_nick (nick)该列只是多列唯一索引的非首列但同时又独立建过普通索引该列是外键自动创建的索引的首列InnoDB 建外键时如果引用列上没有可用索引会自动建一个普通索引名字通常和约束名一致第三种情况特别隐蔽。很多人建了外键约束后发现子表某个字段莫名其妙多了个MUL还以为是哪次手滑加的索引。实际上那是 InnoDB 为了保证外键引用完整性自动补的删外键的时候可以一并清掉。4.2 多列索引的非首列Key 是空的这个点必须单独拎出来说因为它和“没有索引”看起来一模一样但含义完全不同。KEY idx_city_created (city, created_at)这种联合索引city显示MULcreated_at的Key是空白。看到空白不代表created_at上没索引它明明在idx_city_created里排第二位。这个显示设计的逻辑是DESC是按“列”组织的视图而索引是按“列的组合”组织的一个多列索引没法在单列视图里被完整表达只好把标签打给首列后面的列留空。这个空白的实际危害在于误导。有人看到created_at的Key是空判断“这个字段没索引查询会全表扫”于是又补了一个KEY idx_created (created_at)。结果表上多了个冗余索引写性能白掉一截。判断到底有没有索引覆盖永远以SHOW INDEX为准。4.3 前缀索引、降序索引、函数索引的显示前缀索引KEY idx_title (title(20))title显示MULSHOW INDEX里Sub_part是 20。前缀索引只能建在字符串类型上作用是省空间代价是排序和覆盖索引都会失效而且不能用作ORDER BY title的优化。唯一前缀索引UNIQUE KEY uk_title (title(20))是合法的DESC里依然显示UNI但它保证的只是“前 20 个字符不重复”第 21 个字符开始随便。如果你的业务字段长度超过前缀长度这个唯一约束就是残缺的实际生产里我建议宁可改字段类型、缩短长度也别用唯一前缀索引。MySQL 8.0 之后支持降序索引和函数索引DESC的Key列显示规则基本沿用上面的逻辑但函数索引在DESC里可能根本不体现因为它是表达式而不是列。这类索引一律走SHOW INDEX看Expression字段。5. 动手验证一张表把三个字母全造出来理论讲完不如自己跑一遍。下面这张表的设计目标就是在一张表里同时产出PRI、UNI、MUL和空白四种状态并且埋一个“联合唯一索引首列显示 UNI 但实际可重复”的坑。5.1 完整建表脚本DROP TABLE IF EXISTS t_demo; CREATE TABLE t_demo ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, user_id BIGINT UNSIGNED NOT NULL COMMENT 用户ID, mobile VARCHAR(20) NOT NULL COMMENT 手机号唯一非空, email VARCHAR(64) DEFAULT NULL COMMENT 邮箱唯一可空, nick VARCHAR(32) DEFAULT NULL COMMENT 昵称普通索引, city VARCHAR(32) DEFAULT NULL COMMENT 城市, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_mobile (mobile), UNIQUE KEY uk_email (email), UNIQUE KEY uk_user_city (user_id, city), KEY idx_nick (nick), KEY idx_city_created (city, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTPRI/UNI/MUL 验证表;这张表一共 5 个索引1 个主键、3 个唯一索引、2 个普通索引其中city同时出现在uk_user_city第二位和idx_city_created第一位。5.2 DESC 结果逐行解读执行DESC t_demo;输出大致如下FieldTypeNullKeyDefaultExtraidbigint unsignedNOPRINULLauto_incrementuser_idbigint unsignedNOUNINULLmobilevarchar(20)NOUNINULLemailvarchar(64)YESUNINULLnickvarchar(32)YESMULNULLcityvarchar(32)YESMULNULLcreated_atdatetimeNOCURRENT_TIMESTAMP逐行拆开看id显示PRI因为它是PRIMARY KEY (id)的唯一一列Extra里的auto_increment说明它自增。这里有个细节自增列一定是某个索引的第一列但自增列不一定是主键。MyISAM 允许自增列建在普通索引上InnoDB 则要求自增列必须是索引的第一列通常我们就直接绑主键了。user_id显示UNI这是整张表最坑的一行。它确实“唯一”但它唯一的是(user_id, city)这个组合单独一个user_id在表里可以出现无数次只要city不同。如果你只盯着Key列看很容易得出“user_id 不能重复”的错误结论。mobile显示UNINull为NO这是最标准的单列唯一非空约束插重复直接报错。email显示UNINull为YES也是唯一索引但允许多行 NULL。实际验证一下INSERT INTO t_demo (user_id, mobile, email, nick, city) VALUES (1, 13800000001, NULL, a, 北京); INSERT INTO t_demo (user_id, mobile, email, nick, city) VALUES (2, 13800000002, NULL, b, 上海); -- 两条都成功email 都是 NULL 不冲突 INSERT INTO t_demo (user_id, mobile, email, nick, city) VALUES (3, 13800000003, xexample.com, c, 广州); INSERT INTO t_demo (user_id, mobile, email, nick, city) VALUES (4, 13800000004, xexample.com, d, 深圳); -- 第二条报 Duplicate entry xexample.com for key uk_emailnick显示MUL因为它只有idx_nick这一个普通索引。city显示MUL因为它既出现在idx_city_created的第一位普通索引首列又出现在uk_user_city的第二位。按优先级普通索引首列触发MUL。created_at的Key是空白因为它只出现在idx_city_created的第二位没有独立索引。但它的查询性能其实是好的只要WHERE city ? ORDER BY created_at这样用联合索引能完整覆盖。5.3 SHOW INDEX 才是看结构的正确姿势SHOW INDEX FROM t_demo;输出字段比较多我只挑关键的看字段作用Key_name索引名PRIMARY就是主键Non_unique0 唯一索引1 非唯一索引注意是反的Seq_in_index该列在这个索引里排第几从 1 开始Column_name列名Sub_part前缀长度NULL 表示整列索引Cardinality基数估算值越大区分度越高Index_typeBTREE、HASH、FULLTEXT等想要一个更干净、更适合排查的视角直接查information_schema.STATISTICSSELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, SUB_PART, CARDINALITY, NULLABLE, INDEX_TYPE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA DATABASE() AND TABLE_NAME t_demo ORDER BY INDEX_NAME, SEQ_IN_INDEX;跑完你会看到uk_user_city有两条记录user_id是SEQ_IN_INDEX 1city是SEQ_IN_INDEX 2。这一眼就把前面那个“user_id显示UNI但其实不唯一”的谜团解开了。提示Cardinality是采样估算不是精确值。刚建完表、批量导入数据后它可能严重失真导致优化器选错索引。用ANALYZE TABLE t_demo;手动刷新一次比盯着这个数字猜有用。5.4 顺带说清最左前缀联合索引的匹配规则叫“最左前缀”通俗点说就是一本电话簿按“姓氏 名字”排序你只知道名字不知道姓氏就没法高效翻。uk_user_city (user_id, city)能高效支撑WHERE user_id ?、WHERE user_id ? AND city ?但支撑不了WHERE city ?。这条规则和DESC的Key列有个很妙的对应关系首列显示 MUL 或 UNI后面的列显示空白这个显示结构本身就是最左前缀规则的可视化。首列是入口后面的列是排序的次要键。所以看到空白也别急着补索引先想想查询条件是不是总能带上首列。验证方式很直接EXPLAIN SELECT * FROM t_demo WHERE user_id 1 AND city 北京; -- type ref, key uk_user_city, key_len 会用满两列 EXPLAIN SELECT * FROM t_demo WHERE city 北京; -- type index 或 ALL, key 大概率走 idx_city_created第二句之所以还能走idx_city_created是因为city恰好是那个索引的首列。如果没这个索引它就真的全表扫了。这也是为什么同一个字段建不建索引取决于你查询时它是“首列”还是“跟随列”。6. 排查实录几个反复出现的问题6.1 常见现象速查表现象大概率原因处理思路明明建了唯一索引Key 显示 MUL索引是多列唯一索引该列在第二位查SHOW INDEX看Seq_in_index主键列DESC显示 UNI 不是 PRI表上真的没有PRIMARY KEY只有唯一索引查SHOW INDEX有没有Key_name PRIMARY字段 Key 是空白但慢查询走得了索引它是某个联合索引的非首列别补索引优化查询条件带上首列允许 NULL 的唯一列插了两个 NULL 没报错唯一索引允许多个 NULLNULL ! NULL需要真唯一就加NOT NULL外键字段莫名多了个 MULInnoDB 自动建的辅助索引删外键时可一并清理联合主键的两列都显示 PRI联合主键每一列都参与约束唯一性看组合不看单列6.2 隐式类型转换让唯一索引失效这个坑和Key列没直接关系但它是最典型的“明明有UNI却查不到/插不进”的现场值得单独说。假设mobile是VARCHAR(20)你在代码里传了个数字SELECT * FROM t_user WHERE mobile 13800000000。MySQL 会按“字符串转数字”还是“数字转字符串”来比较规则是字符串列和数字比较时会把字符串列转成数字再比。这意味着每一行都要做一次转换索引直接失效全表扫。同时13800000000 带空格、13800000000x这类脏值在做数字转换后都等于13800000000唯一索引里明明两条不同的字符串查出来却是同一条。处理办法只有一条参数类型必须和列类型严格对齐。VARCHAR就老老实实传字符串Java 里别用Long接手机号PHP 里别指望弱类型自动转对。这条规矩我在任何项目里都会写进代码规范。6.3 索引不是越多越好Key列也不代表性能DESC看多了会有一种错觉只要Key列里没字母这列就该补个索引。这个思路很危险。每个索引都是一棵独立的 B 树写入时都要维护。一张表上挂七八个索引插入性能能掉一半以上。而且 MySQL 优化器在候选索引太多时选择成本也会上升偶尔还会选错。我一般的判断顺序是先看慢查询日志里真实的慢 SQL再看EXPLAIN的key和rows最后才考虑加索引。DESC只在接手新表、快速摸清结构时用它从来不是我决定加不加索引的依据。注意删除索引前先确认它没有被外键依赖。InnoDB 里如果某个索引是外键的支撑索引直接DROP INDEX会报错得先删外键约束。7. 我个人的几个使用习惯日常开发里DESC我基本只用来做两件事快速确认主键在不在以及扫一眼有没有明显的UNI误读风险。真正做索引评估一律走SHOW INDEX或者information_schema.STATISTICS因为那里才有Seq_in_index这个决定性字段。还有个小习惯值得分享接手一张陌生的表我会先跑一条 SQL 把所有索引按列展开直接看结构比看DESC快得多SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols, MIN(NON_UNIQUE) AS non_unique_flag FROM information_schema.STATISTICS WHERE TABLE_SCHEMA DATABASE() AND TABLE_NAME t_demo GROUP BY INDEX_NAME;一眼就能看出uk_user_city是user_id,city这样的组合idx_city_created是city,created_at。哪些是联合索引、哪些是单列索引、有没有重复定义的冗余索引全在这一张结果里。最后再补一句关于UNI的实操建议如果你的业务真的要求“某列全局唯一”建表时请把NOT NULL和UNIQUE一起写上别给 NULL 留口子。数据库层面的约束是最后一道防线代码里的判重逻辑再严也挡不住并发和手工改数据。这一点我吃过亏——线上有一批email为 NULL 的重复用户是运营手工导入时留下的等发现的时候已经几千条了清理起来极其麻烦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑