PostgreSQL BRIN索引揭秘

文章目录

一、介绍

本文将重点介绍 PostgreSQL 中的一种索引类型 ------ BRIN(块范围索引),包括它的设计初衷、核心用途,以及在哪些典型场景下能比其他索引方案显著更高效。我们还会通过具体的性能数据对比,展示 BRIN 与 PostgreSQL 其他常用索引(如 B-tree、GIN、GiST 等)之间的性能差异,说明它在合适的使用场景下为何更具优势。

二、什么是 BRIN Index?

BRIN(Block Range Index)是一种索引类型。在 Postgres 中,block 是最基本的存储单位,默认大小为 8KB。BRIN 会对一段连续的 blocks(默认 128 个)进行采样,记录这一范围内第一个 block 的位置,以及该范围内所有值的最小值和最大值。

记住!BRIN 不是逐行索引,而是以 heap table 的 block range(页范围)为单位记录统计信息,如:

  • 最小/最大值(min/max)
  • 是否全为 NULL
  • 是否存在 NULL
  • 其他聚合摘要信息(取决于 opclass)

这种索引对有序的数据集特别适用,不仅能大幅节省存储空间,有时还可以带来比其他索引更好的查询性能。

BRIN Index 是一种颠覆性的索引思想,最早由 PostgreSQL 贡献者 Alvaro Herrera 提出。BRIN 是 "Block Range Index" 的缩写。一个 block range 是一组相邻的页面,在索引中会存储关于这些页面的汇总信息。例如,对于整数、日期等具有线性排序的数据类型,可以只存储该范围内的最小值和最大值。

后来,包括 Oracle 在内的其他数据库系统也陆续推出了类似的功能。BRIN Index 在很多场景下,所带来的性能提升往往可以与对表进行分区(Partitioning)相媲美。

BRIN 是一种轻量级索引,但常常被误解。正确使用时,它可以带来显著的好处,例如节省存储空间和提升查询速度;而使用不当,则可能无法发挥其优势。

BRIN 在对大规模 time-series data 进行高效查询时尤其有用,而且相比标准 B-TREE 索引,占用的磁盘空间要少得多。一个 block range index 条目指向一个页面(Page,这是 PostgreSQL 存储数据的最小单位),并存储两个值:该页面的最小值以及要索引的最大值。

三、BRIN结构

BRIN 索引在磁盘上通常包含以下三类 page:

Page类型 作用
Meta Page (Block 0) 元数据页(Meta Page)- 第0页,记录索引基础配置,如 magic、pagesPerRange、版本等
Revmap Page 反向映射页(Revmap Page)-第1页,记录每个 range summary 在哪个 BRIN summary page + offset
Summary Page 汇总页(Summary Page)-第2页, 真正存储每个 block range 的统计摘要(tuple summaries)

接下来,我们将创建一个测试表以及他的brin索引,用来揭示BRIN索引的内部结构

3.1 建立测试表及BRIN索引

1.创建扩展

bash 复制代码
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS pageinspect;
  1. 创建测试表
bash 复制代码
CREATE TABLE test_brin (
    id BIGSERIAL PRIMARY KEY,
    ts TIMESTAMPTZ NOT NULL DEFAULT now(),
    value INT
);
  1. 插入测试数据
bash 复制代码
INSERT INTO test_brin(ts, value)
SELECT
  generate_series(
    '2020-01-01 00:00:00+07',
    '2025-12-31 00:00:00+07',
    interval '5 seconds'
  ),
  floor(random()*1000);

插入了约37860481条记录,时间跨度从2020年1月1日到2025年12月31日,每5秒钟一条记录:

bash 复制代码
 select count(1) from test_brin;
 count
--------
 37860481
(1 row)
  1. 创建BRIN索引
    最后在时间戳字段上创建BRIN索引:
bash 复制代码
CREATE INDEX test_brin_idx ON test_brin USING BRIN(ts);
  1. 索引大小
bash 复制代码
select relpages from pg_class where relname='test_brin_idx';
 relpages
----------
        3

结果显示:仅需3个页面,BRIN索引的惊人压缩比惊人

对于21万+条记录,传统B-tree索引可能需要数百甚至上千个页面,而BRIN索引只用了3个8KB的页面(共24KB)。这就是BRIN索引的核心优势------极致的空间效率。

