mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4289 字
12 分钟
PostgreSQL 高级索引
2024-07-18

B+ 树索引覆盖不了所有查询场景。全文搜索要倒排、地理空间要 R 树、时序大表要块范围摘要,这些都是 B-tree 不擅长的领域。PostgreSQL 不像 MySQL 只提供 B-tree 和有限的全文/空间索引,而是内置了 GiST、GIN、BRIN、SP-GiST 四种额外索引类型,每种针对特定数据特征优化。

本文梳理 PostgreSQL 的五种索引类型及其适用场景,然后展开代价估计器如何基于统计信息选择执行计划,最后落到索引运维:怎么查索引使用率、什么时候该 REINDEX、部分索引和表达式索引能解决什么问题。MVCC 与 VACUUM 的存储层机制是另一条线,见 PostgreSQL MVCC 与 VACUUM。本文也会和 MySQL 索引原理与失效分析 做对比,两者的索引设计取舍差异很明显。

前置知识#

Important

一、索引类型总览#

PostgreSQL 除了标准的 B-tree 索引外,还提供了 GiST、GIN、BRIN、SP-GiST 等高级索引类型,每种针对特定查询场景优化。

索引类型全称核心数据结构最佳场景空间开销
B-treeB-treeB+ 树等值、范围、排序、前缀匹配中等
GiSTGeneralized Search Tree可定制树地理空间、范围、最近邻搜索中等
GINGeneralized Inverted Index倒排索引数组、JSONB、全文搜索较大
BRINBlock Range Index块范围摘要有序大表的范围查询极小
SP-GiSTSpace-Partitioned GiST空间分区树电话号码、路由等非平衡结构中等

MySQL 的索引对比:MySQL 的 InnoDB 是聚簇索引组织表,主键即数据,二级索引叶子存主键值需要回表。PostgreSQL 是堆表组织,所有索引都是二级索引,叶子节点存的是指向堆表的 CTID(行物理位置),所有索引平等,没有”聚簇索引优先”的概念。这也意味着 PostgreSQL 的索引选择更灵活,可以为同一张表建多种类型的索引应对不同查询。

二、B-tree 索引#

PostgreSQL 的默认索引类型,适用于等值查询、范围查询、排序和前缀匹配:

-- 创建 B-tree 索引(默认类型)
CREATE INDEX idx_users_name ON users (name);
-- 支持的查询模式
SELECT * FROM users WHERE name = 'Alice'; -- 等值
SELECT * FROM users WHERE name > 'Alice'; -- 范围
SELECT * FROM users WHERE name LIKE 'Ali%'; -- 前缀
SELECT * FROM users ORDER BY name; -- 排序
SELECT * FROM users WHERE name = 'Alice' AND age > 20; -- 多列

