在上一章中,我们剖析了数据如何被组织并写入磁盘——B 树页的分裂与合并、LSM 树的分层压缩、WAL 的崩溃恢复机制。存储引擎回答了”数据怎么存”的问题,但数据库的另一个核心问题是:数据怎么找?
一张表可能有上亿行数据,如果每次查询都从头扫到尾,性能将不可接受。索引正是为解决这一问题而生——它用额外的空间换取查询时间的急剧缩短。本章将从索引的根本动机出发,逐一剖析 B+ 树、哈希、位图、空间与全文索引的原理,再深入覆盖索引、索引下推、最左前缀等优化技巧,最后系统分析索引失效的根因与规避方法。
前置知识
- Ch02 存储引擎:B 树页结构是 B+ 树索引的物理基础
- 数据结构基础:B+ 树、哈希表
一、为什么需要索引
1.1 全表扫描的代价
假设一张 orders 表有 1 亿行数据,每行约 200 字节,数据文件约 20 GB。现在执行以下查询:
SELECT * FROM orders WHERE user_id = 42;如果没有索引,数据库只能从数据文件的第一页开始,逐行检查 user_id 是否等于 42。这就是全表扫描(Full Table Scan)——时间复杂度 O(n),需要读取全部数据页。
全表扫描并非总是最差选择。当表很小、或查询需要返回大部分行时,全表扫描反而比索引查找更快——因为索引查找需要额外的随机 I/O。优化器会根据统计信息自动选择,详见查询处理与优化。
1.2 索引加速的原理
索引的本质是一种空间换时间的数据结构:在原始数据之外,维护一份有序的、更小的副本,使得查找不必遍历全部数据。
| 维度 | 全表扫描 | 索引查找 |
|---|---|---|
| 时间复杂度 | O(n) | O(log n) 或 O(1) |
| I/O 模式 | 顺序读(大量页) | 随机读(少量页) |
| 1 亿行约需读取 | ~20 GB 数据 | |
| 适用场景 | 返回大量行、小表 | 精确匹配、范围查询、排序 |
| 额外开销 | 无 | 占用存储空间、写入时需维护 |
以 B+ 树为例,1 亿行数据只需 34 层即可容纳,每次查找最多 34 次磁盘 I/O——从 20 GB 的顺序扫描骤降到个位数的随机读取,这就是索引的威力。
1.3 索引的分类体系
不同查询场景需要不同类型的索引。以下是数据库索引的完整分类:
接下来,将按数据结构逐一深入。
二、B+ 树索引
B+ 树是关系型数据库中最广泛使用的索引结构。MySQL InnoDB、PostgreSQL(默认 B-tree)、Oracle、SQL Server 的默认索引均基于 B+ 树或其变体。
2.1 B+ 树的结构
B+ 树是一种多路平衡搜索树,其核心特征是:所有数据都存储在叶子节点,内部节点只存键值用于路由。叶子节点通过双向链表串联,支持高效的范围扫描。
B+ 树的关键参数:
| 参数 | 含义 | 典型值 |
|---|---|---|
| 阶(Order) | 每个节点最多拥有的子节点数 | InnoDB 约 1200(16 KB 页 / 12 字节键) |
| 层数 | 根到叶子的路径长度 | 3~4 层可容纳数亿行 |
| 叶子链表 | 叶子节点间的双向链表 | 支持范围扫描的 O(k) 遍历 |
| 填充因子 | 节点实际使用率 | 通常 50%~100%,影响分裂频率 |
B+ 树的层数与数据量呈对数关系。假设每个内部节点有 1000 个子节点指针,3 层 B+ 树可容纳 10^9(10 亿)行,4 层可容纳 10^12(万亿)行。这意味着即使数据量从百万增长到十亿,查找开销只增加一次 I/O。
2.2 聚簇索引 vs 二级索引
**聚簇索引(Clustered Index)**将索引与数据合二为一——叶子节点直接存储完整的行数据。一张表只能有一个聚簇索引,因为数据只能按一种方式物理排列。
二级索引(Secondary Index)的叶子节点不存储行数据,而是存储主键值。通过二级索引查找数据时,需要先在二级索引中找到主键,再回到聚簇索引中查找完整行——这个过程称为回表。
-- InnoDB 中,主键就是聚簇索引CREATE TABLE users ( id BIGINT PRIMARY KEY, -- 聚簇索引 email VARCHAR(255), name VARCHAR(100), age INT, INDEX idx_email (email) -- 二级索引,叶子存 id 值);
-- 通过二级索引查找需要回表SELECT * FROM users WHERE email = 'alice@example.com';-- 步骤:idx_email 找到 id=5 → 聚簇索引找 id=5 的完整行| 维度 | 聚簇索引 | 二级索引 |
|---|---|---|
| 叶子节点存储 | 完整行数据 | 主键值 |
| 每表数量 | 1 个 | 多个 |
| 范围查询 | 高效(数据物理有序) | 需回表,可能大量随机 I/O |
| 插入顺序 | 按主键有序插入最优 | 随机插入,可能页分裂 |
| 存储开销 | 无额外索引空间 | 需额外存储索引 + 主键 |
如果二级索引的回表操作量很大(例如范围查询返回大量行),优化器可能放弃索引而选择全表扫描。这就是为什么”索引存在但不被使用”的常见原因之一——回表成本超过了全表扫描成本。
2.3 联合索引与最左前缀
联合索引是在多个列上建立的 B+ 树索引,其排序规则为:先按第一列排序,第一列相同则按第二列排序,依此类推。这决定了联合索引的最左前缀原则——查询条件必须从索引的最左列开始,才能有效利用索引。
-- 联合索引CREATE INDEX idx_status_created ON orders (status, created_at);
-- 可以使用索引(最左前缀匹配)SELECT * FROM orders WHERE status = 'shipped';SELECT * FROM orders WHERE status = 'shipped' AND created_at > '2026-01-01';
-- 无法使用索引(跳过了最左列 status)SELECT * FROM orders WHERE created_at > '2026-01-01';
-- 部分使用索引(仅使用 status 列,created_at 无法用于过滤)SELECT * FROM orders WHERE status = 'shipped' AND amount > 100;联合索引的列顺序至关重要。一般原则是:高选择性(基数大)的列放前面,等值查询的列放前面,范围查询的列放后面。
2.4 索引选择性与基数
**选择性(Selectivity)**衡量索引的区分度,定义为:
选择性 = DISTINCT(col) / COUNT(*)选择性越高,索引过滤效果越好。选择性为 1(如主键、唯一列)时索引效率最高;选择性接近 0(如性别列只有 2 个值)时,索引几乎无法过滤数据。
-- 查看列的基数(不同值的数量)-- MySQLSHOW INDEX FROM orders;
-- PostgreSQLSELECT attname, n_distinct FROM pg_stats WHERE tablename = 'orders';| 选择性范围 | 示例 | 索引效果 |
|---|---|---|
| ≈ 1.0 | 主键、唯一 ID | 极佳,一次定位 |
| 0.1 ~ 0.9 | 用户名、邮箱 | 良好,大幅过滤 |
| < 0.1 | 性别、状态 | 差,可能不如全表扫描 |
| ≈ 0 | 常量列 | 无效,索引无意义 |
三、哈希索引
3.1 精确匹配的 O(1) 利器
哈希索引使用哈希表实现,对等值查询具有 O(1) 的时间复杂度。其原理是:对索引列的值计算哈希,将哈希值映射到哈希槽,槽内存储指向数据行的指针。
-- PostgreSQL 哈希索引CREATE INDEX idx_user_email_hash ON users USING hash (email);
-- 哈希索引擅长:精确匹配SELECT * FROM users WHERE email = 'alice@example.com';
-- 哈希索引不支持:范围查询SELECT * FROM users WHERE email > 'alice@example.com'; -- 无法使用索引3.2 哈希索引的局限性
| 能力 | B+ 树索引 | 哈希索引 |
|---|---|---|
| 等值查询 | O(log n) | O(1) |
| 范围查询 | O(log n + k) | 不支持 |
| 排序 | 天然有序 | 无序 |
| 最左前缀 | 支持 | 不支持 |
| 模糊匹配 | 支持前缀 LIKE | 不支持 |
| 哈希冲突 | 无 | 需处理 |
哈希索引的核心问题是数据无序——哈希函数将相邻的键值映射到完全不同的槽位,因此无法支持范围查询、排序和前缀匹配。
3.3 MySQL 自适应哈希索引
InnoDB 的**自适应哈希索引(Adaptive Hash Index, AHI)**是一种自动优化机制:当 InnoDB 监控到某些 B+ 树索引页被频繁访问时,会在内存中自动为这些页构建哈希索引,将 O(log n) 的 B+ 树查找加速为 O(1) 的哈希查找。
-- 查看自适应哈希索引状态SHOW ENGINE INNODB STATUS\G
-- 开启/关闭自适应哈希索引SET GLOBAL innodb_adaptive_hash_index = ON;AHI 的适用场景与限制:
- 适用:高并发等值查询、B+ 树非叶子页被反复访问
- 不适用:范围查询为主、查询模式频繁变化
- 风险:AHI 的维护需要加锁,在高并发写入场景下可能成为争用热点
3.4 PostgreSQL 哈希索引
PostgreSQL 从 10 版本开始大幅改进了哈希索引(此前因 WAL 支持不完善而不推荐使用)。现在的 PG 哈希索引:
- 完整支持 WAL 日志,崩溃安全
- 支持多并发扫描
- 适用于等值查询密集且不需要范围查询的场景
-- PG 哈希索引适用于等值查询为主的场景CREATE INDEX idx_session_token ON sessions USING hash (token);
-- 对比 B-tree 索引,哈希索引在等值查询上更紧凑-- 但 B-tree 仍然是默认选择,因为其通用性更强四、位图索引
4.1 位图索引的原理
位图索引为索引列的每个不同值创建一个位图(Bitmap),位图的每一位对应表中的一行——如果该行的值等于该不同值,则位为 1,否则为 0。
位图索引的核心优势在于多条件 AND/OR 查询:只需对多个位图进行位运算(AND/OR),即可快速得到结果集。
-- Oracle 位图索引CREATE BITMAP INDEX idx_emp_dept ON employees (department);
-- 多条件查询:位图 AND 运算-- 查找"研发部门 AND 资深"的员工SELECT /*+ INDEX(employees idx_emp_dept idx_emp_level) */ * FROM employeesWHERE department = '研发' AND level = '资深';-- 执行:研发位图 AND 资深位图 → 结果位图 → 行号4.2 低基数列的优势
位图索引在低基数列(不同值很少,如性别、状态、部门)上效果最佳。对比 B+ 树索引:
| 维度 | B+ 树索引 | 位图索引 |
|---|---|---|
| 低基数列效果 | 差(选择性低,大量重复值) | 优(位图紧凑,位运算高效) |
| 高基数列效果 | 优(选择性高,精确过滤) | 差(位图过多,空间膨胀) |
| 多条件 AND/OR | 需合并多个索引扫描 | 位运算极快 |
| 并发写入 | 行级锁,冲突少 | 段级锁,冲突多 |
| 存储空间 | 与数据量成正比 | 与 基数×行数 成正比 |
位图索引在 OLTP 场景中需谨慎使用。由于位图索引的锁粒度较粗(一个位图段覆盖多行),高并发写入时容易产生锁冲突。位图索引更适合 OLAP 场景——大量读取、少量写入、多条件组合过滤。
4.3 Oracle 位图索引与 PostgreSQL 部分索引
Oracle 位图索引是生产环境中最成熟的位图索引实现,广泛应用于数据仓库:
-- Oracle:为低基数的每个列建位图索引CREATE BITMAP INDEX idx_sales_region ON sales (region);CREATE BITMAP INDEX idx_sales_channel ON sales (channel);CREATE BITMAP INDEX idx_sales_quarter ON sales (quarter);
-- 星型查询:位图索引的杀手级场景SELECT region, channel, SUM(amount)FROM salesWHERE region = '华东' AND channel = '线上' AND quarter = 'Q1'GROUP BY region, channel;-- Oracle 自动进行位图 AND → 行号 → 事实表查找**PostgreSQL 部分索引(Partial Index)**是一种不同的思路——不是对列的所有值建索引,而是只对满足条件的子集建索引:
-- PostgreSQL:只对活跃订单建索引CREATE INDEX idx_orders_active ON orders (user_id)WHERE status IN ('pending', 'processing', 'shipped');
-- 查询自动使用部分索引SELECT * FROM ordersWHERE user_id = 42 AND status = 'pending';部分索引的优势:索引更小、维护成本更低、缓存命中率更高。适用于”只关心数据的某个子集”的场景。
五、空间索引与全文索引
5.1 空间索引:R 树与 GiST
空间数据(地理坐标、几何图形)的查询模式与普通数据截然不同——“查找距离我 3 公里内的所有餐厅”涉及二维范围搜索,B+ 树无法高效处理。
R 树(R-Tree)是空间索引的经典数据结构,其核心思想是最小外接矩形(MBR):将空间对象用矩形包围,内部节点存储子矩形的 MBR,查询时通过矩形相交判断快速剪枝。
-- MySQL 空间索引CREATE TABLE locations ( id INT PRIMARY KEY, name VARCHAR(100), point GEOMETRY NOT NULL SRID 4326, SPATIAL INDEX idx_point (point));
-- 空间范围查询SELECT name, ST_Distance_Sphere(point, ST_GeomFromText('POINT(121.47 31.23)', 4326)) AS distFROM locationsWHERE ST_Contains( ST_Buffer(ST_GeomFromText('POINT(121.47 31.23)', 4326), 0.03), point);**PostgreSQL GiST(Generalized Search Tree)**是一种更通用的索引框架,R 树只是 GiST 的一种实现。GiST 允许自定义数据类型的索引策略,PostGIS 就基于 GiST 实现空间索引:
-- PostgreSQL + PostGIS 空间索引CREATE INDEX idx_locations_geom ON locations USING gist (geom);
-- KNN 查询(最近邻,GiST 原生支持排序)SELECT name, geom <-> ST_MakePoint(121.47, 31.23) AS distFROM locationsORDER BY distLIMIT 10;5.2 全文索引:倒排原理
全文索引的核心是倒排索引(Inverted Index)——不是”文档 → 词”的正向映射,而是”词 → 文档列表”的反向映射。
倒排索引的构建过程:
- 分词:将文档拆分为词元(Token)
- 归一化:词干提取、大小写统一、同义词映射
- 构建倒排表:每个词指向包含该词的文档列表(Posting List)
- 记录位置:可选地记录词在文档中的位置,支持短语查询
-- MySQL 全文索引CREATE TABLE articles ( id INT PRIMARY KEY, title VARCHAR(200), content TEXT, FULLTEXT INDEX ft_content (title, content) WITH PARSER ngram);
-- 全文搜索SELECT title, MATCH(title, content) AGAINST('数据库 索引' IN NATURAL LANGUAGE MODE) AS scoreFROM articlesWHERE MATCH(title, content) AGAINST('数据库 索引' IN NATURAL LANGUAGE MODE)ORDER BY score DESC;5.3 PostgreSQL GiST 与 GIN 简介
PostgreSQL 提供两种全文索引实现,各有侧重:
| 维度 | GiST | GIN(Generalized Inverted Index) |
|---|---|---|
| 构建速度 | 快 | 慢(但可并发构建) |
| 查询速度 | 较慢 | 快 |
| 更新成本 | 低 | 高(需更新倒排列表) |
| 适用场景 | 数据频繁更新 | 数据相对静态、查询密集 |
| 全文搜索 | 支持 | 推荐(默认选择) |
-- PostgreSQL GIN 全文索引(推荐)CREATE INDEX idx_articles_fts ON articles USING gin (to_tsvector('simple', title || ' ' || content));
-- 全文搜索SELECT title, ts_rank(to_tsvector('simple', title || ' ' || content), query) AS rankFROM articles, to_tsquery('simple', '数据库 & 索引') queryWHERE to_tsvector('simple', title || ' ' || content) @@ queryORDER BY rank DESC;PostgreSQL 的 GIN 索引本质上就是倒排索引的通用实现——不仅用于全文搜索,还支持 JSONB、数组等数据类型的索引。更多细节见PostgreSQL 深入。
六、索引优化技巧
6.1 覆盖索引:避免回表
当查询所需的所有列都包含在索引中时,数据库可以直接从索引返回结果,无需回表——这就是覆盖索引。
-- 联合索引CREATE INDEX idx_user_email_name ON users (email, name);
-- 覆盖索引:查询只需要 email 和 name,索引中都有SELECT email, name FROM users WHERE email = 'alice@example.com';
-- 非覆盖索引:还需要 age,必须回表SELECT email, name, age FROM users WHERE email = 'alice@example.com';覆盖索引的判断方法:在 EXPLAIN 输出中,Extra 列出现 Using index 即表示使用了覆盖索引。
-- MySQL EXPLAIN 验证覆盖索引EXPLAIN SELECT email, name FROM users WHERE email = 'alice@example.com';-- Extra: Using index ← 覆盖索引,无需回表
-- PostgreSQL EXPLAIN 验证EXPLAIN SELECT email, name FROM users WHERE email = 'alice@example.com';-- Index Only Scan using idx_user_email_name ← 仅索引扫描6.2 索引下推(Index Condition Pushdown, ICP)
索引下推是 MySQL 5.6 引入的优化:将部分 WHERE 条件在索引遍历时就进行过滤,而非等到回表后再过滤。
-- 联合索引 (email, name)-- 查询条件使用了 email(索引第一列)和 name(索引第二列)SELECT * FROM users WHERE email LIKE 'ali%' AND name LIKE '%ce';无 ICP:先通过 email LIKE 'ali%' 从索引找到所有匹配行 → 回表获取完整行 → 再过滤 name LIKE '%ce'
有 ICP:在索引遍历时就检查 name LIKE '%ce' → 只对同时满足两个条件的行回表
-- 查看 ICP 是否生效EXPLAIN SELECT * FROM users WHERE email LIKE 'ali%' AND name LIKE '%ce';-- Extra: Using index condition ← 索引下推生效6.3 前缀索引
对于长字符串列(如 VARCHAR(500) 的 URL),完整列作为索引键会占用大量空间。前缀索引只取列的前 N 个字符作为索引键:
-- 前缀索引:只取 URL 前 20 个字符CREATE INDEX idx_url_prefix ON pages (url(20));
-- 选择前缀长度的原则:选择性接近完整列即可SELECT COUNT(DISTINCT url) / COUNT(*) AS full_selectivity, COUNT(DISTINCT LEFT(url, 10)) / COUNT(*) AS prefix_10, COUNT(DISTINCT LEFT(url, 20)) / COUNT(*) AS prefix_20, COUNT(DISTINCT LEFT(url, 30)) / COUNT(*) AS prefix_30FROM pages;-- 选择使选择性接近 full_selectivity 的最短前缀前缀索引的局限:无法用于覆盖索引(因为索引中不存完整值)、无法用于 ORDER BY / GROUP BY。
6.4 索引列顺序选择
联合索引的列顺序直接影响索引的可用性和效率。核心原则:
- 等值条件列在前,范围条件列在后
- 高选择性列在前(在等值条件列之间)
- 考虑查询频率:最常查询的列组合优先
-- 场景:订单表有 status、user_id、created_at 三列-- 查询模式 1:WHERE user_id = ? AND status = ?(高频)-- 查询模式 2:WHERE user_id = ? AND created_at > ?(中频)-- 查询模式 3:WHERE status = ? AND created_at > ?(低频)
-- 最优索引:user_id 在前(等值+高频),status 在中(等值),created_at 在后(范围)CREATE INDEX idx_uid_status_created ON orders (user_id, status, created_at);
-- 这个索引可以覆盖查询模式 1 和 2-- 查询模式 3 需要单独的索引CREATE INDEX idx_status_created ON orders (status, created_at);6.5 多列索引策略
面对多列查询,是建一个联合索引还是多个单列索引?答案取决于查询模式:
七、索引失效分析
索引存在但未被使用,是数据库性能问题中最常见的陷阱之一。以下是索引失效的六大场景,每种给出错误写法和正确写法。
7.1 函数/运算导致失效
对索引列使用函数或算术运算,会导致优化器无法使用索引。
-- 错误:对 created_at 使用函数SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- 正确:使用范围查询SELECT * FROM ordersWHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- 错误:对 user_id 进行算术运算SELECT * FROM orders WHERE user_id + 1 = 43;
-- 正确:将运算移到等号另一侧SELECT * FROM orders WHERE user_id = 42;7.2 隐式类型转换
当查询条件的值类型与列类型不匹配时,数据库可能进行隐式类型转换,导致索引失效。
-- 假设 user_id 是 VARCHAR 类型-- 错误:传入整数,数据库将 user_id 隐式转为数字SELECT * FROM users WHERE user_id = 123;
-- 正确:传入字符串SELECT * FROM users WHERE user_id = '123';
-- 假设 phone 是 VARCHAR 类型-- 错误:传入数字SELECT * FROM users WHERE phone = 13800138000;
-- 正确:传入字符串SELECT * FROM users WHERE phone = '13800138000';MySQL 的隐式转换规则是:将字符串转为数字进行比较。这意味着对 VARCHAR 列传入整数时,MySQL 会对列值做类型转换(而非对常量做转换),从而导致索引失效。PostgreSQL 的行为不同——它通常会对常量做转换,索引可能仍然有效。
7.3 LIKE 前缀通配符
LIKE 查询以通配符 % 开头时,B+ 树无法利用有序性进行定位。
-- 错误:前缀通配符,无法使用索引SELECT * FROM users WHERE name LIKE '%lice';
-- 正确:前缀匹配,可以使用索引SELECT * FROM users WHERE name LIKE 'ali%';
-- 如需全文搜索,使用全文索引SELECT * FROM users WHERE MATCH(name) AGAINST('lice');7.4 OR 条件
OR 连接的条件中,如果有一个条件列没有索引,整个 OR 子句都无法使用索引。
-- 假设 email 有索引,name 没有索引-- 错误:name 无索引导致整个 OR 无法使用索引SELECT * FROM users WHERE email = 'alice@example.com' OR name = 'Alice';
-- 正确方案 1:为 name 也建索引CREATE INDEX idx_name ON users (name);SELECT * FROM users WHERE email = 'alice@example.com' OR name = 'Alice';-- 优化器可能使用索引合并(Index Merge)
-- 正确方案 2:用 UNION 替代 ORSELECT * FROM users WHERE email = 'alice@example.com'UNIONSELECT * FROM users WHERE name = 'Alice';7.5 违反最左前缀
联合索引的最左前缀原则要求查询条件必须从索引的最左列开始。
-- 索引:idx_status_created (status, created_at)
-- 错误:跳过 status,直接查 created_atSELECT * FROM orders WHERE created_at > '2026-01-01';
-- 正确:包含最左列 statusSELECT * FROM orders WHERE status = 'shipped' AND created_at > '2026-01-01';
-- 错误:status 用范围查询,created_at 无法利用索引排序SELECT * FROM orders WHERE status > 'pending' ORDER BY created_at;
-- 正确:status 用等值查询,created_at 可利用索引排序SELECT * FROM orders WHERE status = 'shipped' ORDER BY created_at;7.6 索引失效决策树
遇到”索引存在但不被使用”的问题时,按以下决策树逐项排查:
八、跨数据库索引对比
不同数据库的索引实现存在显著差异。以下对比 MySQL、PostgreSQL 和 Redis 三种代表性数据库的索引机制:
| 维度 | MySQL (InnoDB) | PostgreSQL | Redis |
|---|---|---|---|
| 默认索引结构 | B+ 树 | B-tree(Lehman-Yao 变体) | 无默认索引 |
| 聚簇索引 | 有(主键即聚簇索引) | 无(堆表组织) | — |
| 哈希索引 | 自适应哈希索引(内存中) | 支持(PG 10+) | 原生哈希表 |
| 全文索引 | FULLTEXT + ngram 分词 | GIN + tsvector | — |
| 空间索引 | R-Tree(SPATIAL) | GiST(PostGIS) | GEOADD + GeoHash |
| 位图索引 | 不支持 | 不支持(有部分索引) | — |
| 部分索引 | 不支持 | 支持(WHERE 子句) | — |
| 表达式索引 | 不支持(8.0 支持函数索引) | 支持 | — |
| 索引覆盖 | Using index | Index Only Scan | — |
| 索引下推 | 支持(ICP) | 不需要(堆表无回表概念) | — |
| BRIN 索引 | 不支持 | 支持(块范围索引) | — |
| 并发索引构建 | Online DDL | CONCURRENTLY 选项 | — |
PostgreSQL 没有”聚簇索引”的概念——它的表是堆表(Heap Table),数据按插入顺序存储,所有索引都是二级索引,叶子节点存储的是 CTID(行标识符)而非主键。这意味着 PG 的索引查找总是需要一次额外的堆表访问,但也避免了聚簇索引的页分裂问题。详见MySQL 深入和PostgreSQL 深入。
Redis 的索引机制
Redis 作为内存数据库,其”索引”概念与传统数据库不同:
- 原生数据结构即索引:Sorted Set 的跳表、Hash 的哈希表、List 的压缩列表/快速列表——每种数据结构本身就是一种索引
- Geo 索引:基于 Sorted Set + GeoHash 实现,支持范围查询和距离计算
- 无 B+ 树:内存中随机访问代价极低,不需要磁盘优化的 B+ 树结构
# Redis Geo 索引示例GEOADD locations 121.47 31.23 "上海站" 121.48 31.24 "南京路"GEORADIUS locations 121.47 31.23 3 km COUNT 10·附、实践:PostgreSQL 索引类型对比
本节在 PostgreSQL 中创建不同类型的索引,对比它们的大小和适用场景。需要 PostgreSQL 环境。
1. 创建测试表和索引
CREATE TABLE products ( id SERIAL PRIMARY KEY, name TEXT, price NUMERIC, tags TEXT[], description TEXT);
INSERT INTO products (name, price, tags, description) VALUES ('Laptop', 999.99, ARRAY['electronics', 'computer'], 'A powerful laptop'), ('Phone', 699.99, ARRAY['electronics', 'mobile'], 'A smart phone'), ('Tablet', 399.99, ARRAY['electronics', 'mobile'], 'A tablet device'), ('Book', 29.99, ARRAY['education', 'reading'], 'A good book'), ('Pen', 1.99, ARRAY['education', 'writing'], 'A simple pen');
-- B-tree 索引(默认,适合范围查询和排序)CREATE INDEX idx_products_price ON products USING btree (price);
-- Hash 索引(只适合等值查询)CREATE INDEX idx_products_name ON products USING hash (name);
-- GIN 索引(适合数组/全文搜索)CREATE INDEX idx_products_tags ON products USING gin (tags);
-- BRIN 索引(适合大表的范围块索引)CREATE INDEX idx_products_price_brin ON products USING brin (price);2. 查看索引类型和大小
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'products'; indexname | indexdef-------------------------+---------------------------------------------------------------------------- products_pkey | CREATE UNIQUE INDEX products_pkey ON public.products USING btree (id) idx_products_price | CREATE INDEX idx_products_price ON public.products USING btree (price) idx_products_name | CREATE INDEX idx_products_name ON public.products USING hash (name) idx_products_tags | CREATE INDEX idx_products_tags ON public.products USING gin (tags) idx_products_price_brin | CREATE INDEX idx_products_price_brin ON public.products USING brin (price)SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) as sizeFROM pg_indexes WHERE tablename = 'products'; indexname | size-------------------------+------- products_pkey | 16 kB idx_products_price | 16 kB idx_products_name | 32 kB idx_products_tags | 16 kB idx_products_price_brin | 24 kB关键观察:
- Hash 索引最大(32 kB):Hash 索引的桶数量是固定的,即使数据量很小也占用较多空间
- BRIN 索引最小(24 kB):BRIN 只存储每个数据块的统计信息(最小值/最大值),空间效率极高
- B-tree 索引适中(16 kB):B-tree 是最通用的选择,支持等值、范围、排序
注意:在小数据集上索引大小差异不明显。当数据量达到百万级时,BRIN 索引可能只有 B-tree 索引的 1/100 大小,但查询精度也相应降低。
九、总结
索引是数据库性能优化的基石。本章从”为什么需要索引”出发,系统梳理了索引的核心知识:
-
B+ 树索引是关系型数据库的默认选择,3~4 层即可容纳数亿数据,支持等值查询、范围查询和排序。理解聚簇索引与二级索引的区别、联合索引的最左前缀原则、索引选择性的计算,是索引设计的基础。
-
哈希索引在等值查询上具有 O(1) 优势,但不支持范围查询和排序。MySQL 的自适应哈希索引是自动优化,PostgreSQL 的哈希索引适合特定场景。
-
位图索引是 OLAP 场景的利器,低基数列上的位图运算极快,但高并发写入时锁冲突严重。PostgreSQL 的部分索引提供了另一种思路——只索引需要的子集。
-
空间索引与全文索引解决了特殊数据类型的查询需求。R 树/GiST 处理空间数据,倒排索引处理全文搜索,PostgreSQL 的 GIN 是通用倒排索引框架。
-
索引优化的核心技巧:覆盖索引避免回表、索引下推提前过滤、前缀索引节省空间、合理选择联合索引列顺序。
-
索引失效是生产环境最常见的性能陷阱——函数运算、隐式转换、LIKE 通配符、OR 条件、违反最左前缀,每种都有明确的规避方法。
索引设计没有银弹——它始终在查询性能与写入开销之间权衡。下一章事务与并发控制将从另一个维度审视数据库:如何在并发访问下保证数据的正确性。而关于优化器如何选择执行计划,将在查询处理与优化中深入探讨。MySQL 和 PostgreSQL 各自的索引实现细节,分别在MySQL 深入和PostgreSQL 深入中展开。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