3.2 揭示BRIN索引的三层结构

通过pageinspect扩展,我们可以窥探BRIN索引的内部结构。BRIN索引由三个关键部分组成:

  1. 元数据页(Meta Page)- 第0页
bash 复制代码
SELECT * from  brin_metapage_info(pg_read_binary_file('base/16448/25517',0,8192));
   magic    | version | pagesperrange | lastrevmappage
------------+---------+---------------+----------------
 0xA8109CFA |       1 |           128 |              1
(1 row)

元数据页存储了索引的全局配置信息:

  • magic: 魔数0xA8109CFA,用于标识这是一个BRIN索引
  • version: 索引格式版本号,当前为1
  • pagesperrange: 关键参数!每个范围包含128个数据页,这意味着每个索引条目汇总了128个数据块的信息
  • lastrevmappage: 最后一个revmap页的页号
  1. 反向映射页(Revmap Page)- 第1页
bash 复制代码
select * from brin_revmap_data(get_raw_page('test_brin_idx',1)) limit 5;
 pages
-------
 (2,1)
 (2,2)
 (2,3)
 (2,4)
 (2,5)
(5 rows)

反向映射页是BRIN索引的"目录"。每一项都是一个指针,格式为(页号, 偏移),指向汇总页中对应块范围的汇总信息。

  • (2,1): 第一个128块范围的汇总信息在第2页的第1项
  • (2,2): 第二个128块范围的汇总信息在第2页的第2项
    依此类推...
    这种设计使得PostgreSQL能够快速定位任意数据块对应的汇总信息。
  1. 汇总页(Summary Page)- 第2页
bash 复制代码
 select * from brin_page_items(get_raw_page('in_ticketing_system_brin',2),'in_ticketing_system_brin') limit 5;
 itemoffset | blknum | attnum | allnulls | hasnulls | placeholder | empty |                       value
------------+--------+--------+----------+----------+-------------+-------+----------------------------------------------------
          1 |      0 |      1 | f        | f        | f           | f     | {2020-01-01 00:00:00+00 .. 2020-01-01 21:19:55+00}
          2 |    128 |      1 | f        | f        | f           | f     | {2020-01-01 21:20:00+00 .. 2020-01-02 18:39:55+00}
          3 |    256 |      1 | f        | f        | f           | f     | {2020-01-02 18:40:00+00 .. 2020-01-03 15:59:55+00}
          4 |    384 |      1 | f        | f        | f           | f     | {2020-01-03 16:00:00+00 .. 2020-01-04 13:19:55+00}
          5 |    512 |      1 | f        | f        | f           | f     | {2020-01-04 13:20:00+00 .. 2020-01-05 10:39:55+00}
(5 rows)

汇总页存储了实际的索引数据。每一项包含:

  • itemoffset: 条目偏移,对应revmap中的位置
  • blknum: 起始数据块号(每隔128递增,与pagesperrange对应)
  • attnum: 列号(这里是第1列,即ts字段)
  • allnulls/hasnulls: 空值标记
  • value: 最关键的部分------该范围内时间戳的最小值和最大值
    例如第一项:块0-127的时间戳范围是2020-01-01 00:00:00到2020-01-01 21:19:55。

四、BRIN索引的工作原理

1. 场景1:顺序扫描所有范围(范围查询)

当执行查询时:

bash 复制代码
SELECT * FROM test_brin 
WHERE ts BETWEEN '2025-01-03 00:00:00' AND '2025-01-04 00:00:00';

PostgreSQL的执行流程:

  1. 读取元数据页:了解每个范围覆盖128个数据块
  2. 扫描汇总页:检查每个范围的最小/最大值
  3. 过滤候选范围:
  • 第3项:2025-01-02 18:40:00 ... 2025-01-03 15:59:55
  • 第4项:2025-01-03 16:00:00 ... 2025-01-04 13:19:55
  • 其他范围跳过
  1. 扫描候选块:只读取块256-383和384-511,跳过其他1600+个块
    这就是BRIN索引的"有损"特性------它不能精确定位到行,但能大幅减少需要扫描的数据块数量。
    在这个流程中,revmap页确实没有被使用!因为我们是顺序扫描汇总页,不需要"反向查找"。

