3 minutes
索引与性能优化
数据量达到百万级后,不合适的索引会让查询慢如蜗牛。本章将系统学习 PostgreSQL 的索引原理和性能优化方法。
为什么需要索引?
没有索引的查询需要全表扫描——逐行检查数据。就像一本没有目录的书,要找一句话就得从头翻到尾。
索引的本质是一个排序过的数据结构(B-Tree),让数据库能在 O(log n) 时间内找到数据,而不是 O(n)。
创建练习数据
-- 创建一张大表来演示索引效果
CREATE TABLE big_orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
product_name VARCHAR(100),
amount NUMERIC(10,2),
status VARCHAR(10),
order_date DATE,
created_at TIMESTAMP DEFAULT NOW()
);
-- 插入 100 万条数据
INSERT INTO big_orders (user_id, product_name, amount, status, order_date)
SELECT
(random() * 100000)::INT + 1,
CASE (random() * 5)::INT
WHEN 0 THEN '笔记本电脑'
WHEN 1 THEN '手机'
WHEN 2 THEN '耳机'
WHEN 3 THEN '运动鞋'
ELSE 'T恤'
END,
(random() * 5000 + 100)::NUMERIC(10,2),
CASE (random() * 4)::INT
WHEN 0 THEN '已完成'
WHEN 1 THEN '已取消'
WHEN 2 THEN '退款中'
ELSE '已完成'
END,
DATE '2024-01-01' + (random() * 730)::INT
FROM generate_series(1, 1000000);
-- 确认数据已插入
SELECT count(*) FROM big_orders;
EXPLAIN 读懂查询计划
-- 查看查询计划
EXPLAIN SELECT * FROM big_orders WHERE user_id = 42;
EXPLAIN ANALYZE SELECT * FROM big_orders WHERE user_id = 42;
EXPLAIN 输出解读:
Seq Scan on big_orders (cost=0.00..18334.00 rows=1 width=57)
Filter: (user_id = 42)
- Seq Scan:全表扫描(效率低)
- cost=0.00..18334.00:估算成本(启动..总成本)
- rows=1:估算行数
- width=57:每行字节数
EXPLAIN ANALYZE 会实际执行查询,显示真实耗时:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM big_orders WHERE user_id = 42;
创建索引
-- 创建 B-Tree 索引(PostgreSQL 默认)
CREATE INDEX idx_orders_user_id ON big_orders(user_id);
-- 创建多列索引(复合索引)
CREATE INDEX idx_orders_user_date ON big_orders(user_id, order_date);
-- 创建唯一索引
CREATE UNIQUE INDEX idx_orders_unique ON orders(order_id);
验证索引效果
-- 先清缓存并做无索引查询
\timing on
-- 全表扫描(应该在无索引时运行)
SELECT count(*) FROM big_orders WHERE user_id > 50000 AND user_id < 50010;
-- 创建索引后重新查询
CREATE INDEX idx_big_orders_user ON big_orders(user_id);
SELECT count(*) FROM big_orders WHERE user_id > 50000 AND user_id < 50010;
复合索引与列顺序
-- 复合索引的列顺序非常重要!
CREATE INDEX idx_orders_user_status ON big_orders(user_id, status);
-- 这个查询能用上索引(前导列 user_id 匹配)
EXPLAIN ANALYZE SELECT * FROM big_orders
WHERE user_id = 42 AND status = '已完成';
-- 这个查询可能用不上索引(没有前导列 user_id)
EXPLAIN ANALYZE SELECT * FROM big_orders
WHERE status = '已完成';
列顺序原则:
- 等值条件列(WHERE col = ?)放前面
- 范围条件列(WHERE col > ?)放后面
- 高选择性的列(唯一值多)放前面
部分索引
只索引表中满足条件的一部分行,体积更小、维护成本更低:
-- 只索引活跃用户(status = 'active' 的只占一小部分)
CREATE INDEX idx_users_active ON users(status) WHERE status = 'active';
-- 只索引已完成的订单(用于统计报表查询)
CREATE INDEX idx_orders_completed ON orders(order_date, total_amount)
WHERE status = '已完成';
覆盖索引(Index-Only Scan)
索引中包含了查询需要的所有列,数据库只需读索引,不需要回表:
-- 如果经常这样查询
SELECT user_id, order_date, status FROM big_orders
WHERE user_id BETWEEN 100 AND 200;
-- 创建覆盖索引(包含查询需要的所有列)
CREATE INDEX idx_orders_covering ON big_orders(user_id, order_date, status);
-- 查询计划会显示 "Index Only Scan"
EXPLAIN ANALYZE SELECT user_id, order_date, status
FROM big_orders WHERE user_id BETWEEN 100 AND 200;
其他索引类型
唯一索引
-- 确保两列组合唯一
CREATE UNIQUE INDEX idx_user_email ON users(email);
表达式索引
-- 经常按小写查询邮箱
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- 查询时必须使用同样的表达式
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
部分唯一索引
-- 只允许一条活跃记录有某个邮箱(软删除场景)
CREATE UNIQUE INDEX idx_users_active_email
ON users(email) WHERE status = 'active';
常用性能诊断查询
-- 查看表大小
SELECT pg_size_pretty(pg_total_relation_size('big_orders')) AS 表大小;
-- 查看索引大小
SELECT pg_size_pretty(pg_indexes_size('big_orders')) AS 索引大小;
-- 查找未被使用的索引
SELECT
schemaname,
tablename,
indexname,
idx_scan AS 使用次数
FROM pg_stat_user_indexes
ORDER BY idx_scan;
-- 查看慢查询(需要启用 pg_stat_statements)
SELECT
query,
calls,
total_exec_time / calls AS avg_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
常见性能问题
1. 索引选择性太差
-- 对布尔列建索引效果不佳(只有 true/false)
CREATE INDEX idx_orders_status ON big_orders(status);
-- 如果 status 只有 3 个值,这个索引帮助有限
2. 在索引列上做运算
-- ❌ 无法使用索引
SELECT * FROM big_orders WHERE EXTRACT(YEAR FROM order_date) = 2025;
-- ✅ 可以使用索引
SELECT * FROM big_orders
WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01';
3. LIKE 无法使用索引
-- ❌ 无法使用普通索引
SELECT * FROM products WHERE name LIKE '%手机%';
-- ✅ 可以使用索引(前缀匹配)
SELECT * FROM products WHERE name LIKE '手机%';
-- 全文搜索场景用 gin 索引
性能优化原则
查询优化
- SELECT 只查需要的列,不要
SELECT * - WHERE 条件用索引列
- JOIN 的关联列要有索引
- 避免在 WHERE 中对列做函数运算
- 聚合前先用 WHERE 过滤
- 大表分页用键集分页,不用 OFFSET
配置优化
-- 查看关键配置
SHOW shared_buffers; -- 共享缓冲区(标准建议:25% 内存)
SHOW work_mem; -- 排序/哈希内存(每个查询)
SHOW maintenance_work_mem; -- 维护操作(如 VACUUM、CREATE INDEX)
SHOW effective_cache_size; -- 系统缓存估算值
SHOW random_page_cost; -- 随机 I/O 成本
VACUUM 维护
-- 查看表的膨胀和死元组
SELECT
schemaname,
tablename,
n_dead_tup AS 死元组数,
n_live_tup AS 活跃元组数,
last_autovacuum AS 上次自动清理时间
FROM pg_stat_user_tables
WHERE tablename = 'big_orders';
-- 手动清理无用的死元组
VACUUM big_orders;
-- 清理并回收空间
VACUUM FULL big_orders;
-- 更新统计信息(让查询计划更准确)
ANALYZE big_orders;
小结
索引是 SQL 性能优化最重要的工具。理解 B-Tree 索引原理、复合索引列顺序、以及 EXPLAIN 的读法,能让你在百万级数据下写出高效的查询。下一篇文章将学习事务与并发控制。
Summary: 索引原理、EXPLAIN 读法、复合索引与性能优化原则。