Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Meu relatório anual travava o Trino: 144 milhões de linhas e o que mudou no script Python

2026年11月05日に「Dev.to」が公開したITニュース「Meu relatório anual travava o Trino: 144 milhões de linhas e o que mudou no script Python」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

大量データ処理でTrinoが停止。Pythonスクリプトの高速化より、データを月別に分割し、データベースで集計、結果を保存する方法で解決した。一度に全データを要求せず、処理を分散させる設計が重要だ。

ITニュース解説

あるシステム開発の現場で、月ごとのデータを集計するPythonプログラムが問題なく動いていた。このプログラムは、Trinoというデータベースシステムから約1200万行のデータを取得し、それをPython上で加工してレポートを作成していた。ところが、この月次レポートを年間単位に変更してほしいという依頼が来た。単純にデータベースへの問い合わせ期間を1ヶ月から12ヶ月に広げたところ、データ量は約1億4400万行に激増し、Trinoが完全に停止してしまった。この問題は、なぜ発生し、どのように解決されたのかを解説する。

まず、なぜプログラムが動かなくなったのか、その原因はいくつかあった。一つ目は、「巨大なクエリを一度に実行したこと」である。Trinoは、たくさんのコンピューターが協力して大量のデータを高速に検索・処理するための特別なシステムだが、一つの問い合わせで使えるコンピューターのメモリ容量には上限がある。1億4400万行という膨大なデータを一度に処理しようとした結果、このメモリ上限を超えてしまい、Trinoが途中で動かなくなってしまったのだ。しかも、失敗するまでに40分もかかり、その間ずっとデータベースのリソースを占有してしまっていた。

二つ目は、「全てのデータと全ての項目を取得しようとしていたこと」である。元のプログラムでは、SELECT *という命令でテーブルの全ての列を、fetchall()という命令で検索結果の全ての行をデータベースから取得していた。これにより、1億4400万行分の全データがTrinoからネットワークを通じてPythonプログラムが動くコンピューターに送られ、そのコンピューターのメモリに全てを格納しようとした。これだけの情報量では、ネットワーク転送にも時間がかかり、Pythonプログラム側のメモリも簡単に限界に達してしまう。

三つ目は、「データの集計をPythonプログラムで行っていたこと」だ。プログラムは1億4400万行もの生データを全てPythonに持ってきてから、特定の条件でデータをまとめたり(groupby)、合計値を計算したりしていた。しかし、レポートで本当に必要なのは集計された後のごく一部のデータである。つまり、Pythonプログラムは、膨大なデータを苦労して手元に集めておきながら、そのほとんどをすぐに捨ててしまうという非常に非効率な処理をしていたのだ。

これらの問題は、データ量が1200万行の月次レポートではなんとか許容範囲内だったが、1億4400万行の年次レポートになった途端、システム全体が破綻する原因となった。

この状況を解決するために、いくつかの戦略が段階的に導入された。 最初の解決策は、「一年間のデータを月に分割して問い合わせる」ことだ。一度に1億4400万行を処理するのではなく、12回に分けてそれぞれ約1200万行を処理するように変更した。これにより、個々の問い合わせのデータ量が月次レポートと同じレベルに戻り、Trinoが安定して処理できるようになった。また、もし途中の月に何らかのエラーが発生しても、その特定の月の処理だけをやり直せばよいため、時間やリソースの無駄を最小限に抑えることができる。さらに、データベースによってはデータを日付ごとに整理整頓して保存している場合(これを「パーティション」と呼ぶ)があり、月ごとに絞り込むことで、Trinoが読み込むデータ量をさらに減らせるという利点もあった。

二つ目の解決策は、「データの集計をPythonではなくTrinoで行う」ことだ。レポートで必要なのは、キャンペーンごとの合計値などの集計結果であり、個々のイベントの生データではない。そこで、Pythonに全データを送るのではなく、Trino側で集計処理(GROUP BYやCOUNT、SUMといった関数)を実行させ、その集計済みの結果だけをPythonに返すように変更した。これにより、TrinoからPythonに送られるデータは、1200万行の生データから、数百〜数千行程度の集計済みデータへと劇的に減少し、ネットワーク転送量もPython側のメモリ使用量も大幅に削減できた。また、SELECT *ではなく、必要な列だけを指定して取得するようになったため、Trinoが読み込むディスクのデータ量も減り、全体の処理速度が向上した。この原則は重要で、もしPythonでデータを集計するつもりなら、その集計処理はできるだけデータベース側で行い、Pythonには集計結果だけを持ってくるのが賢明である。

三つ目の解決策は、「各月の処理が終わるたびに結果をファイルに保存する」ことだ。12ヶ月分の処理を順番に行う中で、もし途中の月の処理でエラーが起きたり、システムが停止したりすると、そこまでに完了していた月の結果が失われてしまう可能性がある。これを防ぐため、各月の集計が完了するたびに、その結果を専用のファイル(Parquet形式など)に保存するようにした。これにより、もし途中で問題が起きても、すでに保存済みのデータは失われず、プログラムを再実行した際には、まだ処理が完了していない月だけを対象に再開できるようになった。これは「チェックポイント」の考え方で、処理の途中経過を記録することで、システムの安定性と継続性を高める。

四つ目の解決策は、「複数の月の処理を同時に行うが、その数を制限する」ことだ。各月の処理を一つずつ順番に行うのは安全だが、時間がかかってしまう。かといって、12ヶ月分の処理を全て同時に実行すると、元の問題と同じようにTrinoに過大な負荷がかかってしまう。そこで、PythonのThreadPoolExecutorという機能を使って、例えば同時に3ヶ月分だけ処理を進めるように調整した。これにより、処理速度を向上させつつ、Trinoへの負荷を適切に制御し、システム全体の安定性を保つことができる。この「同時に処理する数」は、データベースシステムの性能に合わせて調整すべき重要な設定値である。

五つ目の解決策は、「最後に各月の集計結果をまとめて年間の最終レポートを作成する」ことだ。12個の月ごとの集計済みファイルができた後、Pythonプログラムはそれらのファイルを読み込み、結合して年間の最終的なレポートを作成する。この時点では、各ファイルに含まれるデータ量は非常に少ないため、この結合処理は短時間で完了する。

最後に、集計するデータによっては特別な注意が必要な場合もある。「ユニークユーザー数」のような指標は、単純に各月のユニークユーザー数を足し合わせると、月をまたいで利用したユーザーが複数回カウントされてしまい、正しい年間ユニーク数にならない。このような場合は、TrinoのHyperLogLogという機能を使うと良い。これは、厳密ではないが、だいたい正しいユニーク数を高速に計算できる方法で、各月の「ユーザー識別子の特徴を示す情報」を保存し、最後にそれらを結合して年間全体のユニーク数を算出する。精度は約2%程度の誤差があるが、ほとんどのレポート要件には十分対応できる。もし完全に正確な値が必要な場合は、特別な対応が必要となる。

これらの変更の結果、以前はシステムを停止させていた年次レポートが、安定して、かつ効率的に生成できるようになった。この経験から得られた重要な教訓は、データ処理のプログラムは、いま動いているからといって、将来のデータ量増加に耐えられるとは限らない、ということである。常に「もしデータ量が何倍にも増えたらどうなるか」を考え、データ処理の設計段階から、期間を小さく区切る、データベース側で集計する、必要なデータだけ取得する、途中結果を保存する、並列処理を適切に利用するといった工夫を凝らすことが、システムエンジニアとして非常に重要となるだろう。

関連コンテンツ

関連IT用語

関連ITニュース