2. 场景2:revmap页真正发挥作用的时刻

revmap页的作用体现在索引维护和特定块的精确查找场景:

场景A:插入/更新数据时

当向表中插入新数据或更新现有数据时:

bash 复制代码
INSERT INTO test_brin(ts, value) VALUES ('2025-01-05 12:00:00', 500);

假设这条数据被写入了第640号数据块,PostgreSQL需要:

  • 计算块范围编号:640 ÷ 128 = 5(这是第5个范围)
  • revmap页:读取第5项,得到(2, 5)
  • 定位汇总信息:跳转到第2页的第5项
  • 更新汇总值:扩展该范围的最小/最大值

没有revmap页会怎样?PostgreSQL必须扫描整个汇总页,找到blknum=512的那一项------效率低下。

场景B:VACUUM或索引汇总

假设表当前有数据到块767(范围5结束)

汇总页显示:

bash 复制代码
范围0 (块0-127):   {2025-01-01 00:00 .. 2025-01-01 21:19}
范围1 (块128-255): {2025-01-01 21:20 .. 2025-01-02 18:39}
 ...
范围5 (块640-767): {2025-01-05 10:40 .. 2025-01-06 08:00}```
现在插入新数据:

现在开始插入数据,写入到块768(范围6的开始)

bash 复制代码
INSERT INTO test_brin(ts, value) 
VALUES ('2025-01-06 15:00:00', 500);  -- 数据写入块768(范围6的开始)

关键问题 :此时会发生什么?

有两种策略:
策略A:立即更新索引

  • 每次INSERT → 计算范围号 → 查revmap → 更新汇总页
  • 优点:索引始终最新
  • 缺点:每次写入都要更新索引,性能开销大

策略B:延迟汇总(PostgreSQL采用)

  • INSERT时 → 只写数据,不更新索引
  • 定期通过VACUUM → 批量汇总新数据块
  • 优点:写入性能极佳
  • 缺点:新数据可能不在索引中

PostgreSQL选择了策略B,这导致了一个现象:

-- 插入新数据后,立即查询:

bash 复制代码
SELECT * FROM test_brin WHERE ts > '2025-01-06 14:00:00';
  • 此时BRIN索引可能无法帮助过滤,因为范围6的汇总还未创建
  • PostgreSQL可能需要扫描从块768开始的所有新块

VACUUM的作用:汇总未索引的数据块

bash 复制代码
VACUUM test_brin;

VACUUM会执行以下流程:

步骤1:扫描表,找出未汇总的块

复制代码
遍历表的所有数据块:
  块0-767:   已有汇总信息 ✓ 跳过
  块768:     无汇总信息 ✗ 需要处理
  块769:     无汇总信息 ✗ 需要处理
  ...
  块896:     无汇总信息 ✗ 需要处理(范围7开始)

步骤2:为未汇总的块创建/更新汇总

处理块768-895(范围6)

  1. 计算范围编号
bash 复制代码
 范围号 = 768 ÷ 128 = 6
  1. 查询revmap页第6项

    情况A:项不存在(全新范围)

    需要在revmap页添加新项

    需要在汇总页添加新汇总条目

    情况B:项已存在但汇总过期

    读取 revmap[6] = (2, 7)

    跳转到第2页第7项

  2. 扫描块768-895的所有行

    读取 ts 列的值:

bash 复制代码
   2025-01-06 08:00:05
   2025-01-06 08:01:00
   2025-01-06 08:02:15
   ...
   2025-01-06 21:59:45
  1. 计算汇总值* 最小值: 2025-01-06 08:00:05
bash 复制代码
   最大值: 2025-01-06 21:59:45
  1. 写入或更新汇总页
  • 在第2页第7项写入:
bash 复制代码
itemoffset=7, blknum=768, 
value={2025-01-06 08:00:05 .. 2025-01-06 21:59:45}
  1. 更新revmap页
bash 复制代码
   revmap[6] = (2, 7)

revmap在这个过程中的关键作用

关键要点

  • BRIN索引是"懒惰"的:写入时不更新索引,追求极致的写入性能
  • VACUUM是"勤快"的:定期批量汇总新数据,保持索引有效性
  • revmap是"聪明"的:让VACUUM能快速定位和更新特定范围的汇总信息
    这就是为什么使用BRIN索引的表建议:
bash 复制代码
-- 启用自动vacuum
ALTER TABLE test_brin SET (autovacuum_enabled = true);
-- 或定期手动vacuum
VACUUM test_brin;

否则,随着新数据不断插入,BRIN索引会逐渐"失效"------它覆盖的数据越来越少,查询性能也会下降。

适用场景

推荐 不推荐
大型表(数十GB到TB级) 小型表(几百MB以下,B-tree更快)
数据按自然顺序插入(如时间序列、日志、事件流) 数据无序或频繁更新
范围查询为主,顺序写入 需要精确点查询
存储空间敏感 数据分布极不均匀
例如日志、时序、物联网数据 例如用户账户、主键查询

调优参数

bash 复制代码
-- 调整pagesperrange(默认128)
CREATE INDEX idx ON table USING BRIN(column) 
WITH (pages_per_range = 64);  -- 更小的范围,更精确但索引更大
  • 较小值(如32):索引更大,但过滤更精确
  • 较大值(如256):索引更小,但可能扫描更多无关数据

维护建议

定期汇总新数据

bash 复制代码
VACUUM table;

或使用自动汇总(需要配置autovacuum)

bash 复制代码
ALTER TABLE table SET (autovacuum_enabled = true);

五、BRIN、B-Tree 与全表扫描的性能对比实践

我们将以test_brin为基表,分别创建test_full、test_bree,用来观察他们在被query时的表现

1. 全表扫描

首先,以test_brin为基表,创建test_full(没有索引),看看全表扫描的表现:

bash 复制代码
demo=# create table test_full as select * from test_brin;

现在我们尝试查询test_full表,以返回一个月内每天的最大ID。

bash 复制代码
explain (analyze)
SELECT date_trunc('day', ts), max(id) FROM test_full ts WHERE ts BETWEEN                                                                                         '2025-05-01 0:00' AND '2025-05-31 11:59:59' GROUP BY 1 ORDER BY 1;

执行计划输出:

bash 复制代码
                                                                                 QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------
 Finalize GroupAggregate  (cost=502571.88..566446.83 rows=527077 width=16) (actual time=90294.210..90366.007 rows=31 loops=1)
   Group Key: (date_trunc('day'::text, ts))
   ->  Gather Merge  (cost=502571.88..557662.22 rows=439230 width=16) (actual time=90294.167..90365.877 rows=57 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial GroupAggregate  (cost=501571.86..505964.16 rows=219615 width=16) (actual time=90080.387..90181.162 rows=19 loops=3)
               Group Key: (date_trunc('day'::text, ts))
               ->  Sort  (cost=501571.86..502120.90 rows=219615 width=16) (actual time=90077.399..90142.239 rows=175680 loops=3)
                     Sort Key: (date_trunc('day'::text, ts))
                     Sort Method: external merge  Disk: 4104kB
                     Worker 0:  Sort Method: external merge  Disk: 4728kB
                     Worker 1:  Sort Method: external merge  Disk: 4616kB
                     ->  Parallel Seq Scan on test_full ts  (cost=0.00..478331.44 rows=219615 width=16) (actual time=73315.834..89712.102 rows=175680 loops=3)
                           Filter: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-31 11:59:59+00'::timestamp with time zone))
                           Rows Removed by Filter: 12444480
 Planning Time: 0.237 ms
 JIT:
   Functions: 30
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 10.565 ms, Inlining 904.053 ms, Optimization 1371.036 ms, Emission 860.772 ms, Total 3146.426 ms
 Execution Time: 90368.850 ms
(21 rows)
s)

在我的 PostgreSQL 环境中,尽管该次查询在后台隐式启用了并行查询,这个查询执行依然大约花了 90368.850 ms。

2. btree scan

接下来,我们同样以test_brin为基表创建test_bree,并在 ts列上创建 BTREE 索引。

bash 复制代码
demo=# create table test_btree as select * from test_brin;

创建btree索引

bash 复制代码
create index test_btree_idx on test_btree(ts);

与上面访问test_full同样的query访问test_btree,同样访问一个月内每天最大的ID:

bash 复制代码
explain(analyze)
SELECT date_trunc('day', ts), max(id) FROM test_btree ts WHERE ts BETWEEN '2025-05-01 0:00' AND '2025-05-31 11:59:59' GROUP BY 1 ORDER BY 1;

执行计划输出:

bash 复制代码
                                                                                QUERY PLAN                                                                              
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Finalize GroupAggregate  (cost=478914.66..537358.27 rows=482258 width=16) (actual time=3864.283..3931.407 rows=31 loops=1)
   Group Key: (date_trunc('day'::text, ts))
   ->  Gather Merge  (cost=478914.66..529320.63 rows=401882 width=16) (actual time=3861.161..3931.256 rows=93 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial GroupAggregate  (cost=477914.63..481933.45 rows=200941 width=16) (actual time=3340.994..3420.693 rows=31 loops=3)
               Group Key: (date_trunc('day'::text, ts))
               ->  Sort  (cost=477914.63..478416.99 rows=200941 width=16) (actual time=3337.982..3385.741 rows=175680 loops=3)
                     Sort Key: (date_trunc('day'::text, ts))
                     Sort Method: external merge  Disk: 5080kB
                     Worker 0:  Sort Method: external merge  Disk: 4184kB
                     Worker 1:  Sort Method: external merge  Disk: 4184kB
                     ->  Parallel Bitmap Heap Scan on test_btree ts  (cost=10235.71..456778.34 rows=200941 width=16) (actual time=1475.912..3213.709 rows=175680 loops=3)
                           Recheck Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-31 11:59:59+00'::timestamp with time zone))
                           Heap Blocks: exact=1268
                           ->  Bitmap Index Scan on test_btree_idx  (cost=0.00..10115.15 rows=482258 width=0) (actual time=742.093..742.094 rows=527040 loops=1)
                                 Index Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-31 11:59:59+00'::timestamp with time zone))
 Planning Time: 0.270 ms
 JIT:
   Functions: 30
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 6.848 ms, Inlining 1318.194 ms, Optimization 1279.686 ms, Emission 873.182 ms, Total 3477.911 ms
 Execution Time: 3933.677 ms
(23 rows)

整个查询话费大约3933.677 ms,B-Tree 索引的表现超过了full table scan(尽管启用了parellel scan以及bitmap index scan)。现在我们来看看索引的大小。它的大小是 811MB,非常庞大。

bash 复制代码
 select pg_size_pretty(pg_relation_size('test_btree_idx'));
 pg_size_pretty
----------------
 811 MB
(1 row)

BRIN index scan

最后,是时候测试BRIN索引的性能了。

bash 复制代码
SELECT date_trunc('day', ts), max(id) FROM test_brin ts WHERE ts BETWEEN '2025-05-01 0:00' AND '2025-05-31 11:59:59' GROUP BY 1 ORDER BY 1;

执行计划输出:

bash 复制代码
                                                                               QUERY PLAN                                                                               
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Finalize GroupAggregate  (cost=469917.11..531009.19 rows=504112 width=16) (actual time=2596.688..2674.786 rows=31 loops=1)
   Group Key: (date_trunc('day'::text, ts))
   ->  Gather Merge  (cost=469917.11..522607.32 rows=420094 width=16) (actual time=2596.636..2674.623 rows=92 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial GroupAggregate  (cost=468917.08..473118.02 rows=210047 width=16) (actual time=2433.890..2535.406 rows=31 loops=3)
               Group Key: (date_trunc('day'::text, ts))
               ->  Sort  (cost=468917.08..469442.20 rows=210047 width=16) (actual time=2424.750..2489.887 rows=175680 loops=3)
                     Sort Key: (date_trunc('day'::text, ts))
                     Sort Method: external merge  Disk: 4544kB
                     Worker 0:  Sort Method: external merge  Disk: 4320kB
                     Worker 1:  Sort Method: external merge  Disk: 4584kB
                     ->  Parallel Bitmap Heap Scan on test_brin ts  (cost=163.86..446757.56 rows=210047 width=16) (actual time=910.053..2291.206 rows=175680 loops=3)
                           Recheck Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-31 11:59:59+00'::timestamp with time zone))
                           Rows Removed by Index Recheck: 11755
                           Heap Blocks: lossy=1197
                           ->  Bitmap Index Scan on test_brin_idx  (cost=0.00..37.83 rows=522492 width=0) (actual time=33.548..33.549 rows=35820 loops=1)
                                 Index Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-31 11:59:59+00'::timestamp with time zone))
 Planning Time: 234.501 ms
 JIT:
   Functions: 30
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 2.494 ms, Inlining 1264.943 ms, Optimization 828.205 ms, Emission 526.585 ms, Total 2622.227 ms
 Execution Time: 2704.947 ms
(24 rows)

使用了BRIN索引的查询花费了2704.947 ms,BTREE 和 BRIN 索引的查询性能很接近,但 BRIN 索引更胜一筹,超过了 BTREE。不仅如此,BRIN 还节省了大量的索引存储空间。此外,BRIN 还能节省很多索引占用的磁盘空间。

bash 复制代码
select pg_size_pretty(pg_relation_size('test_brin_idx'));
 pg_size_pretty
----------------
 80 kB
(1 row)

是的,BRIN 索引只占用了 80KB!这说明,与在同一份数据上构建的 B-Tree 索引相比,BRIN 所需的存储空间极小,而且在这种**分析型查询(Analytical Queries)**场景下,它的性能甚至更好 / 还要优于 B-Tree。

现在让我们修改查询语句,计算一天中每小时的最大ID。

使用BTREE index:

bash 复制代码
 explain (analyze) SELECT date_trunc('hour', ts), max(id) FROM test_btree ts WHERE ts BETWEEN '2025-05-01 0:00' AND '2025-05-01 11:59:59' GROUP BY 1 ORDER BY 1;
                                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 GroupAggregate  (cost=32476.52..32664.62 rows=9405 width=16) (actual time=42.912..55.617 rows=12 loops=1)
   Group Key: (date_trunc('hour'::text, ts))
   ->  Sort  (cost=32476.52..32500.04 rows=9405 width=16) (actual time=42.613..44.171 rows=8640 loops=1)
         Sort Key: (date_trunc('hour'::text, ts))
         Sort Method: quicksort  Memory: 722kB
         ->  Bitmap Heap Scan on test_btree ts  (cost=200.97..31855.83 rows=9405 width=16) (actual time=4.652..30.314 rows=8640 loops=1)
               Recheck Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-01 11:59:59+00'::timestamp with time zone))
               Heap Blocks: exact=56
               ->  Bitmap Index Scan on test_btree_idx  (cost=0.00..198.62 rows=9405 width=0) (actual time=4.582..4.583 rows=8640 loops=1)
                     Index Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-01 11:59:59+00'::timestamp with time zone))
 Planning Time: 6.645 ms
 Execution Time: 59.472 ms
(12 rows)

使用BRIN index

bash 复制代码
demo=# explain (analyze) SELECT date_trunc('hour', ts), max(id) FROM test_brin ts WHERE ts BETWEEN '2025-05-01 0:00' AND '2025-05-01 11:59:59' GROUP BY 1 ORDER BY 1;
                                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 GroupAggregate  (cost=61697.33..61861.95 rows=8231 width=16) (actual time=36.634..40.022 rows=12 loops=1)
   Group Key: (date_trunc('hour'::text, ts))
   ->  Sort  (cost=61697.33..61717.90 rows=8231 width=16) (actual time=36.487..37.354 rows=8640 loops=1)
         Sort Key: (date_trunc('hour'::text, ts))
         Sort Method: quicksort  Memory: 722kB
         ->  Bitmap Heap Scan on test_brin ts  (cost=39.09..61162.03 rows=8231 width=16) (actual time=11.307..31.154 rows=8640 loops=1)
               Recheck Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-01 11:59:59+00'::timestamp with time zone))
               Rows Removed by Index Recheck: 31169
               Heap Blocks: lossy=254
               ->  Bitmap Index Scan on test_brin_idx  (cost=0.00..37.03 rows=20096 width=0) (actual time=4.722..4.722 rows=2540 loops=1)
                     Index Cond: ((ts >= '2025-05-01 00:00:00+00'::timestamp with time zone) AND (ts <= '2025-05-01 11:59:59+00'::timestamp with time zone))
 Planning Time: 0.683 ms
 Execution Time: 40.252 ms
(13 rows)

这两种索引的性能几乎相同,但 BRIN 索引在存储空间节省方面带来了极其显著的优势,相比 BTREE 能节省非常、非常多的空间。

我在 1 亿(100M)数据量 上做了性能测试,BRIN 在如此庞大的数据规模下依然表现很好。

首先,正如你可能已经注意到的,当表增长到相当大的规模时,BRIN 索引的优势才会真正显现出来。这也体现了 PostgreSQL 具备**纵向扩展(垂直扩展 / Vertical Scaling)**的能力:在尝试解决的很多问题中,尤其是时间序列分析(Temporal Analytics)类查询,BRIN 索引肯定能帮助你更高效地运行查询。

最关键的一点是存储空间:如果你的数据集适合使用 BRIN,那么将索引占用减少 99% 以上(缩小 99%+)会带来巨大的收益。能把索引体积压缩 99% 以上是非常惊人的。

BRIN 索引的工作方式是:它会返回某个数据范围(page range)内所有页面中的全部元组(tuples),因此它是一种有损(lossy)索引,并且需要额外的计算来进一步过滤真正匹配的记录。所以虽然有人可能会认为这不是很好,但它其实也有一些优点/优势。

  • 由于 BRIN 只存储一段连续页面(page range)的摘要信息,因此相比 B-Tree 索引,BRIN 索引通常非常小。可以减少索引本身占用 shared_buffer 的空间,从而让 shared_buffer 里有更多空间留给表(heap/heap page,也就是 heap 数据)
  • BRIN 的"有损性(lossiness)"可以通过设置 每个索引范围包含的页面数量(pages per range) 来控制(此内容将在前续已经讨论)。
  • BRIN 将摘要信息的构建工作交由 VACUUM 或 autovacuum 处理,因此在事务或 DML(INSERT/UPDATE/DELETE)操作中,索引维护开销非常低、影响很小。

六、局限性 / 约束

当索引键值的排序与存储层中数据块(blocks)的物理组织顺序一致时,BRIN 索引才能发挥高效性能。最理想的情况下,这可能要求 表的物理顺序(通常是行的写入/创建顺序)与索引键的逻辑顺序相匹配。因此,基于递增序列(sequence)、时间字段(如 ts)、按生成顺序增长的数值或日期数据,是 BRIN 索引最适合的场景。

在上述例子中,由于 ts 字段的值是 持续递增写入的,BRIN 的表现会非常好。但如果字段中的日期数据存在 频繁更新或被修改,导致顺序不再与物理存储对齐,那么 BRIN 索引的性能就会下降,无法达到预期效果。

相关推荐
jghhh012 分钟前
MATLAB分形维数计算:1D/2D/3D图形的盒维数实现
数据库·matlab
重生之绝世牛码1 小时前
Linux软件安装 —— PostgreSQL高可用集群安装(postgreSQL + repmgr主从复制 + keepalived故障转移)
大数据·linux·运维·数据库·postgresql·软件安装·postgresql高可用
数据知道1 小时前
PostgreSQL 实战:详解 UPSERT(INSERT ON CONFLICT)
数据库·python·postgresql
源力祁老师1 小时前
Odoo日志系统核心组件_logger
网络·数据库·php
洋不写bug2 小时前
数据库基础核心操作——CRUD,超详细解析,搭配表格讲解和需求的实现。
数据库
马猴烧酒.3 小时前
JAVA后端用户登录与鉴权详解
java·数据库·sql
heartbeat..3 小时前
Redis 常用命令全解析:基础、进阶与场景化实战
java·数据库·redis·缓存
数据知道3 小时前
PostgreSQL 实战:一文掌握如何优雅的进行递归查询?
大数据·数据库·postgresql
陌上丨3 小时前
MySQL8.0高可用集群架构实战
数据库·mysql·架构
重生之绝世牛码3 小时前
Linux软件安装 —— ClickHouse单节点安装(rpm安装、tar安装两种安装方式)
大数据·linux·运维·数据库·clickhouse·软件安装·clickhouse单节点