MySQL索引创建三大方式详解:从原理到实战优化
2026/8/4 4:05:35 网站建设 项目流程

1. 索引创建前的核心认知:为什么不能上来就建索引?

在数据库性能优化的工具箱里,索引无疑是那把最锋利、最常用的瑞士军刀。但很多朋友,尤其是刚接触MySQL的朋友,常常会陷入一个误区:一听到查询慢了,第一反应就是“加个索引试试”。这种“头痛医头,脚痛医脚”的做法,往往会导致数据库里塞满了冗余、低效甚至有害的索引,反而拖慢了整体的写入性能,让系统变得更“臃肿”。

所以,在动手创建索引之前,我们必须先搞清楚几个根本问题:索引到底是什么?它解决了什么问题?代价又是什么?这就像你要给家里的工具箱添置新工具,你得先明白这个工具是拧螺丝的还是敲钉子的,用错了地方,不仅活干不好,还可能把家具给弄坏了。

简单来说,索引就是数据库表的“目录”。想象一下一本没有目录的百科全书,你要找“光合作用”的相关内容,只能从第一页开始一页一页地翻。而有了目录(索引),你可以直接翻到“H”开头的部分,快速定位到相关页面。在MySQL的InnoDB引擎中,这个“目录”采用的是B+树数据结构。它有几个关键特性:1)数据存储在叶子节点,并且叶子节点之间通过指针双向链接,这使得范围查询和排序非常高效;2)非叶子节点只存储键值和指向子节点的指针,这使得树的高度可以保持得很低,通常3-4层就能存储海量数据,一次查询只需要3-4次磁盘I/O,性能提升是指数级的。

但是,创建和维护这个“目录”是有成本的:

  1. 空间成本:索引需要额外的磁盘空间来存储。一个表如果本身有10GB数据,其主键索引可能就要占10GB,如果你再为3个字段创建联合索引,可能又需要10GB。索引不是免费的午餐。
  2. 时间成本(写操作):每次执行INSERTUPDATEDELETE操作时,数据库不仅要修改表中的数据,还要更新所有相关的索引,以保持B+树的结构平衡。这意味着写操作会变慢。索引越多,写操作的负担就越重。
  3. 优化器选择成本:当你为多个字段创建了单列索引时,MySQL的查询优化器可能会面临“选择困难症”。例如,你为nameage分别创建了索引,执行WHERE name = ‘张三‘ AND age > 20时,优化器需要判断是使用name索引过滤后再回表查age,还是使用age索引(如果选择性更高)。这个判断过程本身有开销,选错了索引更是灾难。

因此,创建索引的第一原则是:按需创建,精准创建。绝不是越多越好。在决定创建索引前,你应该先使用EXPLAIN命令分析你的慢查询,观察type字段(访问类型,应尽量避免ALL全表扫描)、possible_keys(可能用到的索引)和key(实际用到的索引)。只有当确认是索引缺失或索引使用不当导致性能瓶颈时,才考虑创建或调整索引。

2. 方式一:CREATE INDEX —— 标准且灵活的表级索引创建

这是最常用、最标准的创建索引方式,专门用于在已存在的表上添加新的索引。它的语法清晰,功能灵活,是我们进行后期性能调优的主要手段。

2.1 基础语法与参数解读

其基本语法结构如下:

CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name ON table_name (col_name [(length)], ... ) [USING {BTREE | HASH}] [ALGORITHM = {DEFAULT | INPLACE | COPY}] [LOCK = {DEFAULT | NONE | SHARED | EXCLUSIVE}];

