SQL 索引
索引是数据库中用于加速查询的重要机制。本章将详细介绍索引的类型、创建方法和最佳实践。
什么是索引?
索引是一种数据结构,类似于书籍的目录,可以帮助数据库快速定位数据,而不必扫描整个表。
为什么需要索引?
-- 没有索引时,数据库需要扫描整个表
SELECT * FROM users WHERE email = '[email protected]';
-- 如果表有 100 万行,需要检查 100 万行数据
-- 创建索引后,数据库可以直接定位到目标行
CREATE INDEX idx_email ON users(email);
-- 查询时间从秒级降到毫秒级
解释:
- 索引将查询复杂度从 O(n) 降低到 O(log n)
- 索引占用额外的存储空间
- 索引会降低写入性能(INSERT、UPDATE、DELETE 需要更新索引)
索引类型
1. 主键索引(Primary Key)
主键自动创建索引,每个表只能有一个主键:
-- 创建表时定义主键
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 主键索引特点:
-- 1. 唯一性约束
-- 2. 不能为 NULL
-- 3. 自动创建索引
2. 唯一索引(Unique Index)
确保列中的值唯一,允许 NULL 值:
-- 创建唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);
-- 或在创建表时定义
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE
);
解释:唯一索引与主键的区别:
- 一个表可以有多个唯一索引,但只能有一个主键
- 唯一索引允许 NULL 值,主键不允许
3. 普通索引(Index)
最基本的索引类型,没有唯一性约束:
-- 创建普通索引
CREATE INDEX idx_name ON users(name);
-- 或在创建表时定义
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
INDEX idx_name (name)
);
4. 复合索引(Composite Index)
在多个列上创建的索引:
-- 创建复合索引
CREATE INDEX idx_name_age ON users(last_name, first_name, age);
-- 复合索引遵循最左前缀原则
-- 以下查询可以使用索引:
SELECT * FROM users WHERE last_name = '张';
SELECT * FROM users WHERE last_name = '张' AND first_name = '三';
SELECT * FROM users WHERE last_name = '张' AND first_name = '三' AND age = 25;
-- 以下查询不能使用索引(跳过了 last_name):
SELECT * FROM users WHERE first_name = '三';
SELECT * FROM users WHERE age = 25;
最左前缀原则解释:
索引结构:(last_name, first_name, age)
查询条件 能否使用索引
─────────────────────────────────────────
last_name = '张' ✓ 使用索引
last_name, first_name ✓ 使用索引
last_name, first_name, age ✓ 使用索引
first_name = '三' ✗ 不能使用
age = 25 ✗ 不能使用
first_name, age ✗ 不能使用
last_name, age ✓ 部分使用(只用到 last_name)
5. 全文索引(Full-Text Index)
用于全文搜索,适用于文本内容的搜索:
-- 创建全文索引
CREATE FULLTEXT INDEX idx_content ON articles(title, content);
-- 使用全文搜索
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('数据库优化');
-- 布尔模式搜索
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE);
6. 空间索引(Spatial Index)
用于地理空间数据类型:
-- 创建空间索引
CREATE SPATIAL INDEX idx_location ON stores(coordinates);
-- 空间查询示例
SELECT * FROM stores
WHERE ST_Distance_Sphere(coordinates, POINT(116.4, 39.9)) < 5000;
创建索引
基本语法
-- 创建索引
CREATE INDEX index_name ON table_name(column);
-- 创建唯一索引
CREATE UNIQUE INDEX index_name ON table_name(column);
-- 创建复合索引
CREATE INDEX index_name ON table_name(column1, column2, column3);
-- 指定索引类型
CREATE INDEX index_name ON table_name(column) USING BTREE;
在创建表时定义索引
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
category_id INT,
price DECIMAL(10, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 创建普通索引
INDEX idx_category (category_id),
-- 创建唯一索引
UNIQUE INDEX idx_name (name),
-- 创建复合索引
INDEX idx_category_price (category_id, price)
);
使用 ALTER TABLE 添加索引
-- 添加主键
ALTER TABLE users ADD PRIMARY KEY (id);
-- 添加唯一索引
ALTER TABLE users ADD UNIQUE INDEX idx_email (email);
-- 添加普通索引
ALTER TABLE users ADD INDEX idx_name (name);
-- 添加复合索引
ALTER TABLE users ADD INDEX idx_name_age (name, age);
查看索引
-- 查看表的索引
SHOW INDEX FROM users;
-- 查看表的索引(简化)
SHOW INDEXES FROM users;
-- 使用信息模式查询
SELECT
INDEX_NAME,
COLUMN_NAME,
SEQ_IN_INDEX,
NON_UNIQUE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_NAME = 'users';
输出解释:
| 字段 | 含义 |
|---|---|
| Key_name | 索引名称 |
| Column_name | 索引列名 |
| Seq_in_index | 列在索引中的位置 |
| Non_unique | 是否允许重复值(0=唯一,1=非唯一) |
| Index_type | 索引类型(BTREE、HASH、FULLTEXT) |
删除索引
-- 删除索引
DROP INDEX idx_name ON users;
-- 使用 ALTER TABLE 删除索引
ALTER TABLE users DROP INDEX idx_name;
-- 删除主键
ALTER TABLE users DROP PRIMARY KEY;
索引设计原则
应该创建索引的情况
- WHERE 子句经常使用的列
-- 如果经常按 status 查询
SELECT * FROM orders WHERE status = 'pending';
-- 应该为 status 创建索引
CREATE INDEX idx_status ON orders(status);
- JOIN 操作的关联列
-- JOIN 的关联列应该创建索引
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id;
-- 确保 user_id 和 id 都有索引
- ORDER BY 或 GROUP BY 的列
-- 经常排序的列
SELECT * FROM products ORDER BY price DESC;
CREATE INDEX idx_price ON products(price);
-- 经常分组的列
SELECT category_id, COUNT(*) FROM products GROUP BY category_id;
CREATE INDEX idx_category ON products(category_id);
- 高选择性的列
-- 选择性 = 不同值数量 / 总行数
-- 选择性越高,索引效果越好
-- 高选择性(适合索引)
-- email 列:每个值都不同,选择性 ≈ 1
CREATE INDEX idx_email ON users(email);
-- 低选择性(不适合索引)
-- gender 列:只有 'M' 和 'F',选择性 ≈ 0.5
-- 不建议为 gender 单独创建索引
不应该创建索引的情况
-
频繁更新的列
- 每次更新都会更新索引,影响性能
-
数据量小的表
- 全表扫描可能比索引更快
-
低选择性的列
- 如性别、状态等只有几个不同值的列
-
很少用于查询的列
- 浪费存储空间
索引优化技巧
覆盖索引
如果查询的所有列都在索引中,数据库可以直接从索引获取数据,无需回表:
-- 创建覆盖索引
CREATE INDEX idx_user_info ON users(id, name, email);
-- 查询只需要索引中的列
SELECT id, name, email FROM users WHERE id = 1;
-- 不需要回表,查询更快
索引条件下推(ICP)
MySQL 5.6+ 支持索引条件下推,将 WHERE 条件的过滤下推到存储引擎:
-- 创建复合索引
CREATE INDEX idx_name_age ON users(name, age);
-- 查询条件
SELECT * FROM users WHERE name LIKE '张%' AND age > 20;
-- 在 MySQL 5.6 之前:先通过索引找到 name LIKE '张%' 的行,再过滤 age
-- 在 MySQL 5.6+:直接在索引中过滤 age > 20 的行,减少回表次数
避免索引失效
以下情况会导致索引失效:
-- 1. 在索引列上使用函数
SELECT * FROM users WHERE UPPER(name) = 'ZHANG'; -- 索引失效
SELECT * FROM users WHERE name = 'ZHANG'; -- 索引有效
-- 2. 隐式类型转换
SELECT * FROM users WHERE phone = 13800138000; -- phone 是 VARCHAR,索引失效
SELECT * FROM users WHERE phone = '13800138000'; -- 索引有效
-- 3. 使用 NOT、!=、<>、NOT IN
SELECT * FROM users WHERE status != 0; -- 索引可能失效
-- 4. 使用 OR 连接非索引列
SELECT * FROM users WHERE name = '张三' OR age = 25; -- 如果 age 没有索引,整个索引失效
-- 5. LIKE 以通配符开头
SELECT * FROM users WHERE name LIKE '%张'; -- 索引失效
SELECT * FROM users WHERE name LIKE '张%'; -- 索引有效
-- 6. 复合索引不满足最左前缀
-- 索引:(name, age)
SELECT * FROM users WHERE age = 25; -- 索引失效
使用 EXPLAIN 分析索引使用
-- 分析查询执行计划
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- 查看结果
-- type: ALL(全表扫描)、index(索引扫描)、range(范围扫描)、
-- ref(索引查找)、const(单值匹配)
-- key: 实际使用的索引
-- rows: 预计扫描的行数
-- 更详细的执行计划
EXPLAIN ANALYZE SELECT * FROM users WHERE name = '张三';
MySQL 8.0 索引新特性
1. 隐藏索引
可以隐藏索引而不删除,用于测试:
-- 创建隐藏索引
CREATE INDEX idx_name ON users(name) INVISIBLE;
-- 隐藏现有索引
ALTER TABLE users ALTER INDEX idx_name INVISIBLE;
-- 显示索引
ALTER TABLE users ALTER INDEX idx_name VISIBLE;
解释:隐藏索引不会被优化器使用,但仍然会随数据更新。可以用于测试删除索引后的性能影响。
2. 降序索引
MySQL 8.0 支持真正的降序索引:
-- 创建降序索引
CREATE INDEX idx_price_desc ON products(price DESC);
-- 查询时可以利用降序索引
SELECT * FROM products ORDER BY price DESC;
3. 函数索引
可以为表达式创建索引:
-- 创建函数索引
CREATE INDEX idx_lower_email ON users((LOWER(email)));
-- 查询时可以使用索引
SELECT * FROM users WHERE LOWER(email) = '[email protected]';
索引维护
重建索引
-- 重建表的索引
ALTER TABLE users ENGINE=InnoDB;
-- 分析表(更新统计信息)
ANALYZE TABLE users;
-- 检查表
CHECK TABLE users;
-- 优化表
OPTIMIZE TABLE users;
查看索引使用情况
-- 查看索引统计信息
SELECT
INDEX_NAME,
TABLE_NAME,
CARDINALITY,
SEQ_IN_INDEX
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'your_database';
-- 查看未使用的索引
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE INDEX_NAME IS NOT NULL
AND COUNT_STAR = 0
AND OBJECT_SCHEMA = 'your_database';
小结
本章我们学习了:
- 索引概念:加速查询的数据结构
- 索引类型:主键、唯一、普通、复合、全文、空间索引
- 索引创建:CREATE INDEX、ALTER TABLE
- 最左前缀原则:复合索引的使用规则
- 索引设计原则:何时创建、何时不创建
- 索引优化:覆盖索引、避免索引失效
- MySQL 8.0 新特性:隐藏索引、降序索引、函数索引
练习
- 为一个用户表创建合适的索引
- 使用 EXPLAIN 分析查询的索引使用情况
- 创建一个复合索引并测试最左前缀原则
- 找出并修复索引失效的问题