⭐ Star the repo
🧩 Submit PRs
🐞 Open Issues
💬 Join Discussion
Oracle Database In-Memory(DBIM)と ShannonBase Rapid は、どちらも行ストアを補うインメモリのカラムストアです。ただし、この2つは性質の異なるものです。Rapid が独自の行単位 WAL、MVCC バージョンチェーン、外部アボートの調整処理、テーブル単位の隔離(quarantine)を持つ理由は、この違いから説明できます。
結論から
分かれ目は、変更がどのようにカラムストアへ届くかです。
Oracle:カラムストア内の値は書き換えません。DML は「この行は古くなった」という印(rowid を SMU に記録)だけを残します。新しい値は行ストアにしかなく、可視性は SCN で判定します。
Rapid:カラムストア内の値をその場で書き換えます。変更は非同期に ACTIVE バージョン(commit_scn == 0)として CU に入り、トランザクション境界のオブザーバが後から COMMITTED か undo かを確定させます。このため、カラムストアは未コミットのバイト列を保持しうることになります。
Rapid の伝播モデルは HeatWave 型(DML は待たず、バッチで非同期に反映)です。カラムストア側の構造は Oracle IM に近い、という合成になっています。
アーキテクチャ上の位置づけ
Oracle は1つのインスタンスの中で、バッファキャッシュ(行)と In-Memory Area(列)の二重フォーマットを持ち、整合性維持もカーネルが担います。
Rapid は MySQL 8.4 のセカンダリエンジン(プロセス内プラグイン ha_rapid)で、プライマリストレージは InnoDB のままです。ハンドラは write_row / update_row / delete_row を実装しておらず、書き込みはすべて InnoDB に向かいます。カラムストアに新しい値が入る経路は変更伝播だけです。伝播、独自 WAL、リカバリの面倒はすべてプラグイン側が見ます。
変更の届き方:Oracle の場合
Oracle は DML の最中に、影響を受けた行を IMCU 内で stale と記録し、rowid を SMU に追加します。IMCU 内の元の値は置き換えません。理由は、読み取り一貫性の維持と圧縮構造(辞書・ランレングス)の維持の2つです。
タイミング 起きること
DML 中(コミット前) stale 情報(メタデータ)を登録
常に IMCU 内の値は書き換えない(再構築まで据え置き)
コミット時 SCN が可視になるだけで、カラムストアには何も書かない
新しい SCN のクエリは、IMCU から stale でない部分を読み、stale な行だけを rowid 経由で行ストアから取ってきます。つまり行単位の混合読み取りです。カラムストアがロックやログを追加で必要としないのは、登録するのが「トランザクションに付随するメタデータ」だけだからです。
登録をコミット時まで遅らせると、コミット完了から登録完了までの隙間に新しいスナップショットのクエリが入り、古い値を読む誤りが起こります。そのため、変更が他者に見える前に登録を終えておく必要があります。
「書き換えない」と「行単位フォールバック」は一対
この2つは別々に選べる機能ではなく、同じ設計の表裏です。
値を書き換えない。カラムストアにはロード時点のスナップショットしかない。
したがって更新後の値はカラムストアに存在せず、SMU は rowid しか持たない。
変更が見える読み手は、rowid から行ストアの現行バージョンを取りに行くしかない。
変更率が上がるとフォールバックが増え、スキャンが遅くなる。
そのため、stale 率のしきい値による IMCU の再構築や、トリクル再投入が必要になる。
再構築は「ベーステーブルから読み直して上書き」するだけで、値を書き換えていないからこそ副作用のない冪等な操作になる。
変更の届き方:Rapid の場合
流れは3段階です(populate/log_dml_notification、imcs/cu.cpp、imcs/imcu.cpp)。
捕捉:プライマリの DML 通知(NotifyAfterInsert/Update/Delete)が書き込みパス上で変更レコードを作ってキューに入れます。この時点では InnoDB の実トランザクション ID を持ち、commit_scn == 0、つまり ACTIVE な通知バージョンです。
非同期適用:テーブルごとのワーカーが、行イメージを CU にその場で書き込みます。新しい値を入れ、古い値をバージョンチェーンに積みます。apply_change() は、COMMIT/ABORT の通知が適用より先に届く競合(ABORTED が先なら何も作らない、COMMITTED が先なら最初から COMMITTED として作る)も明示的に処理します。
確定:rapid_transaction_observer が rapid_after_commit / rapid_before_rollback を呼び、サーバのトランザクション境界で確定または巻き戻します。
未コミットのバイト列はどう持つか
ポイントは、「未コミット」はバイト列の属性ではなく、その横にあるバージョンノードの属性だということです。
各 CU は ColumnVersionManager を持ち、次の2つを管理します。
行ごとのバージョンチェーン(m_versions):先頭が最新で、prev をたどると過去へ遡れます。
書き込みトランザクションごとの ACTIVE 索引(m_active_versions):コミット/ロールバックをスキャンなしで O(k) で処理するための索引です。
m_versioned_rows というアトミックなカウンタも持ち、更新履歴のない列ではハッシュ引きを丸ごと省略できます。
CU::update() は同じ m_data_mutex の下で、新しいイメージを組み立ててスロットに一括公開し(リーダーはこのミューテックスを取りません)、古い値を先頭ノードとしてチェーンに積みます。ノードが持つのは、txn_id、scn(0 なら ACTIVE)、論理的な before-image(old_value)、物理的な before-image(old_slot:辞書 ID や可変長参照)、可変長値の所有権、prev などです。
読み取り側の get_before_image_for_snapshot は、チェーンを新しい順にたどり、スナップショットから見える最初のバージョンで止まります。途中の見えないバージョンは before-image で出力を上書きしていき、見えるべき版を復元します。
自分自身の書き込みは見える(read-your-own-writes)。
SQL リーダーは、InnoDB の ReadView(changes_visible)に従う。Rapid 側の非同期コールバックがまだ scn == 0 のままでも、ReadView が優先される。
ReadView を持たない内部・リカバリ用の呼び出しでは、scn == 0 は不可視、それ以外は scn <= reader_scn で可視。
操作 動作
コミット そのトランザクションの全ノードの scn を 0 から commit SCN に書き換え、ACTIVE 登録を外す
ロールバック 先頭ノードを pop して old_slot をスロットに書き戻し、可変長の所有権を返す。ロック順は m_data_mutex → バージョン先頭(update() と同じ)。逆順だと「スロットには未コミット値があるのにバージョンエントリがない」窓ができ、未コミット値がコミット済みとして読まれうる
purge ACTIVE(scn == 0)は回収しない。min_active_scn より古いコミット済みバージョンだけをアンリンクする
シリアライズ 論理層(row_id、txn_id、scn、値)のみ出力する。物理 old_slot はロード時に再構築するため、アロケータ依存のバイト列がディスクに出ない
Oracle にはこの層がそもそもありません。値が動かないので、「未コミット」を表現する必要も、ロールバックでカラムストアを undo する必要もないからです。一方 Rapid では、プールされた可変長参照をバージョンノードが握っている間は、そのブロック全体の回収が遅れるというメモリ面のコストを払います。
変更伝播のスケジューリング
Rapid のトリガは HeatWave 互換の3つです(log_populate.h)。
トリガ 実装
200ms ごと POP_MAX_WAIT_TIMEOUT = 200
バッファが 64MB に達する CHANGE_PROPAGATION_BUFFER_TRIGGER_BYTES、超えると全テーブルのフラッシュを要求
変更データを後続クエリが読む request_table_barrier_impl() が 200ms タイマを待たずにフラッシュを要求し、リーダーは required_change_id をウォーターマークが覆うまで待つ
加えて、DML は伝播を待たないという不変条件があります。バッファが溢れたときはコミットを待たせず、pending_flush を立ててテーブルを隔離し、mark_stale(ERROR_CLUSTER_OOM) を呼んで警告のうえコミットを通します。また、未適用データが pop_buff_sz_max(既定 256MB)の 0.85 倍を超えると、オフロードを拒否します。
Oracle との違いは、しきい値の数値ではなく、伝播という概念があるかどうかです。Oracle のカラムストアはトランザクションに参加しないので、追いつくべきものがありません。代わりに、stale 率しきい値による IMCU 再構築と、2分周期の IMCO トリクル再投入があります。
読み取り一貫性
Oracle:SCN で管理します。古いスナップショットは、動いていない IMCU の元の値をそのまま読みます。
Rapid:バージョンチェーンとリーダーのトランザクション/SCN で管理します。古いスナップショットは before-image を読みます。ACTIVE な書き込みの可視性は、プライマリ InnoDB の ReadView が決めます。また、READ UNCOMMITTED と SERIALIZABLE はオフロードを拒否し、READ COMMITTED と REPEATABLE READ だけが対象です。
Rapid のカラムストアは未コミットのバージョンを持ちうるので、「見えるか見えないか」を判定する独自の層が必要になります。
無効化の粒度と修復
Oracle Rapid
stale の粒度 行単位(IMCU 内の列エントリと SMU の rowid) テーブル単位(伝播の遅延、IsPropagationBroken、mark_stale)
修復 しきい値到達で IMCU 1つをオンラインで再構築 オフロードを止めて InnoDB にフォールバックし、SECONDARY_LOAD で再ロードするまで伝播は再開しない
最悪ケース IMCU 1つ分の再読み込み リロードまでテーブル全体が使えない(InnoDB が代行)
Oracle が局所的に再構築できるのは、IMCU の値を一度も変えていないからです。Rapid は値をその場で書き換えるため、「古いが手つかずのコピー」が存在しません。選べるのは、その場で適用し続けるか、テーブル全体を無効化して再ロードするかのどちらかです。
永続化とリカバリ
Oracle の IM FastStart は、IMCU を専用テーブルスペースにチェックポイントし、再起動時はベーステーブルではなくそこから投入します。チェックポイント時点の SCN と最新の更新 SCN を比べ、IMCU 全体を使う、一部の行を無効にして使う、破棄してディスクから読む、の3通りに分かれます。
Rapid は recovery/table_persistence と独自 WAL で、行ごとに次の順で処理します。
log_row_prepare → wait_durable → 適用 → log_row_commit → mark_applied
PREPARE だけで COMMIT がないレコードはリカバリで無視されます。fsync 失敗は「結果不明」として扱われ、recovery_required になります。また、チェックポイントは Imcu::has_uncommitted_changes() が真の IMCU をスキップするので、ACTIVE バージョンが永続化されることはありません。
スキャンと実行
列プルーニング、述語プッシュダウン、min/max プルーニング(StorageIndex)、辞書プルーニングは両者に共通です。
SIMD:Rapid は SIMD を持っています(utils/SIMD.h)。popcount、sum、min/max、非 NULL カウント、filter_compare_int32/int64 などが _avx2 / _sse4 / _neon / _generic の各版で実装され、__builtin_cpu_supports による実行時ディスパッチで切り替わります。AVX2 版は target("avx2") による関数単位の ISA 指定です。#ifdef AVX2 だけでガードする旧方式は、ベースラインビルドで AVX2 カーネルが存在しない「名ばかりの実行時ディスパッチ」になっていました。
さらに BatchReadable による列指向のバッチ取り込み経路があり、rnd_next_batch() が ColumnChunk を直接埋めるため、行フォーマットを経由しません(仮想関数呼び出しが N 回から ⌈N/B⌉ 回に減ります)。ただしこの経路は条件付きで、次のような場合は従来のスカラ経路にフォールバックします。
ソースが BatchReadable を実装していない(一部の結合・サブクエリノードなど)。
ROLLUP がある、GROUP_CONCAT や JSON_ARRAYAGG のようなベクトル化不能な関数を含む、またはベクトル化が無効。
ハッシュ結合の入力がエンコード済み文字列ペイロードを持つ、または子がレコードイメージを保持している。
Oracle との差は「SIMD や列指向取り込みがあるか」ではなく、次の2点です。
Oracle のカラム形式はベクトルレジスタを埋めるよう設計され、スキャン自体がベクトル化されています。Rapid は、バッチを ColumnChunk に渡したうえで、述語と集約の部分に SIMD カーネルを適用します。
Rapid のバッチ経路はオプトインで、すべての演算子やクエリが使うわけではありません。
数値演算の高速化(In-Memory Optimized Arithmetic)や Join Group に相当する仕組みは、Rapid には現時点でありません。
オフロード判定:フォールバックは「文あたり」
Rapid は独自のエグゼキュータを持たず、MySQL の実行基盤を再利用します。独自オプティマイザ(PredicatePushDown、StorageIndexPrune、AggregationPushDown など)の結果を ToAccessPath() でサーバのアクセスパス/イテレータ木に落とし込み、各演算子は MySQL の RowIterator / TableRowIterator を継承します。
use_secondary_engine の3状態は次のとおりです。
OFF:オフロードしない。
ON(既定):層状の仲裁(コスト、負荷、伝播状況)で決める。
FORCED:仲裁は飛ばすが、RapidPrepareEstimateQueryCosts 内のハード前提条件は守る。
ON でも、判定は負荷を踏まえて Rapid 自身が行います。
条件 分類器
dynamic_offloads = false、またはごく速いクエリ コストしきい値(secondary_engine_cost_threshold)
populator が停止中、または伝播バッファが空 ONNX モデル(shannon_rapid_classifier.onnx)による OLAP/OLTP 判定。モデルがなければプライマリへ
populator が稼働中で伝播バッファが非空(遅延の窓) 全体の遅延が上限を超える、またはクエリ内のいずれかのテーブルが pop キューに残っていれば、InnoDB のまま
つまり「伝播が進行中であること」自体が、テーブルごとのオフロード拒否の理由になります。
フォールバックの粒度は文です。table_offload_blocker() は、クエリ内の全テーブルがロード済みでなければ拒否します。準備に失敗すると、サーバは文全体を PRIMARY_TENTATIVELY → SECONDARY → PRIMARY_ONLY でやり直します。スキャン経路がプライマリエンジンに行データを要求することはありません。したがって、スキャンの途中でエンジンが切り替わることはなく、文全体が Rapid か InnoDB のどちらかで走ります。Oracle の「行ごとに行ストアへ取りに行く」混合読み取りとは対照的です。
この設計がもたらす工学的帰結
これらは欠陥ではなく、「更新可能なカラムストア+非同期伝播」という選択に必然的に付いてくる性質です。
独自の行単位 WAL が要る:カラムストア自身が書き手なので、耐久性を行ストアに任せられません。Oracle が「追加のロックやログが不要」と言えるのは、カラムストアが読み取り専用だからです。
fsync が IMCU の変更ロックの内側に入る:DML 側の要件は「メモリへ公開する前に WAL を永続化する」ことで、公開はメモリ書き込みと同じ変更バリアを共有します。
コミット後の失敗は quarantine にしかできず、my_error で返せない:カラムストアの公開境界とサーバのトランザクション境界は同じ点ではなく、サーバがコミット成功と判断した後の伝播失敗は、文のエラーとして表面化できません。
外部アボートの調整処理が要る:その場書き込みなので、気づかれない暗黙のロールバックが起きると CU の内容が undo されずに残ります(reconcile_on_external_abort())。
チェックポイントは未コミットの IMCU を飛ばす必要がある:そうしないと ACTIVE バージョンが永続化されます。
伝播遅延を DML の待機で解消できない:DML は伝播にブロックされてはならず、不変条件はレプリカ側の劣化(テーブルの隔離)で守ります。
Oracle 方式を真似るべきか
2〜4 のコストを減らすために、Oracle の「書き換えない+行単位 stale+非同期 IMCU 再構築」を取り込むのは適切ではありません。
Oracle が即時置換を避けた動機(読み取り一貫性と圧縮構造の維持)のうち、後者はインメモリでは薄れます。CU の1スロットを探して書き、バージョンノードを1つ積むだけなら安価です。
コストは小さくありません。スキャン中に行単位で InnoDB へ落ちる経路が必要になり、その場書き込みが「2コピー+バックグラウンドでの補充」に変わります。
既存のメタデータを混同しないでください。IMCU ヘッダの TransactionJournal は行の存在を判定する操作の差分チェーンで、ColumnVersionManager は値のバージョンチェーンです。どちらも「その場書き込みを支える補助メタデータ」であり、SMU や IMCU 再構築に相当するものではありません。
現実的な方向は、その場書き込みという前提を保ったまま、コストをクリティカルパスの外に出すことです。
書き込み増幅の削減:MEMCOMPRESS FOR DML の発想で、圧縮率と DML 性能のトレードオフを選べるようにします。圧縮状態の CU に書くたびに CU 全体を decompress_locked() する処理も避けたいところです。
行単位 WAL の fsync を IMCU の変更バリアの外へ出す:バッチ単位やトランザクショングループ単位でコミットしつつ、「WAL が先、メモリ公開が後」の順序は維持します。
伝播と永続化は HeatWave 型の非同期モデル(200ms / 64MiB / クエリ要求)のままにする:DML パスの不変条件には手を入れません。
まとめ
観点 Oracle DBIM ShannonBase Rapid
変更の入り方 DML 中に stale を登録(値は不変) 非同期のその場書き込み(ACTIVE → COMMITTED/undo)
カラムストアの未コミット値 なし あり(ACTIVE バージョン)
コミットの役割 SCN を進める バージョンの状態を反転する
独自ログ 不要 行単位 WAL が必要
可視性の担い手 SCN+行ストア参照 before-image のバージョンチェーン
クエリ内フォールバック 行単位(stale 行は行ストアから) なし(文単位でエンジンを選択)
修復の粒度 IMCU 1つの再構築 テーブル単位の隔離+再ロード
スケジューリング stale しきい値+2分トリクル 200ms / 64MiB / クエリ要求
永続化 IM FastStart(SCN による3通りの判定) table_persistence+行単位 WAL
どちらが優れているかではなく、カラムストアを読み取り専用の派生物と見るか、書き込み可能なレプリカと見るかという前提の違いが、これだけの差を生んでいます。
出典
元記事:Oracle Database In-Memory vs. ShannonBase Rapid(Shannon Data AI)
参照資料:Oracle『Oracle Database In-Memory with Oracle Database 19c — Technical Overview』(2019年2月)、MySQL HeatWave ドキュメント「About Change Propagation」