我们来拆解一下每个关键部分:

  • [UNIQUE | FULLTEXT | SPATIAL]:这是索引的类型修饰符。
    • UNIQUE:创建唯一索引,保证索引列组合的值在整个表中是唯一的。它除了加速查询,还承担了数据唯一性约束的角色。如果尝试插入重复值,语句会失败。
    • FULLTEXT:创建全文索引,专门用于对大文本字段(如TEXT类型)进行全文搜索。它使用倒排索引技术,可以高效地进行MATCH ... AGAINST查询,比如文章关键词搜索。注意:在MySQL 5.6及以上版本的InnoDB表中才支持全文索引。
    • SPATIAL:创建空间索引,用于地理空间数据类型(如GEOMETRY,POINT)。日常业务中较少使用。
    • 如果都不指定,则创建普通的非唯一索引(NONUNIQUE INDEX)。
  • index_name:你为这个索引取的名字。命名最好遵循一定的规范,例如idx_表名_字段名,这样在后期维护时一目了然。
  • ON table_name (col_name ...):指定在哪个表的哪些列上创建索引。你可以指定单列,也可以指定多列(即联合索引)。列的顺序至关重要,这涉及到索引的最左前缀匹配原则,我们稍后详细讨论。
  • [(length)]:可选的索引前缀长度。对于字符串类型的列(CHAR,VARCHAR,TEXT等),你可以只对字段值的前N个字符建立索引。这能显著减小索引大小,提升速度,但会降低选择性(区分度)。通常用于超长字段或前缀区分度足够高的场景。
  • [USING {BTREE | HASH}]:指定索引使用的算法。在InnoDB引擎中,默认且几乎唯一的选择是BTREEHASH索引虽然等值查询极快,但不支持范围查询和排序,且仅在Memory引擎中被支持。所以,在InnoDB的日常使用中,你几乎可以忽略这个选项。
  • [ALGORITHM][LOCK]:这两个是Online DDL相关的选项,对于生产环境操作大型表至关重要。
    • ALGORITHM=INPLACE:尽可能使用in-place算法(原地重建),避免复制整个表,减少磁盘IO和锁持有时间,对业务影响小。这是推荐的方式。
    • ALGORITHM=COPY:使用copy算法,会创建原表的临时副本,操作完成后替换原表。这会占用双倍磁盘空间,且在操作期间表被锁定,影响业务。
    • LOCK=NONE:允许在创建索引时,并发进行读写操作(SELECT, INSERT等),这是对业务最友好的模式。
    • LOCK=SHARED:允许并发读,但阻塞写。
    • LOCK=EXCLUSIVE:阻塞所有读写操作。

实操心得:在生产环境对百万级以上大表添加索引,务必加上ALGORITHM=INPLACE LOCK=NONE。虽然操作时间可能比COPY方式略长,但它能保证业务几乎不受影响。你可以通过SHOW PROCESSLIST或监控系统来观察DDL进度。

2.2 实战案例:联合索引与最左前缀原则

假设我们有一个用户订单表user_orders,包含以下字段:id(主键),user_id,product_id,order_time,status。业务上最常见的查询是:“查看某个用户最近一个月的已完成订单”。

一个新手可能会为user_idorder_time分别创建单列索引。但这样效率并不最优。优化器可能只选择其中一个索引(比如user_id),然后用这个索引找到所有该用户的订单,再回表(根据主键ID去主键索引树查完整数据行)过滤order_timestatus,如果该用户历史订单很多,回表次数就会非常庞大。

更优的方案是创建一个联合索引:

CREATE INDEX idx_user_time_status ON user_orders(user_id, order_time, status);

这个索引的B+树是如何组织的呢?它会先按user_id排序,user_id相同的,再按order_time排序,order_time相同的,再按status排序。数据行的主键id会附加在索引的叶子节点上。

此时,查询SELECT * FROM user_orders WHERE user_id = 123 AND order_time > ‘2024-01-01‘ AND status = ‘completed‘;的执行效率会极高。优化器可以沿着idx_user_time_status索引树,快速定位到user_id=123的叶子节点起始位置,然后沿着叶子节点的双向链表向后扫描,在扫描过程中就同时完成了order_timestatus的过滤,最后只需对少量完全匹配的结果进行回表取完整数据。这个过程被称为“索引覆盖扫描”,是性能最高的查询方式之一。

这里就引出了最左前缀匹配原则:MySQL的联合索引在查询时,会从索引的最左边列开始匹配,向右依次进行,直到遇到范围查询(>,<,BETWEEN,LIKE)就停止匹配。

  • 能使用索引的情况
    • WHERE user_id = 123(使用索引第一列)
    • WHERE user_id = 123 AND order_time > ‘...‘(使用索引第一、二列,第二列是范围,第三列status无法以索引方式过滤)
    • WHERE user_id = 123 AND status = ‘completed‘(使用索引第一列,但跳过了order_timestatus无法以索引方式过滤,只能回表后过滤)
  • 不能使用索引或使用效率低的情况
    • WHERE order_time > ‘...‘(缺少最左列user_id,无法使用该索引)
    • WHERE status = ‘completed‘(同上)
    • WHERE user_id = 123 AND order_time > ‘...‘ AND status = ‘completed‘(能使用索引前两列,status在索引内但处于范围列之后,无法直接用于索引过滤,但若该索引是(user_id, status, order_time),则status可以用于过滤)

踩坑记录:我曾遇到一个慢查询,表上有一个(a, b, c)的联合索引,查询条件是WHERE b = ? AND c = ?。开发同学很疑惑为什么索引没生效。这就是最左前缀原则的典型例子。解决方案要么调整查询条件(如果业务允许),要么为(b, c)创建一个新的索引(需权衡空间和写性能)。

