Skip to content

MySQL 索引创建规范

索引创建原则

必要性原则

  • 只为必要的列创建索引:避免为所有列创建索引
  • 基于查询频率:为频繁查询的列创建索引
  • 基于数据分布:为选择性高的列创建索引

选择性原则

  • 高选择性列:选择区分度高的列创建索引
  • 选择性计算
    sql
    -- 计算列的选择性
    SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name;
  • 选择性阈值:一般选择性大于20%的列适合创建索引

最左前缀原则

  • 组合索引顺序:将选择性高的列放在前面
  • 查询匹配:查询条件要匹配索引的最左前缀
  • 避免索引失效:避免跳过组合索引中的列

最小化原则

  • 索引列长度:使用最小的必要列长度
  • 前缀索引:对长字符串使用前缀索引
  • 索引数量:控制表的索引数量,一般不超过5个

覆盖索引原则

  • 包含查询列:索引包含查询所需的所有列
  • 避免回表:减少回表查询,提高查询性能
  • 适合场景:频繁的列查询和排序操作

索引命名规范

命名格式

索引类型命名格式示例
主键索引PRIMARYPRIMARY
唯一索引uk_表名_列名uk_user_email
普通索引idx_表名_列名idx_user_created_at
前缀索引idx_表名_列名_前缀长度idx_user_name_10
组合索引idx_表名_列名1_列名2idx_user_name_age

命名规则

  • 表名:使用小写字母,单词间用下划线分隔
  • 列名:使用小写字母,单词间用下划线分隔
  • 索引类型:使用标准前缀(uk_、idx_)
  • 长度限制:索引名长度不超过64个字符
  • 一致性:保持命名风格的一致性

示例

sql
-- 主键索引
ALTER TABLE `user` ADD PRIMARY KEY (`id`);

-- 唯一索引
ALTER TABLE `user` ADD UNIQUE KEY `uk_user_email` (`email`);

-- 普通索引
ALTER TABLE `user` ADD INDEX `idx_user_created_at` (`created_at`);

-- 前缀索引
ALTER TABLE `user` ADD INDEX `idx_user_name_10` (`name`(10));

-- 组合索引
ALTER TABLE `user` ADD INDEX `idx_user_name_age` (`name`, `age`);

索引创建规范

主键索引

规范

  • 使用自增ID:优先使用自增整数作为主键
  • 避免复合主键:除非业务需要,否则避免使用复合主键
  • 避免UUID:UUID作为主键会导致索引碎片和性能问题
  • 主键长度:使用最小的必要长度

示例

sql
-- 推荐
CREATE TABLE `user` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

-- 不推荐
CREATE TABLE `user` (
  `uuid` VARCHAR(36) NOT NULL,
  PRIMARY KEY (`uuid`)
) ENGINE=InnoDB;

唯一索引

规范

  • 业务唯一性:用于确保业务逻辑上的唯一性
  • 考虑NULL值:唯一索引允许NULL值
  • 组合唯一索引:适用于多列组合唯一的场景

示例

sql
-- 单列唯一索引
ALTER TABLE `user` ADD UNIQUE KEY `uk_user_email` (`email`);

-- 组合唯一索引
ALTER TABLE `order` ADD UNIQUE KEY `uk_order_user_product` (`user_id`, `product_id`);

普通索引

规范

  • 查询条件:为WHERE子句中频繁使用的列创建索引
  • 排序和分组:为ORDER BY和GROUP BY中的列创建索引
  • 连接条件:为JOIN操作中的连接列创建索引

示例

sql
-- WHERE条件列
ALTER TABLE `user` ADD INDEX `idx_user_status` (`status`);

-- ORDER BY列
ALTER TABLE `user` ADD INDEX `idx_user_created_at` (`created_at`);

-- JOIN连接列
ALTER TABLE `order` ADD INDEX `idx_order_user_id` (`user_id`);

前缀索引

规范

  • 长字符串列:对CHAR、VARCHAR、TEXT类型的长字符串使用前缀索引
  • 前缀长度:选择合适的前缀长度,平衡索引大小和选择性
  • 选择性测试:测试不同前缀长度的选择性

示例

sql
-- 前缀索引创建
ALTER TABLE `user` ADD INDEX `idx_user_name_10` (`name`(10));

