結論

Parquet(パーケイ)は、表のデータを「列ごと」にまとめて圧縮して保存するファイル形式です。同じ100万行で、CSVの約5分の1の大きさになり、1列だけなら約10分の1の時間で読めて、日時・先頭の0・null も崩れずに戻りました。

この記事でわかること

  • CSVとParquetの違いと、100万行で測ったサイズ・速度
  • 行グループ・列チャンク・統計値などファイルの中身
  • pandas・pyarrow・DuckDBでの読み書きとエラー原文

2026年10月29日更新|pyarrow 26.0.0・pandas 3.1.0・DuckDB 2.0.0で実行確認

Parquet のファイルは、先頭と末尾に「PAR1」という目印があり、その間に「行グループ」ごとの列のデータが並び、最後に「フッター(スキーマと統計値)」が付いています。読むプログラムは、まず末尾のフッターを読み、必要な列・必要な行グループの場所だけを取りに行きます。

flowchart TD
    A[PAR1 先頭4バイト] --> B[行グループ 0<br>列チャンク × 列の数]
    B --> C[行グループ 1 以降]
    C --> D[フッター<br>スキーマ・位置・統計値]
    D --> E[フッターの長さ 4バイト]
    E --> F[PAR1 末尾4バイト]

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
Parquetとは 列ごとにまとめて圧縮する、列指向のファイル形式(Apache のオープンソース) Parquetとは
Parquetの読み方 「パーケイ」が多い。「パーケット」とも。元はフランス語の寄木張りの床 Parquetとは
ParquetとCSVの違い Parquetは列ごと・バイナリ・型つき・圧縮つき。CSVは行ごとの文字 ParquetとCSVの違い
どれくらい小さく・速くなるか 100万行で CSV 57.5MB → zstd 11.2MB。1列の読み込みは約10分の1の時間 100万行で実測
圧縮はどれを選ぶか 迷ったら既定の snappy、保存量を減らすなら zstd Parquetの圧縮
中身を見る方法 pyarrow の read_metadata、DuckDB の parquet_metadata() Parquetファイルの中身
Parquetの開き方 DuckDB で SELECT * FROM 'a.parquet'。Excel では直接開けない DuckDBでParquetを開く
pandasで読み書き pd.read_parquet・df.to_parquet(pyarrow が要る) PythonでParquetを読み書き
Parquetが向かない場面 1行ずつの追記・更新、人が目で見る、Excel で渡す 判断表

確認環境:Ubuntu 24.04.5 LTS(Intel Xeon 2.10GHz の2コア・メモリ 8GB)の上で、Python 3.13.18 の venv に pyarrow 26.0.0・pandas 3.1.0(NumPy 2.5.4)・DuckDB 2.0.0(Python パッケージ)を入れて実行しました。データは乱数(シード 86)で作った注文データ 1,000,000行×7列(注文ID・注文日時(タイムゾーンつき)・店舗コード(先頭0つきの5桁)・カテゴリー(日本語5種)・金額・数量・クーポン(null と空文字を含む))です。CSV と Parquet(無圧縮・snappy・zstd・gzip ほか)に保存し、サイズ・書き込み時間・読み込み時間(5回の中央値、書き込みは3回)・特定の列だけ読む時間・DuckDB の集計時間を測りました。ファイルの中身は pyarrow の read_metadata と DuckDB の parquet_metadata()、述語プッシュダウンは DuckDB の EXPLAIN ANALYZE で確かめ、エラーは実際に起こした原文を載せています。Spark・BigQuery・Athena・Power Query は動かしておらず、各公式ドキュメントで確かめた範囲だけを書いています。時間は手元の環境での値で、同じ処理でも回によって1.5倍ほどぶれました(確認日 2026年10月29日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Parquetとは|CSVとの違い・読み書き・中身を100万行で実測」

Parquetとは(意味・読み方・列指向ファイル形式)

この節では、Parquet が何のための形式で、どこが「列指向」なのかが分かります。

Apache Parquet は、データを効率よく保存し、取り出すために作られた、オープンソースの列指向(カラムナー)のデータファイル形式です。 公式サイトは「Apache Parquet is an open source, column-oriented data file format designed for efficient data storage and retrieval.」と説明しています。拡張子は .parquet です。

  • 読み方:英語の発音に近い「パーケイ」と書かれることが多く、「パーケット」とも読まれます。もとはフランス語で、木片を組み合わせた寄木張りの床のことです。
  • 成り立ち:Twitter と Cloudera の共同の取り組みとして始まり、2013年に最初の版が出ました。Google の論文「Dremel」の、入れ子のデータを列に分けて保存する方法を取り入れています。2015年4月27日から Apache ソフトウェア財団のトップレベルプロジェクトです(Wikipedia の Apache Parquet の項)。
  • 仕様と実装:仕様(parquet-format)は公開されていて、読み書きのプログラムは Java(parquet-java)・C++(Arrow の一部)・Rust・Go・DuckDB など、別々に実装されています。

行指向と列指向の違い

同じ3行の表を、行指向(CSV)と列指向(Parquet)で並べると、ディスク上の順番が次のように変わります。

並べ方 ディスク上の順番 得意なこと
行指向(CSV・多くの業務DB) 1,家電,1227 → 2,食品,231 → 3,家電,501 1件まるごとの読み書き・追加・更新
列指向(Parquet・ORC) 1,2,3 → 家電,食品,家電 → 1227,231,501 多くの行の、一部の列だけを集計する

列指向にすると、同じ種類の値が隣り合うので圧縮が効き、集計に使う列だけを読めば済みます。その代わり、1件を書き足したり書き換えたりするのは苦手です。

19.5%CSVに対する大きさ100万行・zstd で 11.2MB
1.8MB金額の合計で読んだ量CSVは 57.5MB 全部を読んだ
1/10統計で読んだ行グループ日時で並べてから保存した場合

ParquetとCSVの違い(一覧表)

この節では、Parquet と CSV の違いを、データの持ち方・型・使い道の面から一覧で比べられます。

CSV は「人が読める文字の表」、Parquet は「プログラムが速く読むための、型つきで圧縮された表」です。 どちらが上というより、向いている場面が違います。

CSV Parquet
中身 文字(テキスト)。メモ帳で開ける バイナリ。専用のツールで開く
並べ方 行ごと 行グループの中で列ごと
型の情報 無い。読む側が推測する ファイルにスキーマ(列名と型)が入っている
null と空文字 どちらも空欄になり区別できない 区別して保存できる
圧縮 無し(ファイル全体を gzip などで包むことはできる) 列ごとに辞書化・圧縮(snappy・zstd など)
一部の列だけ読む 全体を読んでから捨てる その列の場所だけ読む
条件で読み飛ばす できない 行グループの最小値・最大値で読み飛ばせる
入れ子(配列・構造体) 文字にするしかない list・struct・map をそのまま持てる
追記・1行の更新 末尾に足せる できない。新しいファイルを書く
Excel 開ける 直接は開けない

CSVが向く

人が開いて目で見る・Excel で渡す

少ない行数のやり取り・設定の一覧

ログのように1行ずつ書き足す

Parquetが向く

数十万行以上を集計・分析する

型を崩さずにプログラム間で受け渡す

S3 などに置いて SQL で読む(データレイク)

Avro は行指向のバイナリ形式で、1件ずつの書き込みやメッセージのやり取りに向きます。ORC は Parquet と同じ列指向の形式で、Hive の世界で広く使われてきました。Athena の公式ドキュメントは、容量が気になる場合は ORC のほうが小さくなることが多い、と書いています。

100万行で実測:CSVとParquetのサイズと速度

この節では、同じ100万行を CSV と Parquet に保存したときの大きさと、書き込み・読み込みの時間が、実測の表で分かります。

データは乱数で作った注文データ 1,000,000行×7列で、メモリ上では 65.2MB でした。CSV は pandas の to_csv、Parquet は pyarrow の write_table(圧縮の指定だけを変える)で書いています。

import pandas as pd, pyarrow as pa, pyarrow.parquet as pq

df = make()                               # 100万行の DataFrame(乱数で作成)
t = pa.Table.from_pandas(df, preserve_index=False)

df.to_csv("orders.csv", index=False)
for comp in ["none", "snappy", "zstd", "gzip"]:
    pq.write_table(t, f"orders_{comp}.parquet", compression=comp)

大きさと書き込み時間

形式 大きさ(バイト) CSV比 書き込み
CSV(pandas の to_csv) 57,534,488 100% 23.08秒
CSV(pyarrow の write_csv) 59,335,090 103% 1.34秒
Parquet 無圧縮(none) 20,632,732 35.9% 0.24秒
Parquet snappy(既定) 15,394,637 26.8% 0.21秒
Parquet zstd 11,197,694 19.5% 0.23秒
Parquet gzip 10,942,284 19.0% 13.81秒
  • 無圧縮の Parquet でも CSV の約3分の1でした。 数値を文字ではなく8バイトの値で持ち、同じ値の多い列を辞書(よく出る値の一覧と番号)に置き換えているためです。
  • pandas の to_csv は、タイムゾーンつきの日時を1行ずつ文字にするため遅く、23秒かかりました。同じデータを pyarrow の write_csv で書くと1.3秒でした。

読み込み時間(全部の列・1列だけ・DuckDB で集計)

形式 pandas 全列 pyarrow 全列 pandas 1列 DuckDB 集計
CSV 0.188秒 0.274秒 0.104秒 0.227秒
無圧縮 0.076秒 0.082秒 0.012秒 0.009秒
snappy 0.139秒 0.133秒 0.007秒 0.009秒
zstd 0.100秒 0.119秒 0.011秒 0.017秒
gzip 0.161秒 0.161秒 0.018秒 0.035秒

「1列」は金額の列だけ(CSV は usecols=['price']、Parquet は columns=['price'])、「DuckDB 集計」はカテゴリーごとの金額の合計です。

1全列の差は思ったより小さい

pandas 3.1 の read_csv は0.19秒で、Parquet の0.08〜0.16秒と大差がなかった。ただし CSV 側は日時を文字のまま読んでいる。

2型を正しく戻すと CSV は遅くなる

日時の変換と店舗コードの文字指定を足すと0.31秒、クーポンの空文字も残す指定で0.38秒になった。

3差が開くのは「一部の列」

1列だけなら CSV 0.104秒に対し Parquet 0.007〜0.018秒。DuckDB の集計は CSV 0.227秒、Parquet 0.009秒。

読んだバイト数(DuckDB の EXPLAIN ANALYZE)

速さの理由は、読む量の違いです。DuckDB の EXPLAIN ANALYZE が表示する「Data Read」を比べました(snappy のファイル)。

SQL Data Read 時間
Parquet で SELECT sum(price) 1.8MB 0.0081秒
Parquet で4列を集計 10.2MB 0.160秒
Parquet で SELECT * 15.4MB 0.0463秒
CSV で SELECT sum(price) 57.5MB 0.221秒

金額の合計では、Parquet は金額の列の 1.8MB だけを読み、CSV はファイル全体の 57.5MB を読みました。 1.8MB は、次の節で見る金額の列チャンクの大きさ(1,784,292バイト)とほぼ同じです。DuckDB の公式ドキュメントも、Parquet ファイルに対して必要な列だけを読む「projection pushdown」を自動で行うと説明しています。

Parquetの圧縮:snappy・zstd・gzipの選び方

この節では、圧縮方式(コーデック)ごとの大きさと書き込み時間の違いと、どれを選ぶかが分かります。

pyarrow・pandas・Spark・DuckDB の既定はどれも snappy です。 保存する量を減らしたいときは zstd に変えると、手元では snappy より約27%小さくなり、書き込み時間はほとんど変わりませんでした。同じデータで、ほかの指定も試しました。

指定 大きさ(バイト) CSV比 書き込み
snappy・辞書なし(use_dictionary=False) 21,705,110 37.7% 0.20秒
lz4 14,948,312 26.0% 0.24秒
snappy(既定) 15,394,637 26.8% 0.21秒
zstd(レベルは既定) 11,197,694 19.5% 0.23〜0.33秒
brotli 10,231,461 17.8% 1.32秒
zstd レベル19(compression_level=19) 9,856,302 17.1% 9.47秒
gzip 10,942,284 19.0% 13.81秒
  • 辞書(dictionary)エンコーディングを切ると、snappy でも 15.4MB から 21.7MB に増えました。 カテゴリー(5種)やクーポン(3種と null)のように種類の少ない列は、辞書に置き換えるだけで大きく縮みます。pyarrow は既定で辞書を使い、辞書が大きくなりすぎた列は PLAIN(そのままの並び)に切り替えます(pyarrow の write_table の説明)。
  • zstd のレベルを19に上げると、さらに12%小さくなりましたが、書き込みは約30倍の時間でした。何度も書き直すデータには向きません。
  • 圧縮は列ごとに選べます。pyarrow では compression={'price': 'zstd', 'category': 'snappy'} のように辞書で渡します。
  1. 1迷ったら snappy(既定)のままどのツールでも読める。書き込みも読み込みも速い
  2. 2保存の量や転送料を減らすなら zstd手元では snappy より約27%小さく、時間はほぼ同じ
  3. 3一度書いて長く置くだけなら zstd の高いレベル・brotli書き込みは何倍も遅くなる
  4. 4gzip は相手が gzip しか読めないときだけ大きさは zstd と同じくらいで、書き込みがいちばん遅かった

読み込む側が対応しているかも確かめます。BigQuery の公式ドキュメントが挙げる、Parquet の読み込みで使える圧縮は GZip・LZO_1C・LZO_1X・LZ4_RAW・Snappy・ZSTD です。

Parquetファイルの中身(行グループ・列チャンク・統計値)

この節では、Parquet ファイルの中に何が入っているかを、実際のファイルを開いた出力で確かめられます。

Parquet は「行グループ(行のかたまり)→ 列チャンク(その中の1列分)→ ページ(読み書きの最小単位)」の3段でできていて、末尾のフッターに、スキーマ・各列チャンクの位置・最小値と最大値などの統計値が入っています。 公式の仕様によると、ファイルの先頭と末尾は4バイトの「PAR1」で、末尾の PAR1 の直前に、フッターの長さが4バイト(リトルエンディアン)で入っています。

先頭と末尾のバイトを見る

b = open("orders_snappy.parquet", "rb").read()
print(b[:4], b[-8:], int.from_bytes(b[-8:-4], "little"))
b'PAR1' b'\x90\x0f\x00\x00PAR1' 3984

末尾の4バイト手前の 90 0f 00 00 は、リトルエンディアンで 0x0f90=3,984 です。フッターが 3,984 バイトあることを示しています。読むプログラムは、まず末尾の8バイトを読み、フッターの場所を知ります。 途中で切れたファイルが「magic bytes not found in footer」というエラーになるのはこのためです(エラーの節)。

pyarrow.parquet.read_metadata で全体を見る

parquet-tools のような専用のコマンドが無くても、pyarrow の read_metadata でフッターの中身を表示できます。

import pyarrow.parquet as pq
md = pq.read_metadata("orders_snappy.parquet")
print(md)
print(md.schema)
print(md.row_group(0).column(3))   # 4番目の列(category)の列チャンク
<pyarrow._parquet.FileMetaData object at 0x7f25add08e00>
  created_by: parquet-cpp-arrow version 26.0.0
  num_columns: 7
  num_rows: 1000000
  num_row_groups: 1
  format_version: 2.6
  serialized_size: 3984

required group field_id=-1 schema {
  optional int64 field_id=-1 order_id;
  optional int64 field_id=-1 ordered_at (Timestamp(isAdjustedToUTC=true, timeUnit=milliseconds, ...));
  optional binary field_id=-1 store_code (String);
  optional binary field_id=-1 category (String);
  optional double field_id=-1 price;
  optional int32 field_id=-1 qty;
  optional binary field_id=-1 coupon (String);
}

  path_in_schema: category
  statistics:
      has_min_max: True
      min: 家電
      max: 食品
      null_count: 0
      num_values: 1000000
      logical_type: String
  compression: SNAPPY
  encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  has_dictionary_page: True
  total_compressed_size: 379917
  total_uncompressed_size: 379667
  • num_row_groups: 1:pyarrow は既定で、1つの行グループを最大 1,048,576(1024×1024)行にします。100万行は1つに収まりました。
  • 文字列は物理型 binary(BYTE_ARRAY)に、論理型 String を付けて保存されています。日時は int64 に Timestamp の論理型です。物理型(保存の形)と論理型(意味)の2段になっているのが Parquet の型の特徴です。
  • pandas の秒単位の日時は、ミリ秒(timeUnit=milliseconds)で保存されていました。pyarrow の説明に「Parquet には秒の精度の時刻型が無いため、秒は常にミリ秒に変換する」とあります。
  • min: 家電・max: 食品 は、UTF-8 のバイト列としての大小で決まっています。五十音順ではありません。

列ごとの大きさ(DuckDB の parquet_metadata)

DuckDB の parquet_metadata() は、列チャンクごとの統計と大きさを表で返します。抜き出して並べました(snappy のファイル)。

SELECT path_in_schema, stats_min, stats_max, stats_null_count,
       total_compressed_size, total_uncompressed_size
FROM parquet_metadata('orders_snappy.parquet');
列 最小〜最大(統計値) null の数 圧縮後 圧縮前
order_id 1〜1000000 0 4,276,991 8,274,562
ordered_at 2025-12-31 15:00:25〜2026-09-30 14:59:57(UTC) 0 7,078,876 8,273,874
store_code 00001〜00499 0 1,131,887 1,134,108
category 家電〜食品 0 379,917 379,667
price 22.0〜50261.0 0 1,784,292 1,828,348
qty 1〜9 0 505,401 505,149
coupon (空文字)〜SALE10 599,706 233,277 233,025
  • 100万行の category は、辞書に置き換えた時点で 38万バイトまで縮み、snappy はほとんど効いていません(圧縮前と後がほぼ同じ)。大きいのは、ほぼすべて違う値の order_id と ordered_at です。
  • 日時の統計は UTC で入っています。2025-12-31 15:00:25(UTC)は、日本時間の 2026-01-01 00:00:25 です。
  • coupon の null は 599,706件と数えられていて、空文字(最小値が空)とは別に扱われています。

スキーマに入っている pandas の情報

pandas から書いた Parquet には、フッターのキーと値の欄に、pandas の型やインデックスの情報が JSON で入ります。pq.read_schema で見えます。

order_id: int64
ordered_at: timestamp[ms, tz=Asia/Tokyo]
store_code: large_string
category: large_string
price: double
qty: int32
coupon: large_string
-- schema metadata --
pandas: '{"index_columns": [], "column_indexes": [], "columns": [{"name":' + 908

述語プッシュダウン:統計値で行グループを読み飛ばす

この節では、Parquet の「条件に合わない部分を読まない」仕組みと、効かせるためにデータをどう並べればよいかが、実測で分かります。

述語プッシュダウン(predicate pushdown・filter pushdown)は、WHERE の条件を読み込みの段階に持ち込み、フッターの最小値・最大値から「この行グループには条件に合う行が無い」と分かった部分を読まない仕組みです。 Athena の公式ドキュメントも、Parquet と ORC では最大値・最小値などの統計を使ってブロックを読むか飛ばすかを決める、と説明しています。

同じ100万行を、10万行ずつの行グループ(row_group_size=100_000)で、注文日時の順に並べ替えずに保存したものと、並べ替えて保存したものを作り、「2026年3月1日の注文」を読みました。

pq.write_table(t, "unsorted.parquet", compression="zstd", row_group_size=100_000)
pq.write_table(t.sort_by("ordered_at"), "sorted.parquet", compression="zstd", row_group_size=100_000)

lo = pd.Timestamp("2026-03-01", tz="Asia/Tokyo"); hi = pd.Timestamp("2026-03-02", tz="Asia/Tokyo")
pq.read_table("sorted.parquet", filters=[("ordered_at", ">=", lo), ("ordered_at", "<", hi)])

行グループごとの統計値(ordered_at の最小〜最大、UTC)は次のとおりでした。

ソートなし
  RG 0 2025-12-31 15:03:15+00:00 〜 2026-09-30 14:58:19+00:00
  RG 1 2025-12-31 15:03:36+00:00 〜 2026-09-30 14:59:57+00:00
  RG 2 2025-12-31 15:00:25+00:00 〜 2026-09-30 14:58:52+00:00
ordered_atでソート
  RG 0 2025-12-31 15:00:25+00:00 〜 2026-01-27 21:26:32+00:00
  RG 1 2026-01-27 21:26:53+00:00 〜 2026-02-24 02:31:19+00:00
  RG 2 2026-02-24 02:33:09+00:00 〜 2026-03-23 11:34:31+00:00
ファイル 統計で候補の行グループ pyarrow(filters) DuckDB(WHERE)
ソートなし 10個中10個 128.1ミリ秒 23.1ミリ秒
ordered_at でソート 10個中1個 12.0ミリ秒 4.1ミリ秒

どちらも返った行は 3,627行で、結果は同じです。ソートなしでは、どの行グループも1月〜9月の値を含むため、最小値・最大値では1つも飛ばせませんでした。 並べ替えたファイルでは、3月1日を含む行グループは1つだけで、10倍ほど速くなりました。並べ替えたほうが、ファイルも 14,671,830 バイトから 14,058,080 バイトへ少し小さくなりました。

DuckDB の EXPLAIN ANALYZE では、読んだ行グループの数が表示されます(表示を一部省いています)。

Total Time: 0.0033s
Data Read: 538.9 kB
Table Scan
 Function: PARQUET_SCAN
 Filters:
 (ordered_at >= '2026-02-28 15:00:00+00'::TIMESTAMP WITH TIME ZONE) AND (ordered_at < '2026-03-01 15:00:00+00'::TIMESTAMP WITH TIME ZONE)
 Total Files Read: 1
 Row Groups Scanned: 1 / 10
 3,627 rows
  • 1よく絞り込む列で並べてから書く日時・ID・店舗など。並べないと最小値・最大値の幅が全体と同じになり、読み飛ばせない
  • 2行グループを1つにしないpyarrow の既定は最大約105万行。行が少ないと1つになり、読み飛ばす単位が無い
  • 3効いたかは数字で確かめるDuckDB の Row Groups Scanned、読み込み量(Data Read)で確かめる

行グループの大きさは、書くツールで違いました。pyarrow は最大 1,048,576行、DuckDB の COPY ... TO ... (FORMAT parquet) では、同じ100万行が9つの行グループ(先頭は124,576行)に分かれました。公式の仕様のページは、行グループを大きく(512MB〜1GB)することを勧めています(HDFS などで大きなファイルを読む前提の値です)。

CSVで型が崩れる例とParquetの型

この節では、CSV に保存して読み戻すと何が変わってしまうのかと、Parquet ではどう残るのかが、実行結果で分かります。

CSV には型の情報が無いため、読む側が中身から型を推測します。その推測で、先頭の0・日時・null・整数の欠け・配列が変わりました。 2行だけの表を、CSV と Parquet に書いて pandas で読み戻しました。

df = pd.DataFrame({
    "store_code": ["00123", "04560"],
    "ordered_at": pd.to_datetime(["2026-10-29 09:00:00.123456", "2026-10-29 23:59:59.000000"]).tz_localize("Asia/Tokyo"),
    "coupon": [None, ""],
    "qty": pd.array([3, None], dtype="Int64"),
    "paid": [True, False],
    "big_id": [9007199254740993, 1],
    "memo": ["カンマ,あり", '改行\nあり "引用"'],
    "tags": [["a", "b"], []],
})
df.to_csv("t.csv", index=False);  c = pd.read_csv("t.csv")
df.to_parquet("t.parquet");       p = pd.read_parquet("t.parquet")

書き出された CSV です。

store_code,ordered_at,coupon,qty,paid,big_id,memo,tags
00123,2026-10-29 09:00:00.123456+09:00,,3,True,9007199254740993,"カンマ,あり","['a', 'b']"
04560,2026-10-29 23:59:59.000000+09:00,,,False,1,"改行
あり ""引用""",[]
列 元の型 CSV を read_csv した結果 Parquet を read_parquet した結果
store_code 文字 "00123" int64 の 123・4560(先頭の0が消えた) 文字 "00123"・"04560"
ordered_at 日時(Asia/Tokyo) 文字のまま(日時にならない) datetime64[us, Asia/Tokyo]
coupon null と空文字 両方 NaN(区別が消えた) null と空文字のまま
qty 欠けのある整数(Int64) float64 の 3.0・NaN Int64 の 3・<NA>
paid bool bool bool
big_id int64(2の53乗+1) int64(そのまま) int64(そのまま)
memo カンマ・改行・引用符 そのまま(引用符で囲まれた) そのまま
tags 配列 文字 "['a', 'b']" 配列(NumPy の配列で戻る)
  • Parquet では、8列すべての型と値が元のとおりに戻りました。 配列の列だけ、Python のリストではなく NumPy の配列で戻るため、df.equals(p) は False になりました。
  • CSV でも、dtype={'store_code': 'str'}・parse_dates=['ordered_at'] を指定すれば戻せます。ただし100万行で試すと、日時は datetime64[us, UTC+09:00](地域名ではなく時差だけ)になり、数量は int32 から int64 に変わり、クーポンの null(599,706件)と空文字(100,543件)は、keep_default_na=False を付けると両方が空文字の 700,249件にまとまりました。CSV には「null と空文字の違い」を書く場所そのものが無いためです。
  • DuckDB の read_csv は、"00123" を VARCHAR(文字)と推測して先頭の0を残しました。推測の結果は読む道具によって変わります。

型を受け渡すとき

避けたいCSV で渡して、受け取った側が dtype や日付の形式を毎回指定し直す。指定を忘れると先頭の0や null が黙って変わる
→
代わりにParquet で渡す。型・タイムゾーン・null がファイルに入っているので、読む側は read_parquet だけで元に戻る

人が Excel で見るものだけ CSV にする

CSV を Python で読み書きするときの指定と文字化けの対策は、関連記事「PythonでCSVを読み書き|コピペ用テンプレートと文字化け対策」で扱っています。

PythonでParquetを読み書きする(pandas・pyarrow)

この節では、pandas と pyarrow で Parquet を読み書きする基本の書き方と、よく使う引数が分かります。

pandas の to_parquet・read_parquet は、中で pyarrow(または fastparquet)を使います。 pandas だけを入れた環境では動かず、pyarrow を入れます。pandas 3.1.0 の公式ドキュメントでは、engine の 'fastparquet' と 'auto' は非推奨になり、engine を省くか 'pyarrow' を指定する形が案内されています。

python3 -m venv .venv
. .venv/bin/activate
pip install pyarrow pandas duckdb

pandas で書く・読む

import pandas as pd

df.to_parquet("orders.parquet")                         # 既定は snappy
df.to_parquet("orders_zstd.parquet", compression="zstd") # 圧縮を変える
df.to_parquet("orders.parquet", index=False)            # インデックスを保存しない

d = pd.read_parquet("orders.parquet")                                    # 全部
d = pd.read_parquet("orders.parquet", columns=["category", "price"])    # 列を選ぶ
d = pd.read_parquet("orders.parquet", filters=[("category", "=", "家電")])  # 行を絞る

pyarrow で書く・読む

import pyarrow as pa, pyarrow.parquet as pq

t = pa.Table.from_pandas(df, preserve_index=False)
pq.write_table(t, "orders.parquet", compression="zstd", row_group_size=100_000)

t2 = pq.read_table("orders.parquet", columns=["price"])
print(pq.read_metadata("orders.parquet").num_row_groups)
print(pq.read_schema("orders.parquet"))
1

columns

読む列を選ぶ。Parquet の速さがいちばん出る指定。

read_parquet・read_table

2

filters

行を条件で絞る。統計値で行グループを飛ばす。

並べて書くと効く

3

compression

snappy・zstd・gzip・lz4・brotli・none。列ごとにも指定できる。

既定は snappy

4

row_group_size

1つの行グループの最大の行数。pyarrow の既定は 1024×1024。

pyarrow の write_table

1つのファイルに追記はできない

Parquet のファイルには、あとから行を書き足せません。 フッターが末尾にあり、全体を書き終えてから作られるためです。手元で確かめた動きは次のとおりです。

w = pq.ParquetWriter("app.parquet", t.schema)   # 開いている間は何回でも書ける
w.write_table(t); w.write_table(t); w.close()
# → 行 6・行グループ 2(3行のテーブルを2回)

pq.write_table(t, "app.parquet")                 # 同じ名前に書くと上書き
# → 行 3

毎日のデータを足していくときは、ファイルを1つずつ増やし、フォルダーごと読みます(次の DuckDB とパーティションの節)。pandas の使い方全般は、関連記事「pandasの使い方 逆引き81選|pandas 3.0の実行結果つき」で扱っています。

DuckDBでParquetを開く・SQLで集計・CSVと変換

この節では、Parquet ファイルの中身を開いて見る方法と、SQL での集計、CSV との相互変換が分かります。

DuckDB は、Parquet ファイルをテーブルのように SQL で直接読めます。 取り込みの作業は要らず、FROM にファイル名を書くだけです。

import duckdb
con = duckdb.connect()
print(con.sql("DESCRIBE SELECT * FROM 'conv.parquet'"))
print(con.sql("SELECT * FROM 'conv.parquet' LIMIT 3"))
┌──────────┬──────────────────────────┬────────────┬──────────┬────────┬───────┬─────────┐
│ order_id │        ordered_at        │ store_code │ category │ price  │  qty  │ coupon  │
│  int64   │ timestamp with time zone │  varchar   │ varchar  │ double │ int64 │ varchar │
├──────────┼──────────────────────────┼────────────┼──────────┼────────┼───────┼─────────┤
│        1 │ 2026-09-19 11:24:14+09   │ 00268      │ 家電     │ 1227.0 │     8 │ SALE10  │
│        2 │ 2026-06-18 00:30:32+09   │ 00357      │ 食品     │  231.0 │     3 │ NEW     │
│        3 │ 2026-02-20 07:49:18+09   │ 00473      │ 家電     │  501.0 │     6 │ NULL    │
└──────────┴──────────────────────────┴────────────┴──────────┴────────┴───────┴─────────┘

CSV から Parquet へ、Parquet から CSV へ

COPY (SELECT * FROM read_csv('orders.csv'))
  TO 'conv.parquet' (FORMAT parquet, COMPRESSION zstd);

COPY (SELECT * FROM 'conv.parquet') TO 'back.csv' (HEADER);
変換 時間 できたファイル
CSV(57.5MB)→ Parquet zstd 0.72秒 11,224,026バイト
Parquet → CSV 0.57秒 54,534,488バイト
Parquet → Parquet(COPY の既定) ― snappy・9つの行グループ・15,624,309バイト
  • CSV を経由した Parquet では、数量の列が int32 ではなく int64(BIGINT)になりました。CSV の時点で型の情報が消えているため、DuckDB が推測した型で書かれます。Parquet に直すときは、必要なら CAST や read_csv の types で型を決めてから書きます。
  • DuckDB の公式ドキュメントによると、COPY ... TO の Parquet の既定の圧縮は snappy です。

フォルダーの Parquet をまとめて読む

SELECT category, sum(price) FROM read_parquet('daily/*.parquet') GROUP BY 1;
SELECT * FROM read_parquet(['a.parquet', 'b.parquet'], union_by_name = true);
SELECT * FROM read_parquet('daily/*.parquet', filename = true) LIMIT 5;  -- どのファイルの行か

union_by_name は、列の並びや有無が違うファイルを、列名で合わせて読む指定です。指定しないと、最初のファイルのスキーマで読みます(エラーの節に原文があります)。

パーティション分割(hive形式のフォルダー)

この節では、Parquet をフォルダーに分けて保存する「パーティション分割」の形と、読み飛ばしの効果が分かります。

パーティション分割は、「month=2026-03」のような名前のフォルダーに分けて保存し、フォルダー名を列として扱う方法です。 この形は Hive が使い始めたため「hive 形式」と呼ばれ、pyarrow・DuckDB・Spark・Athena・BigQuery が読めます。

import pyarrow.compute as pc
t2 = t.append_column("month", pc.strftime(t["ordered_at"], format="%Y-%m"))
pq.write_to_dataset(t2, "part", partition_cols=["month"], compression="zstd")
part/month=2026-01/01c4ab9e2065485093c66d5e11579514-0.parquet 1669916
part/month=2026-02/01c4ab9e2065485093c66d5e11579514-0.parquet 1460678
part/month=2026-03/01c4ab9e2065485093c66d5e11579514-0.parquet 1695738
...
part/month=2026-09/01c4ab9e2065485093c66d5e11579514-0.parquet 1555580
読み方 返った行 時間・読んだファイル
pyarrow で全部 1,000,000 302.4ミリ秒
pyarrow で filters=[('month','=','2026-03')] 113,231 31.1ミリ秒
DuckDB で WHERE month='2026-03' 113,231 Scanning Files: 1/9
SELECT month, count(*)
FROM read_parquet('part/*/*.parquet', hive_partitioning = true)
WHERE month = '2026-03' GROUP BY 1;
-- EXPLAIN の表示: File Filters: month = '2026-03' / Scanning Files: 1/9
  • フォルダー名で絞れるため、統計値を見る前にファイルごと読み飛ばせます。 DuckDB は9つのファイルのうち1つだけを開きました。
  • pyarrow で読むと、month の列は dictionary<values=string> 型になりました。pyarrow の公式ドキュメントも、パーティションの列は dictionary 型(pandas では categorical)で読まれると説明しています。
  • 細かく分けすぎると、小さいファイルが大量にでき、開くだけで時間がかかります(小さいファイルの問題)。絞り込みによく使い、値の種類が多すぎない列(年月・地域など)を選びます。

S3 に置いた Parquet の一覧やコピーは、関連記事「aws s3コマンド一覧|cp・sync・ls実行例とオプション早見表」で扱っています。

Spark・BigQuery・AthenaでのParquet(公式の範囲)

この節では、分析基盤で Parquet を使うときの基本を、各公式ドキュメントで確かめた範囲で分かります。手元では動かしていません。

基盤 公式ドキュメントで確かめたこと
Apache Spark(4.2.0 の文書) spark.read.parquet・df.write.parquet で読み書き。既定の圧縮 spark.sql.parquet.compression.codec は snappy。違うスキーマのファイルをまとめる mergeSchema は重いため 1.5.0 から既定で無効。key=value のフォルダーからパーティション列を自動で作る
Google BigQuery Parquet はスキーマを持つため、読み込み時にスキーマを指定しなくてよい。bq load --source_format=PARQUET で読み込む。スキーマの違うファイルはワイルドカードでまとめて読めない(列の位置が違うだけでも別のスキーマ扱い)
Amazon Athena 列ごとに型に合った圧縮で S3 の容量と I/O を減らす。最大値・最小値の統計でブロックを読むか飛ばすかを決める。データを分けて複数で並行して読める
Power Query(Power BI など) Parquet コネクタがある。対応製品の一覧は Power BI・Fabric・Power Apps など(2026年10月29日時点で Excel の記載は無い)

BigQuery の読み込みの例です(公式ドキュメントの形)。

bq load \
--source_format=PARQUET \
mydataset.mytable \
gs://mybucket/mydata.parquet

Spark で読み書きする最小の形です(公式ドキュメントの例をもとにしています)。

df = spark.read.json("people.json")
df.write.parquet("people.parquet")
p = spark.read.parquet("people.parquet")          # スキーマはファイルから戻る
m = spark.read.option("mergeSchema", "true").parquet("data/test_table")

Athena・Spark・BigQuery のどれでも、「必要な列だけを SELECT する」「パーティションの列で絞る」の2つで、読む量(多くの場合は料金や時間)が変わります。SELECT * は Parquet の利点を消します。

Parquetのよくあるエラーと直し方(原文)

この節では、Parquet を扱うときに出るエラーの原文と、原因・直し方が分かります。すべて手元で実際に起こしたものです。

エラー(原文の要点) 原因 直し方
ImportError: Unable to find a usable engine; tried using: 'pyarrow', 'fastparquet'. pandas だけ入っていて pyarrow が無い pip install pyarrow
Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file. 途中で切れたファイル(書き込み中・転送の失敗)か、Parquet ではないファイル 書き直す・転送し直す。file や先頭4バイトで中身を確かめる
Parquet file size is 6 bytes, smaller than the minimum file footer (8 bytes) CSV など別の小さいファイルを read_parquet した 読む関数とファイルを合わせる
No match for FieldRef.Name(nope) in id: int64 ... columns に無い列名を指定した pq.read_schema で列名を確かめる
Failed to parse string: '5個' as a scalar of type int32 フォルダー内のファイルで同じ列の型が違う 書く側で型を揃える。読むときにスキーマを指定する
Casting from timestamp[ns] to timestamp[ms] would lose data ナノ秒の時刻を ms で書こうとした allow_truncated_timestamps=True で切り捨てを許す
Conversion failed for column v with type object 1つの列に数値と文字が混ざっている astype(str) などで型を1つにする

pyarrow・pandas のエラー原文

ImportError: Unable to find a usable engine; tried using: 'pyarrow', 'fastparquet'.
A suitable version of pyarrow or fastparquet is required for parquet support.
Trying to import the above resulted in these errors:
 - `Import pyarrow` failed. pyarrow is required for parquet support. Use pip, conda, or your preferred package management tool to install the pyarrow package.

pyarrow.lib.ArrowInvalid: Could not open Parquet input source '.../cut.parquet': Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file.

pyarrow.lib.ArrowInvalid: Could not open Parquet input source '.../x.csv': Parquet file size is 6 bytes, smaller than the minimum file footer (8 bytes)

pyarrow.lib.ArrowInvalid: No match for FieldRef.Name(nope) in id: int64
qty: int32

pyarrow.lib.ArrowInvalid: Failed to parse string: '5個' as a scalar of type int32

pyarrow.lib.ArrowInvalid: Casting from timestamp[ns] to timestamp[ms] would lose data: 1793264400123456789

pyarrow.lib.ArrowInvalid: ("Could not convert 'a' with type str: tried to convert to int64", 'Conversion failed for column v with type object')

FileNotFoundError: .../none.parquet

ナノ秒の時刻は、allow_truncated_timestamps=True を付けると 2026-10-29 09:00:00.123000 に切り捨てて書けました。pyarrow の既定(format の版 2.6)では、ナノ秒はそのまま書けます。ミリ秒で書く指定は、ナノ秒を読めない相手に渡すときに使います。

DuckDB のエラー原文

InvalidInputException: Invalid Input Error: No magic bytes found at end of file '.../cut.parquet'

BinderException: Binder Error: Referenced column "nope" not found in FROM clause!
Candidate bindings: "id"

ConversionException: Conversion Error: Error while reading file ".../mix/b.parquet": failed to cast column "qty" from type VARCHAR to INTEGER: : Could not convert string '5個' to INT32

In file ".../mix/b.parquet" the column "qty" has type VARCHAR, but we are trying to read it as type INTEGER.
This can happen when reading multiple Parquet files. The schema information is taken from the first Parquet file by default.

InvalidInputException: Invalid Input Error: Failed to read file ".../mix2.parquet": schema mismatch in glob: column "qty" was read from the original file ".../ok.parquet", but could not be found in file ".../mix2.parquet".
Candidate names: id, price
If you are trying to read files with different schemas, try setting union_by_name=True

最後のエラーは、メッセージのとおり union_by_name=true を付けると読め、無い列は NULL で埋まりました([(1, 1, None), (2, 2, None), (3, 3, None), (5, None, 1.5)])。型の違い(VARCHAR と INTEGER)は union_by_name では直らず、書く側で型を揃えるのが確実です。

1

本当に Parquet かを確かめる

先頭と末尾の4バイトが PAR1 かを見る。末尾が違えば、途中で切れたか別の形式。

magic bytes のエラー

2

スキーマを見る

pq.read_schema か DuckDB の DESCRIBE で、列名と型を確かめる。

列が無い・型が違う

3

ファイルごとのスキーマを比べる

フォルダーで読むと失敗するなら、ファイルを1つずつ読んで、どのファイルの型が違うかを探す。

複数ファイルの読み込み

4

書く側で型を揃える

pandas なら astype、pyarrow なら schema= を渡して書く。読む側で直すより確実。

再発を防ぐ

Parquetを使う・使わないの判断表

この節では、手元のデータを Parquet にするか、CSV などのままにするかを、場面ごとに決められます。

場面 向く形式 理由
数十万行以上のデータを集計・分析する Parquet 必要な列だけ読む。手元で1列の読み込みが約10分の1の時間
Python・SQL・別のチームの間で表を渡す Parquet 型・タイムゾーン・null が崩れない
S3・GCS に置いて Athena・BigQuery で読む Parquet(パーティション分割) 読む量が減り、時間や料金に効く
機械学習の特徴量や中間データを保存する Parquet pickle と違い、別の言語・版でも読める
人が Excel で開いて確かめる・取引先に送る CSV(または xlsx) Parquet は Excel で直接開けない
数十行の設定・マスターの一覧 CSV・JSON・YAML 小さいと Parquet の利点が出ず、差分も見にくい
1件ずつ追加・更新・削除する データベース Parquet は追記・更新ができない
イベントを1件ずつ送る・受ける JSON・Avro 行指向のほうが1件の読み書きに向く
Parquet のファイルを何度も更新したい Iceberg・Delta Lake(中身は Parquet) テーブルとして更新・削除・履歴を扱える

行指向のデータベースで速く探す仕組み(インデックス)との違いを知りたいときは、関連記事「DBインデックス8選|複合の列順・カバリング・ビットマップ・リビルド判断」で扱っています。

よくある質問

Q. Parquetとは何ですか? A. 表のデータを列ごとにまとめ、列ごとに圧縮して保存するファイル形式です。Apache のオープンソースで、pandas・Spark・DuckDB・BigQuery・Athena など多くの道具が読み書きできます。CSV より小さく、一部の列だけを速く読めます。

Q. Parquetの読み方は? A. 「パーケイ」と書かれることが多く、「パーケット」とも読まれます。もとはフランス語で、寄木張りの床を意味します。

Q. ParquetとCSVの違いは何ですか? A. CSV は行ごとの文字のファイルで、型の情報がありません。Parquet は列ごとのバイナリのファイルで、列名と型、圧縮、統計値を持っています。手元の100万行では、CSV 57.5MB に対し Parquet(zstd)は 11.2MB でした。

Q. Parquetファイルの開き方は?中身を見たいです。 A. DuckDB で SELECT * FROM 'ファイル名.parquet' LIMIT 10 とするのが手軽です。Python なら pd.read_parquet で読み、pq.read_metadata で行グループや統計値を見られます。メモ帳では開けません。

Q. ParquetはExcelで開けますか? A. Excel で直接は開けません。Power Query の Parquet コネクタの対応製品の一覧には、2026年10月29日時点で Power BI などが載っていて、Excel の記載はありません。Excel で見たいときは、DuckDB や pandas で CSV か xlsx に変換して渡します。

Q. ParquetをCSVに変換するには? A. DuckDB なら COPY (SELECT * FROM 'a.parquet') TO 'a.csv' (HEADER);、pandas なら pd.read_parquet('a.parquet').to_csv('a.csv', index=False) です。手元では DuckDB で100万行を0.57秒で変換できました。

Q. snappy・zstd・gzip のどれを選べばよいですか? A. 迷ったら既定の snappy、保存量を減らしたいなら zstd です。手元では zstd は snappy より約27%小さく、書き込み時間はほぼ同じでした。gzip は zstd と同じくらいの大きさで、書き込みが約60倍遅くなりました。

Q. pandasでread_parquetするとImportErrorになります。 A. pandas は Parquet の読み書きに pyarrow か fastparquet を使います。pip install pyarrow で入れます。エラーの原文は「Unable to find a usable engine; tried using: 'pyarrow', 'fastparquet'.」です。

Q. Parquetにすると必ず速くなりますか? A. いいえ。手元では、全部の列を pandas で読む時間は CSV 0.19秒、Parquet 0.08〜0.16秒で、差は小さめでした。差が大きく開くのは、一部の列だけを読むときと、並べて保存した列で絞り込むときです。SELECT * ばかりなら効果は小さくなります。

まとめ

  • Parquet は、列ごとにまとめて圧縮する列指向のファイル形式。ファイルは PAR1・行グループ・列チャンク・フッター(スキーマと統計値)でできている。
  • 100万行で、CSV 57.5MB に対し Parquet は無圧縮 20.6MB・snappy 15.4MB・zstd 11.2MB。1列だけ読む時間は CSV の約10分の1、DuckDB の金額の合計は CSV の 57.5MB に対し 1.8MB だけを読んだ。
  • CSV では先頭の0・日時・null と空文字・欠けのある整数・配列が崩れ、Parquet ではすべて元の型で戻った。
  • 統計値による読み飛ばしは、絞り込む列で並べて書いたときに効いた(10個中1個の行グループだけを読み、約10倍速い)。パーティション分割ではファイルごと飛ばせた。
  • 迷ったら snappy、保存量を減らすなら zstd。Excel で見る・1件ずつ更新するデータは Parquet に向かない。

次に読むとよい記事です。CSV を Excel で開いたときの文字化けは関連記事「CSV文字化けの直し方|エクセルで今すぐ直す手順とUTF-8で直らない原因」で、pandas の操作は関連記事「pandasの使い方 逆引き81選|pandas 3.0の実行結果つき」で扱っています。

参考・出典

確認日はいずれも 2026年10月29日です。

  • Apache Parquet「Overview」:https://parquet.apache.org/docs/overview/
  • Apache Parquet「File Format」:https://parquet.apache.org/docs/file-format/
  • Apache Parquet「Configurations」(Row Group Size・Data Page Size):https://parquet.apache.org/docs/file-format/configurations/
  • Apache Arrow「pyarrow.parquet.write_table」:https://arrow.apache.org/docs/python/generated/pyarrow.parquet.write_table.html
  • Apache Arrow「Reading and Writing the Apache Parquet Format」:https://arrow.apache.org/docs/python/parquet.html
  • pandas「pandas.read_parquet」:https://pandas.pydata.org/docs/reference/api/pandas.read_parquet.html
  • DuckDB「Reading and Writing Parquet Files」:https://duckdb.org/docs/stable/data/parquet/overview
  • Apache Spark「Parquet Files」:https://spark.apache.org/docs/latest/sql-data-sources-parquet.html
  • Google Cloud「Loading Parquet data from Cloud Storage」:https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-parquet
  • AWS「Use columnar storage formats」(Amazon Athena):https://docs.aws.amazon.com/athena/latest/ug/columnar-storage.html
  • Microsoft Learn「Parquet」(Power Query コネクタ):https://learn.microsoft.com/en-us/power-query/connectors/parquet
  • Wikipedia「Apache Parquet」:https://en.wikipedia.org/wiki/Apache_Parquet

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Parquetとは|CSVとの違い・読み書き・中身を100万行で実測」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ
参加受付中

AI駆動開発、 無料セミナー