pyarrowでCloud Storage 上の Parquet ファイルのスキーマ情報を取得した場合、ファイル全量ではなくメタデータのみ取得しているか確認してみた

pyarrowでCloud Storage 上の Parquet ファイルのスキーマ情報を取得した場合、ファイル全量ではなくメタデータのみ取得しているか確認してみた

Clock Icon2025.02.27

概要

Cloud Storage上のParquetファイルのメタデータを取得する際にファイルを全量ダウンロードしてメタデータを取得するのは時間・費用がかかるのでとても非効率です。
しかしながら、Cloud Storage上の数GBのParquetファイルのメタデータを読み取る際にpyarrow.parquetのライブラリを用いたところ思った以上に高速だったので、pyarrowはもしかしたら必要な箇所しか読み込まないのではと思い検証してみました。

検証してみる

※前提として、1.4GB程度のParquetファイルCloud Storageに保存しておきます。
Parquetファイルのスキーマとしては以下となります。

カラム名 データ型
id INT64
name BYTE_ARRAY
age INT64
salary DOUBLE
description BYTE_ARRAY

検証するため、pyarrowを用いてCloud Storage上のParquetファイルのメタデータを取得しスキーマ情報を表示するスクリプトを実装しました。
pyarrow.fs.GcsFileSystem() を使用してCloud Storage上のファイルサイズを取得し、pq.ParquetFile() でメタデータを読み込みます。さらに、gcsfs.GCSFileSystem() を用いてCloud Storageへのリクエストログを取得し、データ転送量がどうなっているかを確認します。また、実行時間やメモリに関しても計測しておきます。

import pyarrow.parquet as pq
import pyarrow.fs as fs
import gcsfs
import logging
import time
import tracemalloc

bucket_name = "バケット名"
object_path = "sample_1gb.parquet"

gcs = fs.GcsFileSystem()


# 実行時間計測
start_time = time.time()
tracemalloc.start()  # メモリ使用量の計測始

file_info = gcs.get_file_info(f"{bucket_name}/{object_path}")
file_size = file_info.size
print(f"Total file size: {file_size / (1024 * 1024):.2f} MB")

# gcsfs のデバッグログを有効化
logging.basicConfig(level=logging.DEBUG)

# gcsfs を使用してCloud Storageへのリクエストを監視
fs_gcs = gcsfs.GCSFileSystem()

print("Fetching Parquet metadata...")
parquet_file = pq.ParquetFile(f"{bucket_name}/{object_path}", filesystem=gcs)

print("カラム名: 物理型, 論理型")
for column in parquet_file.metadata.schema:
    print(f"{column.name}: {column.physical_type}, ({column.logical_type})")

# `gcsfs` のリクエストログを取得
with fs_gcs.open(f"gs://{bucket_name}/{object_path}", "rb") as f:
    f.read(100)  # 100バイトだけ読み込む(ログを確認するため)

end_time = time.time()
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()

print("\nExecution Metrics:")
print(f"Execution time: {end_time - start_time:.4f} seconds")
print(f"Memory usage: {current / (1024 * 1024):.2f} MB (Peak: {peak / (1024 * 1024):.2f} MB)")

実行結果の例としては以下となります。

% python check_parquet.py
Total file size: 1397.65 MB
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:google.auth._default:Checking None for explicit credentials as part of auth process...
DEBUG:google.auth._default:Checking Cloud SDK credentials as part of auth process...
DEBUG:gcsfs.credentials:Connected with method google_default
Fetching Parquet metadata...
カラム名: 物理型, 論理型
id: INT64, (None)
name: BYTE_ARRAY, (String)
age: INT64, (None)
salary: DOUBLE, (None)
description: BYTE_ARRAY, (String)
index_level_0: INT64, (None)
DEBUG:gcsfs:GET: b/{}/o/{}, ('バケット名', 'sample_1gb.parquet'), None
DEBUG:gcsfs.credentials:GCS refresh
DEBUG:google.auth.transport.requests:Making request: POST https://oauth2.googleapis.com/token
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): oauth2.googleapis.com:443
DEBUG:urllib3.connectionpool:https://oauth2.googleapis.com:443 "POST /token HTTP/1.1" 200 None
DEBUG:gcsfs:GET: https://storage.googleapis.com/download/storage/v1/b/バケット名/o/sample_1gb.parquet?alt=media, (), {'Range': 'bytes=0-5242979'}
DEBUG:fsspec:<File-like object GCSFileSystem, バケット名/sample_1gb.parquet> read: 0 - 100  , readahead: 0 hits, 1 misses, 5242980 total requested bytes

Execution Metrics:
Execution time: 1.9332 seconds
Memory usage: 0.78 MB (Peak: 10.79 MB)

スクリプトを実行したらログを確認します。

ログを確認してみる

1. Parquet ファイルのサイズ

Total file size: 1397.65 MB

Cloud Storage上のParquetファイルのサイズは 1.4GB (1397.65MB)

2. Parquetファイルのメタデータの取得

Fetching Parquet metadata...
カラム名: 物理型, 論理型
id: INT64, (None)
name: BYTE_ARRAY, (String)
age: INT64, (None)
salary: DOUBLE, (None)
description: BYTE_ARRAY, (String)
__index_level_0__: INT64, (None)