-- 测试前缀长度的选择性
SELECT
  COUNT(DISTINCT LEFT(name, 5)) / COUNT(*) AS sel_5,
  COUNT(DISTINCT LEFT(name, 10)) / COUNT(*) AS sel_10,
  COUNT(DISTINCT LEFT(name, 15)) / COUNT(*) AS sel_15
FROM `user`;

组合索引

规范

  • 列顺序:按照选择性从高到低排列
  • 最左前缀:考虑查询的最左前缀匹配
  • 覆盖查询:包含常用的查询列
  • 避免冗余:避免创建与已有索引前缀重复的索引

示例

sql
-- 组合索引创建
ALTER TABLE `user` ADD INDEX `idx_user_name_age` (`name`, `age`);

-- 有效查询(匹配最左前缀)
SELECT * FROM `user` WHERE `name` = 'John';
SELECT * FROM `user` WHERE `name` = 'John' AND `age` = 30;

-- 无效查询(不匹配最左前缀)
SELECT * FROM `user` WHERE `age` = 30; -- 索引失效

索引创建步骤

1. 需求分析

  • 查询分析:分析SQL查询语句
  • 业务场景:了解业务使用场景
  • 数据特征:分析数据分布和增长趋势

2. 选择性评估

  • 计算选择性
    sql
    SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name;
  • 数据分布
    sql
    SELECT column_name, COUNT(*) FROM table_name GROUP BY column_name ORDER BY COUNT(*) DESC LIMIT 10;

3. 索引设计

  • 类型选择:根据查询类型选择索引类型
  • 列选择:选择合适的列和顺序
  • 命名规范:遵循命名规范

4. 测试验证

  • 创建索引:创建设计的索引
  • 性能测试:测试查询性能
  • 执行计划分析:分析索引使用情况
    sql
    EXPLAIN SELECT * FROM table_name WHERE column_name = 'value';

5. 上线监控

  • 监控性能:监控查询性能变化
  • 监控开销:监控索引对写入性能的影响
  • 定期评估:定期评估索引的有效性

索引维护规范

定期审查

  • 审查频率:每季度审查一次索引使用情况
  • 审查工具:使用EXPLAIN分析查询计划
  • 审查内容
    • 未使用的索引
    • 重复的索引
    • 低效的索引

索引优化

  • 删除未使用的索引

    sql
    -- 查找未使用的索引
    SELECT * FROM information_schema.statistics WHERE table_schema = 'database_name' AND table_name = 'table_name';
    
    -- 删除未使用的索引
    ALTER TABLE `table_name` DROP INDEX `index_name`;
  • 合并重复索引

    • 检查前缀重复的索引
    • 合并功能相似的索引
  • 优化索引结构

    • 调整组合索引的列顺序
    • 修改前缀索引的长度

索引碎片整理

  • 识别碎片

    sql
    -- 检查表的碎片情况
    SELECT table_name, data_free, engine FROM information_schema.tables WHERE table_schema = 'database_name' AND data_free > 0;
  • 碎片整理

    sql
    -- InnoDB表
    ALTER TABLE `table_name` ENGINE=InnoDB;
    
    -- 或使用OPTIMIZE TABLE
    OPTIMIZE TABLE `table_name`;

监控指标

  • 索引使用率

    • 监控索引的使用频率
    • 识别低效索引
  • 写入性能

    • 监控索引对写入性能的影响
    • 平衡读写性能
  • 存储空间

    • 监控索引占用的存储空间
    • 控制索引大小

索引使用注意事项

避免索引失效

  • 全表扫描:避免使用SELECT * 查询
  • 函数操作:避免在索引列上使用函数
  • 类型转换:避免索引列的类型转换
  • 不等于操作:!= 或 <> 操作可能导致索引失效
  • NULL值判断:IS NULL 或 IS NOT NULL 可能导致索引失效
  • LIKE操作:LIKE '%value' 会导致索引失效
  • OR条件:多个OR条件可能导致索引失效

优化建议

  • 使用覆盖索引

    sql
    -- 推荐:使用覆盖索引
    SELECT id, name FROM `user` WHERE name = 'John';
    
    -- 不推荐:SELECT *
    SELECT * FROM `user` WHERE name = 'John';
  • 避免函数操作

    sql
    -- 不推荐:索引列上使用函数
    SELECT * FROM `user` WHERE DATE(created_at) = '2023-01-01';
    
    -- 推荐:使用范围查询
    SELECT * FROM `user` WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02';
  • 使用合适的查询条件

    sql
    -- 不推荐:LIKE '%value'
    SELECT * FROM `user` WHERE name LIKE '%John';
    
    -- 推荐:LIKE 'value%'
    SELECT * FROM `user` WHERE name LIKE 'John%';

