在MySQL开发中,普通索引、唯一索引和主键索引经常被放在一起讨论,但它们解决的其实不是同一个问题。主键首先解决的是“这条记录是谁”,唯一索引解决的是“这个值能不能重复”,普通索引解决的是“怎样更快找到符合条件的数据”。如果把三者简单理解成不同速度等级的索引,很容易在数据库设计阶段做出错误选择。
对于使用InnoDB的MySQL表,主键还有一个特殊作用:主键索引就是聚簇索引,数据行本身按照主键组织在B+Tree的叶子节点中。因此,主键不仅是一个约束,同时也是InnoDB组织数据的重要基础。
主键应该优先考虑稳定、短小、唯一的标识符。自增整数是传统选择,因为插入记录时通常按照递增方向追加,可以减少随机插入带来的B+Tree页分裂和空间管理压力。不过这并不意味着所有系统都必须使用AUTO_INCREMENT。UUID、业务生成的有序ID以及其他分布式ID方案同样可以作为主键,但需要考虑索引长度、插入顺序以及生成策略。
主键还有一个容易被忽略的影响:InnoDB的普通二级索引叶子节点中会保存主键值,用于回表定位数据。因此主键过长,会放大其他二级索引的存储空间和维护成本。一个包含大量二级索引的大型SaaS系统,如果采用很长的字符串作为主键,影响可能远远超过主键自身占用的空间。
唯一索引解决的是另外一个问题。假设用户表规定邮箱地址不能重复,那么数据库应该通过UNIQUE约束或者唯一索引真正保证这个规则,而不是先执行一次SELECT检查,然后再INSERT。单纯依靠应用层检查存在竞态条件:两个并发请求可能同时检查到“邮箱不存在”,随后同时尝试写入。
唯一索引因此既是索引,也是数据完整性约束。手机号、外部系统订单号、租户内部的业务编号等,只要业务规则明确要求不能重复,就应该考虑唯一索引。
需要特别注意NULL。MySQL允许唯一索引中出现NULL,而且NULL与NULL并不按照普通相等值处理,因此一个唯一索引允许存在多个NULL,而不是只能存在一个NULL。不同数据库系统在NULL唯一性上的行为也可能不同,因此设计跨数据库应用时不能简单套用其他数据库的经验。
唯一性还受到字符集和排序规则影响。例如某些不区分大小写的字符排序规则下,'Test@example.com'和'test@example.com'可能被视为相同值。如果业务要求大小写敏感,就必须根据具体字段定义合适的字符集、COLLATE以及数据规范,而不能笼统地说“MySQL默认不区分大小写”。
普通索引则没有唯一性要求。它主要用于改善查询访问路径,例如订单表经常执行WHERE tenant_id=? AND status=? AND created_at>=?这样的查询,就可以根据实际查询模式设计联合索引。联合索引的字段顺序不能只靠“哪个字段选择性高”决定,而应该结合等值条件、范围条件、排序以及分页方式一起判断。
例如常见查询如果总是按照租户和时间查询,那么(tenant_id, created_at)可能比单独建立tenant_id和created_at两个索引更符合实际访问路径。如果查询还经常固定过滤status,则可能进一步形成(tenant_id, status, created_at)。但最终应该通过EXPLAIN、EXPLAIN ANALYZE、慢查询日志以及实际负载验证,而不是套用固定模板。
覆盖索引也是普通索引优化中经常使用的技术。如果查询所需要的字段已经全部包含在索引中,MySQL在特定执行计划下可以直接从索引获得所需数据,减少回表访问。不过覆盖索引不是“索引字段越多越好”。索引一旦变得过宽,就会增加磁盘空间、Buffer Pool占用以及INSERT、UPDATE、DELETE时的维护成本。
这里也需要纠正一个常见误解:不能简单地说“主键查询一定最快,唯一索引其次,普通索引最慢”。真正的性能取决于访问路径、返回行数、索引结构、缓存命中情况、回表成本以及查询本身。
例如通过主键查找单行记录通常非常高效,因为可以直接定位聚簇索引中的记录。但唯一索引查找同样可以快速定位记录,而普通索引如果具有较好的选择性,同样可以非常高效。反过来,如果普通索引对应的条件会返回表中大量记录,即使使用了索引,也可能需要大量回表,甚至优化器最终选择全表扫描。
写入成本也不能简单排序成“普通索引最差、主键最好”。每增加一个相关索引,INSERT、DELETE以及部分UPDATE操作都可能需要维护相应的索引结构。唯一索引还需要进行唯一性检查;主键则决定聚簇索引中的数据组织方式。因此真正应该关注的是索引数量、索引宽度、写入频率、数据分布和页面访问,而不是给三种索引固定排一个性能顺序。
还有一个实际问题经常被忽略:频繁更新的字段并不是因为“更新”两个字就不能建立索引。真正需要考虑的是更新是否频繁改变索引键值。如果一个字段经常变化,而且它参与多个索引,那么每次修改都可能带来更多索引维护工作。对于主键尤其如此,因为InnoDB的主键与聚簇数据组织直接相关,因此通常应该选择稳定的标识符,而不是把经常变化的业务属性作为主键。
实际数据库设计可以按照一个很简单的顺序判断:首先确定表需要什么稳定的主键;然后找出业务上必须唯一的数据,并使用UNIQUE约束保证完整性;最后根据真实查询建立普通索引和联合索引。索引数量则应该根据SQL访问模式和写入压力持续调整,而不是规定“每张表只能有几个索引”。
对于SaaS系统,还必须把tenant_id纳入整体设计。租户ID是否应该出现在联合索引的第一列,取决于实际SQL。如果绝大多数查询都是“某个租户的数据 + 时间范围”,那么(tenant_id, created_at)通常具有自然的访问路径;如果查询模式不同,索引顺序也应该随之调整。更大的租户与普通租户之间如果存在严重的数据量差异,还需要通过执行计划和实际负载观察是否出现数据倾斜。
因此,主键、唯一索引和普通索引并不是三个可以简单比较“谁最快”的选项。主键负责稳定标识和InnoDB聚簇数据组织,唯一索引负责落实业务唯一性,普通索引负责优化查询访问路径。真正成熟的数据库设计,不是给每个字段机械地加一个索引,而是让约束负责保证数据正确,让索引服务于真实SQL,再通过执行计划、慢查询和生产负载不断验证。数据库规模增长以后,索引之外还可能需要考虑分区、归档、读写分离、缓存甚至分库分表,但这些都应该建立在真实瓶颈已经被测量出来的基础上。
MySQL 普通索引、唯一索引和主键索引应该怎样选择
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP