データベース●●●○○

OLTPとOLAP

OLTP / OLAP ・ おーえるてぃーぴーとおーらっぷ

大量の小さな更新を捌く業務処理と、巨大なデータを集計する分析処理という2つの負荷特性の区分。

✦ 30秒で試す — 航海同じ表なのに、置き方を変えるだけで集計が16倍速くなる注文1件の全項目を取り出すとき、速いのは?予想してみる

概要

OLTP(Online Transaction Processing)と OLAP(Online Analytical Processing)は、データベースにかかる負荷を性質で二分する古典的な区分です。OLTP は「注文を1件登録する」「残高を更新する」のような、少数の行だけに触れる小さな読み書きが大量に飛んでくる業務処理。OLAP は「過去3年の商品カテゴリ別売上推移を出す」のような、数億行を舐めて集計する重い読み取りが少数飛んでくる分析処理です。

この区分が重要なのは、両者に最適なデータベースの内部構造がほぼ正反対だからです。ECサイトの注文処理を捌く RDBMS に重い集計クエリを流すと、分析が遅いだけでなく、本業の注文処理まで巻き添えで遅くなります。「業務のDBと分析のDBは分ける」という現代のデータ基盤の大原則は、この負荷特性の違いから導かれています。

なぜ生まれたか

リレーショナルデータベース(RDBMS)が普及した1980年代、企業はまず業務処理(受発注・会計・在庫)をデータベース化しました。これらはトランザクションの正確さと応答速度が命で、スキーマも更新の整合を保ちやすいよう正規化されています。ところがデータが溜まると、経営層は当然「このデータから傾向を読みたい」と考え始めます。業務システムに分析クエリを流すと、正規化された多数のテーブルを結合しながら全期間のデータを走査することになり、何時間もかかったうえ、その間ロック(排他制御)やディスクの読み取りの奪い合いで業務処理が止まる — 同じデータベースに性質の異なる2種類の仕事をさせることの限界が露呈しました。

1980年代末から90年代初めにかけて、「業務システムから分析専用のデータベースへデータをコピーして分ける」データウェアハウスの考え方が確立し、1993年にはリレーショナルモデルの父 E.F. Codd が分析用途の処理を「OLAP」と名付けて業務処理(OLTP)と区別しました(製品ベンダーの依頼で書かれた白書だったため、中立性には批判もありました)。つまり OLTP/OLAP という語彙は、「1つのDBに全部やらせる」ことの失敗から生まれた、負荷を分離するための設計語彙なのです。

詳細

負荷特性の対比

両者の違いを並べると、要求がほぼ正反対であることが分かります。OLTP はクエリ1本あたり数行に触れるだけですが、毎秒数千〜数万本が並行して飛び、書き込みも多く、ミリ秒単位の応答と ACID な整合性が求められます。ユーザーはエンドユーザーそのもので、遅延は直接体験を損ないます。OLAP はクエリ1本が数百万〜数十億行を走査しますが、本数は少なく、ほぼ読み取り専用で、応答は秒〜分単位でも許されます。ユーザーはアナリストやBIツールで、問われるのは個々の行ではなく集計値です。データの持ち方も、OLTP が現在の状態を正規化して持つのに対し、OLAP は履歴を長期間、集計しやすい形(スタースキーマなどの、あえて正規化を崩した構造)で持ちます。OLTP が トランザクションの境界で1件ずつ正しく書き換えることに心血を注ぐのに対し、OLAP は「確定した過去を大量に読む」ので、書き込みはまとめて追記するだけで済みます。

行指向と列指向 — ストレージ構造が正反対になる理由

この負荷の違いは、ディスク上のデータの並べ方にまで及びます。データベースはディスクを「ページ」(数KB〜数MBの塊)単位で読み書きするので、何をどのページに一緒に置くかで、1本のクエリが読むページ数が決まります。OLTP 向けの RDBMS(PostgreSQL、MySQL など)は行指向ストレージを採り、1行の全カラムをディスク上に連続して置きます。「注文ID 42 の行を丸ごと読む・書く」処理が1か所へのアクセスで済むからです。一方 OLAP 向けデータベースは列指向(カラムナ)ストレージを採り、同じカラムの値だけを全行ぶん連続して置きます。「全注文の金額カラムだけを合計する」クエリが、必要な列だけを読み飛ばしなしで走査できるからです。

行指向 — OLTP向き列指向 — OLAP向きディスク上の並び: 1行ずつ連続ID:1商品:リンゴ金額:120ID:2商品:ミカン金額:80ID:3商品:ブドウ金額:300読むWHERE id = 1 は1か所で完結注文1件の登録・参照に強いSUM(金額) には全行を読むことになるディスク上の並び: 1カラムずつ連続ID:1ID:2ID:3リンゴミカンブドウ12080300読むSUM(金額) は金額の列だけ読む全行の集計・圧縮に強い1行を組み立てるには全部の列を回る同じカラムには似た値が並ぶため、列指向は圧縮も強く効く走査するデータ量がさらに減り、集計クエリが桁違いに速くなる
行指向と列指向 — 同じテーブルでもディスク上の並びが異なり、得意なアクセスが逆になる

