索引原理与应用

# 索引原理与应用

# 概述

索引是数据库中用于快速定位数据的数据结构,类似于书籍的目录。合理使用索引可以大幅提升查询性能,但也会增加存储空间和维护成本。

# 索引的基本原理

# 1. 索引结构

# B+树索引(最常用)

        [10, 20, 30]
       /    |    |    \
   [1,5,8] [12,15] [22,25] [35,40]
     |       |       |       |
   数据页   数据页   数据页   数据页

特点:

  • 叶子节点存储数据或数据指针
  • 非叶子节点只存储键值
  • 叶子节点之间有链表连接
  • 查询时间复杂度:O(log n)

# 哈希索引

-- 适用于等值查询
SELECT * FROM users WHERE id = 123;

-- 不适用于范围查询
SELECT * FROM users WHERE id > 100; -- 无法使用哈希索引

# 2. 索引类型

# 聚簇索引(Clustered Index)

  • 数据行按索引键值物理排序
  • 每个表只能有一个聚簇索引
  • 通常是主键索引
-- MySQL InnoDB中,主键就是聚簇索引
CREATE TABLE users (
    id INT PRIMARY KEY,  -- 聚簇索引
    name VARCHAR(50),
    email VARCHAR(100)
);

# 非聚簇索引(Non-Clustered Index)

  • 索引结构与数据存储分离
  • 索引叶子节点存储指向数据行的指针
  • 一个表可以有多个非聚簇索引
-- 创建非聚簇索引
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_user_name ON users(name);

# 索引的创建与管理

# 1. 创建索引

# 单列索引

-- 基本语法
CREATE INDEX index_name ON table_name(column_name);

-- 示例
CREATE INDEX idx_user_age ON users(age);

# 复合索引

-- 多列索引
CREATE INDEX idx_user_name_age ON users(name, age);

-- 索引列的顺序很重要
-- 可以使用:name, name+age
-- 不能单独使用:age

# 唯一索引

-- 确保列值唯一
CREATE UNIQUE INDEX idx_user_email ON users(email);

-- 复合唯一索引
CREATE UNIQUE INDEX idx_user_name_phone ON users(name, phone);

# 部分索引

-- PostgreSQL支持条件索引
CREATE INDEX idx_active_users ON users(name) WHERE status = 'active';

-- MySQL支持前缀索引
CREATE INDEX idx_user_email_prefix ON users(email(10));

# 2. 删除索引

-- 删除索引
DROP INDEX index_name;

-- MySQL语法
DROP INDEX index_name ON table_name;

-- 查看表的索引
SHOW INDEX FROM table_name;

# 索引使用策略

# 1. 何时创建索引

# 适合创建索引的情况

  • 经常出现在WHERE子句中的列
  • 经常用于JOIN的列
  • 经常用于ORDER BY的列
  • 经常用于GROUP BY的列
-- 经常查询的列
SELECT * FROM orders WHERE customer_id = 123;
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- 经常排序的列
SELECT * FROM products ORDER BY price;
CREATE INDEX idx_products_price ON products(price);

-- 经常分组的列
SELECT category, COUNT(*) FROM products GROUP BY category;
CREATE INDEX idx_products_category ON products(category);

# 不适合创建索引的情况

  • 很少查询的列
  • 频繁更新的列
  • 数据量很小的表
  • 列的基数很低(重复值很多)

# 2. 复合索引的使用

# 最左前缀原则

-- 创建复合索引
CREATE INDEX idx_user_name_age_city ON users(name, age, city);

-- 可以使用索引的查询
SELECT * FROM users WHERE name = 'John';                    -- ✓
SELECT * FROM users WHERE name = 'John' AND age = 25;       -- ✓
SELECT * FROM users WHERE name = 'John' AND age = 25 AND city = 'NYC'; -- ✓

-- 无法使用索引的查询
SELECT * FROM users WHERE age = 25;                         -- ✗
SELECT * FROM users WHERE city = 'NYC';                     -- ✗
SELECT * FROM users WHERE age = 25 AND city = 'NYC';        -- ✗

# 索引列顺序优化

-- 将选择性高的列放在前面
-- 假设:name有1000个不同值,age有50个不同值,city有10个不同值
CREATE INDEX idx_optimal ON users(name, age, city);  -- 推荐
CREATE INDEX idx_poor ON users(city, age, name);     -- 不推荐

# 索引性能分析

# 1. 执行计划分析

# MySQL

-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE name = 'John';

-- 详细执行计划
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE name = 'John';

-- 分析索引使用情况
EXPLAIN SELECT * FROM users WHERE name = 'John' AND age = 25;

# PostgreSQL

-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE name = 'John';

-- 详细执行计划(包含实际执行时间)
EXPLAIN ANALYZE SELECT * FROM users WHERE name = 'John';

