一、PostgreSQL索引架构总览
PostgreSQL 16内置8种索引类型,通过索引访问方法接口可扩展到无限种。索引在物理上独立于表数据,通过TID定位表行。MVCC机制下索引元组的可见性由表的快照信息决定,索引页本身不存储事务状态。
PostgreSQL的索引访问方法接口允许自定义索引引擎,第三方如pgvector的HNSW索引、TimescaleDB的Skip Index都基于此框架实现。
二、B-Tree索引:MVCC下的平衡查找树
B-tree是PostgreSQL默认索引类型,核心特性包括等价与范围查询、排序输出、唯一性约束。
B-tree物理结构:每个节点是一块8KB页,包含元数据、line pointer数组和索引元组。叶子节点链接左右兄弟形成双向链表,支持高效的正向/反向扫描。
插入时采用分裂策略:叶子节点满时分裂为两个各占一半的新节点,父节点满则连锁分裂至根,树高度增加。PostgreSQL 12引入deduplication将重复值的元组合并为posting list,大幅减小索引尺寸。
三、GiST索引:通用搜索树的威力
GiST是框架性索引类型,通过实现7个用户一致性操作、3个union操作即可创建新索引:
几何类型:box、circle、point等类型的空间重叠、包含、距离操作。PostGIS在GiST上扩展地理坐标系的R-tree索引,支持"查找10公里内的餐厅"查询。
范围类型:int4range、tsrange等范围类型的包含、相交、相邻操作。典型场景:时段占用检测(会议室预约系统)。
模糊字符串匹配:pg_trgm模块将三元组向量创建GiST索引,支持相似度和距离查询,实现拼写纠错。
四、GIN索引:倒排索引的精确场景
GIN采用posting list + pending list架构,适用于多值数据类型:
数组查询:intarray模块为integer[]类型的包含、相交操作创建GIN索引。订单的tags字段可快速查找含特定标签的订单。
全文检索:tsvector的GIN索引通过lexeme到(page_id, offset)的倒排映射实现毫秒级文档检索。FASTUPDATE选项将新条目写入pending list,查询时合并扫描。
JSONB:jsonb_ops索引支持包含、存在、路径匹配。jsonb_path_ops仅索引键值对路径,索引更小查询更快。
五、BRIN索引:块级廉价索引
BRIN每页(128页=1MB)仅存储min/max摘要信息:
物理有序假设:BRIN依赖数据在磁盘上的自然有序性。时序数据、日志表满足此假设,BRIN索引大小仅为B-tree的1/1000。
完全排除:BRIN通过区间min/max判断整页是否可能包含目标值,完全不包含的页跳过扫描。
应用场景:时序IoT数据(数十亿行/年)、审计日志。结合TimescaleDB hypertable分区,BRIN实现多区间自动维护。
六、进阶索引策略
覆盖索引:PostgreSQL 11引入Index Only Scan,要求所有查询列都在索引中。CREATE INDEX ON orders (order_date) INCLUDE (status, amount)。
部分索引:WHERE条件的索引仅对满足条件的行建立,极大减少索引大小。
表达式索引:为函数或表达式的计算结果建立索引,如CREATE INDEX ON users (LOWER(email))。
多列索引:最左前缀原则。PostgreSQL 17引入skip scan实现非前缀索引扫描。
七、EXPLAIN ANALYZE深度解读
EXPLAIN (ANALYZE, BUFFERS)输出三个关键指标:
cost:优化器估算成本,含启动成本和总成本。
actual time:实际执行时间,首行显示从开始到首行返回的时间。
Buffers:hit(buffer命中)、read(磁盘读取)。shared hit%低于99%表示随机IO过多。
JOIN策略:Nested Loop适合右表有索引的小结果集;Hash Join适合大结果集;Merge Join要求预排序。
八、pg_stat_statements与HypoPG实战
pg_stat_statements:按queryid归一化记录,mean_exec_time乘calls评估总耗时,shared_blks_read评估缓存压力,temp_blks定位work_mem不足。
HypoPG:虚拟索引模拟器,无需实际创建即可评估索引效益。通过hypopg_create_index创建虚拟索引,EXPLAIN查看优化器估算。关键限制是无法100%模拟真实物理IO,但评估"优化器是否会选择该索引"已足够。节省的生产环境索引创建成本(上亿行表的B-tree需数小时)远超HypoPG的计算成本。
九、常见查询反模式与优化
函数包裹字段:WHERE LOWER(name)='xxx'不走B-tree。改用表达式索引或citext模块。
NOT IN子查询:NOT IN (NULL)永远返回空集,应改用NOT EXISTS。
OFFSET分页:大OFFSET导致大量无意义扫描。Keyset分页(WHERE id>last_seen ORDER BY id LIMIT N)将OFFSET转为常数时间。
SELECT *:多余列使覆盖索引失效。仅SELECT所需列。
LIKE '%keyword%':前后通配符不走B-tree。使用pg_trgm的GiST/GIN索引或专用搜索引擎。

发表评论 取消回复