B-tree 覆盖了 90% 的索引场景,但在以下情况无能为力:

  • LIKE '%abc'(后缀匹配,无法用 B-tree 前缀)
  • 数组包含查询(@>
  • 全文搜索(@@
  • 地理空间范围查询(ST_DWithin

这些场景需要其他索引类型。但要注意,PostgreSQL 的 B-tree 有一个 MySQL 没有的能力:它可以配合可见性映射(VM)做 Index-Only Scan。VM 标记”全部元组对所有人可见”的页,索引扫描时可以直接从索引取值,跳过堆表回查。VM 的维护依赖 VACUUM,详见 MVCC 与 VACUUM 篇

三、GiST 索引#

GiST 是一种可扩展的索引框架,它不定义具体的数据结构,而是提供一套通用的树形搜索接口,由操作符类(Operator Class)决定具体行为。PostGIS 的地理空间索引就基于 GiST。

-- 地理空间索引(需要 PostGIS 扩展)
CREATE INDEX idx_locations_geo ON locations USING gist (geom);
-- 范围类型索引
CREATE INDEX idx_reservations_period ON reservations USING gist (during);
-- 查询示例:查找与某区域重叠的记录
SELECT * FROM reservations
WHERE during && '[2026-04-01, 2026-04-30]'::daterange;
-- 查找附近的位置(5000 米内)
SELECT * FROM locations
WHERE ST_DWithin(geom, ST_MakePoint(116.4, 39.9)::geography, 5000);

GiST 的核心思想是近似加精确验证:索引存储每个子树的”近似边界”(Bounding Box),搜索时先通过近似边界快速排除不可能匹配的子树,再对候选结果做精确验证。这种”粗筛加精验”的模式让 GiST 能支持非规则形状的查询,代价是索引节点要存储边界信息,空间开销比 B-tree 大。

3.1 适用场景:范围查询#

PostgreSQL 原生支持范围类型(int4rangedaterangetsrange 等),GiST 索引可以加速范围重叠查询。典型场景是预订系统:查某时间段内有冲突的预订、排班冲突检测、价格区间筛选。

-- 预订冲突检测:查找与目标时间段重叠的预订
SELECT * FROM room_bookings
WHERE stay_period && '[2026-04-10, 2026-04-15]'::daterange;

这种查询用 B-tree 无法高效处理,因为 B-tree 只能做点查询或单向范围,无法直接判断两个区间是否重叠。GiST 的范围操作符(&& 重叠、@> 包含、<< 之前)直接在索引层过滤。

3.2 适用场景:最近邻搜索#

GiST 支持 KNN(K-Nearest Neighbors)查询,可以按距离排序返回最近的 K 条记录:

-- 查找最近的 10 家咖啡店
SELECT name, ST_Distance(geom, ST_MakePoint(116.4, 39.9)::geography) AS dist
FROM cafes
ORDER BY geom <-> ST_MakePoint(116.4, 39.9)::geography
LIMIT 10;

<-> 操作符让 GiST 在索引层按距离排序,不需要先取全部再排序,对于地图 LBS 查询非常高效。

四、GIN 索引#

GIN(Generalized Inverted Index)是倒排索引,适用于包含多个元素的数据类型:数组、JSONB、全文搜索。它为每个元素值维护一个包含所有包含该元素的行 ID 列表。

-- 数组索引
CREATE INDEX idx_articles_tags ON articles USING gin (tags);
-- 查询:包含特定标签的文章
SELECT * FROM articles WHERE tags @> ARRAY['postgresql', 'database'];
-- JSONB 索引
CREATE INDEX idx_events_data ON events USING gin (data);
-- 查询:JSONB 中包含特定键值对
SELECT * FROM events WHERE data @> '{"type": "login"}'::jsonb;
-- 全文搜索索引
CREATE INDEX idx_docs_content ON documents USING gin (to_tsvector('english', content));
-- 全文搜索查询
SELECT * FROM documents
WHERE to_tsvector('english', content) @@ to_tsquery('english', 'postgresql & index');

4.1 适用场景:全文搜索#

全文搜索是 GIN 最典型的用例。PostgreSQL 内置全文搜索引擎,用 tsvector(分词后的词素列表)和 tsquery(查询表达式)做匹配。GIN 索引加在 to_tsvector 表达式上,可以快速定位包含特定词的文档。

-- 中文全文搜索需要 zhparser 或 jieba 等分词扩展
-- zhparser 只注册名为 zhparser 的 parser,还需自己建文本搜索配置(PostgreSQL 内置只有 english/german/simple 等配置,没有 chinese)
CREATE EXTENSION zhparser;
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
-- 配置 token 到词典的映射,否则分出的词素不进索引
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
CREATE INDEX idx_posts_content ON posts USING gin (to_tsvector('chinese', content));
-- 查询:包含"索引"和"优化"的文章
SELECT * FROM posts
WHERE to_tsvector('chinese', content) @@ to_tsquery('chinese', '索引 & 优化')
ORDER BY ts_rank(to_tsvector('chinese', content), to_tsquery('chinese', '索引 & 优化')) DESC;

相比 MySQL 的 FULLTEXT 索引,PostgreSQL 的全文搜索更灵活:支持多种语言分词、自定义词典、权重排序、模糊匹配。MySQL 的全文索引只在 MyISAM 上成熟,InnoDB 的全文索引在 5.6 后才支持,且功能远不如 PostgreSQL 完善。如果业务有全文搜索需求且不打算引入 Elasticsearch,PostgreSQL 的 GIN 是个现实的选择。

Note

GIN 索引的构建速度较慢(需要收集所有元素后批量构建),但查询速度极快。对于频繁写入的表,可以使用 fastupdate = on(默认开启),将新条目暂存到待处理列表中,查询时合并,延迟索引更新以提升写入性能。但 fastupdate 会让查询变慢(要合并待处理列表),需要权衡。定期 VACUUM 会自动合并待处理列表。

五、BRIN 索引#

BRIN(Block Range Index)是一种超轻量级索引,它不存储每行的索引值,而是存储每个连续数据块范围的摘要信息(最小值、最大值)。适用于物理有序的大表,如按时间追加写入的日志表。

-- 创建 BRIN 索引(指定块范围大小)
CREATE INDEX idx_logs_time ON logs USING brin (created_at)
WITH (pages_per_range = 32);
-- 查询:时间范围查询
SELECT * FROM logs
WHERE created_at BETWEEN '2026-04-01' AND '2026-04-30';
-- BRIN 索引大小对比
SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes WHERE tablename = 'logs';
-- 示例输出:
-- indexname | size
-- ------------------------+--------
-- idx_logs_time_btree | 214 MB <- B-tree
-- idx_logs_time_brin | 288 kB <- BRIN(小 700 倍!)

5.1 适用场景:时序大表#

BRIN 的代价是精度低,它只能排除确定不包含目标值的块范围,但无法精确定位行,仍需在块范围内线性扫描。对于物理有序的数据,这种”粗筛”已经能排除 99% 以上的数据块。

典型场景是日志表、监控数据、交易流水这类按时间单调递增写入的大表。这类表动辄上亿行,建 B-tree 索引可能占几个 GB,而 BRIN 只要几 MB,查询性能却接近。原因是数据物理有序,BRIN 的每个块范围摘要几乎不重叠,一次范围查询只需扫描少数几个块范围。

Warning

BRIN 依赖数据的物理有序性。如果表频繁 UPDATE 导致数据页重排,或者数据本身无序(如随机写入),BRIN 的过滤效果会急剧下降,退化为全表扫描。判断是否适合 BRIN 的标准:查询列的值是否与物理存储位置相关。时序数据天然满足,但随机 UUID 主键就不适合。

5.2 BRIN 与 B-tree 的选择#

维度B-treeBRIN
空间开销中等(每行一条索引)极小(每块范围一条摘要)
查询精度精确定位粗筛块范围
适用数据任何数据物理有序数据
写入性能每行更新索引每块范围更新摘要
维护成本高(索引膨胀)低(几乎不膨胀)

对于 TB 级的时序表,B-tree 索引可能占几十 GB 且维护代价高,BRIN 是更务实的选择。

六、SP-GiST 索引#

SP-GiST(Space-Partitioned GiST)适用于非平衡的数据结构,如电话号码前缀树、路由表等:

-- 电话号码前缀索引
CREATE INDEX idx_phones_number ON phones USING spgist (phone_number);
-- 路由前缀匹配
SELECT * FROM routing_table
WHERE ip_range >>= '192.168.1.0/24'::inet;

SP-GiST 的核心是空间分区:每个节点按某个维度将空间划分为不重叠的分区,查询时只需进入匹配的分区,不需要遍历所有子树。这种结构天然适合前缀匹配、IP 路由这类层级数据。实际业务中 SP-GiST 用得较少,但在电话号码归属地查询、IP 路由表这些特定场景下没有替代品。

七、索引类型选择决策#

flowchart TD START["选择索引类型"] --> Q1{"查询类型?"} Q1 -->|"等值/范围/排序"| BTREE["B-tree"] Q1 -->|"包含/数组/全文"| Q2{"数据特征?"} Q1 -->|"地理空间/范围重叠"| GIST["GiST"] Q1 -->|"有序大表范围查询"| BRIN_IDX["BRIN"] Q2 -->|"高基数(大量不同值)"| GIN["GIN"] Q2 -->|"前缀/非平衡结构"| SPGIST["SP-GiST"] BTREE --> DEFAULT["默认选择,覆盖 90% 场景"] GIST --> POSTGIS["PostGIS / 范围类型"] GIN --> JSONB["JSONB / 数组 / 全文搜索"] BRIN_IDX --> LOG["日志表 / 时序数据<br/>空间开销极小"] SPGIST --> PREFIX["电话号码 / IP 路由"] style BTREE fill:#c8e6c9,stroke:#2e7d32 style GIST fill:#e3f2fd,stroke:#1565c0 style GIN fill:#fff3e0,stroke:#e65100 style BRIN_IDX fill:#fce4ec,stroke:#c62828 style SPGIST fill:#f3e5f5,stroke:#6a1b9a

八、部分索引与表达式索引#

PostgreSQL 的两个索引能力是 MySQL 长期欠缺的(MySQL 8.0 才支持函数索引,但功能不如 PostgreSQL 灵活)。

8.1 部分索引#

部分索引只索引满足 WHERE 条件的行,不索引全表。适合状态字段只有少数值需要查询的场景:

-- 订单表有 1 亿行,其中"待支付"状态只有 10 万行
-- 建全表索引浪费空间,建部分索引只索引待支付行
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';
-- 查询时会自动使用部分索引
SELECT * FROM orders WHERE status = 'pending' AND created_at > '2026-04-01';

部分索引的好处是空间小、维护代价低。查询时只要 WHERE 条件匹配索引条件,优化器就会用部分索引。它特别适合”活跃数据只占总量很小比例”的场景:待处理订单、未读消息、异常记录。

8.2 表达式索引#

表达式索引对函数或表达式的结果建索引,而不是直接对列建索引:

-- 大小写不敏感的用户名查询
CREATE INDEX idx_users_lower_name ON users (lower(username));
-- 查询时会使用表达式索引
SELECT * FROM users WHERE lower(username) = 'alice';
-- JSONB 路径索引
CREATE INDEX idx_events_type ON events ((data->>'type'));
-- 查询 JSONB 路径
SELECT * FROM events WHERE data->>'type' = 'login';

表达式索引的代价是写入时要计算表达式、索引存储的是计算结果。如果查询频繁用某个函数过滤,表达式索引能把函数调用从查询时挪到写入时,大幅提升查询速度。注意查询时必须用完全一致的表达式,优化器才能匹配索引。

九、代价估计器#

PostgreSQL 的查询优化器(Planner)基于**代价估计(Cost Estimation)**选择执行计划。代价不是真实的时间,而是一个无量纲的相对值,用于比较不同执行路径的优劣。

9.1 代价模型#

PostgreSQL 的代价模型由几个核心参数组成:

seq_page_costRequirednumber

default: 1.0

顺序读取一个数据页的代价

random_page_costRequirednumber

default: 4.0

随机读取一个数据页的代价

cpu_tuple_costRequirednumber

default: 0.01

处理一行的 CPU 代价

cpu_index_tuple_costRequirednumber

default: 0.005

处理一条索引条目的 CPU 代价

cpu_operator_costRequirednumber

default: 0.0025

执行一个操作符的 CPU 代价

9.2 扫描代价#

-- 顺序扫描代价 = 顺序读页数 × seq_page_cost + 行数 × cpu_tuple_cost
-- 假设表有 10000 页、500000 行
-- Seq Scan Cost = 10000 × 1.0 + 500000 × 0.01 = 15000
-- 索引扫描代价 = 随机读页数 × random_page_cost + 索引条目数 × cpu_index_tuple_cost
-- + 堆表行数 × cpu_tuple_cost
-- 假设 B-tree 深度 3、返回 100 行
-- Index Scan Cost = 3 × 4.0 + 100 × 0.005 + 100 × 0.01 = 13.5

优化器比较两种扫描的代价,选低的。上面这个例子,索引扫描代价远低于顺序扫描,优化器会选 Index Scan。但如果查询返回的行数很多(比如 30% 以上),索引扫描的随机 I/O 代价会超过顺序扫描,优化器会转而选 Seq Scan。

对于 SSD,random_page_cost 应调低到 1.1 到 1.5,因为 SSD 的随机读性能远好于机械硬盘,默认 4.0 会高估随机 I/O 代价,导致优化器不必要地回避索引扫描:

-- SSD 环境下调低随机读代价
ALTER SYSTEM SET random_page_cost = 1.1;
SELECT pg_reload_conf();

9.3 Join 代价#

PostgreSQL 支持三种 Join 策略,每种有不同的代价计算:

Join 方式代价模型适用场景
Nested Loop外表行数 × 内表扫描代价小表驱动大表,有索引
Hash Join构建哈希表代价 + 探测代价等值连接,中等大小表
Merge Join两侧排序代价 + 合并代价等值连接,数据已排序
-- 强制使用特定 Join 方式(仅调试用)
SET enable_nestloop = off;
SET enable_hashjoin = off;
-- 此时优化器只能选 Merge Join
-- 恢复默认
RESET enable_nestloop;
RESET enable_hashjoin;

调试执行计划时,临时关掉某种 Join 方式可以观察优化器的替代选择,判断它选的 Join 是否合理。

9.4 统计信息#

代价估计的准确性取决于统计信息。PostgreSQL 通过 ANALYZE 命令收集表的统计信息,存储在 pg_statistic 系统表中:

-- 查看表的统计信息
SELECT attname, n_distinct, null_frac, avg_width
FROM pg_stats
WHERE tablename = 'users';
-- 示例输出:
-- attname | n_distinct | null_frac | avg_width
-- ---------+------------+-----------+----------
-- id | -1 | 0 | 8
-- name | -0.25 | 0 | 16
-- city | 100 | 0.05 | 12
-- n_distinct < 0 表示比例(-0.25 = 25% 不同值)
-- n_distinct > 0 表示绝对数量
-- 手动触发统计信息收集
ANALYZE users;
-- 增加统计信息精度(默认 100,最大 10000)
ALTER TABLE users ALTER COLUMN city SET STATISTICS 500;
ANALYZE users;

统计信息的精度直接影响优化器的选择。default_statistics_target 默认为 100,意味着对每列最多采样 100 × 300 = 30000 行来构建直方图。对于数据分布不均匀的列,增大统计目标可以避免优化器误判。

9.5 EXPLAIN 实战#

-- 基本执行计划
EXPLAIN SELECT * FROM users WHERE city = 'Beijing';
-- 带实际执行时间
EXPLAIN ANALYZE SELECT * FROM users WHERE city = 'Beijing';
-- 带 I/O 统计(最常用)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE city = 'Beijing';
-- 示例输出:
-- QUERY PLAN
-- ---------------------------------------------------------------
-- Index Scan using idx_users_city on users
-- (cost=0.29..8.31 rows=1 width=72)
-- (actual time=0.015..0.016 rows=1 loops=1)
-- Index Cond: (city = 'Beijing'::text)
-- Buffers: shared hit=4
-- Planning Time: 0.089 ms
-- Execution Time: 0.032 ms

解读要点:

字段含义
cost=0.29..8.31启动代价..总代价(估计值)
rows=1预估返回行数
width=72预估每行平均字节数
actual time=0.015..0.016实际启动时间..实际总时间(毫秒)
rows=1 (actual)实际返回行数
shared hit=4命中 Shared Buffers 的页数(无磁盘 I/O)
shared read=0需要从磁盘读取的页数
Warning

EXPLAIN 的预估行数与实际行数差距很大时(如预估 1 行实际 10000 行),说明统计信息失真,需要执行 ANALYZE 或增大统计目标。这是查询性能突然下降的最常见原因。优化器基于错误的行数估计,可能选了 Nested Loop 而非 Hash Join,导致查询慢几个数量级。

EXPLAIN 的解读逻辑和 MySQL 的 EXPLAIN 类似,都是看预估行数与实际行数是否匹配。区别在于 PostgreSQL 的 EXPLAIN 输出代价数值和 Buffer 命中信息,比 MySQL 的 EXPLAIN 更详细。MySQL 的 EXPLAIN 解读见 MySQL 索引原理与失效分析

十、踩坑与运维#

10.1 索引使用率监控#

PostgreSQL 的索引不是建了就一定被用。统计信息失真、数据分布变化、查询条件不匹配,都可能让优化器放弃索引。监控索引使用率是运维的基本功:

-- 查看各索引的使用情况
SELECT schemaname, relname, indexrelname,
idx_scan AS scans,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- 示例输出:
-- relname | indexrelname | scans | tuples_read | size
-- ----------+----------------------+-------+-------------+------
-- orders | idx_orders_status | 0 | 0 | 45 MB <- 从未使用
-- orders | idx_orders_created | 15234 | 1200000 | 50 MB

idx_scan = 0 的索引从未被查询使用,是建了没用的”僵尸索引”。这种索引白白占用磁盘空间、拖慢写入(每次 INSERT/UPDATE 都要维护它)。排查清楚后可以删除:

-- 删除从未使用的索引(先确认确实不需要)
DROP INDEX CONCURRENTLY idx_orders_status;

CONCURRENTLY 关键字让删除操作不阻塞写入,代价是执行时间稍长。生产环境删索引务必用 CONCURRENTLY

10.2 索引膨胀与 REINDEX#

PostgreSQL 的 Append-Only MVCC 下,索引也会积累死条目(指向已被 VACUUM 回收的堆表元组的索引条目)。普通 VACUUM 会清理这些死索引条目,但如果表频繁更新且索引列变化频繁,索引页会碎片化,导致索引扫描变慢。

判断索引是否膨胀,可以对比索引的实际大小与预期大小:

-- 查看索引大小和扫描次数
SELECT indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
idx_scan
FROM pg_stat_user_indexes
WHERE relname = 'orders';
-- 如果索引大小远超预期(例如 B-tree 深度异常、叶子页稀疏),考虑 REINDEX

PostgreSQL 12+ 支持 REINDEX CONCURRENTLY,不阻塞读写:

-- 并发重建单个索引(推荐)
REINDEX INDEX CONCURRENTLY idx_orders_created;
-- 并发重建某表所有索引
REINDEX TABLE CONCURRENTLY orders;

CONCURRENTLY 的原理是先建一个新索引(与原索引同名加 _ccnew 后缀),同步完成后原子切换。代价是需要额外磁盘空间、构建时间更长。如果不用 CONCURRENTLYREINDEX 会锁表,生产环境大表上几乎不可用。

Tip

定期 REINDEX 的频率取决于表的更新频率。频繁更新的表建议每月跑一次 REINDEX CONCURRENTLY,低频更新的表每季度一次即可。更省力的方案是配 pg_repack,它在重建表的同时重建所有索引,一步到位。

10.3 索引类型选错#

最常见的索引选错是把全文搜索、JSONB 查询这种本该用 GIN 的场景用了 B-tree。B-tree 无法高效处理 @>@@ 这类操作符,查询会退化为全表扫描。

判断索引是否匹配查询的方法:跑 EXPLAIN,看 Index Cond 是否出现期望的索引名。如果 EXPLAIN 显示 Seq Scan 而非 Index Scan,且查询条件用了 @>@@<-> 等操作符,大概率是索引类型建错了。

-- 错误:JSONB 查询用 B-tree,不生效
CREATE INDEX idx_events_data_btree ON events (data); -- B-tree 无法索引 JSONB 内部
SELECT * FROM events WHERE data @> '{"type": "login"}'::jsonb; -- 全表扫描
-- 正确:JSONB 查询用 GIN
CREATE INDEX idx_events_data_gin ON events USING gin (data);
SELECT * FROM events WHERE data @> '{"type": "login"}'::jsonb; -- 走 GIN 索引

10.4 统计信息失真导致索引不被选#

另一个常见坑是统计信息过时导致优化器放弃索引。典型场景:表数据量大幅变化后没跑 ANALYZE,优化器以为表很小,选了 Seq Scan 而非 Index Scan。

-- 手动更新统计信息
ANALYZE orders;
-- 或针对单列更新
ANALYZE orders (status);
-- 对数据分布不均的列,提高统计精度
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500;
ANALYZE orders;

AutoVacuum 会自动跑 ANALYZE,但触发阈值(默认 10% 变更)对大表可能不够及时。批量导入数据后建议手动跑一次 ANALYZE,让优化器立刻看到新数据分布。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

PostgreSQL 高级索引
https://blog.souloss.cn/posts/middleware/db/postgresql-advanced-indexes/
作者
Souloss
发布于
2024-07-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时