-- 查看索引使用统计
SELECT * FROM pg_stat_user_indexes WHERE relname = 'users';

# 2. 索引效果监控

-- MySQL:查看索引使用情况
SHOW STATUS LIKE 'Handler_read%';

-- 查看未使用的索引
SELECT 
    t.TABLE_SCHEMA,
    t.TABLE_NAME,
    s.INDEX_NAME,
    s.COLUMN_NAME
FROM information_schema.TABLES t
LEFT JOIN information_schema.STATISTICS s ON t.TABLE_NAME = s.TABLE_NAME
WHERE t.TABLE_SCHEMA = 'your_database'
AND s.INDEX_NAME NOT IN (
    SELECT DISTINCT INDEX_NAME 
    FROM performance_schema.table_io_waits_summary_by_index_usage
    WHERE OBJECT_SCHEMA = 'your_database'
);

# 实战案例

# 案例1:电商订单查询优化

-- 原始查询(慢)
SELECT * FROM orders 
WHERE customer_id = 123 
AND order_date >= '2024-01-01' 
AND status = 'completed'
ORDER BY order_date DESC;

-- 创建优化索引
CREATE INDEX idx_orders_customer_date_status 
ON orders(customer_id, order_date, status);

-- 或者根据查询频率调整顺序
CREATE INDEX idx_orders_customer_status_date 
ON orders(customer_id, status, order_date);

# 案例2:用户搜索功能优化

-- 用户名模糊搜索
SELECT * FROM users WHERE name LIKE 'John%';

-- 创建前缀索引(MySQL)
CREATE INDEX idx_user_name_prefix ON users(name(10));

-- 全文搜索索引
CREATE FULLTEXT INDEX idx_user_name_fulltext ON users(name);
SELECT * FROM users WHERE MATCH(name) AGAINST('John' IN NATURAL LANGUAGE MODE);

# 案例3:分页查询优化

-- 传统分页(大偏移量时很慢)
SELECT * FROM products ORDER BY id LIMIT 10000, 20;

-- 优化方案:使用索引覆盖
SELECT p.* FROM products p
INNER JOIN (
    SELECT id FROM products ORDER BY id LIMIT 10000, 20
) tmp ON p.id = tmp.id;

-- 或使用游标分页
SELECT * FROM products WHERE id > 10000 ORDER BY id LIMIT 20;

# 索引维护

# 1. 索引碎片整理

-- MySQL:重建索引
ALTER TABLE table_name ENGINE=InnoDB;

-- 或者
OPTIMIZE TABLE table_name;

-- PostgreSQL:重建索引
REINDEX INDEX index_name;
REINDEX TABLE table_name;

# 2. 索引统计信息更新

-- MySQL:更新统计信息
ANALYZE TABLE table_name;

-- PostgreSQL:更新统计信息
ANALYZE table_name;

-- SQL Server:更新统计信息
UPDATE STATISTICS table_name;

# 常见问题与解决方案

# 1. 索引失效问题

-- 函数操作导致索引失效
SELECT * FROM users WHERE UPPER(name) = 'JOHN';  -- ✗ 索引失效
SELECT * FROM users WHERE name = 'John';         -- ✓ 使用索引

-- 类型转换导致索引失效
SELECT * FROM users WHERE id = '123';  -- ✗ 可能导致索引失效
SELECT * FROM users WHERE id = 123;    -- ✓ 使用索引

-- 前导通配符导致索引失效
SELECT * FROM users WHERE name LIKE '%John';   -- ✗ 索引失效
SELECT * FROM users WHERE name LIKE 'John%';   -- ✓ 使用索引

# 2. 索引选择性问题

-- 检查列的选择性
SELECT 
    COUNT(DISTINCT column_name) / COUNT(*) as selectivity
FROM table_name;

-- 选择性太低的列不适合单独建索引
-- 可以考虑复合索引
CREATE INDEX idx_status_date ON orders(status, order_date);

# 最佳实践

# 1. 索引设计原则

  • 为经常查询的列创建索引
  • 复合索引遵循最左前缀原则
  • 避免过多的索引
  • 定期监控索引使用情况

# 2. 性能优化建议

  • 使用覆盖索引减少回表操作
  • 合理设置索引长度
  • 避免在小表上创建索引
  • 定期维护索引统计信息

# 3. 监控与维护

  • 定期检查慢查询日志
  • 监控索引使用率
  • 及时删除无用索引
  • 定期进行索引碎片整理

# 总结

索引是数据库性能优化的重要手段,但需要在查询性能和维护成本之间找到平衡。通过合理的索引设计、定期的性能监控和及时的维护,可以显著提升数据库的查询性能。

在实际应用中,建议根据具体的业务场景和查询模式来设计索引策略,并通过持续的监控和优化来确保索引的有效性。