3. 方式二:ALTER TABLE ADD INDEX —— 与表结构变更协同操作

ALTER TABLE ... ADD INDEX在功能上与CREATE INDEX几乎完全等价。它的核心价值在于,当你需要对表进行多项结构变更时,可以将添加索引的操作与其他ALTER操作合并到一条语句中执行,从而减少总的表重建次数,这对于大表来说能节省大量时间。

3.1 语法与使用场景

其语法如下:

ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name (col_name, ...) [USING {BTREE | HASH}] [ALGORITHM | LOCK]; -- 同样支持Online DDL选项

你可以把它看作是CREATE INDEX的一个“马甲”,底层实现是一样的。但在以下场景中,使用ALTER TABLE会更合适:

  1. 批量表变更:假设你需要在创建索引的同时,还想增加一个字段,或者修改某个字段的类型。

    • 低效做法
      CREATE INDEX idx_name ON t1(name); -- 第一次表重建 ALTER TABLE t1 ADD COLUMN new_col INT; -- 第二次表重建
    • 高效做法
      ALTER TABLE t1 ADD INDEX idx_name (name), ADD COLUMN new_col INT; -- 一次ALTER,完成所有变更,只重建一次表
  2. 某些MySQL版本或工具的兼容性:一些老的数据库管理工具或脚本可能更习惯使用ALTER TABLE语法来管理索引。

3.2 与CREATE INDEX的细微差异及选择建议

尽管功能相同,但在某些极其细微的层面,两者在元数据操作上可能有区别,但这对于99.9%的应用场景没有影响。对于开发者来说,可以遵循以下选择建议:

  • 单一操作:如果你的目标非常纯粹,就是“给某个表加一个索引”,那么使用CREATE INDEX语句更清晰、更直观,意图明确。
  • 复合操作:如果你计划在一条SQL语句中完成“加字段、加索引、改字段默认值”等多个操作,那么ALTER TABLE ... ADD INDEX, ADD COLUMN, ALTER COLUMN ...是你的不二之选。
  • 个人/团队习惯:统一团队内的SQL规范。如果团队约定使用ALTER TABLE来管理所有表结构变更(包括索引),那就保持一致。

从性能角度,只要使用了相同的ALGORITHMLOCK选项,两者最终的执行效率是一致的。关键在于你是否利用了“批量操作”的优势。

注意事项:无论是CREATE INDEX还是ALTER TABLE ... ADD INDEX,在操作执行期间,都会获取表的元数据锁(MDL)。虽然ALGORITHM=INPLACE LOCK=NONE可以减少数据层面的阻塞,但元数据锁在操作开始和结束的瞬间仍然需要短暂排他锁。如果此时有未提交的长事务正在访问该表,这个DDL操作可能会被阻塞,直到长事务结束。因此,执行DDL前,检查information_schema.innodb_trx表,避开业务高峰和长事务,是一个好习惯。

4. 方式三:建表时定义索引 —— 设计优先的实践

第三种方式是在使用CREATE TABLE语句创建新表时,直接定义好索引。这是一种“设计优先”的思路,在项目初期,表结构明确、数据量为零时,这是最高效、最规范的做法。

4.1 在建表语句中嵌入索引定义

你可以在CREATE TABLE的列定义之后,使用INDEXUNIQUE INDEXPRIMARY KEYFULLTEXT INDEXSPATIAL INDEX等关键字来定义索引。

CREATE TABLE `employee` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `emp_no` varchar(20) NOT NULL COMMENT ‘员工工号‘, `name` varchar(100) NOT NULL COMMENT ‘姓名‘, `department_id` int(11) NOT NULL COMMENT ‘部门ID‘, `hire_date` date NOT NULL COMMENT ‘入职日期‘, `salary` decimal(10,2) DEFAULT NULL COMMENT ‘薪资‘, `resume` text COMMENT ‘简历‘, `location` point DEFAULT NULL COMMENT ‘办公地点坐标‘, PRIMARY KEY (`id`), -- 主键索引 UNIQUE KEY `uk_emp_no` (`emp_no`), -- 唯一索引 KEY `idx_department_hire` (`department_id`,`hire_date`), -- 联合索引 KEY `idx_name` (`name`(10)), -- 前缀索引(只取name的前10个字符) FULLTEXT KEY `ft_resume` (`resume`), -- 全文索引 SPATIAL KEY `sp_location` (`location`) -- 空间索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘员工表‘;

