王叔是市档案馆最资深的馆长,他的保管室里有一整面墙的日记本,从墙的最左边到最右边,严格按年份排列着——1930 年的在左,2024 年的在右。今天有个读者想借阅 1963 年的几本日记。王叔面临一个经典难题:如果没有任何记录,他只能从第一本开始一本本翻过去,翻遍上千本才能确定“没有更早的了”(全表扫描);如果给每本日记做一张目录卡片,查询是快了,但归档 100 万本日记、每本写一张卡片,占用空间比日记本身还多,维护成本巨大(B+树索引)。王叔想出一个折中方案:他给每一层书架贴一张小小的便签,只写两个数字——“这一层最早的一本是哪年,最晚的一本是哪年”。读者来找 1963 年的日记时,王叔只需沿着书架扫一眼便签,就能跳过“1970 年以后”的楼层,直接走到写着“1960–1969”的那一层,再在这一层里翻找。这张“范围便签”,就是数据库里 BRIN 索引的核心思想。
对 AI 与数据从业者来说,理解 BRIN 意味着在面对海量时序特征、日志和模型监控数据时,能设计出节省百倍存储空间且不牺牲查询速度的索引方案,这是构建高性价比数据管道的关键能力。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
索引需求背景
王叔的档案馆里有一整面墙的日记,读者常常按年份借阅。最初档案馆没有任何检索工具,找一本 1963 年的日记只能从 1930 年那本开始,一本本翻过去,直到翻完 1963 年才敢确认目标就在这里。这就是数据库里的全表扫描(Sequential Scan):把表的每一页都读一遍,逐一比对过滤条件。表越大,翻得越久,耗时与表的体积成正比。
后来王叔请人做了完整的目录卡片,每本日记一张,标好年份和位置。这就是 B+ 树索引的思路:为每一行数据建立一条索引项,查询时通过树形结构快速定位。目录的好处是精确,但代价也大——100 万本日记就要 100 万张卡片,目录柜占了档案馆整整一面墙,而且每新增一本日记都要同步更新卡片。在数据库里,B+ 树索引的体积通常是表数据的 10%~20%,对于动辄几 GB 的大表,这个开销不可忽视。
王叔摸了摸目录柜,又看了看那面墙,心想:能不能不建目录柜,只在书架层板上贴一张小便签?这个念头,就是 BRIN 索引的起点——用极小的存储代价,换取对大部分无关数据的“一眼跳过”。
- B+ 树索引总体积 \(S_{\text{B+tree}}\)表示整个 B+ 树索引占用的磁盘空间,它随行数线性增长。
- 表的总行数 \(N_{\text{rows}}\)B+ 树要为每一行都建立一条索引项,所以行数直接决定索引规模。
- 每条索引项的大小 \(S_{\text{key}} + S_{\text{pointer}}\)每一条索引项都包含索引键值本身(如时间戳的 8 字节)和指向对应行的指针(通常 6~8 字节)。
BRIN核心原理
王叔的折中方案很简单:他把书架按层分组,每层架子贴一张便签,上面只写两个数字——这一层最早一本的年份,和最晚一本的年份。例如第三层写“1960–1969”,第四层写“1970–1979”。读者来找 1963 年的日记,王叔沿书架扫过去:第一层“1930–1939”,太早了;第二层“1940–1949”,太早了;第三层“1960–1969”,在区间内,需要查看;第四层“1970–1979”,太晚了,跳过。这样他只需要真正“翻箱倒柜”地查一层,而不是整面墙。
BRIN(Block Range Index,块范围索引)把这个思想搬进了数据库。它把一张表在物理上连续的若干数据页划分为一个“块范围”(Block Range),每个块范围只记录该范围内每一列的最小值和最大值,这套信息叫“范围摘要”(Range Summary)。在建索引时,数据库并不为每一行建立索引项,而是为每一个块范围建立一条摘要。查询时,优化器先读取所有块范围的摘要,凡是与查询条件完全不相交的块范围直接跳过,只有可能命中的块范围才被纳入真正的扫描。
王叔的便签之所以好用,是因为它把“定位”的粒度从每一本日记提升到了每一层书架——虽然不能精确到哪一本,但能快速排除绝大多数不可能的楼层,这就是 BRIN“以空间换时间、以精度换速度”的精髓。
- 块范围总数 \(N_{\text{ranges}}\)表示整张表被划分出的“书架层数”,也就是 BRIN 索引中范围摘要的条数。
- 表的总页数 \(N_{\text{pages}}\)表在磁盘上占用的物理数据页总数,例如一张 1 GB 的表,在 8 KB 页大小下就是 131072 页。
- 每块范围的页数 \(P_{\text{per range}}\)相当于“每层书架放几本日记”,这正是 BRIN 的核心参数 pages_per_range,PostgreSQL 默认是 128。
- 向上取整 \(\lceil \cdot \rceil\)总页数未必能整除每块范围的页数,末尾不足一整个块范围时也要算作一个块范围。
物理有序前提
如果日记本没有按年份摆放,而是随手乱插,王叔的便签就彻底失效了——第三层的便签可能写着“1940–2005”,横跨大半个世纪,无论查哪个年份都要翻这一层。BRIN 索引能工作的前提,是数据在物理存储上“大致有序”,也就是行的物理顺序与索引列的值存在较强的相关性。
数据天然满足这种有序性的场景其实非常常见。时序数据(传感器读数、股票 tick、监控指标)通常按照时间戳写入;日志数据(应用日志、访问日志)天然带有追加写入的特性,后写的行时间戳更大。对于这类数据,物理顺序几乎等于时间顺序,BRIN 的便签就会非常“紧”——每层的 min 到 max 区间很窄,查询任意一个窄时间窗口都能跳过绝大部分块范围。
反过来说,如果一张表的数据是随机插入的,或者频繁地随机更新导致行的物理位置乱序,同一个块范围内的 min 和 max 会被迅速“撑大”,最终每个块范围的区间都覆盖全表,BRIN 便签形同虚设,查询退化为全表扫描。因此在设计表时,如果打算用 BRIN,就应该尽量采用追加写入(append-only)模式,并把时序列作为主排序键,保证数据物理有序。
存储结构机制
王叔的目录柜越来越占地方,而他的便签只是一小叠纸片,塞在抽屉角落里。BRIN 存储结构的最大优势就在于此。每个块范围的范围摘要只包含:该范围内每列的最小值、最大值,以及一些辅助标志位。以一张只有时间戳列的表为例,每条摘要大约只需要 16~24 字节。
在 PostgreSQL 中,BRIN 索引的更新与 B+ 树有很大不同:B+ 树每次插入都要更新对应叶子节点,而 BRIN 只需在数据写满一个新块范围时更新一条摘要。它随表结构存储在独立的索引文件中,内核在插入数据时自动维护,对应用层完全透明。
王叔的纸片之所以能长期有效,是因为他每整理完一层书架就顺手更新一次便签,而 BRIN 的自动同步机制正是替数据库做了这件事——代价小到可以忽略,收益却极其可观。
- BRIN 索引总体积 \(S_{\text{brin}}\)BRIN 索引占用的磁盘空间,通常远小于 B+ 树索引。
- 块范围条数 \(N_{\text{ranges}}\)索引摘要条数来自表的总页数与每块范围页数的比值,见前面的公式。
- 每条摘要体积 \(S_{\text{summary}}\)一条摘要需要存储每列的 min 和 max,对 8 字节时间戳列大约 16 字节,加上辅助标志约 24 字节。
- 页数与每块范围页数的比值 \(\frac{N_{\text{pages}}}{P_{\text{per range}}}\)块范围越多索引越大,块范围越粗索引越小,二者成反比。
查询加速原理
读者来找 1963 年的日记,王叔不会真的去翻整面墙。他会先把 12 层架子的便签全部扫一遍(这一步极快,因为只有 12 张纸片),然后挑出写着“1960–1969”的第三层和可能覆盖边界的第四层,只对这两层做细致翻找。省下的时间,就是跳过其余 10 层所带来的。
数据库执行 BRIN 查询时也是同样的节奏。PostgreSQL 的查询计划通常分两步:第一步,读取 BRIN 索引的所有范围摘要,判断每个块范围与查询过滤条件是否重叠(Bitmap Index Scan);第二步,对可能存在匹配行的块范围,将其中所有数据页取出并逐行过滤(Bitmap Heap Scan)。因为第一步的摘要极小,读取成本几乎可以忽略,第二步所扫描的页面数就决定了总耗时。
需要注意,BRIN 不会告诉你目标行在哪一页,它只能告诉你“哪些块范围值得翻”。所以查询的“选择性”是加速比的决定性因素——如果你查询的年份跨度占了全部年份的 80%,几乎所有便签都会命中,BRIN 也就无从加速了。
- 加速比相对于全表扫描,BRIN 索引把需要读取的页数压缩了多少倍,数值越大说明越高效。
- 总页数 \(N_{\text{pages}}\)全表扫描必须读取的页数,是加速比的分子。
- 命中的块范围数 \(N_{\text{matched}}\)与查询条件区间重叠的块范围个数,取决于查询选择性。
- 每块范围的页数 \(P_{\text{per range}}\)每个命中的块范围内部仍然需要逐页扫描,所以命中块范围条数乘以每块页数,就是实际扫描页数。
粒度参数调优
王叔发现,便签贴得越细,定位越准,但便签也要准备得越多。如果给每层书架(10 年跨度)贴一张,一堵墙只需要 10 张便签;如果给每一个格子(1 年跨度)贴一张,则需要 100 张。这就是 BRIN 中 pages_per_range 参数的权衡本质。
pages_per_range 表示一个块范围包含多少数据页,默认值是 128。调小它,每个块范围覆盖的页更少,min–max 区间更窄,查询跳过的精度更高,但索引的摘要条数变多、体积变大;调大它,摘要条数变少、索引更小,但每个块范围的区间可能被撑宽,扫描的页面增多。
实践中的调参策略通常是:先使用默认值 128,然后在 EXPLAIN 中观察实际扫描的页面数。如果发现命中的块范围过宽导致扫描页数偏多,再逐步调小 pages_per_range 并重建索引;如果索引体积占用量过大而查询选择性尚可,则调大该值。这就像王叔根据读者的查询习惯调整便签的疏密——查得越细,便签就该贴得越密。
- 查询总开销 \(C_{\text{query}}\)执行一次范围查询需要读取的页数,是衡量性能的核心指标。
- 扫描命中块范围的开销 \(N_{\text{matched}} \times P_{\text{per range}}\)命中的每个块范围都需要逐页扫描,块范围越大,这一段开销越高。
- 读取摘要的开销 \(\frac{N_{\text{pages}}}{P_{\text{per range}}}\)BRIN 索引本身的摘要条数,块范围越大,摘要越少,这一步越便宜。
适用场景边界
档案馆里不仅有按年份排列的日记,还有一沓按写信人姓名随机归档的信件。王叔的便签法对日记很有效,但面对信件就完全派不上用场——因为信件没有天然的顺序。BRIN 索引也有同样清晰的适用边界。
BRIN 最适合的场景有三大特征:一是表足够大,至少是数十万行以上,否则全表扫描本身也不慢,索引反而多余;二是数据物理有序,通常来自时序数据、日志数据、遥测数据这类追加写入的数据源;三是查询以范围过滤为主,例如“查最近 7 天”“查 10:00 到 11:00 之间的所有记录”。在这类场景中,BRIN 凭借极小的体积和极快的摘要扫描速度,往往能以不到 B+ 树 1% 的体积实现接近的查询性能,性价比极高。
反过来,如果应用以单行点查询为主(如按用户 ID 精确查一条记录),或者数据频繁随机更新导致物理有序性被破坏,BRIN 就不合适了。点查询无法利用块范围的区间信息——它依然要扫描目标所在的整个块范围。此时 B+ 树的精确行定位优势无法替代。在实际工程中,这两种索引并不互斥:一个大表可以同时拥有用于精确查询的 B+ 树索引和用于范围过滤的 BRIN 索引,让数据库优化器根据查询特征各取所需。
王叔的记账本上写着这样一句话:好的工具不是万能的,而是把一件事做到极致。BRIN 的极致,就是用一张薄薄的便签,在物理有序的大表上实现近乎零成本的范围过滤。理解它的适用边界,你就能在合适的数据上,用最小的代价换来最大的加速。
AI 应用场景
特征存储中的时间窗口查询
阶段:特征工程与特征存储
解决的问题:机器学习训练和线上推理都需要按时间窗口拉取特征,例如“取过去 7 天的用户行为特征”。传统做法是每天跑定时任务全表扫描,把整张特征表重新计算一遍,耗时数小时且浪费大量计算资源。使用 BRIN 索引后,特征表按时间戳物理有序存储,范围查询直接跳过大部分历史块,将窗口拉取时间从小时级压缩到分钟级。
具体案例:某推荐系统团队维护着一张 50 亿行的用户行为特征表,每天需要为近 100 万活跃用户生成“最近 3 天行为特征”。在时间戳列上建立 BRIN 索引后,特征拉取耗时从 2.5 小时降至约 8 分钟,索引体积仅占表数据的 0.3%,远小于 B+ 树的 12%。
模型推理日志与可观测性分析
阶段:模型部署与监控
解决的问题:大模型服务每秒产生大量推理日志,包含时间戳、请求延迟、输入长度、输出 token 数等指标。当线上出现延迟异常时,需要快速按时间窗口定位异常日志。BRIN 索引让“查询 14:00–14:05 的所有推理日志”这类操作无需全表扫描,极大缩短故障排查时间。
具体案例:某 AI 中台团队将每天约 2 亿条推理日志写入 PostgreSQL 分区表,并按时间戳建立 BRIN 索引。当线上某个模型实例延迟突然升高 10 倍时,运维人员能在几秒内拉取指定 5 分钟窗口内的全部日志,准确分析出是由于某次部署引起的输入长度骤增所致。
物联网传感器数据仓库
阶段:数据采集与预处理
解决的问题:IoT 场景下,成千上万个传感器以固定频率上报数据,数据量每月增长数十亿行。这些数据最典型的查询是“查看某个设备在某段时间内的读数序列”,天然属于时间范围查询。BRIN 索引配合时序数据追加写入的特点,能够以极低的存储开销支撑高效的范围扫描。
具体案例:某智慧农业平台在田间部署了 5000 个环境传感器,每 10 秒上报一次温湿度、光照和土壤数据。平台将数据按设备 ID + 时间戳分区存储,并为时间戳列建立 BRIN 索引后,查询单个设备一周的温湿度曲线从 40 秒降至不足 2 秒,同时索引存储成本比 B+ 树方案节省了约 95%。
Python 演示
"""BRIN 索引模拟器: 展示块范围摘要如何加速范围查询""" class BRIN: """将数据页划分为若干块范围, 每个范围只记录最小值和最大值. """ def __init__(self, pages, pages_per_range=4): self.pages = pages self.per_range = pages_per_range self.summaries = [] self._build() def _build(self): """扫描所有页, 为每个块范围生成一条摘要: (min, max, 起始页, 结束页)""" total = len(self.pages) for start in range(0, total, self.per_range): end = min(start + self.per_range, total) values = [v for page in self.pages[start:end] for v in page] self.summaries.append( (min(values), max(values), start, end) ) def query(self, low, high): """范围查询, 返回可能包含目标行的候选页索引列表""" candidates = [] for smin, smax, start, end in self.summaries: if not (smax < low or smin > high): candidates.extend(range(start, end)) return candidates def index_size(self): """模拟每条摘要占用约 24 字节""" return len(self.summaries) * 24 def make_time_series_pages(total_pages, page_size=10): """生成物理有序的时间序列数据, 模拟传感器按时间戳追加写入""" pages = [] ts = 0 for _ in range(total_pages): page = [] for _ in range(page_size): page.append(ts) ts += 1 pages.append(page) return pages def main(): pages = make_time_series_pages(1000, page_size=10) for ppr in (1, 4, 16): idx = BRIN(pages, pages_per_range=ppr) hits = idx.query(2000, 3000) print("pages_per_range=%2d 候选页=%4d/%4d 索引体积=%dB" % (ppr, len(hits), len(pages), idx.index_size())) if __name__ == "__main__": main()
class BRIN:定义 BRIN 索引模拟器,核心是 summaries 列表保存每个块范围的 min/max 摘要。self.summaries.append((min(values), max(values), start, end))每个块范围只存一条四元组摘要,这就是“书架上的便签”。def query(self, low, high)范围查询逻辑:跳过smax < low or smin > high的块范围,其余块范围整体加入候选。return candidates返回的是候选页集合而非精确行位置,需要在后续扫描中进一步过滤。for ppr in (1, 4, 16)对比不同 pages_per_range 下的效果差异,观察索引体积与候选页数量的此消彼长。hits = idx.query(2000, 3000)在全表 10000 行数据中查找值在 2000~3000 之间的行,覆盖约 10% 的数据。
常见误区
误区既然 BRIN 索引那么小,那我把所有大表都加上 BRIN 索引,查询速度肯定都能大幅提升吧?
正解BRIN 能加速的前提是数据在物理上有序或近似有序。如果一张表的写入是随机的,块范围摘要的 min–max 区间会迅速被撑大到覆盖全表,查询时每个块范围都命中,索引完全失效。这就像给乱序摆放的档案架贴便签,便签上写着“1930–2024”,等于没贴。
为何常见:BRIN 索引体积小、创建快,容易让人误以为它是“免费的加速器”,忽略了它成立的核心条件。
误区既然我建了 BRIN 索引,那按条件查单条记录的时候,数据库也应该能像 B+ 树那样直接定位到那一行。
正解BRIN 根本不知道自己块范围内每一行的具体位置,它只知道整个范围的 min 和 max。点查询时,数据库必须把目标行所在块范围的每一页都读出来逐行过滤。且范围越精准,BRIN 的精度越低(一个块范围覆盖 128 页)。精确单行定位仍然需要 B+ 树或哈希索引。
为何常见:人们习惯性地把所有索引都想象成“书的目录”,忽略了 BRIN 其实更像“层架标签”,定位粒度完全不同。
误区块范围越细,min–max 区间越窄,跳过就越精准,那把 pages_per_range 改成 1 是不是最优?
正解pages_per_range 过小会导致两个问题:一是摘要条数暴增,索引体积随之变大,读取摘要的时间也变长;二是数据新增会导致摘要更新频繁,写入放大。真正的调优是在索引体积、摘要读取成本和跳过精度之间找平衡点,而不是单纯追求某一项极致。
为何常见:只看到了“更细粒度 = 更精准”的一面,忽略了任何索引在空间和维护上都有代价。
误区BRIN 索引建立之后,不管表后续怎么更新删除,索引都能自动保持高效。
正解BRIN 索引确实会自动维护,但它的高效依赖物理有序性。如果表上有频繁的随机 UPDATE 或大批量 DELETE,页面的物理顺序会被打乱,旧块范围和新写入的数据混在一起,min–max 区间会逐渐变宽,查询性能持续退化。此时需要重建索引,或者用 CLUSTER 命令重新整理表的物理顺序。
为何常见:把“自动维护”误解为“自动保持高性能”,忽视了物理有序性这个根基会因写模式漂移而瓦解。
误区BRIN 又小又快,那我干脆把所有 B+ 树索引都删掉,全部换成 BRIN 算了。
正解两种索引解决的是不同维度的问题。B+ 树适合高选择性的点查询和精确匹配,BRIN 适合大表上的低选择性范围查询。在许多生产系统中,它们共存才是最优解——B+ 树负责精确命中,BRIN 负责粗粒度过滤,数据库优化器会按查询特征选择最合适的路径。
为何常见:拿 BRIN 的“快”直接对比 B+ 树的“慢”,忽略了应用场景的差异,陷入了“万能工具”思维。
面试题
Q1. BRIN 索引为每个块范围存储的核心信息是什么?
- A.每一行的完整物理地址列表
- B.范围内各列的最小值和最大值← 正确答案
- C.范围内所有数据行的内容副本
- D.每列数据的哈希校验汇总值
答案:B。BRIN 的“范围摘要”只保存每个块范围内各列的 min 与 max,不存行地址也不存数据副本。这就像书架便签上只写最早和最晚的年份,不列出每一本书。选项 B 才是正解。
Q2. BRIN 索引能够高效工作的关键前提是什么?
- A.表中的数据在物理上保持有序性← 正确答案
- B.表中没有任何重复的数据记录
- C.表必须经过多次 VACUUM 清理
- D.表数据必须全部加载到内存
答案:A。BRIN 通过“便签区间是否与查询重叠”来跳过块范围,只有数据物理有序时便签区间才足够窄。物理有序性才是 BRIN 发挥作用的根。选项 B/C/D 均与 BRIN 的核心机制无关。
Q3. 下列哪种查询场景最适合使用 BRIN 索引?
- A.按磁盘物理地址精确定位某一特定行
- B.在大型时间序列表中查询指定时间范围← 正确答案
- C.对全表数据做无条件的聚合统计操作
- D.在随机写入的小表中查询唯一记录
答案:B。BRIN 是专为大表时间范围查询设计的,物理有序 + 范围过滤是最佳组合。A 是精确点查询,C 是全表聚合,D 是小表随机查询,都不适合 BRIN。
Q4. 执行 BRIN 查询时,数据库核心的扫描动作是什么?
- A.对每个块范围判断其区间与查询条件是否重叠← 正确答案
- B.用二分查找在每个页面内精确定位目标行
- C.将全部块范围内的数据加载到工作内存中
- D.对整个表重新计算所有列的最小和最大值
答案:A。BRIN 查询先读取全部摘要,判断每个块范围的 [min,max] 是否与查询条件重叠,重叠范围进入候选集合。B 是 B+ 树的定位方式,C 是全表加载,D 是建索引时的操作而非查询时的操作。
Q5. 某表有 1 万个数据页,pages_per_range 设为 100,BRIN 索引大约有多少条块范围摘要?
- A.十条
- B.一百条← 正确答案
- C.一千条
- D.一万条
答案:B。块范围摘要条数 = 总页数 ÷ pages_per_range = 10000 ÷ 100 = 100 条。这相当于一面墙分成 100 层,每层贴一张便签。选 B。
Q6. 关于 BRIN 索引与 B+ 树索引的对比,下列说法正确的有哪些?
- A.BRIN 索引体积通常远小于 B+ 树索引← 正确答案
- B.BRIN 能精确给出每个块范围的首个行号
- C.B+ 树索引适合高精度的单行点查询← 正确答案
- D.BRIN 适合大表上的时间范围过滤← 正确答案
答案:ACD。BRIN 索引体积可小至 B+ 树的 1%,适合大表范围过滤,A、D 正确。B+ 树精确到行,适合点查询,C 正确。B 错,BRIN 只记录 min 和 max,不记录任何行号。
Q7. 将 pages_per_range 从 128 调小到 32,会带来什么直接影响?
- A.索引条数变少且索引体积明显减小
- B.索引条数变多且索引体积随之增大← 正确答案
- C.索引不再要求数据具备物理有序性
- D.查询精度下降而扫描的页面数增加
答案:B。pages_per_range 变小意味着每个块范围包含的页更少,摘要条数变多,索引体积增大,但每个便签的区间更窄、定位更精准。A 和 D 描述的方向恰好相反,C 是错误认知。
Q8. 下列哪些情况下 BRIN 索引的查询性能会明显退化?
- A.表上频繁随机更新破坏了物理有序性← 正确答案
- B.块范围摘要区间很宽几乎覆盖全表← 正确答案
- C.查询条件只覆盖很小的数据时间区间
- D.数据仅追加写入且写入时间戳严格递增
答案:AB。物理序被打乱后 min–max 区间被撑宽,便签失去筛选力,A、B 都会导致退化。C 窄区间查询恰好是 BRIN 最擅长的情况,D 是 BRIN 的理想工作负载,不会退化。
Q9. B+ 树索引与 BRIN 索引在“存储内容”上的本质区别是什么?
- A.B+ 树存每行的键值,BRIN 存块范围摘要← 正确答案
- B.B+ 树存范围摘要,BRIN 存每行的键值
- C.两者都存每行键值但采用不同的压缩方式
- D.两者都存范围摘要但采用不同的排序规则
答案:A。B+ 树为每一行建立索引项,精确到行;BRIN 为每个块范围建立一条摘要,只存区间。两者在存储粒度上有本质差异,A 正确,其余选项都混淆了二者的存储对象。
Q10. 在实际工程中,选择使用 BRIN 索引的合理条件有哪些?
- A.表体量足够大(通常数十万行以上)← 正确答案
- B.查询以范围过滤为主且选择性较好← 正确答案
- C.索引列与数据的物理写入顺序存在相关性← 正确答案
- D.表中存在大量高频的单行随机更新操作
答案:ABC。BRIN 需要在“大表、范围查询、物理相关”三要素下才能发挥作用,A、B、C 是典型适用条件。D 是高频随机更新,会破坏物理有序性,是 BRIN 的禁忌场景。