特殊场景处理

  • 大量插入

    • 插入前禁用索引
    • 插入后重建索引
  • 大表索引

    • 在线添加索引(MySQL 5.6+)
    • 使用pt-online-schema-change工具
  • 分区表索引

    • 分区键包含在索引中
    • 考虑本地索引和全局索引

索引案例分析

案例1:用户表索引设计

表结构

sql
CREATE TABLE `user` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL,
  `email` VARCHAR(100) NOT NULL,
  `age` INT UNSIGNED NOT NULL,
  `status` TINYINT UNSIGNED NOT NULL,
  `created_at` DATETIME NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

查询场景

  1. 根据email查询用户
  2. 根据status查询用户列表,按created_at排序
  3. 根据name和age查询用户

索引设计

sql
-- 唯一索引:email
ALTER TABLE `user` ADD UNIQUE KEY `uk_user_email` (`email`);

-- 组合索引:status, created_at
ALTER TABLE `user` ADD INDEX `idx_user_status_created_at` (`status`, `created_at`);

-- 组合索引:name, age
ALTER TABLE `user` ADD INDEX `idx_user_name_age` (`name`, `age`);

案例2:订单表索引设计

表结构

sql
CREATE TABLE `order` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` INT UNSIGNED NOT NULL,
  `product_id` INT UNSIGNED NOT NULL,
  `amount` DECIMAL(10,2) NOT NULL,
  `status` TINYINT UNSIGNED NOT NULL,
  `created_at` DATETIME NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

查询场景

  1. 根据user_id查询订单列表,按created_at排序
  2. 根据product_id查询订单
  3. 根据status和created_at查询订单

索引设计

sql
-- 组合索引:user_id, created_at
ALTER TABLE `order` ADD INDEX `idx_order_user_id_created_at` (`user_id`, `created_at`);

-- 普通索引:product_id
ALTER TABLE `order` ADD INDEX `idx_order_product_id` (`product_id`);

-- 组合索引:status, created_at
ALTER TABLE `order` ADD INDEX `idx_order_status_created_at` (`status`, `created_at`);

索引工具

分析工具

EXPLAIN

  • 功能:分析SQL执行计划
  • 使用
    sql
    EXPLAIN SELECT * FROM `user` WHERE `name` = 'John';
  • 输出分析
    • type:访问类型(ALL, index, range, ref, eq_ref, const, system, NULL)
    • key:使用的索引
    • rows:估计扫描的行数
    • Extra:额外信息

SHOW INDEX

  • 功能:查看表的索引信息
  • 使用
    sql
    SHOW INDEX FROM `user`;

INFORMATION_SCHEMA.STATISTICS

  • 功能:查询索引元数据
  • 使用
    sql
    SELECT * FROM information_schema.statistics WHERE table_schema = 'database_name' AND table_name = 'table_name';

优化工具

MySQLTuner

  • 功能:分析MySQL配置和索引使用情况
  • 使用
    bash
    perl mysqltuner.pl --user=root --pass=password

pt-index-usage

  • 功能:分析索引使用情况
  • 使用
    bash
    pt-index-usage --user=root --password=password /var/log/mysql/slow-query.log

pt-duplicate-key-checker

  • 功能:查找重复的索引
  • 使用
    bash
    pt-duplicate-key-checker --user=root --password=password

常见问题(FAQ)

Q1: 如何判断一个列是否适合创建索引?

A1: 判断列是否适合创建索引的方法:

  1. 选择性分析

    sql
    SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name;
    • 选择性大于20%的列适合创建索引
  2. 查询频率

    • 频繁出现在WHERE、ORDER BY、GROUP BY子句中的列
    • 频繁作为JOIN条件的列
  3. 数据分布

    • 数据分布均匀的列
    • 避免为枚举值较少的列创建索引

Q2: 如何选择组合索引的列顺序?

A2: 选择组合索引列顺序的原则:

  1. 选择性优先:将选择性高的列放在前面

    sql
    -- 假设name的选择性高于age
    ALTER TABLE `user` ADD INDEX `idx_user_name_age` (`name`, `age`);
  2. 查询频率:将频繁使用的列放在前面

  3. 排序和分组:将用于排序和分组的列放在后面

    sql
    -- 用于WHERE和ORDER BY
    ALTER TABLE `user` ADD INDEX `idx_user_status_created_at` (`status`, `created_at`);
  4. 覆盖查询:包含查询所需的所有列

Q3: 如何处理长字符串列的索引?

A3: 处理长字符串列索引的方法:

  1. 使用前缀索引

    sql
    -- 对长字符串使用前缀索引
    ALTER TABLE `user` ADD INDEX `idx_user_name_10` (`name`(10));
  2. 选择合适的前缀长度

    • 测试不同前缀长度的选择性
    • 平衡索引大小和选择性
  3. 考虑哈希索引

    • 对完全匹配的场景,可考虑使用哈希列
    sql
    ALTER TABLE `user` ADD COLUMN `name_hash` BIGINT UNSIGNED GENERATED ALWAYS AS (CRC32(`name`)) STORED;
    ALTER TABLE `user` ADD INDEX `idx_user_name_hash` (`name_hash`);

Q4: 索引越多越好吗?

A4: 不是,索引过多会带来以下问题:

  1. 写入性能下降:每次写入操作都需要更新索引
  2. 存储空间增加:索引会占用额外的存储空间
  3. 查询优化器负担:过多的索引会增加查询优化器的选择时间
  4. 维护成本增加:索引需要定期维护和优化

建议

  • 控制表的索引数量,一般不超过5个
  • 定期审查和清理未使用的索引
  • 优先考虑覆盖索引和组合索引

Q5: 如何处理索引失效的情况?

A5: 处理索引失效的方法:

  1. 分析执行计划

    sql
    EXPLAIN SELECT * FROM table_name WHERE column_name = 'value';
  2. 常见原因和解决方案

    • 函数操作:避免在索引列上使用函数
    • 类型转换:确保查询值与列类型一致
    • LIKE操作:避免使用LIKE '%value'
    • OR条件:考虑使用UNION替代OR
    • 最左前缀:遵循组合索引的最左前缀原则
  3. 优化查询

    • 重写查询语句
    • 调整索引结构
    • 使用强制索引(谨慎使用)
    sql
    SELECT * FROM table_name FORCE INDEX (index_name) WHERE column_name = 'value';

Q6: 如何监控索引的使用情况?

A6: 监控索引使用情况的方法:

  1. 使用Performance Schema

    sql
    -- 启用索引使用统计
    UPDATE performance_schema.setup_instruments SET enabled = 'YES' WHERE name LIKE 'wait/%';
    
    -- 查看索引使用情况
    SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage WHERE object_schema = 'database_name' AND object_name = 'table_name';
  2. 使用慢查询日志

    • 启用慢查询日志
    • 分析慢查询中的索引使用情况
  3. 使用pt-index-usage

    bash
    pt-index-usage --user=root --password=password /var/log/mysql/slow-query.log
  4. 定期审查

    • 每季度审查一次索引使用情况
    • 删除未使用的索引

Q7: 如何在大表上添加索引?

A7: 在大表上添加索引的方法:

  1. 在线添加索引(MySQL 5.6+):

    sql
    -- 在线添加索引
    ALTER TABLE `large_table` ADD INDEX `idx_large_table_column` (`column_name`) ALGORITHM=INPLACE, LOCK=NONE;
  2. 使用pt-online-schema-change

    bash
    pt-online-schema-change --alter="ADD INDEX idx_large_table_column (column_name)" --user=root --password=password D=database_name,t=large_table --execute
  3. 选择合适的时间

    • 在业务低峰期执行
    • 提前通知相关团队
  4. 监控执行过程

    • 监控服务器负载
    • 监控执行进度

Q8: 如何处理索引碎片?

A8: 处理索引碎片的方法:

  1. 识别碎片

    sql
    -- 检查表的碎片情况
    SELECT table_name, data_free, engine FROM information_schema.tables WHERE table_schema = 'database_name' AND data_free > 0;
  2. 重建索引

    sql
    -- InnoDB表重建
    ALTER TABLE `table_name` ENGINE=InnoDB;
    
    -- 或使用OPTIMIZE TABLE
    OPTIMIZE TABLE `table_name`;
  3. 定期维护

    • 为经常更新的表定期重建索引
    • 监控碎片增长情况
  4. 预防措施

    • 使用合适的主键类型
    • 避免频繁的删除操作
    • 控制索引数量