在这个例子中,我们在建表时一口气定义了6个索引,涵盖了各种类型。这样做的好处是:

  • 一步到位:表结构和索引同时创建,无需后续再执行额外的DDL。
  • 避免数据迁移:如果表创建后再添加索引,对于大表,ALGORITHM=COPY会复制数据,ALGORITHM=INPLACE虽然不复制数据,但也要重建索引树。而在空表上定义索引,几乎没有成本。
  • 设计文档化:建表语句本身就是最好的表结构文档,索引作为性能设计的一部分,清晰地记录在案,便于团队协作和后续维护。

4.2 主键索引的特殊性与最佳实践

主键索引(PRIMARY KEY)是所有索引中最特殊的一个。在InnoDB引擎中,表就是按照主键索引组织的一个B+树,这被称为“聚集索引”。也就是说,数据行实际存储在主键索引的叶子节点上。因此:

  1. 每个InnoDB表必须有且只有一个主键。如果你没有显式定义,InnoDB会首先找一个非空唯一索引(UNIQUE NOT NULL)来充当主键。如果也没有,则会自动生成一个6字节的隐藏行ID(DB_ROW_ID)作为主键。这个隐藏主键对性能监控和复制可能不友好,所以强烈建议显式定义主键
  2. 主键的选择至关重要。一个好的主键应该具备:
    • 唯一且非空:这是基本要求。
    • 长度短:因为所有二级索引的叶子节点都存储着主键值(用于回表)。主键过长,会导致所有二级索引体积膨胀。
    • 顺序递增:最好使用AUTO_INCREMENT的整型(如BIGINT)。顺序写入能充分利用B+树的特性,减少页分裂,提升插入性能。使用无序的UUID或业务字段(如身份证号)作为主键,在插入时可能导致频繁的页分裂和随机I/O,严重影响写入吞吐。
    • 业务无关性:尽量避免使用具有业务含义的字段(如身份证号、手机号)作为主键。业务规则可能变化,而主键一旦确立,修改成本极高。使用自增ID作为代理主键是行业最佳实践。

经验之谈:我曾接手过一个系统,使用VARCHAR(32)的UUID作为主键。随着数据量增长到千万级,插入性能急剧下降,磁盘空间占用也比预期大很多。后来我们通过增加一个BIGINT自增ID作为主键,并将原UUID作为唯一业务标识列,并为其创建唯一索引,性能得到了显著改善。虽然增加了一个索引,但瘦身后的二级索引和顺序写入带来的收益远大于代价。

4.3 前缀索引与索引选择性计算

对于VARCHAR(255)TEXT这类长字符串列,为其创建完整长度的索引会非常庞大。这时可以考虑前缀索引,即只对字段值的前N个字符建立索引。关键是如何确定这个N?

这里需要引入“索引选择性”的概念:选择性 = 不重复的索引值数量 / 总记录数。选择性越高(越接近1),索引的过滤效果越好。

我们可以通过查询来估算不同前缀长度的选择性:

-- 计算整个列的选择性 SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name; -- 计算前N个字符的选择性 SELECT COUNT(DISTINCT LEFT(column_name, 10)) / COUNT(*) AS selectivity_10, COUNT(DISTINCT LEFT(column_name, 15)) / COUNT(*) AS selectivity_15, COUNT(DISTINCT LEFT(column_name, 20)) / COUNT(*) AS selectivity_20 FROM table_name;

目标是找到一个最小的N,使得selectivity_N非常接近完整列的选择性。例如,完整列选择性是0.85,前10个字符是0.82,前15个字符是0.84,那么选择10或15作为前缀长度都是不错的折衷。创建前缀索引的语法就是在列名后加上长度,如KEY idx_name (name(10))

警告:前缀索引有一个明显的缺点:它无法用于ORDER BYGROUP BY操作,也无法覆盖扫描(因为索引里只有部分数据)。同时,LIKE ‘%keyword‘这种左模糊查询,前缀索引也无能为力。因此,它通常只用于等值查询(WHERE name = ‘...‘)且字段确实很长的场景。

5. 索引创建后的管理与优化实战

创建索引不是一劳永逸的事情。索引需要像汽车一样定期“保养”,并且要根据业务查询的变化进行“调校”。

5.1 如何查看与评估现有索引