列指向にはもうひとつ大きな利点があります。同じカラムには型も傾向も似た値が並ぶため(金額なら数値だけ、日付なら単調増加、店舗なら数十種類の繰り返し)、同じ値の連続を「値 × 回数」にまとめる、差分だけを持つ、辞書の番号に置き換える、といった圧縮が極めてよく効きます。データが10分の1に縮めば読むべきページも10分の1になり、集計はさらに速くなります。加えて、同じ型の値が連続して並んでいることは、CPU が複数の値をまとめて計算する命令(SIMD)やCPUキャッシュとも相性がよく、読んだ後の計算も速くなります(ベクトル化実行)。

逆に「1行を丸ごと読む・足す」処理では、列ごとのファイルに散らばった値を1つずつ集めることになり、列指向は苦手です。得意・不得意はきれいに裏返しの関係にあります。下の部品で、同じ注文表を行指向と列指向のディスクに置いて、クエリごとに読むページを比べてください。表の見出しを押すとその列の集計、行を押すとその1行の取り出し、「+」で1行の追加です。列の数を増やすと、集計で読むページ数の差がどう開くかも確かめられます。

⚡ 体験: 同じ表を、行ごと・列ごとに並べて読む
列の数
-- 表の見出し・行・+を押すとクエリが走りますid日付商品金額店舗顧客数量単価419/3緑茶1200渋谷K122600429/3豆3400梅田K0713400439/4菓子800渋谷K314200+ 注文を1件追加する(INSERT)全256行のうち3行を表示行指向のディスク1ページ = 4行ぶん列指向のディスク1列 = 8ページ行指向—列指向—
クエリが必要とする値ほかの列の値読んだ(書いた)ページ
実行したクエリ
まだ実行していません
    ○ 集計は列指向が少なく読む○ 列を増やすと差が開く○ 1行の取り出しは逆転○ 1行の追加も逆転

    表の見出しの「金額」を押して、全行の金額を合計させてみてください。左右のディスクで、読んだページだけが金に光ります。

    DWHへの分離とデータの流れ

    実際のシステムでは、OLTP を担う業務データベースから、OLAP を担うデータウェアハウス(DWH)へデータを定期的にコピーして分離します。この転送と変換の工程が ETL です。近年は、業務DBの変更ログ(WAL)を読み取って変更を1件ずつ取り出す CDC(変更データキャプチャ)で、レプリケーションと同じ仕組みを使いながら DWH へほぼリアルタイムに流し込む構成も一般的になりました。流れてくる変更をその場で変換・集計する部分はストリーム処理の仕事です。OLAP 側の代表的な製品は Snowflake、BigQuery、Amazon Redshift、ClickHouse などで、いずれも列指向ストレージと大規模並列処理を核にしています。より生データ寄りの置き場としてデータレイクを挟む構成や、両特性を1つのエンジンで担おうとする HTAP(Hybrid Transactional/Analytical Processing)という方向性もありますが、負荷特性の違いという原理そのものは変わりません。

    実務での判断基準

    この語彙の実務的な使いどころは、「そのクエリはどちらの性質か」を見分けて置き場所を決めることです。管理画面の集計が遅いからと業務DBにインデックスを足し続けるより、そのクエリはOLAPの仕事だと見抜いてDWHやマテリアライズドビューへ逃がすほうが筋がよい、という判断ができるようになります。逆にDWHに「1件だけ更新したい」処理を持ち込むのもミスマッチです。また、分析クエリを本番DBのレプリカに流す構成は手軽な第一歩ですが、行指向のままでは大規模集計の遅さは解決しないことも押さえておきたいポイントです。データベースをひとつの箱としてではなく、負荷特性で使い分ける複数の道具として見る — OLTP/OLAP はそのための最初の分類軸です。

    試問

    同じ注文表(256行)を、1行ぶんの全列を並べる行指向のディスクと、1つの列の値をまとめて並べる列指向のディスクに置いています(1ページに値が32個入る)。表の見出しを押すとその列の全行の集計、行を押すとその1行の取り出し、「+」で1行の追加が走り、それぞれのディスクで読んだページ数が出ます。上の切り替えで、表の列の数(4・8・16)を変えられます。

    ✦ 命題挑戦中

    同じ注文表で、列指向が行指向の10分の1以下のページで済むクエリと、逆に列指向が8倍以上のページに触れるクエリを、両方走らせよ

    • 列指向の読むページが、行指向の 10分の1以下で済むクエリを走らせる
    • 逆に、列指向のほうが行指向の 8倍以上のページに触れるクエリを走らせる
    ⚡ 体験: 同じ表を、行ごと・列ごとに並べて読む
    列の数
    -- 表の見出し・行・+を押すとクエリが走りますid日付商品金額店舗顧客数量単価419/3緑茶1200渋谷K122600429/3豆3400梅田K0713400439/4菓子800渋谷K314200+ 注文を1件追加する(INSERT)全256行のうち3行を表示行指向のディスク1ページ = 4行ぶん列指向のディスク1列 = 8ページ行指向—列指向—
    クエリが必要とする値ほかの列の値読んだ(書いた)ページ
    実行したクエリ
    まだ実行していません

      表の見出しの「金額」を押して、全行の金額を合計させてみてください。左右のディスクで、読んだページだけが金に光ります。