PostgreSQL 索引 #
索引概述 #
索引是数据库中用于加速查询的数据结构,类似于书籍的目录。
text
┌─────────────────────────────────────────────────────────────┐
│ 索引的作用 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 无索引:全表扫描 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Row 1 → Row 2 → Row 3 → ... → Row 1000000 │ │
│ └─────────────────────────────────────────────────────┘ │
│ 时间复杂度:O(n) │
│ │
│ 有索引:索引查找 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 索引结构 → 快速定位 → 返回结果 │ │
│ └─────────────────────────────────────────────────────┘ │
│ 时间复杂度:O(log n) │
│ │
└─────────────────────────────────────────────────────────────┘
索引类型 #
B-tree 索引(默认) #
sql
-- B-tree 是默认索引类型,适用于:
-- - 等值查询(=)
-- - 范围查询(<, <=, >, >=, BETWEEN)
-- - 排序(ORDER BY)
-- - 前缀匹配(LIKE 'xxx%')
-- 创建 B-tree 索引
CREATE INDEX idx_users_email ON users(email);
-- 显式指定 B-tree
CREATE INDEX idx_users_name ON users USING btree(name);
-- 示例表
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
age INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email, age)
SELECT
'User ' || i,
'user' || i || '@example.com',
(random() * 50 + 18)::INTEGER
FROM generate_series(1, 100000) AS i;
-- 创建索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_age ON users(age);
-- 查看执行计划
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user50000@example.com';
-- Index Scan using idx_users_email on users (cost=0.42..8.44 rows=1 width=...)
-- 范围查询
EXPLAIN ANALYZE SELECT * FROM users WHERE age BETWEEN 25 AND 35;
-- Bitmap Heap Scan on users
-- Recheck Cond: ((age >= 25) AND (age <= 35))
-- -> Bitmap Index Scan on idx_users_age
Hash 索引 #
sql
-- Hash 索引只支持等值查询
-- 比 B-tree 快,但功能有限
CREATE INDEX idx_users_email_hash ON users USING hash(email);
-- 只能用于等值查询
SELECT * FROM users WHERE email = 'user50000@example.com';
-- 不能用于范围查询
-- SELECT * FROM users WHERE email > 'user'; -- 不能使用 Hash 索引
GiST 索引 #
sql
-- GiST (Generalized Search Tree) 适用于:
-- - 几何数据(点、线、多边形)
-- - 范围类型
-- - 全文搜索
-- 创建几何数据表
CREATE TABLE locations (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
point POINT
);
INSERT INTO locations (name, point)
SELECT
'Location ' || i,
POINT(random() * 100, random() * 100)
FROM generate_series(1, 10000) AS i;
-- 创建 GiST 索引
CREATE INDEX idx_locations_point ON locations USING gist(point);
-- 范围查询
SELECT * FROM locations
WHERE point <@ box(POINT(0, 0), POINT(50, 50));
-- 范围类型索引
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
room_id INTEGER,
during TSRANGE
);
CREATE INDEX idx_reservations_during ON reservations USING gist(during);
GIN 索引 #
sql
-- GIN (Generalized Inverted Index) 适用于:
-- - 数组类型
-- - JSONB
-- - 全文搜索
-- 数组索引
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
tags VARCHAR(50)[]
);
INSERT INTO articles (title, tags)
SELECT
'Article ' || i,
ARRAY['tag' || (random() * 10)::INTEGER, 'tag' || (random() * 10)::INTEGER]
FROM generate_series(1, 10000) AS i;
-- 创建 GIN 索引
CREATE INDEX idx_articles_tags ON articles USING gin(tags);
-- 数组包含查询
SELECT * FROM articles WHERE tags @> ARRAY['tag1', 'tag2'];
-- JSONB 索引
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
attributes JSONB
);
CREATE INDEX idx_products_attributes ON products USING gin(attributes);
-- JSONB 包含查询
SELECT * FROM products WHERE attributes @> '{"color": "red"}';
-- JSONB 路径索引
CREATE INDEX idx_products_attributes_path ON products USING gin(attributes jsonb_path_ops);
BRIN 索引 #
sql
-- BRIN (Block Range Index) 适用于:
-- - 大表
-- - 数据按索引列有序存储
-- - 占用空间小
-- 创建大表
CREATE TABLE logs (
id SERIAL,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO logs (message)
SELECT 'Log message ' || i
FROM generate_series(1, 1000000) AS i;
-- 创建 BRIN 索引(非常小)
CREATE INDEX idx_logs_created_at ON logs USING brin(created_at);
-- BRIN 索引大小对比
SELECT
pg_size_pretty(pg_relation_size('idx_logs_created_at')) AS brin_size;
-- B-tree 索引会大很多
-- CREATE INDEX idx_logs_created_at_btree ON logs(created_at);
-- SELECT pg_size_pretty(pg_relation_size('idx_logs_created_at_btree'));
创建和管理索引 #
创建索引 #
sql
-- 基本语法
CREATE INDEX index_name ON table_name (column_name);
-- 多列索引
CREATE INDEX idx_users_name_email ON users(name, email);
-- 唯一索引
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
-- 并发创建(不锁表)
CREATE INDEX CONCURRENTLY idx_users_name ON users(name);
-- 表达式索引
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
-- 部分索引
CREATE INDEX idx_users_active ON users(email) WHERE is_active = TRUE;
-- 函数索引
CREATE INDEX idx_users_created_date ON users((created_at::DATE));
查看索引 #
sql
-- 查看表的所有索引
\di
-- 查看特定表的索引
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE tablename = 'users';
-- 查看索引大小
SELECT
indexrelname AS index_name,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE relname = 'users';
-- 查看索引使用情况
SELECT
indexrelname AS index_name,
idx_scan AS times_used,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched
FROM pg_stat_user_indexes
WHERE relname = 'users';
删除索引 #
sql
-- 删除索引
DROP INDEX idx_users_email;
-- 如果存在则删除
DROP INDEX IF EXISTS idx_users_email;
-- 并发删除
DROP INDEX CONCURRENTLY idx_users_email;
重建索引 #
sql
-- 重建索引
REINDEX INDEX idx_users_email;
-- 重建表的所有索引
REINDEX TABLE users;
-- 重建数据库的所有索引
REINDEX DATABASE mydb;
特殊索引 #
部分索引 #
sql
-- 只索引满足条件的行
CREATE INDEX idx_orders_pending ON orders(created_at)
WHERE status = 'pending';
-- 只索引活跃用户
CREATE INDEX idx_users_active_email ON users(email)
WHERE is_active = TRUE;
-- 优点:
-- 1. 索引更小
-- 2. 维护成本更低
-- 3. 查询更快
表达式索引 #
sql
-- 对表达式结果建立索引
CREATE INDEX idx_users_lower_name ON users(LOWER(name));
-- 查询时使用相同表达式才能利用索引
SELECT * FROM users WHERE LOWER(name) = 'alice';
-- 计算列索引
CREATE INDEX idx_orders_year ON orders(EXTRACT(YEAR FROM created_at));
SELECT * FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2026;
多列索引 #
sql
-- 多列索引的列顺序很重要
CREATE INDEX idx_users_name_age ON users(name, age);
-- 可以使用索引的查询
SELECT * FROM users WHERE name = 'Alice'; -- 使用索引
SELECT * FROM users WHERE name = 'Alice' AND age = 25; -- 使用索引
SELECT * FROM users WHERE name = 'Alice' AND age > 20; -- 使用索引
-- 不能使用索引的查询
SELECT * FROM users WHERE age = 25; -- 不使用索引(缺少第一列)
-- 最左前缀原则
-- 索引 (a, b, c) 可以用于:
-- WHERE a = ?
-- WHERE a = ? AND b = ?
-- WHERE a = ? AND b = ? AND c = ?
-- 不能用于:
-- WHERE b = ?
-- WHERE c = ?
覆盖索引 #
sql
-- 创建包含查询所需所有列的索引
CREATE INDEX idx_users_email_name_age ON users(email, name, age);
-- 查询只需访问索引,不需要访问表
SELECT name, age FROM users WHERE email = 'alice@example.com';
-- 使用 INCLUDE 子句(PostgreSQL 11+)
CREATE INDEX idx_users_email_include ON users(email) INCLUDE (name, age);
SELECT name, age FROM users WHERE email = 'alice@example.com';
-- Index Only Scan
索引优化 #
选择合适的索引类型 #
text
┌─────────────────┬─────────────────────────────────────────────┐
│ 场景 │ 推荐索引类型 │
├─────────────────┼─────────────────────────────────────────────┤
│ 等值、范围查询 │ B-tree │
│ 仅等值查询 │ Hash │
│ 数组、JSONB │ GIN │
│ 几何、范围类型 │ GiST │
│ 大表有序数据 │ BRIN │
│ 全文搜索 │ GIN 或 GiST │
└─────────────────┴─────────────────────────────────────────────┘
索引使用建议 #
sql
-- 1. 为 WHERE 条件列创建索引
SELECT * FROM users WHERE email = ?;
CREATE INDEX idx_users_email ON users(email);
-- 2. 为 JOIN 条件列创建索引
SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 3. 为 ORDER BY 列创建索引
SELECT * FROM users ORDER BY created_at DESC;
CREATE INDEX idx_users_created_at ON users(created_at DESC);
-- 4. 避免过度索引
-- 索引会占用存储空间
-- 索引会降低写入性能
-- 5. 定期分析索引使用情况
SELECT
schemaname,
relname AS table_name,
indexrelname AS index_name,
idx_scan AS times_used,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC, pg_relation_size(indexrelid) DESC;
-- 查找未使用的索引
SELECT
schemaname || '.' || relname AS table,
indexrelname AS index,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS index_scans
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
使用 EXPLAIN 分析 #
sql
-- 查看查询计划
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
-- 查看详细执行信息
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';
-- 查看更详细的信息
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email = 'alice@example.com';
-- 常见扫描类型:
-- Seq Scan - 顺序扫描(全表扫描)
-- Index Scan - 索引扫描
-- Index Only Scan - 仅索引扫描
-- Bitmap Scan - 位图扫描
索引维护 #
sql
-- 更新统计信息
ANALYZE users;
-- 查看索引膨胀
SELECT
current_database(),
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC;
-- 重建索引(解决膨胀)
REINDEX INDEX CONCURRENTLY idx_users_email;
学习路径 #
text
高级阶段
├── 索引(本文)
├── 视图
├── 存储过程
├── 触发器
└── 事务与锁
下一步 #
掌握了索引后,接下来学习 视图,了解如何创建和使用视图!
最后更新:2026-03-29