はじめに
今回は、Iceberg、Delta Lake、Hudi などのオープンテーブルフォーマットを理解する前提として、それらが登場するきっかけとなった前世代のテーブルフォーマットである Hive について調べてみました。
Hive は、Hadoop 時代のデータレイク分析を大きく前進させた重要な仕組みでした。一方で、データ規模や利用用途が広がるにつれて、ファイル単位の更新や同時更新、トランザクション、クエリ性能などに限界が見えてきました。
なぜ Iceberg などの新しいテーブルフォーマットが必要になったのかを理解するための背景として、Hive をおさらいします。
Hive ― 最初のテーブルフォーマット
Hive は、Hadoop データレイク上の分析を SQL で実行できるようにするため、Facebook(現 Meta)によって 2009 年に開発されたフレームワークです。
Hadoop とは、大量のデータを 1 台の大きなコンピュータではなく、複数のサーバーに分散して保存・処理するための仕組み(分散処理エンジン)です。
たとえば、何十億行ものアクセスログや購買履歴を、1 台のマシンではなく、たくさんのサーバーに分けて保存し、それぞれのサーバーで並行して処理するイメージです。
それ以前は MapReduce の Java ジョブを書く必要があり、分析のハードルが高い状態でした。
MapReduce とは、大量データを分散処理するための古い処理方式です。
たとえば「全ユーザーの購入金額を合計したい」とします。1 台で全部計算するのではなく、まず複数のサーバーがそれぞれ自分の担当分を集計し、最後にその結果をまとめる、という流れです。
- Map:データを分けて、それぞれの場所で計算する
- Reduce:分けて計算した結果を最後に集約する
ただし、MapReduce を使うには Java で処理内容を細かく書く必要がありました。SQL のように簡単に「この条件で集計して」と書けなかったため、アナリストにとっては扱いにくいものでした。
Hive は SQL 文を MapReduce ジョブに変換して実行します。
つまり、ユーザーは Java の複雑な処理を書かなくても、SQL で次のように書けるようになりました。
SELECT month, SUM(amount)
FROM sales
GROUP BY month;
Hive がこの SQL を裏側で MapReduce の処理に変換してくれます。
そのために、Hadoop ストレージ上のどのデータがテーブルを構成するのかを管理する仕組みとして、Hive テーブルフォーマットと Hive メタストアが作られました。
Hive テーブルフォーマットとは、Hadoop 上に置かれた大量のファイルを「これは 1 つのテーブルです」と見なすためのルールです。
普通のデータベースのようにデータがきれいに表形式で入っているのではなく、実際にはストレージ上にたくさんのファイルとして置かれています。Hive はそれらのファイル群をまとめて 1 つの表として扱えるようにしました。
Hive メタストアとは、「どのテーブルが、どの場所にあり、どんな列を持っているか」を管理する台帳のようなものです。
たとえば、sales というテーブルは /data/sales/ にあり、列は date、user_id、amount である、といった情報を持っています。
Hive テーブルフォーマットの仕組み
Hive テーブルフォーマットでは、特定のディレクトリ配下にあるすべてのファイルを 1 つのテーブルとして扱います。
たとえば、売上データのテーブルがある場合、ストレージ上では次のような形で保存されているイメージです。
/data/sales/
year=2024/
month=01/
file1.parquet
file2.parquet
month=02/
file3.parquet
file4.parquet
year=2025/
month=01/
file5.parquet
この場合、Hive は /data/sales/ 配下のファイル群をまとめて sales テーブルとして扱います。
パーティションとは、データを条件ごとに分けて保存する単位です。
上の例では、year=2024 や month=01 のように、年や月ごとにフォルダが分かれています。これがパーティションです。
たとえば「2024 年 1 月の売上だけを見たい」場合、すべてのファイルを読む必要はありません。
year=2024/month=01/ のフォルダだけを読めばよくなります。これにより、処理が速くなります。
クエリエンジンとは、SQL などの問い合わせを実行する処理エンジンです。
ユーザーが SQL を投げると、クエリエンジンは Hive メタストアに「このテーブルのデータはどこにありますか?」と確認し、対象のファイルを読み込みます。
イメージとしては、以下のような流れです。
ユーザー
↓ SQL を実行
クエリエンジン
↓ テーブルの場所を確認
Hive メタストア
↓ 場所を返す
Hadoop / オブジェクトストレージ上のファイル
↓
結果を返す
Hive テーブルフォーマットの主な利点
Hive テーブルフォーマットには、次のような利点がありました。
パーティショニングやバケッティングにより、フルスキャンを避けた効率的なクエリが可能
フルスキャンとは、テーブルに含まれるすべてのデータを最初から最後まで読むことです。
たとえば、10 年分の売上データがあるのに「2024 年 1 月だけ」を調べたい場合、10 年分すべてを読むのは無駄です。
パーティショニングされていれば、対象のフォルダだけを読めます。
全データを読む場合:
/data/sales/ 配下を全部読む
2024 年 1 月だけ読む場合:
/data/sales/year=2024/month=01/ だけ読む
これにより、読み込むデータ量が減り、クエリが速くなります。
バケッティングとは、データをさらに均等に分ける仕組みです。
たとえば user_id をもとにハッシュ計算を行い、ユーザーを複数のファイルに分散して保存します。
bucket_1:user_id のハッシュ値が特定範囲のユーザー
bucket_2:別の範囲のユーザー
bucket_3:さらに別の範囲のユーザー
これにより、特定のユーザーに関する処理や、同じキーでの結合処理を効率化しやすくなります。
Parquet、Avro、CSV、TSV など、さまざまなファイル形式を利用できる
Hive テーブルは、特定のファイル形式に縛られません。
たとえば、同じ sales テーブルでも、実際のファイルは次のような形式で保存できます。
- CSV:カンマ区切りのテキストファイル
- TSV:タブ区切りのテキストファイル
- Avro:スキーマ情報を持てるデータ保存形式
- Parquet:分析処理に向いた列指向のファイル形式
特に Parquet は、分析用途でよく使われます。
たとえば、テーブルに user_id、date、amount、region という列があるとします。
普通の行指向の形式では、1 行ずつまとめて保存します。
user_id, date, amount, region
001, 2024-01-01, 5000, Tokyo
002, 2024-01-02, 3000, Osaka
一方、Parquet のような列指向フォーマットでは、列ごとに効率よく読みやすい形で保存されます。
そのため、「amount 列だけを集計したい」という場合に、不要な列を読まずに済みます。
つまり Hive は、データの置き方は「ディレクトリ配下のファイルをテーブルと見なす」というルールにしておき、実際のファイル形式は用途に応じて選べるようにした、ということです。
パーティション単位であれば、ディレクトリの入れ替えによってアトミックな変更が可能
アトミックとは、「全部成功する」か「全部失敗する」かのどちらかになる性質です。
途中までだけ反映される状態を避ける考え方です。
たとえば、2024 年 1 月分の売上データを更新したいとします。
/data/sales/year=2024/month=01/
このパーティション全体を、新しいディレクトリに差し替えることで、ユーザーから見ると「更新前」か「更新後」のどちらかだけが見えるようにできます。
イメージとしては、レストランのメニュー表を 1 ページずつ手書きで修正するのではなく、新しいメニュー表を裏で作っておき、最後に丸ごと差し替えるようなものです。
差し替えの途中の中途半端な状態を見せないことができます。
多くのデータツールと連携できる事実上の標準となった
Hive テーブルフォーマットは広く使われるようになったため、多くのデータ処理ツールが Hive メタストアを参照できるようになりました。
つまり、あるツールで作ったテーブルを、別のツールでも読めるようになりました。
たとえば、以下のようなイメージです。
Hive
Spark
Presto
Trino
その他の分析ツール
これらのツールが Hive メタストアを見に行けば、「このテーブルはどこにあるのか」「どんな列があるのか」を共通の情報として利用できます。
そのため、Hive テーブルフォーマットはデータレイクにおける共通ルールのような存在になりました。
「このテーブルに含まれるデータはどれか」を統一的に管理できるようになった
Hadoop 上では、データは単なるファイルとして置かれています。
そのままだと、どのファイルがどのテーブルに属しているのか分かりにくくなります。
たとえば、ストレージ上に次のようなファイルがあるとします。
/data/sales/file1.parquet
/data/sales/file2.parquet
/data/customer/file1.parquet
/data/logs/file1.parquet
Hive メタストアがないと、「sales テーブルはどのファイルを含むのか」をツールごとに判断しなければなりません。
Hive メタストアがあれば、次のように管理できます。
sales テーブル → /data/sales/
customer テーブル → /data/customer/
logs テーブル → /data/logs/
このように、データの場所とテーブル定義を統一して管理できる点が大きな利点でした。
Hive テーブルフォーマットの主な制約
一方で、データ規模やユースケースが大きくなるにつれて、以下のような問題が明らかになりました。
ファイル単位の更新が苦手
Hive テーブルフォーマットでは、基本的にディレクトリ配下のファイル群をまとめてテーブルとして扱います。
そのため、1 つのファイルだけを安全に更新する仕組みが弱いです。
たとえば、あるパーティションに 100 個のファイルがあるとします。
/data/sales/year=2024/month=01/
file1.parquet
file2.parquet
...
file100.parquet
この中の file37.parquet だけを更新したい場合でも、Hive の仕組みではファイル単位でアトミックに差し替えるのが難しいです。
そのため、パーティション全体を作り直して差し替える必要が出てきます。
つまり、小さな修正でも大きな単位で処理しなければならず、非効率になります。
複数パーティションをまとめてアトミックに更新できない
Hive では、1 つのパーティション単位であれば差し替えができます。
しかし、複数のパーティションをまとめて 1 つの処理として更新するのは苦手です。
たとえば、売上データを次の 3 か月分まとめて修正したいとします。
/data/sales/year=2024/month=01/
/data/sales/year=2024/month=02/
/data/sales/year=2024/month=03/
理想的には、この 3 つをまとめて「全部更新される」か「全部更新されない」状態にしたいです。
しかし Hive では、月ごとのパーティションは個別に更新されます。
そのため、更新処理の途中でユーザーがクエリを実行すると、次のような中途半端な状態が見えてしまう可能性があります。
2024年1月 → 更新済み
2024年2月 → 更新済み
2024年3月 → 未更新
このように、データ全体として一貫性が崩れる可能性があります。
ここでいう トランザクション とは、複数の処理を 1 つのまとまりとして扱う仕組みです。
銀行振込で「A さんの口座からお金を引く」と「B さんの口座にお金を入れる」が必ずセットで成功する必要があるのと同じ考え方です。
同時更新への対応が弱い
Hive テーブルフォーマットは、複数の処理が同じテーブルを同時に更新するケースに強くありません。
たとえば、同じ sales テーブルに対して、次の 2 つの処理が同時に走ったとします。
処理A:2024年1月の売上データを更新する
処理B:2024年1月の売上データを別の内容で更新する
このとき、どちらの更新が正しいのか、どちらを優先するのか、片方の更新が上書きされていないか、といった問題が起こりやすくなります。
特に Hive 以外のツール、たとえば Spark や別のデータ処理エンジンが同じテーブルを直接更新すると、Hive メタストア側と実際のファイル状態の整合性を保つのが難しくなります。
ファイルやディレクトリの列挙に時間がかかる
Hive テーブルフォーマットでは、クエリを実行するときに「対象のデータがどのファイルにあるか」を確認する必要があります。
テーブルが小さいうちは問題ありません。
しかし、ファイル数やパーティション数が非常に多くなると、ファイルやディレクトリを探すだけで時間がかかります。
たとえば、次のようなテーブルがあるとします。
10年分のログデータ
1日ごとにパーティション
1パーティション内に数千ファイル
この場合、実際のデータを読む前に、「どのディレクトリがあるか」「どのファイルがあるか」を確認する作業だけで大きな負荷になります。
これは、巨大な倉庫で商品を探すときに、在庫リストが十分に整理されておらず、棚を一つひとつ確認しなければならない状態に近いです。
パーティション列をユーザーが意識しないと最適化されない
パーティショニングは便利ですが、ユーザーがパーティション列を意識して SQL を書かないと効果が出ないことがあります。
たとえば、実際のデータには timestamp という列があるとします。
timestamp = 2024-01-15 10:30:00
一方で、パーティションはこの timestamp から作った month 列で分けられているとします。
month = 2024-01
このとき、ユーザーが次のような SQL を書いたとします。
SELECT *
FROM sales
WHERE timestamp >= '2024-01-01'
AND timestamp < '2024-02-01';
人間から見ると、「2024 年 1 月だけ見たい」と分かります。
しかし Hive 側が必ずしも month=2024-01 のパーティションだけを読めるとは限りません。
本来は、次のようにパーティション列も条件に入れる必要があります。
SELECT *
FROM sales
WHERE month = '2024-01'
AND timestamp >= '2024-01-01'
AND timestamp < '2024-02-01';
ユーザーが month 列の存在を知らなかったり、指定し忘れたりすると、パーティションが活用されず、テーブル全体を読んでしまうことがあります。
これが「パーティション列をユーザーが意識しないと最適化されない」という問題です。
統計情報が古くなりやすい
統計情報とは、クエリエンジンが効率よく SQL を実行するために使う補助情報です。
たとえば、以下のような情報です。
このテーブルには何行あるか
この列にはどんな値が多いか
この列の最大値・最小値は何か
NULL がどれくらいあるか
ファイルサイズはどれくらいか
クエリエンジンは、これらの情報を使って「どの順番で処理すれば速いか」を判断します。
たとえば、巨大なテーブルと小さなテーブルを結合する場合、どちらを先に読むかによって性能が変わります。
統計情報が正確であれば、効率の良い実行計画を立てられます。
しかし Hive では、統計情報は別の非同期ジョブで後から収集されることが多いです。
非同期ジョブとは、メインの処理とは別に、後から実行される処理のことです。
つまり、データが更新された直後には、統計情報がまだ古いままの場合があります。
たとえば、実際には 10 億行あるテーブルなのに、統計情報上は 1 億行のままだと、クエリエンジンが誤った判断をする可能性があります。
その結果、クエリが遅くなることがあります。
オブジェクトストレージとの相性問題
オブジェクトストレージとは、クラウド環境でよく使われるストレージのことです。
代表例としては Amazon S3、Google Cloud Storage、Azure Blob Storage などがあります。
通常のファイルシステムでは、フォルダの中にファイルがあるように見えます。
一方、オブジェクトストレージでは、厳密にはフォルダではなく、キーやプレフィックスという文字列でファイルの場所を表します。
たとえば、次のようなパスがあるとします。
s3://my-bucket/sales/year=2024/month=01/file1.parquet
この場合、sales/year=2024/month=01/ の部分が プレフィックス のように扱われます。見た目はディレクトリに近いですが、実際にはオブジェクト名の先頭部分です。
問題は、同じプレフィックスの下に大量のファイルが集中すると、オブジェクトストレージ側の処理やリクエストに負荷がかかりやすいことです。
たとえば、1 つのパーティションに小さなファイルが 100 万個あるとします。
/sales/year=2024/month=01/file000001.parquet
/sales/year=2024/month=01/file000002.parquet
...
/sales/year=2024/month=01/file1000000.parquet
この場合、クエリを実行すると大量のファイル一覧取得や読み込みリクエストが発生します。
その結果、クエリが遅くなったり、ストレージ側の制限に引っかかったりする可能性があります。
結論
Hive テーブルフォーマットは、Hadoop 上のデータ分析を SQL で扱えるようにし、データレイク活用を大きく前進させた重要な仕組みです。
イメージとしては、もともと Hadoop 上には大量のファイルがバラバラに置かれていました。Hive はそれらを「これは 1 つのテーブルです」と定義し、SQL で分析できるようにしました。
ただし、Hive テーブルフォーマットは、基本的に「ディレクトリ配下のファイル群をテーブルとして扱う」仕組みです。そのため、ファイル単位の細かい更新、複数パーティションをまたぐ一貫した更新、同時更新、巨大なファイル一覧の管理、正確な統計情報の維持などには限界がありました。
データ規模の拡大とともにこれらの課題が深刻化し、後に Iceberg、Delta Lake、Hudi などの新しいテーブルフォーマットが登場するきっかけとなりました。