# 概要 - [[PostgreSQL]] 14で導入された、[[BRIN]](Block Range Index)の演算子クラス(Operator Class)のファミリー(例: `int4_minmax_multi_ops`, `timestamptz_minmax_multi_ops` など) - [[Skipping Index]] - 従来の `minmax` 演算子クラスが1つのブロックレンジ(ページ範囲)につき**単一の最小値・最大値のペア `[min, max]`** のみを保持するのに対し、`minmax-multi` は**複数の小さな区間(interval)や単一値(point)の集合**として要約データを保持する - これにより、外れ値(Outlier)の混入やUPDATEによるタプル移動、物理的なソート順の局所的な崩れがあっても、ギャップ(空白区間)を保持して検索時のブロックスキップ効率を高く維持できる # minmaxの課題 - [[BRIN]] はテーブルの物理的な連続ブロック群(`pages_per_range`、デフォルト128ページ=1MB)ごとに、格納されている値の要約情報(Summary)だけを記録する軽量なインデックス(Zone Map / Data Skipping Indexの一種) - 従来の `minmax` は各ブロックレンジに対して `[min, max]` の1区間しか持てないため、**外れ値(Outlier)に極めて弱い**という問題がある - 例:あるブロックレンジに `1000`〜`2000` の値が格納されている状態で、`1000000` という値が1件だけ挿入(またはHOT更新失敗等で別ページから移動)されたとする - `minmax` の場合:要約区間が `[1000, 1000000]` に拡大し、幅が約1000倍になる。結果として `WHERE col = 50000` のようなクエリでもこのブロックレンジをスキップできず、不要なSequential Scan(Bitmap Heap Scan)が発生する - `minmax-multi` の場合:要約を `[1000, 2000]` と `1000000`(単一値)の2つに分けて保持できるため、`2001`〜`999999` の間にある検索キーに対して引き続きこのブロックレンジを除外(スキップ)できる # アルゴリズム ## 要約データの構造 - 各ブロックレンジの要約は、**区間(range)** `[min, max]` と **単一値(point)** の集合(`brin_minmax_multi.c` の `Ranges` 構造体) - 区間は境界値を2つ、点は1つ消費する - 制約: $2 \times (\text{区間数}) + (\text{点の数}) \le$ `values_per_range`(デフォルト32、指定可能範囲は8〜256) - 区間同士は重ならず、区間・点はそれぞれソートされた状態で保持される - 区間の「間隔(gap)」を測るため、演算子クラスは **distance関数**(サポートプロシージャ11、例: `brin_minmax_multi_distance_int4`)を提供する - 整数なら $b - a$、timestampなら時間差、`inet` ならアドレス差、といった型ごとの「距離」 ## 値の追加(add_value)とcompaction 1. 新しい値はまずソートされていないバッファに追記される(毎回ソートするのを避けるため、メモリ上では `values_per_range` の数倍の領域を確保している) 2. バッファが一杯になるか、ディスクに書き出す(serialize)タイミングで **compaction** を行う 1. 点をソートして重複を除去する 2. 区間と点を「区間のリスト」(点は `min = max` の縮退区間)に展開してソートし、重なっている区間をマージする 3. 隣接する区間の間のgap `distance(r[i].max, r[i+1].min)` を全て計算し、**大きい順に並べる** 4. 上位 `values_per_range / 2 - 1` 個のgapだけを残し、それ以外のgapを埋める(=隣り合う区間を結合する) - 残す境界値は「全体の最小値・最大値」と「残すgapの両端」なので、結果の区間数は最大 `values_per_range / 2` 個になる - 結合後に `min = max` になったものは点として保存される(1スロットで済む) - 要するに、**値が密集している部分を区間にまとめ、大きな空白区間(gap)を優先的に残す**貪欲法 - 情報は損失するが、結合後の区間は必ず元の値を全て包含するため、偽陰性(本来ヒットすべきブロックの除外)は起きない ## 検索(consistent) - `col = k`: `k` が含まれる区間があるか、または `k` と一致する点があるかを二分探索で調べる。どちらも無ければこのブロックレンジはスキップ - `col < k`, `col >= k` などの不等号: 全体の最小値・最大値だけを見れば判定できる(この場合は `minmax` と同等の効果) - 偽陽性(区間内だが実際にはその値が無い)は起こり得るので、ヒットしたブロックはBitmap Heap Scanで再チェック(recheck)される ## 要約の統合(union) - 並列ビルドや `brin_summarize_range` などで2つの要約を統合する場合も、両方の区間・点を連結してから上記と同じcompactionを行う ## Running example 簡単のためバッファの存在は無視し、`values_per_range = 8`(最小値)でシリアライズ時のcompactionだけを追う。整数カラムで、distanceは差 $b - a$ とする。 ### Step 1: 初回の要約 あるブロックレンジに次の9個の値が格納されているとする。 `10, 12, 15, 20, 25, 30, 500, 1000, 1020` 点として持つと9スロット必要で、上限8を超えるのでcompactionする。各値を縮退区間とみなし、隣接gapを計算する。 | gap | 距離 | | --- | ---: | | 10 → 12 | 2 | | 12 → 15 | 3 | | 15 → 20 | 5 | | 20 → 25 | 5 | | 25 → 30 | 5 | | 30 → 500 | **470** | | 500 → 1000 | **500** | | 1000 → 1020 | **20** | 残せるgapは $8/2 - 1 = 3$ 個なので、大きい順に `500→1000`(500), `30→500`(470), `1000→1020`(20) を残す。 境界値は「全体のmin/max」+「残すgapの両端」: - 全体: `10`, `1020` - gap: `30, 500` / `500, 1000` / `1000, 1020` ソートすると `10, 30, | 500, 500, | 1000, 1000, | 1020, 1020` となり、2つずつペアにすると: - 区間 `[10, 30]`(2スロット) - 点 `500`, `1000`, `1020`(各1スロット) 合計5スロットで上限8に収まる。`10〜30` の密集部分は1つの区間にまとめられ、大きな空白は残っている。 ### Step 2: 値の追加 その後、このブロックレンジに `505, 510, 990, 995` が挿入された。展開すると `[10,30], 500, 505, 510, 990, 995, 1000, 1020` で、スロット数は $2 + 7 = 9$ となり上限8を超えるのでcompactionする(上限内に収まっている間はcompactionはスキップされる)。 | gap | 距離 | | --- | ---: | | 30 → 500 | **470** | | 500 → 505 | 5 | | 505 → 510 | 5 | | 510 → 990 | **480** | | 990 → 995 | 5 | | 995 → 1000 | 5 | | 1000 → 1020 | **20** | 上位3つ `510→990`(480), `30→500`(470), `1000→1020`(20) を残す。境界値は `10, 1020` と `510, 990` / `30, 500` / `1000, 1020` で、ソートすると `10, 30, | 500, 510, | 990, 1000, | 1020, 1020` となり: - 区間 `[10, 30]` - 区間 `[500, 510]`(点 `500, 505, 510` が結合された) - 区間 `[990, 1000]`(点 `990, 995, 1000` が結合された) - 点 `1020` 合計 $2+2+2+1 = 7$ スロット。 ### Step 3: 検索 この要約に対するクエリの判定: | クエリ | 判定 | 理由 | | --- | --- | --- | | `col = 700` | **スキップ** | どの区間にも含まれず、点 `1020` とも一致しない | | `col = 503` | スキャン(偽陽性) | `[500, 510]` に含まれるが、実際には `503` は存在しない | | `col = 20` | スキャン | `[10, 30]` に含まれる(実際に存在する) | | `col = 1010` | **スキップ** | `[990, 1000]` と点 `1020` の間のgapに落ちる | | `col < 5` | **スキップ** | 全体の最小値が `10` | | `col > 1000` | スキャン | 全体の最大値が `1020` | 同じデータに対して従来の `minmax` は `[10, 1020]` の1区間しか持たないため、`col = 700` や `col = 1010` でもこのブロックレンジをスキップできない。 # 参考 - [PostgreSQL Documentation: Built-in Operator Classes for BRIN](https://www.postgresql.org/docs/current/brin-builtin-opclasses.html) - [postgres/src/backend/access/brin/brin_minmax_multi.c](https://github.com/postgres/postgres/blob/master/src/backend/access/brin/brin_minmax_multi.c) - [PostgreSQL 14 Internals (Part III) - BRIN indexes](https://postgrespro.com/blog/pgsql/5967951)