Parquetファイルのスキーマ情報が取得できている
各カラムのデータ型を確認できる

  • id: INT64
  • name: BYTE_ARRAY (String)
  • age: INT64
  • salary: DOUBLE
  • description: BYTE_ARRAY (String)
  • __index_level_0__: INT64 (pandasのインデックス)

3. Cloud Storageへのリクエストログ

DEBUG:gcsfs:GET: b/{}/o/{}, ('バケット名', 'sample_1gb.parquet'), None
DEBUG:gcsfs.credentials:GCS refresh
DEBUG:google.auth.transport.requests:Making request: POST https://oauth2.googleapis.com/token
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): oauth2.googleapis.com:443
DEBUG:urllib3.connectionpool:https://oauth2.googleapis.com:443 "POST /token HTTP/1.1" 200 None

Cloud Storageへの認証リクエストが行われ、OAuth2 トークンを取得

4. Cloud Storageからのデータ取得

DEBUG:gcsfs:GET: https://storage.googleapis.com/download/storage/v1/b/バケット名/o/sample_1gb.parquet?alt=media, (), {'Range': 'bytes=0-5242979'}

Cloud StorageへRange: bytes=0-5242979リクエスト
これはファイルの最初の5MB((5242979バイト)を取得していることを示すしている
つまりファイル全体 (1397.65MB) をダウンロードしていない

5. 実行時間

Execution time: 1.9332 seconds

実行時間は2秒未満
1.4GBのファイル全体をダウンロードすると、数十秒~数分かかるため、メタデータのみ取得していると考えられる

6. メモリ使用量

Memory usage: 0.78 MB (Peak: 10.79 MB)

メモリ使用量は 1MB 未満(ピーク時でも 10MB 程度)
ファイル全体をダウンロードすると、メモリ使用量が数百 MB ~ 数 GB になるため、メタデータのみ取得していると考えられる

7. 結論

  • pyarrow.parquet.ParquetFile() はファイル全体をダウンロードせず、必要なメタデータ部分のみを取得している
  • 実行時間は 2 秒未満で、メモリ使用量も 1MB 未満(ピーク時でも 10MB 程度)

上記よりpyarrow.parquet.ParquetFile() は効率的に動作しており、ファイル全体をダウンロードしていないと考えられます。
また、上記のスクリプト以外にread_metadata()を用いた場合でも同様にファイル全体をダウンロードせずに必要なメタデータ部分のみを取得していました。

余談

メタデータがファイル全体をダウンロードしていないことがわかりましたが、例えば列の合計を取得する場合はどうなのかも試してみました。

# "salary" 列のみを取得
salary_column = parquet_file.read(columns=["salary"]).to_pandas()
# 列の合計を計算
total_salary = salary_column["salary"].sum()
print(f"Total Salary: {total_salary}")

上記を実行してみると以下のログとなりました。

Total file size: 1397.65 MB
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:google.auth._default:Checking None for explicit credentials as part of auth process...
DEBUG:google.auth._default:Checking Cloud SDK credentials as part of auth process...
DEBUG:gcsfs.credentials:Connected with method google_default
Fetching 'salary' column data and calculating sum...
Total Salary: 7199860009243.785

Checking GCS request details for 'salary' column...
DEBUG:gcsfs:GET: b/{}/o/{}, ('バケット名', 'sample_1gb.parquet'), None
DEBUG:gcsfs.credentials:GCS refresh
DEBUG:google.auth.transport.requests:Making request: POST https://oauth2.googleapis.com/token
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): oauth2.googleapis.com:443
DEBUG:urllib3.connectionpool:https://oauth2.googleapis.com:443 "POST /token HTTP/1.1" 200 None
DEBUG:gcsfs:GET: https://storage.googleapis.com/download/storage/v1/b/バケット名/o/sample_1gb.parquet?alt=media, (), {'Range': 'bytes=0-5242979'}
DEBUG:fsspec:<File-like object GCSFileSystem, バケット名/sample_1gb.parquet> read: 0 - 100  , readahead: 0 hits, 1 misses, 5242980 total requested bytes

Execution Metrics:
Execution time: 22.6811 seconds
Memory usage: 21.24 MB (Peak: 96.88 MB)

実行時間は22秒程度、メモリも最大100MB程度となっておりファイル全量ではなく必要データのみを読み出しているようです(ざっくりで申し訳ありません)。

所感

今回の検証を通じて、pyarrow.parquet.ParquetFile()pyarrow.parquet.read_metadata()を使用すると、Cloud Storage上のParquetファイルのメタデータを取得する際に、ファイル全体をダウンロードせずに必要な部分のみを取得していることが確認できました。
Cloud Storageのクライアントライブラリを用いてParquetファイルのメタデータ部分のみを取得する方法に比べるとスマートさは劣り、取得データ量もわずかに増えてしまう可能性はありますがそれでも手元でサクッとメタデータを見たい場合はpyarrowread_metadataを用いてCloud Storage上のParquetファイルを覗いてみるのもありだと思います。

処理対象データのみを読み込んでいるようなので、この辺りはソースをみるなどしてもう少し深掘りしてみたいなとも思います。
それではまた。

参考

https://arrow.apache.org/docs/python/generated/pyarrow.parquet.read_metadata.html
https://arrow.apache.org/docs/python/generated/pyarrow.fs.GcsFileSystem.html

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.