首先,你得知道库里有哪些索引,它们的使用情况如何。

  1. 查看表结构SHOW CREATE TABLE table_name\G可以清晰地看到所有索引的定义。
  2. 查看索引统计信息SHOW INDEX FROM table_name;这个命令非常有用,它会列出表中所有索引的详细信息,包括:
    • Cardinality(基数):索引中不重复值的估计值。这个值对于优化器决定是否使用该索引至关重要。Cardinality/ 表总行数 约等于该索引的选择性。注意:这是一个采样估计值,有时可能严重失准。
    • Index_type:索引类型(大部分是BTREE)。
    • Comment:可能包含更多信息。
  3. 分析索引使用情况:MySQL提供了performance_schemasys库来监控索引使用。一个更直接的命令是SELECT * FROM sys.schema_unused_indexes;(需要先安装sys库)。这个视图会列出可能从未被使用过的索引,它们是“删除候选者”。

5.2 索引维护:重建与优化

随着数据的增删改,B+树索引可能会产生碎片(比如大量删除后,页中留下空洞,或者页分裂导致的不连续),这会影响索引的扫描效率。

  • OPTIMIZE TABLE table_name;:这是一个重量级操作,相当于重建表并优化索引,会锁表,释放未使用的空间。适用于表数据经过大量修改(如删除了一半数据)后的彻底优化。
  • ALTER TABLE table_name ENGINE=InnoDB;:通过重新指定引擎来重建表,也能达到优化索引和整理碎片的效果,与OPTIMIZE TABLE类似。
  • ANALYZE TABLE table_name;:这个操作不整理数据碎片,而是重新收集表的统计信息(包括Cardinality)。当发现优化器执行计划选择错误,或者SHOW INDEX中的Cardinality值看起来明显不合理时,应该执行此命令。它比OPTIMIZE轻量,通常不锁表(读锁)。

维护策略建议:对于核心业务表,可以定期(例如每周低峰期)对变更频繁的表执行ANALYZE TABLE。对于经历了大规模数据删除的表,可以考虑在维护窗口执行OPTIMIZE TABLE

5.3 索引失效的常见陷阱与排查

即使创建了索引,查询也未必会走索引。以下是一些常见的索引失效场景:

  1. 对索引列进行运算或函数操作WHERE YEAR(create_time) = 2024会导致create_time上的索引失效。应改为WHERE create_time >= ‘2024-01-01‘ AND create_time < ‘2025-01-01‘
  2. 隐式类型转换:如果列user_id是字符串类型(VARCHAR),而查询写成了WHERE user_id = 123(数字),MySQL会进行隐式转换,导致索引失效。务必保持类型一致。
  3. 使用OR连接条件:如果OR前后的条件列分别有索引,MySQL有时可能使用index_merge优化,但很多时候它会直接选择全表扫描。例如WHERE a = 1 OR b = 2,如果(a)(b)上有单独索引,可能不如为(a,b)创建一个联合索引,或者拆成两个查询用UNION
  4. LIKE以通配符开头WHERE name LIKE ‘%张‘无法使用name上的索引。如果业务必须支持模糊查询,可以考虑使用全文索引,或者将数据同步到专门的搜索引擎(如Elasticsearch)。
  5. 索引列参与!=<>判断:大多数情况下,WHERE status != ‘active‘无法有效利用status上的索引。
  6. 查询优化器“误判”:当表中数据量很少,或者优化器认为使用索引的回表成本高于直接全表扫描时,它可能会放弃使用索引。这时可以通过FORCE INDEX (index_name)强制使用索引,但这只是临时方案,更好的办法是更新统计信息(ANALYZE TABLE)或重新审视索引设计。

排查索引是否失效,最强大的工具就是EXPLAIN。重点关注type列(从优到劣:system>const>eq_ref>ref>range>index>ALL),key列(实际使用的索引),以及Extra列(是否出现Using filesort,Using temporary等)。

5.4 何时应该删除索引?

索引不是银弹,冗余的索引就是负担。以下情况应考虑删除索引:

  1. 重复索引:如已经存在联合索引(A, B),再创建一个单列索引(A)就是冗余的,因为前者可以完全覆盖后者的功能。(A)(A,B)的前缀,应该删除(A)
  2. 从未被使用过的索引:通过sys.schema_unused_indexes或慢查询日志分析,确认某个索引在很长时间内从未被任何查询使用过。
  3. 选择性极差的索引:例如在一个“性别”列上创建索引,只有‘M‘和‘F‘两个值,选择性低于0.5,这种索引几乎无法有效过滤数据,优化器通常也不会选择它。
  4. 维护成本过高的索引:在更新极其频繁的列上创建索引,会严重拖慢写入速度。如果业务上对该列的查询需求并不迫切,可以考虑删除。

删除索引的语法很简单:DROP INDEX index_name ON table_name;或者ALTER TABLE table_name DROP INDEX index_name;。删除前,务必确认该索引确实不再需要,并选择在业务低峰期操作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询